Skip to content

Commit

Permalink
[eclipse-ditto#926] added possibility to use JWT claims being a jsona…
Browse files Browse the repository at this point in the history
…rray of strings instead of only plain strings

* the JwtPlaceholder works the same
* added "expansion" algorithm to expand inlines JsonArrays to multiple SubjectIds to TokenIntegrationSubjectIdFactory
* adjusted PolicyActionCommands to work on multiple subjects/subjectIds
* adjusted the default token-integration-subject to "integration:{{policy-entry:label}}:{{jwt:aud}}"

Signed-off-by: Thomas Jaeckle <thomas.jaeckle@bosch.io>
  • Loading branch information
thjaeckle committed Jan 20, 2021
1 parent 0a4d841 commit 3b2163c
Show file tree
Hide file tree
Showing 41 changed files with 528 additions and 253 deletions.
Expand Up @@ -64,7 +64,7 @@ default PipelineElement resolve(final String expressionTemplate) {
* Resolves a complete expression template starting with a {@link Placeholder} followed by optional pipeline stages
* (e.g. functions). Keep unresolvable expressions as is.
*
* @param expressionTemplate the expressionTemplate to resolve {@link org.eclipse.ditto.model.placeholders.Placeholder}s and and execute optional
* @param expressionTemplate the expressionTemplate to resolve {@link Placeholder}s and and execute optional
* pipeline stages
* @return the resolved String, a signifier for resolution failure, or one for deletion.
* @throws PlaceholderFunctionTooComplexException thrown if the {@code expressionTemplate} contains a placeholder
Expand Down
Expand Up @@ -563,7 +563,7 @@ public void activateTopLevelTokenIntegrationWithoutPermission() {
final SubjectId subjectId = SubjectId.newInstance("issuer:{{policy-entry:label}}:subject");
final Instant expiry = Instant.now();
final TopLevelPolicyActionCommand command = TopLevelPolicyActionCommand.of(
ActivateTokenIntegration.of(POLICY_ID, Label.of("-"), subjectId, expiry, DITTO_HEADERS),
ActivateTokenIntegration.of(POLICY_ID, Label.of("-"), Collections.singleton(subjectId), expiry, DITTO_HEADERS),
List.of());

enforcer.tell(command, getRef());
Expand All @@ -579,7 +579,7 @@ public void deactivateTopLevelTokenIntegrationWithoutPermission() {
new TestKit(system) {{
final SubjectId subjectId = SubjectId.newInstance("issuer:{{policy-entry:label}}:subject");
final TopLevelPolicyActionCommand command = TopLevelPolicyActionCommand.of(
DeactivateTokenIntegration.of(POLICY_ID, Label.of("-"), subjectId, DITTO_HEADERS),
DeactivateTokenIntegration.of(POLICY_ID, Label.of("-"), Collections.singleton(subjectId), DITTO_HEADERS),
List.of());

enforcer.tell(command, getRef());
Expand All @@ -596,7 +596,7 @@ public void activateTokenIntegrationWithoutPermission() {
final SubjectId subjectId = SubjectId.newInstance("issuer:{{policy-entry:label}}:subject");
final Instant expiry = Instant.now();
final ActivateTokenIntegration activateTokenIntegration =
ActivateTokenIntegration.of(POLICY_ID, Label.of("forbidden"), subjectId, expiry, DITTO_HEADERS);
ActivateTokenIntegration.of(POLICY_ID, Label.of("forbidden"), Collections.singleton(subjectId), expiry, DITTO_HEADERS);

enforcer.tell(activateTokenIntegration, getRef());

Expand All @@ -611,7 +611,7 @@ public void deactivateTokenIntegrationWithoutPermission() {
new TestKit(system) {{
final SubjectId subjectId = SubjectId.newInstance("issuer:{{policy-entry:label}}:subject");
final DeactivateTokenIntegration deactivateTokenIntegration =
DeactivateTokenIntegration.of(POLICY_ID, Label.of("forbidden"), subjectId, DITTO_HEADERS);
DeactivateTokenIntegration.of(POLICY_ID, Label.of("forbidden"), Collections.singleton(subjectId), DITTO_HEADERS);

enforcer.tell(deactivateTokenIntegration, getRef());

Expand All @@ -627,7 +627,7 @@ public void activateTopLevelTokenIntegration() {
final SubjectId subjectId = SubjectId.newInstance("issuer:{{policy-entry:label}}:subject");
final Instant expiry = Instant.now();
final TopLevelPolicyActionCommand command = TopLevelPolicyActionCommand.of(
ActivateTokenIntegration.of(POLICY_ID, Label.of("-"), subjectId, expiry, DITTO_HEADERS),
ActivateTokenIntegration.of(POLICY_ID, Label.of("-"), Collections.singleton(subjectId), expiry, DITTO_HEADERS),
List.of()
);

Expand All @@ -639,7 +639,7 @@ public void activateTopLevelTokenIntegration() {
final TopLevelPolicyActionCommand
forwarded = policiesShardRegionProbe.expectMsgClass(TopLevelPolicyActionCommand.class);
assertThat(forwarded).isEqualTo(TopLevelPolicyActionCommand.of(
ActivateTokenIntegration.of(POLICY_ID, Label.of("-"), subjectId, expiry, DITTO_HEADERS),
ActivateTokenIntegration.of(POLICY_ID, Label.of("-"), Collections.singleton(subjectId), expiry, DITTO_HEADERS),
List.of(Label.of("allowed"))
));
}};
Expand All @@ -650,7 +650,7 @@ public void deactivateTopLevelTokenIntegration() {
new TestKit(system) {{
final SubjectId subjectId = SubjectId.newInstance("issuer:{{policy-entry:label}}:subject");
final TopLevelPolicyActionCommand command = TopLevelPolicyActionCommand.of(
DeactivateTokenIntegration.of(POLICY_ID, Label.of("-"), subjectId, DITTO_HEADERS),
DeactivateTokenIntegration.of(POLICY_ID, Label.of("-"), Collections.singleton(subjectId), DITTO_HEADERS),
List.of()
);

Expand All @@ -662,7 +662,7 @@ public void deactivateTopLevelTokenIntegration() {
final TopLevelPolicyActionCommand
forwarded = policiesShardRegionProbe.expectMsgClass(TopLevelPolicyActionCommand.class);
assertThat(forwarded).isEqualTo(TopLevelPolicyActionCommand.of(
DeactivateTokenIntegration.of(POLICY_ID, Label.of("-"), subjectId, DITTO_HEADERS),
DeactivateTokenIntegration.of(POLICY_ID, Label.of("-"), Collections.singleton(subjectId), DITTO_HEADERS),
List.of(Label.of("allowed"))
));
}};
Expand All @@ -674,7 +674,7 @@ public void activateTokenIntegration() {
final SubjectId subjectId = SubjectId.newInstance("issuer:{{policy-entry:label}}:subject");
final Instant expiry = Instant.now();
final ActivateTokenIntegration activateTokenIntegration =
ActivateTokenIntegration.of(POLICY_ID, Label.of("allowed"), subjectId, expiry, DITTO_HEADERS);
ActivateTokenIntegration.of(POLICY_ID, Label.of("allowed"), Collections.singleton(subjectId), expiry, DITTO_HEADERS);

enforcer.tell(activateTokenIntegration, getRef());

Expand All @@ -692,7 +692,7 @@ public void deactivateTokenIntegration() {
new TestKit(system) {{
final SubjectId subjectId = SubjectId.newInstance("issuer:{{policy-entry:label}}:subject");
final DeactivateTokenIntegration deactivateTokenIntegration =
DeactivateTokenIntegration.of(POLICY_ID, Label.of("allowed"), subjectId, DITTO_HEADERS);
DeactivateTokenIntegration.of(POLICY_ID, Label.of("allowed"), Collections.singleton(subjectId), DITTO_HEADERS);

enforcer.tell(deactivateTokenIntegration, getRef());

Expand Down
Expand Up @@ -12,6 +12,10 @@
*/
package org.eclipse.ditto.services.gateway.endpoints.routes.policies;

import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.ditto.model.base.headers.DittoHeaders;
import org.eclipse.ditto.model.jwt.JsonWebToken;
import org.eclipse.ditto.model.placeholders.ExpressionResolver;
Expand Down Expand Up @@ -42,11 +46,15 @@ public static OAuthTokenIntegrationSubjectIdFactory of(final OAuthConfig oAuthCo
}

@Override
public SubjectId getSubjectId(final DittoHeaders dittoHeaders, final JsonWebToken jwt) {
public Set<SubjectId> getSubjectIds(final DittoHeaders dittoHeaders, final JsonWebToken jwt) {
final ExpressionResolver expressionResolver = PlaceholderFactory.newExpressionResolver(
PlaceholderFactory.newPlaceholderResolver(PlaceholderFactory.newHeadersPlaceholder(), dittoHeaders),
PlaceholderFactory.newPlaceholderResolver(JwtPlaceholder.getInstance(), jwt)
);
return SubjectId.newInstance(expressionResolver.resolvePartially(subjectTemplate));
final String issuerWithSubject = expressionResolver.resolvePartially(subjectTemplate);
return TokenIntegrationSubjectIdFactory.expandJsonArraysInResolvedSubject(issuerWithSubject)
.map(SubjectId::newInstance)
.collect(Collectors.toCollection(LinkedHashSet::new));
}

}
Expand Up @@ -17,6 +17,7 @@
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

import org.eclipse.ditto.json.JsonFactory;
Expand Down Expand Up @@ -198,19 +199,19 @@ private Route policyActions(final RequestContext ctx, final DittoHeaders dittoHe
private TopLevelPolicyActionCommand topLevelActivateTokenIntegration(final DittoHeaders dittoHeaders,
final PolicyId policyId, final JsonWebToken jwt) {

final SubjectId subjectId = tokenIntegrationSubjectIdFactory.getSubjectId(dittoHeaders, jwt);
final Set<SubjectId> subjectIds = tokenIntegrationSubjectIdFactory.getSubjectIds(dittoHeaders, jwt);
final Instant expiry = jwt.getExpirationTime();
final ActivateTokenIntegration activateTokenIntegration =
ActivateTokenIntegration.of(policyId, DUMMY_LABEL, subjectId, expiry, dittoHeaders);
ActivateTokenIntegration.of(policyId, DUMMY_LABEL, subjectIds, expiry, dittoHeaders);
return TopLevelPolicyActionCommand.of(activateTokenIntegration, List.of());
}

private TopLevelPolicyActionCommand topLevelDeactivateTokenIntegration(final DittoHeaders dittoHeaders,
final PolicyId policyId, final JsonWebToken jwt) {

final SubjectId subjectId = tokenIntegrationSubjectIdFactory.getSubjectId(dittoHeaders, jwt);
final Set<SubjectId> subjectIds = tokenIntegrationSubjectIdFactory.getSubjectIds(dittoHeaders, jwt);
final DeactivateTokenIntegration deactivateTokenIntegration =
DeactivateTokenIntegration.of(policyId, DUMMY_LABEL, subjectId, dittoHeaders);
DeactivateTokenIntegration.of(policyId, DUMMY_LABEL, subjectIds, dittoHeaders);
return TopLevelPolicyActionCommand.of(deactivateTokenIntegration, List.of());
}

Expand Down
Expand Up @@ -15,6 +15,8 @@
import static org.eclipse.ditto.model.base.exceptions.DittoJsonException.wrapJsonRuntimeException;
import static org.eclipse.ditto.services.gateway.endpoints.routes.policies.PoliciesRoute.extractJwt;

import java.util.Set;

import org.eclipse.ditto.json.JsonFactory;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.model.base.headers.DittoHeaders;
Expand Down Expand Up @@ -384,14 +386,14 @@ private Route policyEntryActions(final RequestContext ctx, final DittoHeaders di

private ActivateTokenIntegration activateTokenIntegration(final DittoHeaders dittoHeaders, final PolicyId policyId,
final String label, final JsonWebToken jwt) {
final SubjectId subjectId = tokenIntegrationSubjectIdFactory.getSubjectId(dittoHeaders, jwt);
return ActivateTokenIntegration.of(policyId, Label.of(label), subjectId, jwt.getExpirationTime(), dittoHeaders);
final Set<SubjectId> subjectIds = tokenIntegrationSubjectIdFactory.getSubjectIds(dittoHeaders, jwt);
return ActivateTokenIntegration.of(policyId, Label.of(label), subjectIds, jwt.getExpirationTime(), dittoHeaders);
}

private DeactivateTokenIntegration deactivateTokenIntegration(final DittoHeaders dittoHeaders,
final PolicyId policyId, final String label, final JsonWebToken jwt) {
final SubjectId subjectId = tokenIntegrationSubjectIdFactory.getSubjectId(dittoHeaders, jwt);
return DeactivateTokenIntegration.of(policyId, Label.of(label), subjectId, dittoHeaders);
final Set<SubjectId> subjectIds = tokenIntegrationSubjectIdFactory.getSubjectIds(dittoHeaders, jwt);
return DeactivateTokenIntegration.of(policyId, Label.of(label), subjectIds, dittoHeaders);
}

private static ResourceKey resourceKeyFromUnmatchedPath(final String resource) {
Expand Down
Expand Up @@ -12,6 +12,13 @@
*/
package org.eclipse.ditto.services.gateway.endpoints.routes.policies;

import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.eclipse.ditto.json.JsonArray;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.model.base.headers.DittoHeaders;
import org.eclipse.ditto.model.jwt.JsonWebToken;
import org.eclipse.ditto.model.policies.SubjectId;
Expand All @@ -22,12 +29,48 @@
public interface TokenIntegrationSubjectIdFactory {

/**
* Compute the token integration subject ID from headers and JWT.
* Compiled Pattern of a string containing any unresolved JsonArray-String notations inside.
*/
Pattern JSON_ARRAY_PATTERN = Pattern.compile(".*(\\[\".*\"])+?.*");

/**
* Compute the token integration subject IDs from headers and JWT.
*
* @param dittoHeaders the Ditto headers.
* @param jwt the JWT.
* @return the computed subject ID.
* @return the computed subject IDs.
*/
SubjectId getSubjectId(DittoHeaders dittoHeaders, JsonWebToken jwt);
Set<SubjectId> getSubjectIds(DittoHeaders dittoHeaders, JsonWebToken jwt);

/**
* Checks whether the passed {@code resolvedSubject} (resolved via JWT and header placeholder mechanism) contains
* JsonArrays ({@code ["..."]} and expands those JsonArrays to multiple resolved subjects returned as resulting
* stream of this operation.
* <p>
* Is able to handle an arbitrary amount of JsonArrays in the passed resolvedSubjects.
*
* @param resolvedSubject the resolved subjects potentially containing JsonArrays as JsonArray-String values.
* @return a stream of a single subject when the passed in {@code resolvedSubject} did not contain any
* JsonArray-String notation or else a stream of multiple subjects with the JsonArrays being resolved to multiple
* results of the stream.
*/
static Stream<String> expandJsonArraysInResolvedSubject(final String resolvedSubject) {
final Matcher jsonArrayMatcher = JSON_ARRAY_PATTERN.matcher(resolvedSubject);
final int group = 1;
if (jsonArrayMatcher.matches()) {
final String beforeMatched = resolvedSubject.substring(0, jsonArrayMatcher.start(group));
final String matchedStr =
resolvedSubject.substring(jsonArrayMatcher.start(group), jsonArrayMatcher.end(group));
final String afterMatched = resolvedSubject.substring(jsonArrayMatcher.end(group));
return JsonArray.of(matchedStr).stream()
.filter(JsonValue::isString)
.map(JsonValue::asString)
.flatMap(arrayStringElem -> expandJsonArraysInResolvedSubject(beforeMatched) // recurse!
.flatMap(before -> expandJsonArraysInResolvedSubject(afterMatched) // recurse!
.map(after -> before.concat(arrayStringElem).concat(after))
)
);
}
return Stream.of(resolvedSubject);
}
}
Expand Up @@ -16,6 +16,7 @@
import java.time.Instant;
import java.util.List;

import org.eclipse.ditto.json.JsonArray;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.model.jwt.Audience;
import org.eclipse.ditto.model.jwt.JsonWebToken;
Expand All @@ -42,6 +43,8 @@ public JsonObject getBody() {
return JsonObject.newBuilder()
.set("sub", "dummy-subject")
.set("iss", "dummy-issuer")
.set("aud", JsonArray.of("aud-1", "aud-2"))
.set("foo", JsonArray.of("bar1", "bar2", "bar3"))
.build();
}

Expand Down
Expand Up @@ -12,6 +12,8 @@
*/
package org.eclipse.ditto.services.gateway.endpoints.routes.policies;

import java.util.Set;

import org.assertj.core.api.Assertions;
import org.eclipse.ditto.model.base.headers.DittoHeaders;
import org.eclipse.ditto.model.policies.SubjectId;
Expand All @@ -33,7 +35,9 @@ public void resolveSubjectId() {
final DittoHeaders dittoHeaders = DittoHeaders.newBuilder()
.putHeader("owner", "Ditto")
.build();
final SubjectId subjectId = sut.getSubjectId(dittoHeaders, new DummyJwt());
final Set<SubjectId> subjectIds = sut.getSubjectIds(dittoHeaders, new DummyJwt());
Assertions.assertThat(subjectIds).hasSize(1);
final SubjectId subjectId = subjectIds.stream().findFirst().orElseThrow();
Assertions.assertThat(subjectId.getIssuer()).hasToString("dummy-issuer");
Assertions.assertThat(subjectId).hasToString("dummy-issuer:static-part:dummy-subject:Ditto");
}
Expand All @@ -45,10 +49,40 @@ public void resolveSubjectIdWithUnresolvedPlaceholder() {
final DittoHeaders dittoHeaders = DittoHeaders.newBuilder()
.putHeader("my-custom-header", "foo")
.build();
final SubjectId subjectId = sut.getSubjectId(dittoHeaders, new DummyJwt());
final Set<SubjectId> subjectIds = sut.getSubjectIds(dittoHeaders, new DummyJwt());
Assertions.assertThat(subjectIds).hasSize(1);
final SubjectId subjectId = subjectIds.stream().findFirst().orElseThrow();
Assertions.assertThat(subjectId).hasToString("dummy-issuer:{{policy-entry:label}}:dummy-subject:foo");
}

@Test
public void resolveSubjectIdWithJsonArrayJwtClaim() {
final String subjectPattern = "integration:{{jwt:aud}}:static";
final OAuthTokenIntegrationSubjectIdFactory sut = createSut(subjectPattern);
final DittoHeaders dittoHeaders = DittoHeaders.newBuilder().build();
final Set<SubjectId> subjectIds = sut.getSubjectIds(dittoHeaders, new DummyJwt());
Assertions.assertThat(subjectIds).hasSize(2).containsExactly(
SubjectId.newInstance("integration:aud-1:static"),
SubjectId.newInstance("integration:aud-2:static")
);
}

@Test
public void resolveSubjectIdWithMultipleJsonArrayJwtClaims() {
final String subjectPattern = "{{jwt:iss}}:{{jwt:aud}}:static:{{jwt:foo}}";
final OAuthTokenIntegrationSubjectIdFactory sut = createSut(subjectPattern);
final DittoHeaders dittoHeaders = DittoHeaders.newBuilder().build();
final Set<SubjectId> subjectIds = sut.getSubjectIds(dittoHeaders, new DummyJwt());
Assertions.assertThat(subjectIds).hasSize(6).containsExactly(
SubjectId.newInstance("dummy-issuer:aud-1:static:bar1"),
SubjectId.newInstance("dummy-issuer:aud-2:static:bar1"),
SubjectId.newInstance("dummy-issuer:aud-1:static:bar2"),
SubjectId.newInstance("dummy-issuer:aud-2:static:bar2"),
SubjectId.newInstance("dummy-issuer:aud-1:static:bar3"),
SubjectId.newInstance("dummy-issuer:aud-2:static:bar3")
);
}

private static OAuthTokenIntegrationSubjectIdFactory createSut(final String subjectPattern) {
final DefaultOAuthConfig oAuthConfig = DefaultOAuthConfig.of(
ConfigFactory.empty().withValue("oauth.token-integration-subject",
Expand Down

0 comments on commit 3b2163c

Please sign in to comment.