From: PatrikBuhr Date: Tue, 15 Nov 2022 07:51:35 +0000 (+0100) Subject: NONRTRIC - Fine grained authorization in ICS X-Git-Tag: 1.4.0~1 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=1ec2b095eb9ad74e5478e73c17f49f14735fc199;p=nonrtric%2Fplt%2Finformationcoordinatorservice.git NONRTRIC - Fine grained authorization in ICS Call to an "authorization agent" to get access control. Change-Id: I95615144046c1bf90d4e8204d10a326d048273a4 Signed-off-by: PatrikBuhr Issue-ID: NONRTRIC-815 --- diff --git a/api/ics-api.json b/api/ics-api.json index d3b72cc..199c284 100644 --- a/api/ics-api.json +++ b/api/ics-api.json @@ -32,6 +32,16 @@ "description": "Information for an EI type", "type": "object" }, + "MonoResponseEntityObject": {"type": "object"}, + "authorization_result": { + "description": "Result of authorization", + "type": "object", + "required": ["result"], + "properties": {"result": { + "description": "If true, the access is granted", + "type": "boolean" + }} + }, "service_status_info": { "type": "object", "required": [ @@ -204,6 +214,12 @@ } } }, + "subscription_authorization": { + "description": "Authorization request for subscription requests", + "type": "object", + "required": ["input"], + "properties": {"input": {"$ref": "#/components/schemas/input"}} + }, "producer_info_type_info": { "description": "Information for an Information Type", "type": "object", @@ -250,6 +266,38 @@ } } }, + "input": { + "description": "input", + "type": "object", + "required": [ + "access_type", + "auth_token", + "info_type_id", + "job_definition" + ], + "properties": { + "access_type": { + "description": "Access type", + "type": "string", + "enum": [ + "READ", + "WRITE" + ] + }, + "info_type_id": { + "description": "Information type identifier", + "type": "string" + }, + "job_definition": { + "description": "Information type specific job data", + "type": "object" + }, + "auth_token": { + "description": "Authorization token", + "type": "string" + } + } + }, "consumer_job": { "description": "Information for an Information Job", "type": "object", @@ -651,7 +699,7 @@ "operationId": "deleteJobsForOwner", "responses": {"204": { "description": "No Content", - "content": {"application/json": {"schema": {"type": "object"}}} + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MonoResponseEntityObject"}}} }}, "parameters": [{ "schema": {"type": "string"}, @@ -1238,6 +1286,20 @@ }], "tags": ["Data producer (registration)"] }}, + "/example-subscription-auth": {"post": { + "summary": "Request for access authorization.", + "requestBody": { + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/subscription_authorization"}}}, + "required": true + }, + "description": "The authorization function decides if access is granted.", + "operationId": "subscriptionAuth", + "responses": {"200": { + "description": "OK", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/authorization_result"}}} + }}, + "tags": ["Authorization API"] + }}, "/actuator/heapdump": {"get": { "summary": "Actuator web endpoint 'heapdump'", "operationId": "heapdump", @@ -1293,6 +1355,10 @@ "description": "Spring Boot Actuator Web API Documentation", "url": "https://docs.spring.io/spring-boot/docs/current/actuator-api/html/" } + }, + { + "name": "Authorization API", + "description": "API used for authorization of information job access (this is provided by an authorization producer such as OPA)" } ] } \ No newline at end of file diff --git a/api/ics-api.yaml b/api/ics-api.yaml index ab9ca1f..2f0fec3 100644 --- a/api/ics-api.yaml +++ b/api/ics-api.yaml @@ -49,6 +49,9 @@ tags: externalDocs: description: Spring Boot Actuator Web API Documentation url: https://docs.spring.io/spring-boot/docs/current/actuator-api/html/ +- name: Authorization API + description: API used for authorization of information job access (this is provided + by an authorization producer such as OPA) paths: /data-producer/v1/info-types: get: @@ -478,7 +481,7 @@ paths: content: application/json: schema: - type: object + $ref: '#/components/schemas/MonoResponseEntityObject' /actuator/loggers/{name}: get: tags: @@ -1226,6 +1229,26 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + /example-subscription-auth: + post: + tags: + - Authorization API + summary: Request for access authorization. + description: The authorization function decides if access is granted. + operationId: subscriptionAuth + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/subscription_authorization' + required: true + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/authorization_result' /actuator/heapdump: get: tags: @@ -1267,6 +1290,17 @@ components: EiTypeObject: type: object description: Information for an EI type + MonoResponseEntityObject: + type: object + authorization_result: + required: + - result + type: object + properties: + result: + type: boolean + description: If true, the access is granted + description: Result of authorization service_status_info: required: - no_of_jobs @@ -1406,6 +1440,14 @@ components: type: object description: EI type specific job data description: Information for an Enrichment Information Job + subscription_authorization: + required: + - input + type: object + properties: + input: + $ref: '#/components/schemas/input' + description: Authorization request for subscription requests producer_info_type_info: required: - info_job_data_schema @@ -1443,6 +1485,30 @@ components: description: Type identity for the job description: The body of the Information Producer callbacks for Information Job creation and deletion + input: + required: + - access_type + - auth_token + - info_type_id + - job_definition + type: object + properties: + access_type: + type: string + description: Access type + enum: + - READ + - WRITE + info_type_id: + type: string + description: Information type identifier + job_definition: + type: object + description: Information type specific job data + auth_token: + type: string + description: Authorization token + description: input consumer_job: required: - info_type_id diff --git a/config/application.yaml b/config/application.yaml index ce6b1ef..4e4ca3e 100644 --- a/config/application.yaml +++ b/config/application.yaml @@ -67,6 +67,8 @@ app: vardata-directory: /var/information-coordinator-service # If the file name is empty, no authorization token is used auth-token-file: + # A URL to a REST based service that provides authorization. This can for instance be Open Policy Agent, OPA + info-job-authorization-agent: # S3 object store usage is enabled by defining the bucket to use. This will override the vardata-directory parameter. s3: endpointOverride: http://localhost:9000 diff --git a/src/main/java/org/oransc/ics/configuration/ApplicationConfig.java b/src/main/java/org/oransc/ics/configuration/ApplicationConfig.java index 6f48cd3..dfd2626 100644 --- a/src/main/java/org/oransc/ics/configuration/ApplicationConfig.java +++ b/src/main/java/org/oransc/ics/configuration/ApplicationConfig.java @@ -21,6 +21,7 @@ package org.oransc.ics.configuration; import lombok.Getter; +import lombok.Setter; import org.oransc.ics.configuration.WebClientConfig.HttpProxyConfig; import org.slf4j.Logger; @@ -82,6 +83,11 @@ public class ApplicationConfig { @Value("${app.s3.bucket:}") private String s3Bucket; + @Getter + @Setter + @Value("${app.info-job-authorization-agent:}") + private String authAgentUrl; + private WebClientConfig webClientConfig = null; public WebClientConfig getWebClientConfig() { diff --git a/src/main/java/org/oransc/ics/controllers/ErrorResponse.java b/src/main/java/org/oransc/ics/controllers/ErrorResponse.java index ebad66a..43c5748 100644 --- a/src/main/java/org/oransc/ics/controllers/ErrorResponse.java +++ b/src/main/java/org/oransc/ics/controllers/ErrorResponse.java @@ -38,7 +38,7 @@ import org.springframework.http.ResponseEntity; import reactor.core.publisher.Mono; public class ErrorResponse { - private static Gson gson = new GsonBuilder().create(); + private static Gson gson = new GsonBuilder().disableHtmlEscaping().create(); private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); // Returned as body for all failed REST calls @@ -88,6 +88,10 @@ public class ErrorResponse { this.message = message; } + public static Mono> createMono(Throwable e) { + return Mono.just(create(e, HttpStatus.INTERNAL_SERVER_ERROR)); + } + public static Mono> createMono(Throwable e, HttpStatus code) { return Mono.just(create(e, code)); } diff --git a/src/main/java/org/oransc/ics/controllers/a1e/A1eCallbacks.java b/src/main/java/org/oransc/ics/controllers/a1e/A1eCallbacks.java index 82b4c37..288c31a 100644 --- a/src/main/java/org/oransc/ics/controllers/a1e/A1eCallbacks.java +++ b/src/main/java/org/oransc/ics/controllers/a1e/A1eCallbacks.java @@ -50,7 +50,7 @@ import reactor.core.publisher.Mono; public class A1eCallbacks { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static Gson gson = new GsonBuilder().create(); + private static Gson gson = new GsonBuilder().disableHtmlEscaping().create(); private final AsyncRestClient restClient; private final InfoJobs eiJobs; diff --git a/src/main/java/org/oransc/ics/controllers/a1e/A1eController.java b/src/main/java/org/oransc/ics/controllers/a1e/A1eController.java index 9665576..ac0cd3d 100644 --- a/src/main/java/org/oransc/ics/controllers/a1e/A1eController.java +++ b/src/main/java/org/oransc/ics/controllers/a1e/A1eController.java @@ -38,11 +38,14 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.json.JSONObject; import org.oransc.ics.configuration.ApplicationConfig; import org.oransc.ics.controllers.ErrorResponse; import org.oransc.ics.controllers.VoidResponse; +import org.oransc.ics.controllers.authorization.AuthorizationCheck; +import org.oransc.ics.controllers.authorization.SubscriptionAuthRequest.Input.AccessType; import org.oransc.ics.controllers.r1producer.ProducerCallbacks; import org.oransc.ics.exceptions.ServiceException; import org.oransc.ics.repository.InfoJob; @@ -61,6 +64,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -89,7 +93,10 @@ public class A1eController { @Autowired ProducerCallbacks producerCallbacks; - private static Gson gson = new GsonBuilder().create(); + @Autowired + private AuthorizationCheck authorization; + + private static Gson gson = new GsonBuilder().disableHtmlEscaping().create(); @GetMapping(path = "/eitypes", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "EI type identifiers", description = "") @@ -172,9 +179,7 @@ public class A1eController { this.infoJobs.getJobs().forEach(job -> result.add(job.getId())); } return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK); - } catch ( - - Exception e) { + } catch (Exception e) { return ErrorResponse.create(e, HttpStatus.NOT_FOUND); } } @@ -192,14 +197,14 @@ public class A1eController { description = "Enrichment Information job is not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public ResponseEntity getIndividualEiJob( // - @PathVariable("eiJobId") String eiJobId) { - try { - InfoJob job = this.infoJobs.getJob(eiJobId); - return new ResponseEntity<>(gson.toJson(toEiJobInfo(job)), HttpStatus.OK); - } catch (Exception e) { - return ErrorResponse.create(e, HttpStatus.NOT_FOUND); - } + public Mono> getIndividualEiJob( // + @PathVariable("eiJobId") String eiJobId, // + @RequestHeader Map headers) { + + return this.infoJobs.getJobMono(eiJobId) + .flatMap(job -> authorization.authorizeDataJob(headers, job, AccessType.READ)) // + .map(job -> new ResponseEntity(gson.toJson(toEiJobInfo(job)), HttpStatus.OK)) + .onErrorResume(ErrorResponse::createMono); } @GetMapping(path = "/eijobs/{eiJobId}/status", produces = MediaType.APPLICATION_JSON_VALUE) @@ -248,15 +253,14 @@ public class A1eController { description = "Enrichment Information job is not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public ResponseEntity deleteIndividualEiJob( // - @PathVariable("eiJobId") String eiJobId) { - try { - InfoJob job = this.infoJobs.getJob(eiJobId); - this.infoJobs.remove(job, this.infoProducers); - return new ResponseEntity<>(HttpStatus.NO_CONTENT); - } catch (Exception e) { - return ErrorResponse.create(e, HttpStatus.NOT_FOUND); - } + public Mono> deleteIndividualEiJob( // + @PathVariable("eiJobId") String eiJobId, // + @RequestHeader Map headers) { + + return this.infoJobs.getJobMono(eiJobId) + .flatMap(job -> authorization.authorizeDataJob(headers, job, AccessType.WRITE)) // + .doOnNext(job -> this.infoJobs.remove(job, this.infoProducers)) + .map(x -> new ResponseEntity<>(HttpStatus.NO_CONTENT)).onErrorResume(ErrorResponse::createMono); } @PutMapping( @@ -285,18 +289,17 @@ public class A1eController { @ApiResponse( responseCode = "409", description = "Cannot modify job type", // - content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) - - }) - + content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))}) public Mono> putIndividualEiJob( // @PathVariable("eiJobId") String eiJobId, // - @RequestBody A1eEiJobInfo eiJobObject) throws ServiceException { + @RequestBody A1eEiJobInfo eiJobObject, // + @RequestHeader Map headers) throws ServiceException { final boolean isNewJob = this.infoJobs.get(eiJobId) == null; InfoType eiType = this.infoTypes.getCompatibleType(eiJobObject.eiTypeId); - return validatePutEiJob(eiJobId, eiType, eiJobObject) // + return authorization.authorizeDataJob(headers, eiType, eiJobObject.jobDefinition, AccessType.WRITE) // + .flatMap(x -> validatePutEiJob(eiJobId, eiType, eiJobObject)) // .flatMap(job -> startEiJob(job, eiType)) // .doOnNext(newEiJob -> this.infoJobs.put(newEiJob)) // .map(newEiJob -> new ResponseEntity<>(isNewJob ? HttpStatus.CREATED : HttpStatus.OK)) // diff --git a/src/main/java/org/oransc/ics/controllers/authorization/AuthorizationCheck.java b/src/main/java/org/oransc/ics/controllers/authorization/AuthorizationCheck.java new file mode 100644 index 0000000..de098c4 --- /dev/null +++ b/src/main/java/org/oransc/ics/controllers/authorization/AuthorizationCheck.java @@ -0,0 +1,112 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2022 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.ics.controllers.authorization; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.lang.invoke.MethodHandles; +import java.util.Map; + +import org.oransc.ics.clients.AsyncRestClient; +import org.oransc.ics.clients.AsyncRestClientFactory; +import org.oransc.ics.clients.SecurityContext; +import org.oransc.ics.configuration.ApplicationConfig; +import org.oransc.ics.controllers.authorization.SubscriptionAuthRequest.Input.AccessType; +import org.oransc.ics.exceptions.ServiceException; +import org.oransc.ics.repository.InfoJob; +import org.oransc.ics.repository.InfoType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class AuthorizationCheck { + + private final ApplicationConfig applicationConfig; + private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private final AsyncRestClient restClient; + private static Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + + @Autowired + public AuthorizationCheck(ApplicationConfig applicationConfig, SecurityContext securityContext) { + + this.applicationConfig = applicationConfig; + AsyncRestClientFactory restClientFactory = + new AsyncRestClientFactory(applicationConfig.getWebClientConfig(), securityContext); + this.restClient = restClientFactory.createRestClientUseHttpProxy(""); + } + + public Mono authorizeDataJob(Map receivedHttpHeaders, InfoJob job, AccessType accessType) { + return authorizeDataJob(receivedHttpHeaders, job.getType(), job.getJobData(), accessType) // + .map(x -> job); + } + + public Mono authorizeDataJob(Map receivedHttpHeaders, InfoType type, Object jobDefinition, + AccessType accessType) { + if (this.applicationConfig.getAuthAgentUrl().isEmpty()) { + return Mono.just(type); + } + + String tkn = getAuthToken(receivedHttpHeaders); + SubscriptionAuthRequest.Input input = SubscriptionAuthRequest.Input.builder() // + .accessType(accessType) // + .authToken(tkn) // + .infoTypeId(type.getId()) // + .jobDefinition(jobDefinition) // + .build(); + + SubscriptionAuthRequest req = SubscriptionAuthRequest.builder().input(input).build(); + + String url = this.applicationConfig.getAuthAgentUrl(); + return this.restClient.post(url, gson.toJson(req)) // + .doOnError(t -> logger.warn("Error returned from auth server: {}", t.getMessage())) // + .onErrorResume(t -> Mono.just("")) // + .flatMap(this::checkAuthResult) // + .map(rsp -> type); + + } + + private String getAuthToken(Map httpHeaders) { + String tkn = httpHeaders.get("authorization"); + if (tkn == null) { + logger.debug("No authorization token received in {}", httpHeaders); + return ""; + } + tkn = tkn.substring("Bearer ".length()); + return tkn; + } + + private Mono checkAuthResult(String response) { + logger.debug("Auth result: {}", response); + try { + AuthorizationResult res = gson.fromJson(response, AuthorizationResult.class); + return res != null && res.isResult() ? Mono.just(response) + : Mono.error(new ServiceException("Not authorized", HttpStatus.UNAUTHORIZED)); + } catch (Exception e) { + return Mono.error(e); + } + } + +} diff --git a/src/main/java/org/oransc/ics/controllers/authorization/AuthorizationConsts.java b/src/main/java/org/oransc/ics/controllers/authorization/AuthorizationConsts.java new file mode 100644 index 0000000..0493383 --- /dev/null +++ b/src/main/java/org/oransc/ics/controllers/authorization/AuthorizationConsts.java @@ -0,0 +1,35 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2022 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.ics.controllers.authorization; + +public class AuthorizationConsts { + + public static final String AUTH_API_NAME = "Authorization API"; + public static final String AUTH_API_DESCRIPTION = + "API used for authorization of information job access (this is provided by an authorization producer such as OPA)"; + + public static final String GRANT_ACCESS_SUMMARY = "Request for access authorization."; + public static final String GRANT_ACCESS_DESCRIPTION = "The authorization function decides if access is granted."; + + private AuthorizationConsts() { + } + +} diff --git a/src/main/java/org/oransc/ics/controllers/authorization/AuthorizationResult.java b/src/main/java/org/oransc/ics/controllers/authorization/AuthorizationResult.java new file mode 100644 index 0000000..ef986ea --- /dev/null +++ b/src/main/java/org/oransc/ics/controllers/authorization/AuthorizationResult.java @@ -0,0 +1,40 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2022 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.ics.controllers.authorization; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Schema(name = "authorization_result", description = "Result of authorization") +@Builder +public class AuthorizationResult { + + @Schema(name = "result", description = "If true, the access is granted", required = true) + @JsonProperty(value = "result", required = true) + @SerializedName("result") + @Getter + private boolean result; + +} diff --git a/src/main/java/org/oransc/ics/controllers/authorization/SubscriptionAuthRequest.java b/src/main/java/org/oransc/ics/controllers/authorization/SubscriptionAuthRequest.java new file mode 100644 index 0000000..1b07ac9 --- /dev/null +++ b/src/main/java/org/oransc/ics/controllers/authorization/SubscriptionAuthRequest.java @@ -0,0 +1,81 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2022 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.ics.controllers.authorization; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Schema(name = "subscription_authorization", description = "Authorization request for subscription requests") +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@Getter +public class SubscriptionAuthRequest { + + @Schema(name = "input", description = "input") + @Builder + @AllArgsConstructor + @NoArgsConstructor + @Getter + @ToString + public static class Input { + + @Schema(name = "acces_type", description = "Access type") + public enum AccessType { + READ, WRITE + } + + @Schema(name = "access_type", description = "Access type", required = true) + @JsonProperty(value = "access_type", required = true) + @SerializedName("access_type") + private AccessType accessType; + + @Schema(name = "info_type_id", description = "Information type identifier", required = true) + @SerializedName("info_type_id") + @JsonProperty(value = "info_type_id", required = true) + private String infoTypeId; + + @Schema(name = "job_definition", description = "Information type specific job data", required = true) + @SerializedName("job_definition") + @JsonProperty(value = "job_definition", required = true) + private Object jobDefinition; + + @Schema(name = "auth_token", description = "Authorization token", required = true) + @SerializedName("auth_token") + @JsonProperty(value = "auth_token", required = true) + private String authToken; + + } + + @Schema(name = "input", description = "Input", required = true) + @JsonProperty(value = "input", required = true) + @SerializedName("input") + private Input input; + +} diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerCallbacks.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerCallbacks.java index 3db904d..bc1316d 100644 --- a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerCallbacks.java +++ b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerCallbacks.java @@ -40,7 +40,7 @@ import reactor.core.publisher.Mono; @Component public class ConsumerCallbacks implements InfoTypeSubscriptions.ConsumerCallbackHandler { - private static Gson gson = new GsonBuilder().create(); + private static Gson gson = new GsonBuilder().disableHtmlEscaping().create(); private final AsyncRestClient restClient; diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java index 7de9813..1039ce3 100644 --- a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java +++ b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java @@ -39,10 +39,13 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import org.json.JSONObject; import org.oransc.ics.controllers.ErrorResponse; import org.oransc.ics.controllers.VoidResponse; +import org.oransc.ics.controllers.authorization.AuthorizationCheck; +import org.oransc.ics.controllers.authorization.SubscriptionAuthRequest.Input.AccessType; import org.oransc.ics.controllers.r1producer.ProducerCallbacks; import org.oransc.ics.exceptions.ServiceException; import org.oransc.ics.repository.InfoJob; @@ -63,9 +66,12 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; + +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string .. @@ -80,16 +86,19 @@ public class ConsumerController { private final InfoProducers infoProducers; private final ProducerCallbacks producerCallbacks; private final InfoTypeSubscriptions infoTypeSubscriptions; - private static Gson gson = new GsonBuilder().create(); + private static Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + private final AuthorizationCheck authorization; - public ConsumerController(@Autowired InfoJobs jobs, @Autowired InfoTypes infoTypes, - @Autowired InfoProducers infoProducers, @Autowired ProducerCallbacks producerCallbacks, - @Autowired InfoTypeSubscriptions infoTypeSubscriptions) { + @Autowired + public ConsumerController(InfoJobs jobs, InfoTypes infoTypes, InfoProducers infoProducers, + ProducerCallbacks producerCallbacks, InfoTypeSubscriptions infoTypeSubscriptions, + AuthorizationCheck authorization) { this.infoProducers = infoProducers; this.infoJobs = jobs; this.infoTypeSubscriptions = infoTypeSubscriptions; this.infoTypes = infoTypes; this.producerCallbacks = producerCallbacks; + this.authorization = authorization; } @GetMapping(path = "/info-types", produces = MediaType.APPLICATION_JSON_VALUE) @@ -187,19 +196,22 @@ public class ConsumerController { value = { // @ApiResponse(responseCode = "204") // }) - public ResponseEntity deleteJobsForOwner( // - + public Mono> deleteJobsForOwner( // @Parameter( name = ConsumerConsts.OWNER_PARAM, required = true, // description = ConsumerConsts.OWNER_PARAM_DESCRIPTION) // - @RequestParam(name = ConsumerConsts.OWNER_PARAM, required = true) String owner) { - - for (InfoJob job : this.infoJobs.getJobsForOwner(owner)) { - logger.debug("DELETE info jobs, id: {}, type: {}, owner: {}", job.getId(), job.getType().getId(), owner); - this.infoJobs.remove(job, this.infoProducers); - } - return new ResponseEntity<>(HttpStatus.NO_CONTENT); + @RequestParam(name = ConsumerConsts.OWNER_PARAM, required = true) String owner, // + @RequestHeader Map headers) { + + return Flux.fromIterable(this.infoJobs.getJobsForOwner(owner)) + .doOnNext(job -> logger.debug("DELETE info jobs, id: {}, type: {}, owner: {}", job.getId(), + job.getType().getId(), owner)) + .flatMap(job -> this.authorization.authorizeDataJob(headers, job, AccessType.WRITE)) // + .doOnNext(job -> this.infoJobs.remove(job, this.infoProducers)) // + .collectList() // + .map(l -> new ResponseEntity<>(HttpStatus.NO_CONTENT)) // + .onErrorResume(ErrorResponse::createMono); } @GetMapping(path = "/info-jobs/{infoJobId}", produces = MediaType.APPLICATION_JSON_VALUE) // @@ -215,15 +227,15 @@ public class ConsumerController { description = "Information subscription job is not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public ResponseEntity getIndividualInfoJob( // - @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) { - try { - logger.debug("GET info job, id: {}", infoJobId); - InfoJob job = this.infoJobs.getJob(infoJobId); - return new ResponseEntity<>(gson.toJson(toInfoJobInfo(job)), HttpStatus.OK); - } catch (Exception e) { - return ErrorResponse.create(e, HttpStatus.NOT_FOUND); - } + public Mono> getIndividualInfoJob( // + @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId, // + @RequestHeader Map headers) { + + logger.debug("GET info job, id: {}", infoJobId); + return this.infoJobs.getJobMono(infoJobId) // + .flatMap(job -> authorization.authorizeDataJob(headers, job, AccessType.READ)) // + .map(job -> new ResponseEntity(gson.toJson(toInfoJobInfo(job)), HttpStatus.OK)) // + .onErrorResume(ErrorResponse::createMono); } @GetMapping(path = "/info-jobs/{infoJobId}/status", produces = MediaType.APPLICATION_JSON_VALUE) @@ -278,16 +290,16 @@ public class ConsumerController { description = "Information subscription job is not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public ResponseEntity deleteIndividualInfoJob( // - @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId) { - try { - logger.debug("DELETE info job, id: {}", jobId); - InfoJob job = this.infoJobs.getJob(jobId); - this.infoJobs.remove(job, this.infoProducers); - return new ResponseEntity<>(HttpStatus.NO_CONTENT); - } catch (Exception e) { - return ErrorResponse.create(e, HttpStatus.NOT_FOUND); - } + public Mono> deleteIndividualInfoJob( // + @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId, // + @RequestHeader Map headers) { + + logger.debug("DELETE info job, id: {}", jobId); + return this.infoJobs.getJobMono(jobId) // + .flatMap(job -> authorization.authorizeDataJob(headers, job, AccessType.WRITE)) // + .doOnNext(job -> this.infoJobs.remove(job, this.infoProducers)) // + .map(job -> new ResponseEntity<>(HttpStatus.NO_CONTENT)) // + .onErrorResume(ErrorResponse::createMono); } @PutMapping( @@ -319,18 +331,20 @@ public class ConsumerController { content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))}) public Mono> putIndividualInfoJob( // @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId, // - @RequestBody ConsumerJobInfo informationJobObject) throws ServiceException { + @RequestBody ConsumerJobInfo informationJobObject, // + @RequestHeader Map headers) throws ServiceException { final boolean isNewJob = this.infoJobs.get(jobId) == null; logger.debug("PUT info job, id: {}, obj: {}", jobId, informationJobObject); InfoType infoType = this.infoTypes.getCompatibleType(informationJobObject.infoTypeId); - return validatePutInfoJob(jobId, infoType, informationJobObject) // + return authorization.authorizeDataJob(headers, infoType, informationJobObject.jobDefinition, AccessType.WRITE) // + .flatMap(x -> validatePutInfoJob(jobId, infoType, informationJobObject)) // .flatMap(job -> startInfoSubscriptionJob(job, infoType)) // .doOnNext(this.infoJobs::put) // .map(newJob -> new ResponseEntity<>(isNewJob ? HttpStatus.CREATED : HttpStatus.OK)) // - .onErrorResume(throwable -> Mono.just(ErrorResponse.create(throwable, HttpStatus.NOT_FOUND))); + .onErrorResume(ErrorResponse::createMono); } @GetMapping(path = "/info-type-subscription", produces = MediaType.APPLICATION_JSON_VALUE) @@ -380,7 +394,8 @@ public class ConsumerController { content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) public ResponseEntity getIndividualTypeSubscription( // - @PathVariable(ConsumerConsts.SUBSCRIPTION_ID_PATH) String subscriptionId) { + @PathVariable(ConsumerConsts.SUBSCRIPTION_ID_PATH) String subscriptionId, // + @RequestHeader Map headers) { try { logger.debug("GET info type subscription, subscriptionId: {}", subscriptionId); InfoTypeSubscriptions.SubscriptionInfo subscription = diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java index 2cb598e..24c1044 100644 --- a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java +++ b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java @@ -50,7 +50,7 @@ import reactor.util.retry.Retry; public class ProducerCallbacks { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static Gson gson = new GsonBuilder().create(); + private static Gson gson = new GsonBuilder().disableHtmlEscaping().create(); private final AsyncRestClient restClient; diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java index 15ea11a..826b091 100644 --- a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java +++ b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java @@ -71,7 +71,7 @@ import org.springframework.web.bind.annotation.RestController; @Tag(name = ProducerConsts.PRODUCER_API_NAME, description = ProducerConsts.PRODUCER_API_DESCRIPTION) public class ProducerController { - private static Gson gson = new GsonBuilder().create(); + private static Gson gson = new GsonBuilder().disableHtmlEscaping().create(); private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @Autowired diff --git a/src/main/java/org/oransc/ics/repository/InfoJobs.java b/src/main/java/org/oransc/ics/repository/InfoJobs.java index db3b9af..8cb5448 100644 --- a/src/main/java/org/oransc/ics/repository/InfoJobs.java +++ b/src/main/java/org/oransc/ics/repository/InfoJobs.java @@ -39,7 +39,9 @@ import org.oransc.ics.exceptions.ServiceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; + import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Dynamic representation of all existing Information Jobs. @@ -111,6 +113,14 @@ public class InfoJobs { return new Vector<>(allEiJobs.values()); } + public synchronized Mono getJobMono(String id) { + InfoJob job = allEiJobs.get(id); + if (job == null) { + return Mono.error(new ServiceException("Could not find Information job: " + id, HttpStatus.NOT_FOUND)); + } + return Mono.just(job); + } + public synchronized InfoJob getJob(String id) throws ServiceException { InfoJob ric = allEiJobs.get(id); if (ric == null) { diff --git a/src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java b/src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java index 1fff616..a7961f8 100644 --- a/src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java +++ b/src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java @@ -57,7 +57,7 @@ public class InfoTypeSubscriptions { private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final Map allSubscriptions = new HashMap<>(); private final MultiMap subscriptionsByOwner = new MultiMap<>(); - private final Gson gson = new GsonBuilder().create(); + private final Gson gson = new GsonBuilder().disableHtmlEscaping().create(); private final Map callbackHandlers = new HashMap<>(); private final DataStore dataStore; diff --git a/src/test/java/org/oransc/ics/ApplicationTest.java b/src/test/java/org/oransc/ics/ApplicationTest.java index 4f12c12..0a2f4b1 100644 --- a/src/test/java/org/oransc/ics/ApplicationTest.java +++ b/src/test/java/org/oransc/ics/ApplicationTest.java @@ -52,11 +52,14 @@ import org.oransc.ics.configuration.WebClientConfig; import org.oransc.ics.configuration.WebClientConfig.HttpProxyConfig; import org.oransc.ics.controller.A1eCallbacksSimulatorController; import org.oransc.ics.controller.ConsumerSimulatorController; +import org.oransc.ics.controller.OpenPolicyAgentSimulatorController; import org.oransc.ics.controller.ProducerSimulatorController; import org.oransc.ics.controllers.a1e.A1eConsts; import org.oransc.ics.controllers.a1e.A1eEiJobInfo; import org.oransc.ics.controllers.a1e.A1eEiJobStatus; import org.oransc.ics.controllers.a1e.A1eEiTypeInfo; +import org.oransc.ics.controllers.authorization.SubscriptionAuthRequest; +import org.oransc.ics.controllers.authorization.SubscriptionAuthRequest.Input.AccessType; import org.oransc.ics.controllers.r1consumer.ConsumerConsts; import org.oransc.ics.controllers.r1consumer.ConsumerInfoTypeInfo; import org.oransc.ics.controllers.r1consumer.ConsumerJobInfo; @@ -152,7 +155,10 @@ class ApplicationTest { @Autowired SecurityContext securityContext; - private static Gson gson = new GsonBuilder().create(); + @Autowired + OpenPolicyAgentSimulatorController openPolicyAgentSimulatorController; + + private static Gson gson = new GsonBuilder().disableHtmlEscaping().create(); /** * Overrides the BeanFactory. @@ -178,6 +184,8 @@ class ApplicationTest { this.consumerSimulator.getTestResults().reset(); this.a1eCallbacksSimulator.getTestResults().reset(); this.securityContext.setAuthTokenFilePath(null); + this.applicationConfig.setAuthAgentUrl(""); + this.openPolicyAgentSimulatorController.getTestResults().reset(); } @AfterEach @@ -229,7 +237,7 @@ class ApplicationTest { putInfoProducerWithOneType("producer1", TYPE_ID); putInfoJob(TYPE_ID, "jobId1"); putInfoJob(TYPE_ID, "jobId2"); - putInfoJob(TYPE_ID, "jobId3", "otherOwner"); + putEiJob(TYPE_ID, "jobId3", "otherOwner"); assertThat(this.infoJobs.size()).isEqualTo(3); String url = ConsumerConsts.API_ROOT + "/info-jobs?owner=owner"; restClient().delete(url).block(); @@ -422,7 +430,7 @@ class ApplicationTest { } @Test - void consumerDeleteEiJob() throws Exception { + void consumerDeleteInfoJob() throws Exception { putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID); putInfoJob(TYPE_ID, "jobId"); assertThat(this.infoJobs.size()).isEqualTo(1); @@ -549,7 +557,7 @@ class ApplicationTest { putInfoProducerWithOneType(REG_TYPE_ID5, REG_TYPE_ID5); String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId"; - String body = gson.toJson(consumerJobInfo(PUT_TYPE_ID, "jobId")); + String body = gson.toJson(consumerJobInfo(PUT_TYPE_ID, "jobId", "owner")); ResponseEntity resp = restClient().putForEntity(url, body).block(); assertThat(this.infoJobs.size()).isEqualTo(1); assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED); @@ -637,7 +645,7 @@ class ApplicationTest { putInfoJob("typeId1", "jobId"); String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId"; - String body = gson.toJson(consumerJobInfo("typeId2", "jobId")); + String body = gson.toJson(consumerJobInfo("typeId2", "jobId", "owner")); testErrorCode(restClient().put(url, body), HttpStatus.CONFLICT, "Not allowed to change type for existing job"); } @@ -1192,6 +1200,37 @@ class ApplicationTest { "Could not find Information subscription: junk"); } + @Test + void testAuthorization() throws Exception { + this.applicationConfig.setAuthAgentUrl(baseUrl() + OpenPolicyAgentSimulatorController.SUBSCRIPTION_AUTH_URL); + final String AUTH_TOKEN = "testToken"; + Path authFile = Files.createTempFile("icsTestAuthToken", ".txt"); + Files.write(authFile, AUTH_TOKEN.getBytes()); + this.securityContext.setAuthTokenFilePath(authFile); + putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID); + putInfoJob(TYPE_ID, "jobId"); + + var testResults = openPolicyAgentSimulatorController.getTestResults(); + + await().untilAsserted(() -> assertThat(testResults.receivedRequests).hasSize(1)); + + // Test OPA check + SubscriptionAuthRequest authRequest = testResults.receivedRequests.get(0); + assertThat(authRequest.getInput().getAccessType()).isEqualTo(AccessType.WRITE); + assertThat(authRequest.getInput().getInfoTypeId()).isEqualTo(TYPE_ID); + assertThat(authRequest.getInput().getAuthToken()).isEqualTo(AUTH_TOKEN); + + // Test rejection from OPA + this.applicationConfig + .setAuthAgentUrl(baseUrl() + OpenPolicyAgentSimulatorController.SUBSCRIPTION_REJECT_AUTH_URL); + + String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId"; + testErrorCode(restClient().delete(url), HttpStatus.UNAUTHORIZED, "Not authorized"); + assertThat(testResults.receivedRequests).hasSize(2); + authRequest = testResults.receivedRequests.get(1); + assertThat(authRequest.getInput().getAccessType()).isEqualTo(AccessType.WRITE); + } + @Test void testAuthHeader() throws Exception { final String AUTH_TOKEN = "testToken"; @@ -1208,7 +1247,7 @@ class ApplicationTest { Files.delete(authFile); - // Test that it works. The cached header is used + // Test that it works when the file is deleted. The cached header is used putInfoJob(TYPE_ID, "jobId2"); await().untilAsserted(() -> assertThat(this.infoJobs.size()).isEqualByComparingTo(2)); headers = this.producerSimulator.getTestResults().receivedHeaders.get(1); @@ -1258,12 +1297,12 @@ class ApplicationTest { } private ConsumerJobInfo consumerJobInfo() throws JsonMappingException, JsonProcessingException { - return consumerJobInfo(TYPE_ID, EI_JOB_ID); + return consumerJobInfo(TYPE_ID, EI_JOB_ID, "owner"); } - ConsumerJobInfo consumerJobInfo(String typeId, String infoJobId) + ConsumerJobInfo consumerJobInfo(String typeId, String infoJobId, String owner) throws JsonMappingException, JsonProcessingException { - return new ConsumerJobInfo(typeId, jsonObject(), "owner", "https://junk.com", + return new ConsumerJobInfo(typeId, jsonObject(), owner, "https://junk.com", baseUrl() + A1eCallbacksSimulatorController.getJobStatusUrl(infoJobId)); } @@ -1314,19 +1353,21 @@ class ApplicationTest { return jsonObject("{ " + EI_JOB_PROPERTY + " : \"value\" }"); } - private InfoJob putInfoJob(String infoTypeId, String jobId, String owner) throws Exception { + private InfoJob putEiJob(String infoTypeId, String jobId, String owner) throws Exception { String url = A1eConsts.API_ROOT + "/eijobs/" + jobId; String body = gson.toJson(eiJobInfo(infoTypeId, jobId, owner)); restClient().putForEntity(url, body).block(); - return this.infoJobs.getJob(jobId); } private InfoJob putInfoJob(String infoTypeId, String jobId) throws Exception { + return putInfoJob(infoTypeId, jobId, "owner"); + } - String url = A1eConsts.API_ROOT + "/eijobs/" + jobId; - String body = gson.toJson(eiJobInfo(infoTypeId, jobId)); + private InfoJob putInfoJob(String infoTypeId, String jobId, String owner) throws Exception { + String url = ConsumerConsts.API_ROOT + "/info-jobs/" + jobId; + String body = gson.toJson(consumerJobInfo(infoTypeId, jobId, owner)); restClient().putForEntity(url, body).block(); return this.infoJobs.getJob(jobId); diff --git a/src/test/java/org/oransc/ics/controller/OpenPolicyAgentSimulatorController.java b/src/test/java/org/oransc/ics/controller/OpenPolicyAgentSimulatorController.java new file mode 100644 index 0000000..89b23c2 --- /dev/null +++ b/src/test/java/org/oransc/ics/controller/OpenPolicyAgentSimulatorController.java @@ -0,0 +1,121 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2022 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.ics.controller; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import lombok.Getter; + +import org.oransc.ics.controllers.VoidResponse; +import org.oransc.ics.controllers.authorization.AuthorizationConsts; +import org.oransc.ics.controllers.authorization.AuthorizationResult; +import org.oransc.ics.controllers.authorization.SubscriptionAuthRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +@RestController("OpenPolicyAgentSimulatorController") +@Tag(name = AuthorizationConsts.AUTH_API_NAME, description = AuthorizationConsts.AUTH_API_DESCRIPTION) +public class OpenPolicyAgentSimulatorController { + private static Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + + private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public static final String SUBSCRIPTION_AUTH_URL = "/example-subscription-auth"; + public static final String SUBSCRIPTION_REJECT_AUTH_URL = "/example-subscription-auth-reject"; + + public static class TestResults { + + public List receivedRequests = + Collections.synchronizedList(new ArrayList()); + + public TestResults() { + } + + public void reset() { + receivedRequests.clear(); + + } + } + + @Getter + private TestResults testResults = new TestResults(); + + @PostMapping(path = SUBSCRIPTION_AUTH_URL, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = AuthorizationConsts.GRANT_ACCESS_SUMMARY, + description = AuthorizationConsts.GRANT_ACCESS_DESCRIPTION) + @ApiResponses( + value = { // + @ApiResponse( + responseCode = "200", + description = "OK", // + content = @Content(schema = @Schema(implementation = AuthorizationResult.class))) // + }) + public ResponseEntity subscriptionAuth( // + @RequestHeader Map headers, // + @RequestBody SubscriptionAuthRequest request) { + logger.info("Auth {}", request); + testResults.receivedRequests.add(request); + + String res = gson.toJson(AuthorizationResult.builder().result(true).build()); + return new ResponseEntity<>(res, HttpStatus.OK); + } + + @PostMapping(path = SUBSCRIPTION_REJECT_AUTH_URL, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Rejecting", description = "", hidden = true) + @ApiResponses( + value = { // + @ApiResponse( + responseCode = "200", + description = "OK", // + content = @Content(schema = @Schema(implementation = VoidResponse.class))) // + }) + public ResponseEntity subscriptionAuthReject( // + @RequestHeader Map headers, // + @RequestBody SubscriptionAuthRequest request) { + logger.info("Auth Reject {}", request); + testResults.receivedRequests.add(request); + String res = gson.toJson(AuthorizationResult.builder().result(false).build()); + return new ResponseEntity<>(res, HttpStatus.OK); + } + +} diff --git a/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java b/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java index af219f6..0e6aac1 100644 --- a/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java +++ b/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java @@ -132,7 +132,8 @@ public class ProducerSimulatorController { content = @Content(schema = @Schema(implementation = VoidResponse.class))) // }) public ResponseEntity jobDeletedCallback( // - @RequestHeader Map headers, @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) { + @RequestHeader Map headers, // + @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) { try { logHeaders(headers); logger.info("Job deleted callback {}", infoJobId);