NONRTRIC - Fine grained authorization in ICS 68/9668/2
authorPatrikBuhr <patrik.buhr@est.tech>
Tue, 15 Nov 2022 07:51:35 +0000 (08:51 +0100)
committerPatrikBuhr <patrik.buhr@est.tech>
Wed, 16 Nov 2022 13:44:20 +0000 (14:44 +0100)
Call to an "authorization agent" to get access control.

Change-Id: I95615144046c1bf90d4e8204d10a326d048273a4
Signed-off-by: PatrikBuhr <patrik.buhr@est.tech>
Issue-ID: NONRTRIC-815

20 files changed:
api/ics-api.json
api/ics-api.yaml
config/application.yaml
src/main/java/org/oransc/ics/configuration/ApplicationConfig.java
src/main/java/org/oransc/ics/controllers/ErrorResponse.java
src/main/java/org/oransc/ics/controllers/a1e/A1eCallbacks.java
src/main/java/org/oransc/ics/controllers/a1e/A1eController.java
src/main/java/org/oransc/ics/controllers/authorization/AuthorizationCheck.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/authorization/AuthorizationConsts.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/authorization/AuthorizationResult.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/authorization/SubscriptionAuthRequest.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerCallbacks.java
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java
src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java
src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java
src/main/java/org/oransc/ics/repository/InfoJobs.java
src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java
src/test/java/org/oransc/ics/ApplicationTest.java
src/test/java/org/oransc/ics/controller/OpenPolicyAgentSimulatorController.java [new file with mode: 0644]
src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java

index d3b72cc..199c284 100644 (file)
             "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": [
                 }
             }
         },
+        "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",
                 }
             }
         },
+        "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",
                 "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"},
             }],
             "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",
                 "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
index ab9ca1f..2f0fec3 100644 (file)
@@ -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
index ce6b1ef..4e4ca3e 100644 (file)
@@ -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
index 6f48cd3..dfd2626 100644 (file)
@@ -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() {
index ebad66a..43c5748 100644 (file)
@@ -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<ResponseEntity<Object>> createMono(Throwable e) {
+        return Mono.just(create(e, HttpStatus.INTERNAL_SERVER_ERROR));
+    }
+
     public static Mono<ResponseEntity<Object>> createMono(Throwable e, HttpStatus code) {
         return Mono.just(create(e, code));
     }
index 82b4c37..288c31a 100644 (file)
@@ -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;
index 9665576..ac0cd3d 100644 (file)
@@ -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<Object> 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<ResponseEntity<Object>> getIndividualEiJob( //
+        @PathVariable("eiJobId") String eiJobId, //
+        @RequestHeader Map<String, String> headers) {
+
+        return this.infoJobs.getJobMono(eiJobId)
+            .flatMap(job -> authorization.authorizeDataJob(headers, job, AccessType.READ)) //
+            .map(job -> new ResponseEntity<Object>(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<Object> 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<ResponseEntity<Object>> deleteIndividualEiJob( //
+        @PathVariable("eiJobId") String eiJobId, //
+        @RequestHeader Map<String, String> 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<ResponseEntity<Object>> putIndividualEiJob( //
         @PathVariable("eiJobId") String eiJobId, //
-        @RequestBody A1eEiJobInfo eiJobObject) throws ServiceException {
+        @RequestBody A1eEiJobInfo eiJobObject, //
+        @RequestHeader Map<String, String> 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 (file)
index 0000000..de098c4
--- /dev/null
@@ -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<InfoJob> authorizeDataJob(Map<String, String> receivedHttpHeaders, InfoJob job, AccessType accessType) {
+        return authorizeDataJob(receivedHttpHeaders, job.getType(), job.getJobData(), accessType) //
+            .map(x -> job);
+    }
+
+    public Mono<InfoType> authorizeDataJob(Map<String, String> 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<String, String> 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<String> 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 (file)
index 0000000..0493383
--- /dev/null
@@ -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 (file)
index 0000000..ef986ea
--- /dev/null
@@ -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 (file)
index 0000000..1b07ac9
--- /dev/null
@@ -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;
+
+}
index 3db904d..bc1316d 100644 (file)
@@ -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;
 
index 7de9813..1039ce3 100644 (file)
@@ -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<Object> deleteJobsForOwner( //
-
+    public Mono<ResponseEntity<Object>> 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<String, String> 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<Object> 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<ResponseEntity<Object>> getIndividualInfoJob( //
+        @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId, //
+        @RequestHeader Map<String, String> 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<Object>(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<Object> 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<ResponseEntity<Object>> deleteIndividualInfoJob( //
+        @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId, //
+        @RequestHeader Map<String, String> 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<ResponseEntity<Object>> putIndividualInfoJob( //
         @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId, //
-        @RequestBody ConsumerJobInfo informationJobObject) throws ServiceException {
+        @RequestBody ConsumerJobInfo informationJobObject, //
+        @RequestHeader Map<String, String> 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<Object> getIndividualTypeSubscription( //
-        @PathVariable(ConsumerConsts.SUBSCRIPTION_ID_PATH) String subscriptionId) {
+        @PathVariable(ConsumerConsts.SUBSCRIPTION_ID_PATH) String subscriptionId, //
+        @RequestHeader Map<String, String> headers) {
         try {
             logger.debug("GET info type subscription, subscriptionId: {}", subscriptionId);
             InfoTypeSubscriptions.SubscriptionInfo subscription =
index 2cb598e..24c1044 100644 (file)
@@ -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;
 
index 15ea11a..826b091 100644 (file)
@@ -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
index db3b9af..8cb5448 100644 (file)
@@ -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<InfoJob> 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) {
index 1fff616..a7961f8 100644 (file)
@@ -57,7 +57,7 @@ public class InfoTypeSubscriptions {
     private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
     private final Map<String, SubscriptionInfo> allSubscriptions = new HashMap<>();
     private final MultiMap<String, SubscriptionInfo> subscriptionsByOwner = new MultiMap<>();
-    private final Gson gson = new GsonBuilder().create();
+    private final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
     private final Map<String, ConsumerCallbackHandler> callbackHandlers = new HashMap<>();
     private final DataStore dataStore;
 
index 4f12c12..0a2f4b1 100644 (file)
@@ -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<String> 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 (file)
index 0000000..89b23c2
--- /dev/null
@@ -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<SubscriptionAuthRequest> receivedRequests =
+            Collections.synchronizedList(new ArrayList<SubscriptionAuthRequest>());
+
+        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<Object> subscriptionAuth( //
+        @RequestHeader Map<String, String> 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<Object> subscriptionAuthReject( //
+        @RequestHeader Map<String, String> 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);
+    }
+
+}
index af219f6..0e6aac1 100644 (file)
@@ -132,7 +132,8 @@ public class ProducerSimulatorController {
                 content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
         })
     public ResponseEntity<Object> jobDeletedCallback( //
-        @RequestHeader Map<String, String> headers, @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) {
+        @RequestHeader Map<String, String> headers, //
+        @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) {
         try {
             logHeaders(headers);
             logger.info("Job deleted callback {}", infoJobId);