Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: base64-url encoded participantId in API controllers #272

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading