Feature/profile navigation (#1464)

- Fix bug where profile settings wasn't shown if user cannot change password
- Add missing "ApiKey" entry in the single user menu

Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
Florian Scholdei
2020-12-16 09:23:05 +01:00
committed by GitHub
parent 3f018c2255
commit 88b93dc8b8
30 changed files with 598 additions and 132 deletions

View File

@@ -0,0 +1,68 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import org.junit.Test;
import sonia.scm.security.ApiKey;
import java.net.URI;
import java.util.Collections;
import static java.time.Instant.now;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ApiKeyCollectionToDtoMapperTest {
public static final ApiKey API_KEY = new ApiKey("1", "key 1", "READ", now());
private final ApiKeyToApiKeyDtoMapper apiKeyDtoMapper = mock(ApiKeyToApiKeyDtoMapper.class);
private final ApiKeyCollectionToDtoMapper apiKeyCollectionToDtoMapper = new ApiKeyCollectionToDtoMapper(apiKeyDtoMapper, ResourceLinksMock.createMock(URI.create("/")));
@Test
public void shouldMapCollection() {
ApiKeyDto expectedApiKeyDto = new ApiKeyDto();
when(apiKeyDtoMapper.map(API_KEY, "user")).thenReturn(expectedApiKeyDto);
HalRepresentation halRepresentation = apiKeyCollectionToDtoMapper.map(Collections.singletonList(API_KEY), "user");
assertThat(halRepresentation.getEmbedded().hasItem("keys")).isTrue();
assertThat(halRepresentation.getEmbedded().getItemsBy("keys")).containsExactly(expectedApiKeyDto);
}
@Test
public void shouldEmbedLinks() {
ApiKeyDto expectedApiKeyDto = new ApiKeyDto();
when(apiKeyDtoMapper.map(API_KEY, "user")).thenReturn(expectedApiKeyDto);
HalRepresentation halRepresentation = apiKeyCollectionToDtoMapper.map(Collections.singletonList(API_KEY), "user");
assertThat(halRepresentation.getLinks().getLinkBy("self")).isPresent();
assertThat(halRepresentation.getLinks().getLinkBy("create")).isPresent();
}
}

View File

@@ -79,8 +79,9 @@ public class MeResourceTest {
@Rule
public ShiroRule shiro = new ShiroRule();
private RestDispatcher dispatcher = new RestDispatcher();
private final RestDispatcher dispatcher = new RestDispatcher();
private final MockHttpResponse response = new MockHttpResponse();
private final ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
@Mock
@@ -105,13 +106,10 @@ public class MeResourceTest {
@InjectMocks
private ApiKeyToApiKeyDtoMapperImpl apiKeyMapper;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
@Mock
private PasswordService passwordService;
private User originalUser;
private MockHttpResponse response = new MockHttpResponse();
@Before
public void prepareEnvironment() {
@@ -144,7 +142,7 @@ public class MeResourceTest {
assertThat(response.getContentAsString()).contains("\"name\":\"trillian\"");
assertThat(response.getContentAsString()).contains("\"self\":{\"href\":\"/v2/me/\"}");
assertThat(response.getContentAsString()).contains("\"delete\":{\"href\":\"/v2/users/trillian\"}");
assertThat(response.getContentAsString()).contains("\"apiKeys\":{\"href\":\"/v2/me/api_keys\"}");
assertThat(response.getContentAsString()).contains("\"apiKeys\":{\"href\":\"/v2/users/trillian/api_keys\"}");
}
private void applyUserToSubject(User user) {
@@ -233,7 +231,7 @@ public class MeResourceTest {
@Test
public void shouldGetAllApiKeys() throws URISyntaxException, UnsupportedEncodingException {
when(apiKeyService.getKeys())
when(apiKeyService.getKeys("trillian"))
.thenReturn(asList(
new ApiKey("1", "key 1", "READ", now()),
new ApiKey("2", "key 2", "WRITE", now())));
@@ -245,13 +243,13 @@ public class MeResourceTest {
assertThat(response.getContentAsString()).contains("\"displayName\":\"key 1\",\"permissionRole\":\"READ\"");
assertThat(response.getContentAsString()).contains("\"displayName\":\"key 2\",\"permissionRole\":\"WRITE\"");
assertThat(response.getContentAsString()).contains("\"self\":{\"href\":\"/v2/me/api_keys\"}");
assertThat(response.getContentAsString()).contains("\"create\":{\"href\":\"/v2/me/api_keys\"}");
assertThat(response.getContentAsString()).contains("\"self\":{\"href\":\"/v2/users/trillian/api_keys\"}");
assertThat(response.getContentAsString()).contains("\"create\":{\"href\":\"/v2/users/trillian/api_keys\"}");
}
@Test
public void shouldGetSingleApiKey() throws URISyntaxException, UnsupportedEncodingException {
when(apiKeyService.getKeys())
when(apiKeyService.getKeys("trillian"))
.thenReturn(asList(
new ApiKey("1", "key 1", "READ", now()),
new ApiKey("2", "key 2", "WRITE", now())));
@@ -263,13 +261,14 @@ public class MeResourceTest {
assertThat(response.getContentAsString()).contains("\"displayName\":\"key 1\"");
assertThat(response.getContentAsString()).contains("\"permissionRole\":\"READ\"");
assertThat(response.getContentAsString()).contains("\"self\":{\"href\":\"/v2/me/api_keys/1\"}");
assertThat(response.getContentAsString()).contains("\"delete\":{\"href\":\"/v2/me/api_keys/1\"}");
assertThat(response.getContentAsString()).contains("\"self\":{\"href\":\"/v2/users/trillian/api_keys/1\"}");
assertThat(response.getContentAsString()).contains("\"delete\":{\"href\":\"/v2/users/trillian/api_keys/1\"}");
}
@Test
public void shouldCreateNewApiKey() throws URISyntaxException, UnsupportedEncodingException {
when(apiKeyService.createNewKey("guide", "READ")).thenReturn(new ApiKeyService.CreationResult("abc", "1"));
when(apiKeyService.createNewKey("trillian","guide", "READ"))
.thenReturn(new ApiKeyService.CreationResult("abc", "1"));
final MockHttpRequest request = MockHttpRequest
.post("/" + MeResource.ME_PATH_V2 + "api_keys/")
@@ -280,12 +279,13 @@ public class MeResourceTest {
assertThat(response.getStatus()).isEqualTo(201);
assertThat(response.getContentAsString()).isEqualTo("abc");
assertThat(response.getOutputHeaders().get("Location")).containsExactly(URI.create("/v2/me/api_keys/1"));
assertThat(response.getOutputHeaders().get("Location")).containsExactly(URI.create("/v2/users/trillian/api_keys/1"));
}
@Test
public void shouldIgnoreInvalidNewApiKey() throws URISyntaxException, UnsupportedEncodingException {
when(apiKeyService.createNewKey("guide", "READ")).thenReturn(new ApiKeyService.CreationResult("abc", "1"));
when(apiKeyService.createNewKey("trillian","guide", "READ"))
.thenReturn(new ApiKeyService.CreationResult("abc", "1"));
final MockHttpRequest request = MockHttpRequest
.post("/" + MeResource.ME_PATH_V2 + "api_keys/")
@@ -303,9 +303,10 @@ public class MeResourceTest {
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(204);
verify(apiKeyService).remove("1");
verify(apiKeyService).remove("trillian","1");
}
private User createDummyUser(String name) {
User user = new User();
user.setName(name);

View File

@@ -21,14 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.repository.NamespaceAndName;
import java.net.URI;
@@ -78,6 +77,12 @@ public class ResourceLinksTest {
assertEquals(BASE_URL + UserRootResource.USERS_PATH_V2, url);
}
@Test
public void shouldCreateCorrectUserApiKeysUrl() {
String url = resourceLinks.user().apiKeys("ich");
assertEquals(BASE_URL + UserRootResource.USERS_PATH_V2 + "ich/api_keys", url);
}
@Test
public void shouldCreateCorrectGroupSelfUrl() {
String url = resourceLinks.group().self("nobodies");
@@ -179,6 +184,7 @@ public class ResourceLinksTest {
String url = resourceLinks.source().sourceWithPath("foo", "bar", "rev", "file");
assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "foo/bar/sources/rev/file", url);
}
@Test
public void shouldCreateCorrectPermissionCollectionUrl() {
String url = resourceLinks.source().selfWithoutRevision("space", "repo");

View File

@@ -0,0 +1,147 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import com.google.inject.util.Providers;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.security.ApiKey;
import sonia.scm.security.ApiKeyService;
import sonia.scm.web.RestDispatcher;
import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import static java.time.Instant.now;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
public class UserApiKeyResourceTest {
private final RestDispatcher dispatcher = new RestDispatcher();
private final MockHttpResponse response = new MockHttpResponse();
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
@Mock
private ApiKeyService apiKeyService;
@InjectMocks
private ApiKeyToApiKeyDtoMapperImpl apiKeyMapper;
@Before
public void prepareEnvironment() {
initMocks(this);
ApiKeyCollectionToDtoMapper apiKeyCollectionMapper = new ApiKeyCollectionToDtoMapper(apiKeyMapper, resourceLinks);
UserApiKeyResource userApiKeyResource = new UserApiKeyResource(apiKeyService, apiKeyCollectionMapper, apiKeyMapper, resourceLinks);
UserRootResource userRootResource = new UserRootResource(null, null, Providers.of(userApiKeyResource));
dispatcher.addSingletonResource(userRootResource);
}
@Test
public void shouldGetAllApiKeys() throws URISyntaxException, UnsupportedEncodingException {
when(apiKeyService.getKeys("trillian"))
.thenReturn(asList(
new ApiKey("1", "key 1", "READ", now()),
new ApiKey("2", "key 2", "WRITE", now())));
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "trillian/api_keys");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertThat(response.getContentAsString()).contains("\"displayName\":\"key 1\",\"permissionRole\":\"READ\"");
assertThat(response.getContentAsString()).contains("\"displayName\":\"key 2\",\"permissionRole\":\"WRITE\"");
assertThat(response.getContentAsString()).contains("\"self\":{\"href\":\"/v2/users/trillian/api_keys\"}");
assertThat(response.getContentAsString()).contains("\"create\":{\"href\":\"/v2/users/trillian/api_keys\"}");
}
@Test
public void shouldGetSingleApiKey() throws URISyntaxException, UnsupportedEncodingException {
when(apiKeyService.getKeys("trillian"))
.thenReturn(asList(
new ApiKey("1", "key 1", "READ", now()),
new ApiKey("2", "key 2", "WRITE", now())));
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "trillian/api_keys/1");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertThat(response.getContentAsString()).contains("\"displayName\":\"key 1\"");
assertThat(response.getContentAsString()).contains("\"permissionRole\":\"READ\"");
assertThat(response.getContentAsString()).contains("\"self\":{\"href\":\"/v2/users/trillian/api_keys/1\"}");
assertThat(response.getContentAsString()).contains("\"delete\":{\"href\":\"/v2/users/trillian/api_keys/1\"}");
}
@Test
public void shouldCreateNewApiKey() throws URISyntaxException, UnsupportedEncodingException {
when(apiKeyService.createNewKey("trillian", "guide", "READ")).thenReturn(new ApiKeyService.CreationResult("abc", "1"));
final MockHttpRequest request = MockHttpRequest
.post("/" + UserRootResource.USERS_PATH_V2 + "trillian/api_keys/")
.contentType(VndMediaType.API_KEY)
.content("{\"displayName\":\"guide\",\"permissionRole\":\"READ\"}".getBytes());
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(201);
assertThat(response.getContentAsString()).isEqualTo("abc");
assertThat(response.getOutputHeaders().get("Location")).containsExactly(URI.create("/v2/users/trillian/api_keys/1"));
}
@Test
public void shouldIgnoreInvalidNewApiKey() throws URISyntaxException {
when(apiKeyService.createNewKey("trillian", "guide", "READ")).thenReturn(new ApiKeyService.CreationResult("abc", "1"));
final MockHttpRequest request = MockHttpRequest
.post("/" + UserRootResource.USERS_PATH_V2 + "trillian/api_keys/")
.contentType(VndMediaType.API_KEY)
.content("{\"displayName\":\"guide\",\"pemissionRole\":\"\"}".getBytes());
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
public void shouldDeleteExistingApiKey() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.delete("/" + UserRootResource.USERS_PATH_V2 + "trillian/api_keys/1");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(204);
verify(apiKeyService).remove("trillian", "1");
}
}

View File

@@ -28,7 +28,6 @@ import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.io.Resources;
import com.google.inject.util.Providers;
import com.sun.mail.iap.Argument;
import org.apache.shiro.authc.credential.PasswordService;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
@@ -42,6 +41,7 @@ import org.mockito.Mock;
import sonia.scm.ContextEntry;
import sonia.scm.NotFoundException;
import sonia.scm.PageResult;
import sonia.scm.security.ApiKeyService;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.user.ChangePasswordNotAllowedException;
@@ -83,7 +83,7 @@ public class UserRootResourceTest {
@Rule
public ShiroRule shiro = new ShiroRule();
private RestDispatcher dispatcher = new RestDispatcher();
private final RestDispatcher dispatcher = new RestDispatcher();
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
@@ -92,6 +92,8 @@ public class UserRootResourceTest {
@Mock
private UserManager userManager;
@Mock
private ApiKeyService apiKeyService;
@Mock
private PermissionAssigner permissionAssigner;
@InjectMocks
private UserDtoToUserMapperImpl dtoToUserMapper;
@@ -99,6 +101,8 @@ public class UserRootResourceTest {
private UserToUserDtoMapperImpl userToDtoMapper;
@InjectMocks
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
@InjectMocks
private ApiKeyToApiKeyDtoMapperImpl apiKeyMapper;
@Captor
private ArgumentCaptor<User> userCaptor;
@@ -122,8 +126,10 @@ public class UserRootResourceTest {
userCollectionToDtoMapper, resourceLinks, passwordService);
UserPermissionResource userPermissionResource = new UserPermissionResource(permissionAssigner, permissionCollectionToDtoMapper);
UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService, userPermissionResource);
ApiKeyCollectionToDtoMapper apiKeyCollectionToDtoMapper = new ApiKeyCollectionToDtoMapper(apiKeyMapper, resourceLinks);
UserApiKeyResource userApiKeyResource = new UserApiKeyResource(apiKeyService, apiKeyCollectionToDtoMapper, apiKeyMapper, resourceLinks);
UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource),
Providers.of(userResource));
Providers.of(userResource), Providers.of(userApiKeyResource));
dispatcher.addSingletonResource(userRootResource);
}

View File

@@ -89,7 +89,7 @@ class ApiKeyServiceTest {
@Test
void shouldCreateNewKeyAndStoreItHashed() {
service.createNewKey("1", "READ");
service.createNewKey("dent","1", "READ");
ApiKeyCollection apiKeys = store.get("dent");
@@ -105,7 +105,7 @@ class ApiKeyServiceTest {
@Test
void shouldReturnRoleForKey() {
String newKey = service.createNewKey("1", "READ").getToken();
String newKey = service.createNewKey("dent","1", "READ").getToken();
ApiKeyService.CheckResult role = service.check(newKey);
@@ -114,20 +114,20 @@ class ApiKeyServiceTest {
@Test
void shouldHandleNewUser() {
assertThat(service.getKeys()).isEmpty();
assertThat(service.getKeys("zaphod")).isEmpty();
}
@Test
void shouldNotReturnAnythingWithWrongKey() {
service.createNewKey("1", "READ");
service.createNewKey("dent","1", "READ");
assertThrows(AuthorizationException.class, () -> service.check("dent", "1", "wrong"));
}
@Test
void shouldAddSecondKey() {
ApiKeyService.CreationResult firstKey = service.createNewKey("1", "READ");
ApiKeyService.CreationResult secondKey = service.createNewKey("2", "WRITE");
ApiKeyService.CreationResult firstKey = service.createNewKey("dent","1", "READ");
ApiKeyService.CreationResult secondKey = service.createNewKey("dent","2", "WRITE");
ApiKeyCollection apiKeys = store.get("dent");
@@ -136,16 +136,16 @@ class ApiKeyServiceTest {
assertThat(service.check(firstKey.getToken())).extracting("permissionRole").isEqualTo("READ");
assertThat(service.check(secondKey.getToken())).extracting("permissionRole").isEqualTo("WRITE");
assertThat(service.getKeys()).extracting("id")
assertThat(service.getKeys("dent")).extracting("id")
.contains(firstKey.getId(), secondKey.getId());
}
@Test
void shouldRemoveKey() {
String firstKey = service.createNewKey("first", "READ").getToken();
String secondKey = service.createNewKey("second", "WRITE").getToken();
String firstKey = service.createNewKey("dent","first", "READ").getToken();
String secondKey = service.createNewKey("dent","second", "WRITE").getToken();
service.remove("1");
service.remove("dent","1");
assertThrows(AuthorizationException.class, () -> service.check(firstKey));
assertThat(service.check(secondKey)).extracting("permissionRole").isEqualTo("WRITE");
@@ -153,23 +153,23 @@ class ApiKeyServiceTest {
@Test
void shouldFailWhenAddingSameNameTwice() {
String firstKey = service.createNewKey("1", "READ").getToken();
String firstKey = service.createNewKey("dent","1", "READ").getToken();
assertThrows(AlreadyExistsException.class, () -> service.createNewKey("1", "WRITE"));
assertThrows(AlreadyExistsException.class, () -> service.createNewKey("dent","1", "WRITE"));
assertThat(service.check(firstKey)).extracting("permissionRole").isEqualTo("READ");
}
@Test
void shouldIgnoreCorrectPassphraseWithWrongName() {
String firstKey = service.createNewKey("1", "READ").getToken();
String firstKey = service.createNewKey("dent","1", "READ").getToken();
assertThrows(AuthorizationException.class, () -> service.check("dent", "other", firstKey));
}
@Test
void shouldDeleteTokensWhenUserIsDeleted() {
service.createNewKey("1", "READ").getToken();
service.createNewKey("dent","1", "READ").getToken();
assertThat(store.get("dent").getKeys()).hasSize(1);