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(impl): [#528] Improvements Policy Store Input Validation #597

Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
11ba40f
feat(impl): [#528] allow only BPNL
dsmf May 7, 2024
55b09f8
feat(impl): [#528] harden tests
dsmf May 7, 2024
573767c
feat(impl): [#528] code formatting
dsmf May 7, 2024
84ea791
Merge branch 'refs/heads/main' into feat/528-Policy-Store-API-Input-V…
dsmf May 7, 2024
c7efaa6
feat(impl): [#528] add @Future to validUntil attribute
dsmf May 7, 2024
e7f0ccd
feat(impl): [#528] add @Builder to request entities
dsmf May 7, 2024
1240f77
feat(impl): [#528] add validUntil validation tests
dsmf May 7, 2024
d3e1dcf
feat(impl): [#528] add and use PolicyStoreTestUtil
dsmf May 7, 2024
4936139
feat(impl): [#528] add more validation tests and improve test structure
dsmf May 7, 2024
6fb0e3e
feat(impl): [#528] improve exception handling
dsmf May 7, 2024
225d591
feat(impl): [#528] fix PMD error
dsmf May 7, 2024
1263ee5
feat(impl): [#528] add validators
dsmf May 8, 2024
3af30fe
feat(impl): [#528] format code and use policy builder
dsmf May 8, 2024
0e2aa48
feat(impl): [#528] replace dummy policyIds with UUIDs
dsmf May 8, 2024
fc804c3
feat(impl): [#528] add cause
dsmf May 8, 2024
923199a
feat(impl): [#528] replace dummy policyIds with UUIDs
dsmf May 8, 2024
db2f1a3
feat(impl): [#528] init with builder, use clock
dsmf May 8, 2024
1da88e0
feat(impl): [#528] move more validation code to PolicyValidator and u…
dsmf May 8, 2024
daaea59
feat(impl): [#528] replace dummy policyIds with UUIDs
dsmf May 8, 2024
3306946
feat(impl): [#528] replace old BPN pattern for CreatePolicyRequest in…
dsmf May 8, 2024
84b4b41
feat(impl): [#528] improve exception handling for parsing policy JSON
dsmf May 8, 2024
4376e76
feat(impl): [#528] replace dummy policyIds with UUIDs
dsmf May 8, 2024
59cb21a
feat(impl): [#528] replace dummy policyIds with UUIDs and use builder
dsmf May 8, 2024
14a82e8
feat(impl): [#528] fix test
dsmf May 8, 2024
b9a120b
feat(impl): [#528] fix PMD warnings
dsmf May 9, 2024
fa34764
feat(impl): [#528] validate policyIDs for update and delete
dsmf May 9, 2024
2ad9f99
feat(impl): [#528] update CHANGELOG.md
dsmf May 9, 2024
6afcdd4
feat(impl): [#528] update CHANGELOG.md
dsmf May 10, 2024
f88f916
feat(impl): [#528] harden ListOfPolicyIdsValidator to check for dupli…
dsmf May 10, 2024
4db48e9
feat(impl): [#528] add step definition for cucumber tests for validat…
dsmf May 10, 2024
aba7f8e
feat(impl): [#528] add step definition for cucumber tests for validat…
dsmf May 10, 2024
a101fdd
feat(impl): [#528] add step definitions for cucumber tests for valida…
dsmf May 10, 2024
37fa6e5
Merge branch 'refs/heads/main' into feat/528-Policy-Store-API-Input-V…
dsmf May 10, 2024
0f84aca
feat(impl): [#528] remove obsolete uncommented test code
dsmf May 13, 2024
2711640
feat(impl): [#528] fix CHANGELOG.md
dsmf May 13, 2024
67ea850
feat(impl): [#528] extract constraints to constants
dsmf May 13, 2024
ea44d2f
feat(impl): [#528] extract constraints to constants
dsmf May 13, 2024
94b5977
feat(impl): [#528] extract constraints to constants
dsmf May 13, 2024
25ced4c
feat(impl): [#528] policyId allow "urn:uuid:" prefix
dsmf May 13, 2024
fe55a21
feat(impl): [#528] fix PMD warning
dsmf May 13, 2024
85be5fe
Merge pull request #931 from eclipse-tractusx/main
ds-jhartmann May 13, 2024
720ff96
Merge pull request #932 from eclipse-tractusx/main
ds-jhartmann May 13, 2024
cc5ca9b
Merge branch 'refs/heads/main' into feat/528-Policy-Store-API-Input-V…
dsmf May 13, 2024
d13dfb5
feat(impl): [#528] relax validation of policyId to safe path paramete…
dsmf May 13, 2024
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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ _**For better traceability add the corresponding GitHub issue number in each cha

### Fixed

- Fixed issue in EDR Token renewal #358
- Fixed issue in EDR token renewal. #358

### Added

- Cucumber test step definitions for Policy Store API (Happy Path) including some test helper utilities. #518

### Changed

- Improved policy store API input validation. #528
dsmf marked this conversation as resolved.
Show resolved Hide resolved

## [5.1.0] - 2024-05-06

Expand Down
10 changes: 5 additions & 5 deletions docs/src/api/irs-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -815,9 +815,9 @@ paths:
payload:
'@context':
odrl: http://www.w3.org/ns/odrl/2/
'@id': policy-id3
'@id': e917f5f-8dac-49ac-8d10-5b4d254d2b48
policy:
policyId: p3
policyId: e917f5f-8dac-49ac-8d10-5b4d254d2b48
createdOn: 2024-03-28T03:34:42.9454448Z
validUntil: 2025-12-12T23:59:59.999Z
permissions:
Expand Down Expand Up @@ -1796,7 +1796,7 @@ components:
This parameter is optional.
If not set the policy is registered for each existing BPN.
example: BPNL1234567890AB
pattern: "(BPN)[LSA][\\w\\d]{10}[\\w\\d]{2}"
pattern: "(BPNL)[\\w\\d]{10}[\\w\\d]{2}"
payload:
type: object
additionalProperties:
Expand All @@ -1805,7 +1805,7 @@ components:
example:
'@context':
odrl: http://www.w3.org/ns/odrl/2/
'@id': policy-id
'@id': e917f5f-8dac-49ac-8d10-5b4d254d2b48
'@type': PolicyDefinitionRequestDto
policy:
'@type': Policy
Expand Down Expand Up @@ -2198,7 +2198,7 @@ components:
example:
'@context':
odrl: http://www.w3.org/ns/odrl/2/
'@id': policy-id
'@id': e917f5f-8dac-49ac-8d10-5b4d254d2b48
'@type': PolicyDefinitionRequestDto
policy:
'@type': Policy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID;

import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -55,15 +56,18 @@ public class IrsExceptionHandler {
*/
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<ErrorResponse> handleResponseStatusException(final ResponseStatusException exception) {
log.info(exception.getClass().getName(), exception);
final UUID errorRef = UUID.randomUUID();
final String messageTemplate = "%s (errorRef: %s)";
log.info(messageTemplate.formatted(exception.getClass().getName(), errorRef), exception);

final HttpStatus httpStatus = HttpStatus.valueOf(exception.getStatusCode().value());

return ResponseEntity.status(httpStatus)
.body(ErrorResponse.builder()
.withStatusCode(httpStatus)
.withError(httpStatus.getReasonPhrase())
.withMessages(List.of(exception.getReason()))
.withMessages(List.of(messageTemplate.formatted(exception.getReason(),
errorRef)))
.build());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,27 @@ public void theCreatePolicyResponseShouldHaveStatus(final int httpStatus) {
this.createPoliciesResponse.statusCode(httpStatus);
}

@Then("the create policy response should have message containing {string}")
public void theCreatePolicyResponseShouldHaveMessageContaining(final String string) {
final ValidatableResponse validatableResponse = this.createPoliciesResponse;
assertThatResponseHasMessageContaining(validatableResponse, string);
}

@Then("the delete policy response should have message containing {string}")
public void thedeletePolicyResponseShouldHaveMessageContaining(final String string) {
assertThatResponseHasMessageContaining(this.deletePoliciesResponse, string);
}

@Then("the update policy response should have message containing {string}")
public void theUpdatePolicyResponseShouldHaveMessageContaining(final String string) {
assertThatResponseHasMessageContaining(this.updatePoliciesResponse, string);
}

private static void assertThatResponseHasMessageContaining(final ValidatableResponse validatableResponse,
final String string) {
validatableResponse.body("messages", Matchers.hasItem(Matchers.containsString(string)));
}

@Then("the update policy response should have HTTP status {int}")
public void theUpdatePolicyResponseShouldHaveStatus(final int httpStatus) {
this.updatePoliciesResponse.statusCode(httpStatus);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ protected JsonObjectToIrsPolicyTransformer(final ObjectMapper objectMapper) {
try {
final Object result = objectMapper.readerFor(Policy.class).readValue(v.asJsonObject().toString());
builder.permissions(((Policy) result).getPermissions());
} catch (JsonProcessingException e) {
throw new JsonParseException(e);
} catch (ClassCastException | JsonProcessingException e) {
throw new JsonParseException("Invalid policy", e);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ void shouldReturnStoredPolicies() {

assertThat(acceptedPolicies).hasSize(1);
}

@Test
void shouldRemoveStoredPolicies() {
testee.addAcceptedPolicies(List.of(policy()));
Expand All @@ -60,8 +61,9 @@ void shouldRemoveStoredPolicies() {

assertThat(testee.getAcceptedPolicies("testBpn")).isEmpty();
}

@NotNull
private static AcceptedPolicy policy() {
return new AcceptedPolicy(new Policy(), OffsetDateTime.now());
return new AcceptedPolicy(Policy.builder().build(), OffsetDateTime.now());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class JsonObjectToIrsPolicyTransformerTest {
"cx-policy": "https://w3id.org/catenax/policy/",
"odrl": "http://www.w3.org/ns/odrl/2/"
},
"@id": "policy-id",
"@id": "e917f5f-8dac-49ac-8d10-5b4d254d2b48",
"policy": {
"odrl:permission": [
{
Expand Down Expand Up @@ -104,7 +104,7 @@ void shouldTransformJsonObjectToPolicyCorrectly() {
final Policy transformed = jsonObjectToIrsPolicyTransformer.transform(jsonObject, transformerContext);

// then
assertThat(transformed.getPolicyId()).isEqualTo("policy-id");
assertThat(transformed.getPolicyId()).isEqualTo("e917f5f-8dac-49ac-8d10-5b4d254d2b48");
final Permission permission = transformed.getPermissions().get(0);
assertThat(permission.getAction()).isEqualTo(PolicyType.USE);
final List<Constraint> and = permission.getConstraint().getAnd();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
* Exception when parsing JSON
*/
public class JsonParseException extends RuntimeException {

public JsonParseException(final String message, final Throwable throwable) {
super(message, throwable);
}

public JsonParseException(final Throwable cause) {
super(cause);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.eclipse.tractusx.irs.common.auth.IrsRoles;
import org.eclipse.tractusx.irs.data.JsonParseException;
import org.eclipse.tractusx.irs.dtos.ErrorResponse;
import org.eclipse.tractusx.irs.edc.client.policy.Policy;
import org.eclipse.tractusx.irs.policystore.models.CreatePoliciesResponse;
Expand All @@ -55,6 +56,7 @@
import org.eclipse.tractusx.irs.policystore.services.PolicyStoreService;
import org.eclipse.tractusx.irs.policystore.validators.BusinessPartnerNumberListValidator;
import org.eclipse.tractusx.irs.policystore.validators.ValidListOfBusinessPartnerNumbers;
import org.eclipse.tractusx.irs.policystore.validators.ValidPolicyId;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
Expand Down Expand Up @@ -127,7 +129,13 @@ public class PolicyStoreController {
@PreAuthorize("hasAuthority('" + IrsRoles.ADMIN_IRS + "')")
public CreatePoliciesResponse registerAllowedPolicy(@Valid @RequestBody final CreatePolicyRequest request) {

final Policy registeredPolicy = service.registerPolicy(request);
final Policy registeredPolicy;

try {
registeredPolicy = service.registerPolicy(request);
} catch (JsonParseException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage(), e);
}

if (registeredPolicy == null) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Policy was not registered");
Expand Down Expand Up @@ -213,7 +221,7 @@ public Map<String, List<PolicyResponse>> getPolicies(//
@DeleteMapping("/policies/{policyId}")
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasAuthority('" + IrsRoles.ADMIN_IRS + "')")
public void deleteAllowedPolicy(@PathVariable("policyId") final String policyId) {
public void deleteAllowedPolicy(@ValidPolicyId @PathVariable("policyId") final String policyId) {
service.deletePolicy(policyId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,23 @@

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.json.JsonObject;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Builder;

/**
* Object for API to create policy
*/
@SuppressWarnings("FileTabCharacter")
@Schema(description = "Request to add a policy")
@Builder
public record CreatePolicyRequest(

@Schema(description = "Timestamp after which the policy will no longer be accepted in negotiations.",
example = "2025-12-12T23:59:59.999Z") //
@NotNull //
@Future(message = "must be in future") //
ds-jhartmann marked this conversation as resolved.
Show resolved Hide resolved
OffsetDateTime validUntil, //

@Schema(description = """
Expand All @@ -66,7 +70,7 @@ The business partner number (BPN) for which the policy should be registered.
"@context": {
"odrl": "http://www.w3.org/ns/odrl/2/"
},
"@id": "policy-id",
"@id": "e917f5f-8dac-49ac-8d10-5b4d254d2b48",
"@type": "PolicyDefinitionRequestDto",
"policy": {
"@type": "Policy",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ public record PolicyResponse(OffsetDateTime validUntil, Payload payload) {
"@context": {
"odrl": "http://www.w3.org/ns/odrl/2/"
},
"@id": "policy-id3",
"@id": "e917f5f-8dac-49ac-8d10-5b4d254d2b48",
"policy": {
"policyId": "p3",
"policyId": "e917f5f-8dac-49ac-8d10-5b4d254d2b48",
"createdOn": "2024-03-28T03:34:42.9454448Z",
"validUntil": "2025-12-12T23:59:59.999Z",
"permissions": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,23 @@
import java.util.List;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import org.eclipse.tractusx.irs.policystore.validators.ListOfPolicyIds;
import org.eclipse.tractusx.irs.policystore.validators.ValidListOfBusinessPartnerNumbers;

/**
* Request object for policy update
*/
@Schema(description = "Request to update a policy")
@Builder
public record UpdatePolicyRequest(

@Schema(description = "Timestamp after which the policy will no longer be accepted in negotiations.") //
@NotNull //
@Future(message = "must be in future") //
ds-jhartmann marked this conversation as resolved.
Show resolved Hide resolved
OffsetDateTime validUntil, //

@Schema(description = "Business Partner Number (BPN).") //
Expand All @@ -48,6 +53,7 @@ public record UpdatePolicyRequest(
@Schema(description = "The IDs of the policies to be updated.") //
@NotNull //
@NotEmpty //
@ListOfPolicyIds //
List<String> policyIds //
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import org.eclipse.tractusx.irs.policystore.models.CreatePolicyRequest;
import org.eclipse.tractusx.irs.policystore.models.UpdatePolicyRequest;
import org.eclipse.tractusx.irs.policystore.persistence.PolicyPersistence;
import org.eclipse.tractusx.irs.policystore.validators.PolicyValidator;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
Expand All @@ -78,9 +79,6 @@ public class PolicyStoreService implements AcceptedPoliciesProvider {

private final Clock clock;

private static final String MISSING_REQUEST_FIELD_MESSAGE =
"Request does not contain all required fields. " + "Missing: %s";

private static final String DEFAULT = "default";

/**
Expand All @@ -100,10 +98,12 @@ private static final class ConfiguredDefaultPolicy {

public PolicyStoreService(final DefaultAcceptedPoliciesConfig defaultAcceptedPoliciesConfig,
final PolicyPersistence persistence, final EdcTransformer edcTransformer, final Clock clock) {

this.clock = clock;

this.allowedPoliciesFromConfig = createDefaultPolicyFromConfig(defaultAcceptedPoliciesConfig);
this.persistence = persistence;
this.edcTransformer = edcTransformer;
this.clock = clock;
}

/**
Expand All @@ -126,8 +126,8 @@ public Policy registerPolicy(final CreatePolicyRequest request) {
return registeredPolicy;
}

public Policy doRegisterPolicy(final Policy policy, final String businessPartnersNumber) {
validatePolicy(policy);
/* package */ Policy doRegisterPolicy(final Policy policy, final String businessPartnersNumber) {
PolicyValidator.validate(policy);
policy.setCreatedOn(OffsetDateTime.now(clock));
log.info("Registering new policy with id {}, valid until {}", policy.getPolicyId(), policy.getValidUntil());
try {
Expand All @@ -137,24 +137,6 @@ public Policy doRegisterPolicy(final Policy policy, final String businessPartner
}
}

/**
* Checks whether policy from register policy request has all required fields
*
* @param policy policy to register
*/
private void validatePolicy(final Policy policy) {

if (policy.getPermissions() == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
String.format(MISSING_REQUEST_FIELD_MESSAGE, "odrl:permission"));
}

if (policy.getPermissions().stream().anyMatch(p -> p.getConstraint() == null)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
String.format(MISSING_REQUEST_FIELD_MESSAGE, "odrl:constraint"));
}
}

/**
* Finds policies by list of BPN.
*
Expand Down Expand Up @@ -321,11 +303,17 @@ private List<Policy> createDefaultPolicyFromConfig(
new Constraint(acceptedPolicy.getLeftOperand(),
new Operator(OperatorType.fromValue(acceptedPolicy.getOperator())),
acceptedPolicy.getRightOperand())));
final Policy policy = new Policy(ConfiguredDefaultPolicy.DEFAULT_POLICY_ID, OffsetDateTime.now(),
OffsetDateTime.now().plusYears(ConfiguredDefaultPolicy.DEFAULT_POLICY_LIFETIME_YEARS),
List.of(new Permission(PolicyType.USE, new Constraints(constraints, constraints))));

return List.of(policy);
final OffsetDateTime now = OffsetDateTime.now(clock);
return List.of(Policy.builder()
.policyId(ConfiguredDefaultPolicy.DEFAULT_POLICY_ID)
.createdOn(now)
.validUntil(now.plusYears(ConfiguredDefaultPolicy.DEFAULT_POLICY_LIFETIME_YEARS))
.permissions(List.of(Permission.builder()
.action(PolicyType.USE)
.constraint(new Constraints(constraints, constraints))
.build()))
.build());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class BusinessPartnerNumberListValidator
/**
* Regex for BPN.
*/
public static final String BPN_REGEX = "(BPN)[LSA][\\w\\d]{10}[\\w\\d]{2}";
public static final String BPN_REGEX = "(BPNL)[\\w\\d]{10}[\\w\\d]{2}";

private static final Pattern PATTERN = Pattern.compile(BPN_REGEX);

Expand Down
Loading
Loading