Skip to content

Commit

Permalink
feat: base64-url encoded participantId in API controllers (#272)
Browse files Browse the repository at this point in the history
feat: base64-url encoded participantId
  • Loading branch information
bscholtes1A authored Feb 13, 2024
1 parent 2096032 commit 0be42d0
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.eclipse.edc.identityhub.spi.ParticipantContextId.onEncoded;
import static org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY;
import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper;

Expand Down Expand Up @@ -82,6 +83,7 @@ public Response queryPresentation(@PathParam("participantId") String participant
}
validatorRegistry.validate(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY, query).orElseThrow(ValidationFailureException::new);

participantContextId = onEncoded(participantContextId).orElseThrow(InvalidRequestException::new);
var presentationQuery = transformerRegistry.transform(query, PresentationQueryMessage.class).orElseThrow(InvalidRequestException::new);

if (presentationQuery.getPresentationDefinition() != null) {
Expand Down
7 changes: 4 additions & 3 deletions docs/developer/architecture/mgmt-api.security.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

## 1. Definition of terms

- _Service principal_: the identifier for the entity that owns a resource. In IdentityHub, this is the ID of
the `ParticipantContext`. Not that this is **not** a user! Also referred to as: principal
- _Service principal_ (also referred as _principal_): the identifier for the entity that owns a resource. In
IdentityHub, this is the ID of
the `ParticipantContext`. Note that this is **not** a user!
- _User_: a physical entity that may be able to perform different operations on a resource belonging to a service
principal. While a participant (context) would be analogous to a company or an organization, a user would be one
single individual within that company / participant. Invidual users don't exist as first-level concept in
single individual within that company / participant. **Individual users don't exist as first-level concept in
IdentityHub!**
- _Participant context_: this is the unit of management, that owns all resources. Its identifier must be equal to
the `participantId` that is defined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;
import java.util.stream.IntStream;
Expand Down Expand Up @@ -59,7 +60,6 @@ void findById_notAuthorized() {
var user1 = "user1";
createParticipant(user1);


// create second user
var user2 = "user2";
var user2Context = ParticipantContext.Builder.newInstance()
Expand All @@ -75,7 +75,7 @@ void findById_notAuthorized() {
RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", user2Token))
.get("/v1/participants/%s/keypairs/%s".formatted(user1, key))
.get("/v1/participants/%s/keypairs/%s".formatted(toBase64(user1), key))
.then()
.log().ifValidationFails()
.statusCode(403)
Expand All @@ -87,14 +87,13 @@ void findById() {
var user1 = "user1";
var token = createParticipant(user1);


var key = createKeyPair(user1);

assertThat(Arrays.asList(token, getSuperUserApiKey()))
.allSatisfy(t -> RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", t))
.get("/v1/participants/%s/keypairs/%s".formatted(user1, key))
.get("/v1/participants/%s/keypairs/%s".formatted(toBase64(user1), key))
.then()
.log().ifValidationFails()
.statusCode(200)
Expand All @@ -106,7 +105,6 @@ void findForParticipant_notAuthorized() {
var user1 = "user1";
createParticipant(user1);


// create second user
var user2 = "user2";
var user2Context = ParticipantContext.Builder.newInstance()
Expand All @@ -116,13 +114,13 @@ void findForParticipant_notAuthorized() {
.build();
var user2Token = storeParticipant(user2Context);

var key = createKeyPair(user1);
createKeyPair(user1);

// attempt to publish user1's DID document, which should fail
var res = RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", user2Token))
.get("/v1/participants/%s/keypairs".formatted(user1))
.get("/v1/participants/%s/keypairs".formatted(toBase64(user1)))
.then()
.log().ifValidationFails()
.statusCode(200)
Expand All @@ -136,15 +134,13 @@ void findForParticipant_notAuthorized() {
void findForParticipant() {
var user1 = "user1";
var token = createParticipant(user1);


var key = createKeyPair(user1);
createKeyPair(user1);

assertThat(Arrays.asList(token, getSuperUserApiKey()))
.allSatisfy(t -> RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", t))
.get("/v1/participants/%s/keypairs".formatted(user1))
.get("/v1/participants/%s/keypairs".formatted(toBase64(user1)))
.then()
.log().ifValidationFails()
.statusCode(200)
Expand All @@ -167,7 +163,7 @@ void addKeyPair() {
.contentType(JSON)
.header(new Header("x-api-key", t))
.body(keyDesc)
.put("/v1/participants/%s/keypairs?participantId=%s".formatted(user1, user1))
.put("/v1/participants/%s/keypairs".formatted(toBase64(user1)))
.then()
.log().ifValidationFails()
.statusCode(204)
Expand Down Expand Up @@ -198,7 +194,7 @@ void addKeyPair_notAuthorized() {
.contentType(JSON)
.header(new Header("x-api-key", token2))
.body(keyDesc)
.put("/v1/participants/%s/keypairs?participantId=%s".formatted(user1, user1))
.put("/v1/participants/%s/keypairs".formatted(toBase64(user1)))
.then()
.log().ifValidationFails()
.statusCode(403)
Expand Down Expand Up @@ -232,7 +228,7 @@ void rotate() {
.contentType(JSON)
.header(new Header("x-api-key", t))
.body(keyDesc)
.post("/v1/participants/%s/keypairs/%s/rotate".formatted(user1, keyId))
.post("/v1/participants/%s/keypairs/%s/rotate".formatted(toBase64(user1), keyId))
.then()
.log().ifValidationFails()
.statusCode(204)
Expand Down Expand Up @@ -303,7 +299,7 @@ void revoke() {
.contentType(JSON)
.header(new Header("x-api-key", t))
.body(keyDesc)
.post("/v1/participants/%s/keypairs/%s/revoke".formatted(user1, keyId))
.post("/v1/participants/%s/keypairs/%s/revoke".formatted(toBase64(user1), keyId))
.then()
.log().ifValidationFails()
.statusCode(204)
Expand All @@ -317,7 +313,7 @@ void revoke() {
@Test
void revoke_notAuthorized() {
var user1 = "user1";
var token = createParticipant(user1);
var token1 = createParticipant(user1);

var user2 = "user2";
var token2 = createParticipant(user2);
Expand All @@ -330,7 +326,7 @@ void revoke_notAuthorized() {
.contentType(JSON)
.header(new Header("x-api-key", token2))
.body(keyDesc)
.post("/v1/participants/%s/keypairs/%s/revoke".formatted(user1, keyId))
.post("/v1/participants/%s/keypairs/%s/revoke".formatted(toBase64(user1), keyId))
.then()
.log().ifValidationFails()
.statusCode(403)
Expand Down Expand Up @@ -419,4 +415,8 @@ private String createKeyPair(String participantId) {
return descriptor.getKeyId();
}

private String toBase64(String s) {
return Base64.getUrlEncoder().encodeToString(s.getBytes());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.junit.jupiter.params.provider.ValueSource;

import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.stream.IntStream;

Expand All @@ -53,7 +54,7 @@ void getUserById() {

var su = RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest()
.header(new Header("x-api-key", apikey))
.get("/v1/participants/" + SUPER_USER)
.get("/v1/participants/" + toBase64(SUPER_USER))
.then()
.statusCode(200)
.extract().body().as(ParticipantContext.class);
Expand Down Expand Up @@ -82,7 +83,7 @@ void getUserById_notOwner_expect403() {
RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest()
.header(new Header("x-api-key", apiToken1))
.contentType(ContentType.JSON)
.get("/v1/participants/" + user2)
.get("/v1/participants/" + toBase64(user2))
.then()
.log().ifValidationFails()
.statusCode(403);
Expand Down Expand Up @@ -180,7 +181,7 @@ void activateParticipant_principalIsSuperser() {
RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest()
.header(new Header("x-api-key", getSuperUserApiKey()))
.contentType(ContentType.JSON)
.post("/v1/participants/%s/state?isActive=true".formatted(participantId))
.post("/v1/participants/%s/state?isActive=true".formatted(toBase64(participantId)))
.then()
.log().ifError()
.statusCode(204);
Expand All @@ -205,7 +206,7 @@ void deleteParticipant() {
RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest()
.header(new Header("x-api-key", getSuperUserApiKey()))
.contentType(ContentType.JSON)
.delete("/v1/participants/%s".formatted(participantId))
.delete("/v1/participants/%s".formatted(toBase64(participantId)))
.then()
.log().ifError()
.statusCode(204);
Expand All @@ -215,15 +216,14 @@ void deleteParticipant() {

@Test
void regenerateToken() {

var participantId = "another-user";
var userToken = createParticipant(participantId);

assertThat(Arrays.asList(userToken, getSuperUserApiKey()))
.allSatisfy(t -> RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest()
.header(new Header("x-api-key", t))
.contentType(ContentType.JSON)
.post("/v1/participants/%s/token".formatted(participantId))
.post("/v1/participants/%s/token".formatted(toBase64(participantId)))
.then()
.log().ifError()
.statusCode(200)
Expand All @@ -239,7 +239,7 @@ void updateRoles() {
.header(new Header("x-api-key", getSuperUserApiKey()))
.contentType(ContentType.JSON)
.body(List.of("role1", "role2", "admin"))
.put("/v1/participants/%s/roles".formatted(participantId))
.put("/v1/participants/%s/roles".formatted(toBase64(participantId)))
.then()
.log().ifError()
.statusCode(204);
Expand All @@ -257,7 +257,7 @@ void updateRoles_whenNotSuperuser(String role) {
.header(new Header("x-api-key", userToken))
.contentType(ContentType.JSON)
.body(List.of(role))
.put("/v1/participants/%s/roles".formatted(participantId))
.put("/v1/participants/%s/roles".formatted(toBase64(participantId)))
.then()
.log().ifError()
.statusCode(403);
Expand Down Expand Up @@ -334,4 +334,8 @@ void getAll_notAuthorized() {
.log().ifValidationFails()
.statusCode(403);
}

private String toBase64(String s) {
return Base64.getUrlEncoder().encodeToString(s.getBytes());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentMatchers;

import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
Expand All @@ -57,7 +58,8 @@

@ComponentTest
public class PresentationApiComponentTest {
public static final String VALID_QUERY_WITH_SCOPE = """

private static final String VALID_QUERY_WITH_SCOPE = """
{
"@context": [
"https://identity.foundation/presentation-exchange/submission/v1",
Expand All @@ -74,6 +76,7 @@ public class PresentationApiComponentTest {
.id("identity-hub")
.build();
private static final String TEST_PARTICIPANT_CONTEXT_ID = "test-participant";
private static final String TEST_PARTICIPANT_CONTEXT_ID_ENCODED = Base64.getUrlEncoder().encodeToString(TEST_PARTICIPANT_CONTEXT_ID.getBytes());
// todo: these mocks should be replaced, once their respective implementations exist!
private static final CredentialQueryResolver CREDENTIAL_QUERY_RESOLVER = mock();
private static final VerifiablePresentationService PRESENTATION_GENERATOR = mock();
Expand All @@ -97,7 +100,7 @@ void query_tokenNotPresent_shouldReturn401() {
createParticipant(TEST_PARTICIPANT_CONTEXT_ID);
IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest()
.contentType("application/json")
.post("/v1/participants/test-participant/presentation/query")
.post("/v1/participants/%s/presentation/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED))
.then()
.statusCode(401)
.extract().body().asString();
Expand All @@ -119,7 +122,7 @@ void query_validationError_shouldReturn400() {
.contentType(JSON)
.header(AUTHORIZATION, generateSiToken())
.body(query)
.post("/v1/participants/test-participant/presentation/query")
.post("/v1/participants/%s/presentation/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED))
.then()
.statusCode(400)
.extract().body().asString();
Expand All @@ -144,7 +147,7 @@ void query_withPresentationDefinition_shouldReturn503() {
.contentType(JSON)
.header(AUTHORIZATION, generateSiToken())
.body(query)
.post("/v1/participants/test-participant/presentation/query")
.post("/v1/participants/%s/presentation/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED))
.then()
.statusCode(503)
.extract().body().asString();
Expand All @@ -160,7 +163,7 @@ void query_tokenVerificationFails_shouldReturn401() {
.contentType(JSON)
.header(AUTHORIZATION, token)
.body(VALID_QUERY_WITH_SCOPE)
.post("/v1/participants/test-participant/presentation/query")
.post("/v1/participants/%s/presentation/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED))
.then()
.statusCode(401)
.log().ifValidationFails()
Expand All @@ -179,7 +182,7 @@ void query_queryResolutionFails_shouldReturn403() {
.contentType(JSON)
.header(AUTHORIZATION, token)
.body(VALID_QUERY_WITH_SCOPE)
.post("/v1/participants/test-participant/presentation/query")
.post("/v1/participants/%s/presentation/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED))
.then()
.statusCode(403)
.log().ifValidationFails()
Expand All @@ -199,7 +202,7 @@ void query_presentationGenerationFails_shouldReturn500() {
.contentType(JSON)
.header(AUTHORIZATION, token)
.body(VALID_QUERY_WITH_SCOPE)
.post("/v1/participants/test-participant/presentation/query")
.post("/v1/participants/%s/presentation/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED))
.then()
.statusCode(500)
.log().ifValidationFails();
Expand All @@ -220,7 +223,7 @@ void query_success() throws JOSEException {
.contentType(JSON)
.header(AUTHORIZATION, token)
.body(VALID_QUERY_WITH_SCOPE)
.post("/v1/participants/test-participant/presentation/query")
.post("/v1/participants/%s/presentation/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED))
.then()
.statusCode(200)
.log().ifValidationFails()
Expand Down
Loading

0 comments on commit 0be42d0

Please sign in to comment.