mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-26 17:29:12 +01:00
Create rest endpoint to create new api keys
This commit is contained in:
@@ -33,6 +33,7 @@ import javax.inject.Inject;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
public class ApiKeyCollectionToDtoMapper {
|
||||
@@ -48,8 +49,9 @@ public class ApiKeyCollectionToDtoMapper {
|
||||
|
||||
public HalRepresentation map(Collection<ApiKey> keys) {
|
||||
List<ApiKeyDto> dtos = keys.stream().map(apiKeyDtoMapper::map).collect(toList());
|
||||
final Links.Builder links = Links.linkingTo();
|
||||
links.self(resourceLinks.apiKeyCollection().self());
|
||||
final Links.Builder links = Links.linkingTo()
|
||||
.self(resourceLinks.apiKeyCollection().self())
|
||||
.single(link("create", resourceLinks.apiKeyCollection().create()));
|
||||
return new HalRepresentation(links.build(), Embedded.embedded("apiKeys", dtos));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,12 @@ package sonia.scm.api.v2.resources;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class ApiKeyDto extends HalRepresentation {
|
||||
private String displayName;
|
||||
private String role;
|
||||
|
||||
@@ -26,6 +26,7 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.headers.Header;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
@@ -35,11 +36,19 @@ import sonia.scm.security.ApiKeyService;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import static javax.ws.rs.core.Response.Status.CREATED;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
public class ApiKeyResource {
|
||||
@@ -47,12 +56,14 @@ public class ApiKeyResource {
|
||||
private final ApiKeyService apiKeyService;
|
||||
private final ApiKeyCollectionToDtoMapper apiKeyCollectionMapper;
|
||||
private final ApiKeyToApiKeyDtoMapper apiKeyMapper;
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@Inject
|
||||
public ApiKeyResource(ApiKeyService apiKeyService, ApiKeyCollectionToDtoMapper apiKeyCollectionMapper, ApiKeyToApiKeyDtoMapper apiKeyMapper) {
|
||||
public ApiKeyResource(ApiKeyService apiKeyService, ApiKeyCollectionToDtoMapper apiKeyCollectionMapper, ApiKeyToApiKeyDtoMapper apiKeyMapper, ResourceLinks links) {
|
||||
this.apiKeyService = apiKeyService;
|
||||
this.apiKeyCollectionMapper = apiKeyCollectionMapper;
|
||||
this.apiKeyMapper = apiKeyMapper;
|
||||
this.resourceLinks = links;
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -114,4 +125,37 @@ public class ApiKeyResource {
|
||||
.map(apiKeyMapper::map).findAny()
|
||||
.orElseThrow(() -> notFound(ContextEntry.ContextBuilder.entity(ApiKey.class, id)));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.API_KEY)
|
||||
@Operation(summary = "Create new api key for the current user", description = "Creates a new api key for the given user with the role specified in the given key.", tags = "User")
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "create success",
|
||||
headers = @Header(
|
||||
name = "Location",
|
||||
description = "uri to the created user",
|
||||
schema = @Schema(type = "string")
|
||||
),
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "409", description = "conflict, a key with the given display name already exists")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response create(@Valid ApiKeyDto apiKey) {
|
||||
final ApiKeyService.CreationResult newKey = apiKeyService.createNewKey(apiKey.getDisplayName(), apiKey.getRole());
|
||||
return Response.status(CREATED)
|
||||
.entity(newKey.getToken())
|
||||
.location(URI.create(resourceLinks.apiKey().self(newKey.getId())))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,6 +218,10 @@ class ResourceLinks {
|
||||
String self() {
|
||||
return collectionLinkBuilder.method("apiKeys").parameters().method("getForCurrentUser").parameters().href();
|
||||
}
|
||||
|
||||
String create() {
|
||||
return collectionLinkBuilder.method("apiKeys").parameters().method("create").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public ApiKeyLinks apiKey() {
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.util.concurrent.Striped;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
@@ -70,11 +72,12 @@ public class ApiKeyService {
|
||||
this.passphraseGenerator = passphraseGenerator;
|
||||
}
|
||||
|
||||
public String createNewKey(String name, String role) {
|
||||
public CreationResult createNewKey(String name, String role) {
|
||||
String user = currentUser();
|
||||
String passphrase = passphraseGenerator.get();
|
||||
String hashedPassphrase = passwordService.encryptPassword(passphrase);
|
||||
ApiKeyWithPassphrase key = new ApiKeyWithPassphrase(keyGenerator.createKey(), name, role, hashedPassphrase);
|
||||
final String id = keyGenerator.createKey();
|
||||
ApiKeyWithPassphrase key = new ApiKeyWithPassphrase(id, name, role, hashedPassphrase);
|
||||
Lock lock = locks.get(user).writeLock();
|
||||
lock.lock();
|
||||
try {
|
||||
@@ -87,7 +90,8 @@ public class ApiKeyService {
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return tokenHandler.createToken(user, new ApiKey(key), passphrase);
|
||||
final String token = tokenHandler.createToken(user, new ApiKey(key), passphrase);
|
||||
return new CreationResult(token, id);
|
||||
}
|
||||
|
||||
public void remove(String id) {
|
||||
@@ -160,4 +164,11 @@ public class ApiKeyService {
|
||||
.stream()
|
||||
.anyMatch(key -> key.getDisplayName().equals(name));
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class CreationResult {
|
||||
private final String token;
|
||||
private final String id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ public class MeResourceTest {
|
||||
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
|
||||
when(userManager.getDefaultType()).thenReturn("xml");
|
||||
ApiKeyCollectionToDtoMapper apiKeyCollectionMapper = new ApiKeyCollectionToDtoMapper(apiKeyMapper, resourceLinks);
|
||||
ApiKeyResource apiKeyResource = new ApiKeyResource(apiKeyService, apiKeyCollectionMapper, apiKeyMapper);
|
||||
ApiKeyResource apiKeyResource = new ApiKeyResource(apiKeyService, apiKeyCollectionMapper, apiKeyMapper, resourceLinks);
|
||||
MeResource meResource = new MeResource(meDtoFactory, userManager, passwordService, of(apiKeyResource));
|
||||
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/"));
|
||||
when(scmPathInfoStore.get()).thenReturn(uriInfo);
|
||||
@@ -238,6 +238,7 @@ public class MeResourceTest {
|
||||
assertThat(response.getContentAsString()).contains("\"displayName\":\"key 1\",\"role\":\"READ\"");
|
||||
assertThat(response.getContentAsString()).contains("\"displayName\":\"key 2\",\"role\":\"WRITE\"");
|
||||
assertThat(response.getContentAsString()).contains("\"self\":{\"href\":\"/v2/me/apiKeys\"}");
|
||||
assertThat(response.getContentAsString()).contains("\"create\":{\"href\":\"/v2/me/apiKeys\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -254,6 +255,22 @@ public class MeResourceTest {
|
||||
assertThat(response.getContentAsString()).contains("\"self\":{\"href\":\"/v2/me/apiKeys/1\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateNewApiKey() throws URISyntaxException, UnsupportedEncodingException {
|
||||
when(apiKeyService.createNewKey("guide", "READ")).thenReturn(new ApiKeyService.CreationResult("abc", "1"));
|
||||
|
||||
final MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + MeResource.ME_PATH_V2 + "apiKeys/")
|
||||
.contentType(VndMediaType.API_KEY)
|
||||
.content("{\"displayName\":\"guide\",\"role\":\"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/me/apiKeys/1"));
|
||||
}
|
||||
|
||||
private User createDummyUser(String name) {
|
||||
User user = new User();
|
||||
user.setName(name);
|
||||
|
||||
@@ -101,7 +101,7 @@ class ApiKeyServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldReturnRoleForKey() {
|
||||
String newKey = service.createNewKey("1", "READ");
|
||||
String newKey = service.createNewKey("1", "READ").getToken();
|
||||
|
||||
Optional<String> role = service.check(newKey);
|
||||
|
||||
@@ -119,23 +119,24 @@ class ApiKeyServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldAddSecondKey() {
|
||||
String firstKey = service.createNewKey("1", "READ");
|
||||
String secondKey = service.createNewKey("2", "WRITE");
|
||||
ApiKeyService.CreationResult firstKey = service.createNewKey("1", "READ");
|
||||
ApiKeyService.CreationResult secondKey = service.createNewKey("2", "WRITE");
|
||||
|
||||
ApiKeyCollection apiKeys = store.get("dent");
|
||||
|
||||
assertThat(apiKeys.getKeys()).hasSize(2);
|
||||
|
||||
assertThat(service.check(firstKey)).contains("READ");
|
||||
assertThat(service.check(secondKey)).contains("WRITE");
|
||||
assertThat(service.check(firstKey.getToken())).contains("READ");
|
||||
assertThat(service.check(secondKey.getToken())).contains("WRITE");
|
||||
|
||||
assertThat(service.getKeys()).extracting("id").contains("1", "2");
|
||||
assertThat(service.getKeys()).extracting("id")
|
||||
.contains(firstKey.getId(), secondKey.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveKey() {
|
||||
String firstKey = service.createNewKey("1", "READ");
|
||||
String secondKey = service.createNewKey("2", "WRITE");
|
||||
String firstKey = service.createNewKey("1", "READ").getToken();
|
||||
String secondKey = service.createNewKey("2", "WRITE").getToken();
|
||||
|
||||
service.remove("1");
|
||||
|
||||
@@ -145,7 +146,7 @@ class ApiKeyServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldFailWhenAddingSameNameTwice() {
|
||||
String firstKey = service.createNewKey("1", "READ");
|
||||
String firstKey = service.createNewKey("1", "READ").getToken();
|
||||
|
||||
assertThrows(AlreadyExistsException.class, () -> service.createNewKey("1", "WRITE"));
|
||||
|
||||
@@ -154,7 +155,7 @@ class ApiKeyServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldIgnoreCorrectPassphraseWithWrongName() {
|
||||
String firstKey = service.createNewKey("1", "READ");
|
||||
String firstKey = service.createNewKey("1", "READ").getToken();
|
||||
|
||||
assertThat(service.check("dent", "other", firstKey)).isEmpty();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user