diff options
15 files changed, 1098 insertions, 55 deletions
diff --git a/a1-policy-management/api/pms-api.json b/a1-policy-management/api/pms-api.json index 7574032c..9efa7b75 100644 --- a/a1-policy-management/api/pms-api.json +++ b/a1-policy-management/api/pms-api.json @@ -28,6 +28,15 @@ "type": "string" }} }, + "authorization_result": { + "description": "Result of authorization", + "type": "object", + "required": ["result"], + "properties": {"result": { + "description": "If true, the access is granted", + "type": "boolean" + }} + }, "ric_info_v2": { "description": "Information for a Near-RT RIC", "type": "object", @@ -148,6 +157,40 @@ "type": "object" }} }, + "input": { + "description": "input", + "type": "object", + "required": [ + "access_type", + "auth_token", + "policy_type_id" + ], + "properties": { + "access_type": { + "description": "Access type", + "type": "string", + "enum": [ + "READ", + "WRITE", + "DELETE" + ] + }, + "auth_token": { + "description": "Authorization token", + "type": "string" + }, + "policy_type_id": { + "description": "Policy type identifier", + "type": "string" + } + } + }, + "policy_authorization": { + "description": "Authorization request for A1 policy requests", + "type": "object", + "required": ["input"], + "properties": {"input": {"$ref": "#/components/schemas/input"}} + }, "policytype_id_list_v2": { "description": "Information about policy types", "type": "object", @@ -298,6 +341,20 @@ ], "tags": ["A1 Policy Management"] }}, + "/example-authz-check": {"post": { + "summary": "Request for access authorization.", + "requestBody": { + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/policy_authorization"}}}, + "required": true + }, + "description": "The authorization function decides if access is granted.", + "operationId": "performAccessControl", + "responses": {"200": { + "description": "OK", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/authorization_result"}}} + }}, + "tags": ["Authorization API"] + }}, "/actuator/threaddump": {"get": { "summary": "Actuator web endpoint 'threaddump'", "operationId": "threaddump", @@ -946,12 +1003,18 @@ "title": "A1 Policy Management Service", "version": "1.1.0" }, - "tags": [{ - "name": "Actuator", - "description": "Monitor and interact", - "externalDocs": { - "description": "Spring Boot Actuator Web API Documentation", - "url": "https://docs.spring.io/spring-boot/docs/current/actuator-api/html/" + "tags": [ + { + "name": "Authorization API", + "description": "API used for authorization of information A1 policy access (this is provided by an authorization producer such as OPA).\nNote that this API is called by PMS, it is not provided.\n" + }, + { + "name": "Actuator", + "description": "Monitor and interact", + "externalDocs": { + "description": "Spring Boot Actuator Web API Documentation", + "url": "https://docs.spring.io/spring-boot/docs/current/actuator-api/html/" + } } - }] + ] }
\ No newline at end of file diff --git a/a1-policy-management/api/pms-api.yaml b/a1-policy-management/api/pms-api.yaml index 0cd28d07..a905c40e 100644 --- a/a1-policy-management/api/pms-api.yaml +++ b/a1-policy-management/api/pms-api.yaml @@ -31,6 +31,10 @@ info: servers: - url: / tags: +- description: "API used for authorization of information A1 policy access (this is\ + \ provided by an authorization producer such as OPA).\nNote that this API is called\ + \ by PMS, it is not provided.\n" + name: Authorization API - description: Monitor and interact externalDocs: description: Spring Boot Actuator Web API Documentation @@ -93,6 +97,26 @@ paths: summary: Query for A1 policy instances tags: - A1 Policy Management + /example-authz-check: + post: + description: The authorization function decides if access is granted. + operationId: performAccessControl + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/policy_authorization' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/authorization_result' + description: OK + summary: Request for access authorization. + tags: + - Authorization API /actuator/threaddump: get: operationId: threaddump @@ -957,6 +981,17 @@ components: description: status text type: string type: object + authorization_result: + description: Result of authorization + example: + result: true + properties: + result: + description: "If true, the access is granted" + type: boolean + required: + - result + type: object ric_info_v2: description: Information for a Near-RT RIC example: @@ -1113,6 +1148,35 @@ components: http://json-schema.org/draft-07/schema type: object type: object + input: + description: input + properties: + access_type: + description: Access type + enum: + - READ + - WRITE + - DELETE + type: string + auth_token: + description: Authorization token + type: string + policy_type_id: + description: Policy type identifier + type: string + required: + - access_type + - auth_token + - policy_type_id + type: object + policy_authorization: + description: Authorization request for A1 policy requests + properties: + input: + $ref: '#/components/schemas/input' + required: + - input + type: object policytype_id_list_v2: description: Information about policy types example: diff --git a/a1-policy-management/api/pms-api/index.html b/a1-policy-management/api/pms-api/index.html index c8f97a81..8b29a313 100644 --- a/a1-policy-management/api/pms-api/index.html +++ b/a1-policy-management/api/pms-api/index.html @@ -846,6 +846,17 @@ ul.nav-tabs { <script> // Script section to load models into a JS Var var defs = {} + defs["authorization_result"] = { + "required" : [ "result" ], + "type" : "object", + "properties" : { + "result" : { + "type" : "boolean", + "description" : "If true, the access is granted" + } + }, + "description" : "Result of authorization" +}; defs["error_information"] = { "type" : "object", "properties" : { @@ -863,6 +874,26 @@ ul.nav-tabs { }, "description" : "Problem as defined in https://tools.ietf.org/html/rfc7807" }; + defs["input"] = { + "required" : [ "access_type", "auth_token", "policy_type_id" ], + "type" : "object", + "properties" : { + "access_type" : { + "type" : "string", + "description" : "Access type", + "enum" : [ "READ", "WRITE", "DELETE" ] + }, + "auth_token" : { + "type" : "string", + "description" : "Authorization token" + }, + "policy_type_id" : { + "type" : "string", + "description" : "Policy type identifier" + } + }, + "description" : "input" +}; defs["Link"] = { "type" : "object", "properties" : { @@ -874,6 +905,16 @@ ul.nav-tabs { } } }; + defs["policy_authorization"] = { + "required" : [ "input" ], + "type" : "object", + "properties" : { + "input" : { + "$ref" : "#/components/schemas/input" + } + }, + "description" : "Authorization request for A1 policy requests" +}; defs["policy_id_list_v2"] = { "type" : "object", "properties" : { @@ -1185,6 +1226,10 @@ ul.nav-tabs { <li data-group="Actuator" data-name="threaddump" class=""> <a href="#api-Actuator-threaddump">threaddump</a> </li> + <li class="nav-header" data-group="AuthorizationAPI"><a href="#api-AuthorizationAPI">API Methods - AuthorizationAPI</a></li> + <li data-group="AuthorizationAPI" data-name="performAccessControl" class=""> + <a href="#api-AuthorizationAPI-performAccessControl">performAccessControl</a> + </li> <li class="nav-header" data-group="Callbacks"><a href="#api-Callbacks">API Methods - Callbacks</a></li> <li data-group="Callbacks" data-name="serviceCallback" class=""> <a href="#api-Callbacks-serviceCallback">serviceCallback</a> @@ -9221,6 +9266,368 @@ pub fn main() { </div> <hr> </section> + <section id="api-AuthorizationAPI"> + <h1>AuthorizationAPI</h1> + <div id="api-AuthorizationAPI-performAccessControl"> + <article id="api-AuthorizationAPI-performAccessControl-0" data-group="User" data-name="performAccessControl" data-version="0"> + <div class="pull-left"> + <h1>performAccessControl</h1> + <p>Request for access authorization.</p> + </div> + <div class="pull-right"></div> + <div class="clearfix"></div> + <p></p> + <p class="marked">The authorization function decides if access is granted.</p> + <p></p> + <br /> + <pre class="prettyprint language-html prettyprinted" data-type="post"><code><span class="pln">/example-authz-check</span></code></pre> + <p> + <h3>Usage and SDK Samples</h3> + </p> + <ul class="nav nav-tabs nav-tabs-examples"> + <li class="active"><a href="#examples-AuthorizationAPI-performAccessControl-0-curl">Curl</a></li> + <li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-java">Java</a></li> + <li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-android">Android</a></li> + <!--<li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-groovy">Groovy</a></li>--> + <li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-objc">Obj-C</a></li> + <li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-javascript">JavaScript</a></li> + <!--<li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-angular">Angular</a></li>--> + <li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-csharp">C#</a></li> + <li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-php">PHP</a></li> + <li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-perl">Perl</a></li> + <li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-python">Python</a></li> + <li class=""><a href="#examples-AuthorizationAPI-performAccessControl-0-rust">Rust</a></li> + </ul> + + <div class="tab-content"> + <div class="tab-pane active" id="examples-AuthorizationAPI-performAccessControl-0-curl"> + <pre class="prettyprint"><code class="language-bsh">curl -X POST \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + "http://localhost/example-authz-check" \ + -d '' +</code></pre> + </div> + <div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-java"> + <pre class="prettyprint"><code class="language-java">import org.openapitools.client.*; +import org.openapitools.client.auth.*; +import org.openapitools.client.model.*; +import org.openapitools.client.api.AuthorizationAPIApi; + +import java.io.File; +import java.util.*; + +public class AuthorizationAPIApiExample { + public static void main(String[] args) { + + // Create an instance of the API class + AuthorizationAPIApi apiInstance = new AuthorizationAPIApi(); + PolicyAuthorization policyAuthorization = ; // PolicyAuthorization | + + try { + authorization_result result = apiInstance.performAccessControl(policyAuthorization); + System.out.println(result); + } catch (ApiException e) { + System.err.println("Exception when calling AuthorizationAPIApi#performAccessControl"); + e.printStackTrace(); + } + } +} +</code></pre> + </div> + + <div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-android"> + <pre class="prettyprint"><code class="language-java">import org.openapitools.client.api.AuthorizationAPIApi; + +public class AuthorizationAPIApiExample { + public static void main(String[] args) { + AuthorizationAPIApi apiInstance = new AuthorizationAPIApi(); + PolicyAuthorization policyAuthorization = ; // PolicyAuthorization | + + try { + authorization_result result = apiInstance.performAccessControl(policyAuthorization); + System.out.println(result); + } catch (ApiException e) { + System.err.println("Exception when calling AuthorizationAPIApi#performAccessControl"); + e.printStackTrace(); + } + } +}</code></pre> + </div> + <!-- + <div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-groovy"> + <pre class="prettyprint language-json prettyprinted" data-type="json"><code>Coming Soon!</code></pre> + </div> --> + <div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-objc"> + <pre class="prettyprint"><code class="language-cpp"> + +// Create an instance of the API class +AuthorizationAPIApi *apiInstance = [[AuthorizationAPIApi alloc] init]; +PolicyAuthorization *policyAuthorization = ; // + +// Request for access authorization. +[apiInstance performAccessControlWith:policyAuthorization + completionHandler: ^(authorization_result output, NSError* error) { + if (output) { + NSLog(@"%@", output); + } + if (error) { + NSLog(@"Error: %@", error); + } +}]; +</code></pre> + </div> + + <div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-javascript"> + <pre class="prettyprint"><code class="language-js">var A1PolicyManagementService = require('a1_policy_management_service'); + +// Create an instance of the API class +var api = new A1PolicyManagementService.AuthorizationAPIApi() +var policyAuthorization = ; // {PolicyAuthorization} + +var callback = function(error, data, response) { + if (error) { + console.error(error); + } else { + console.log('API called successfully. Returned data: ' + data); + } +}; +api.performAccessControl(policyAuthorization, callback); +</code></pre> + </div> + + <!--<div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-angular"> + <pre class="prettyprint language-json prettyprinted" data-type="json"><code>Coming Soon!</code></pre> + </div>--> + <div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-csharp"> + <pre class="prettyprint"><code class="language-cs">using System; +using System.Diagnostics; +using Org.OpenAPITools.Api; +using Org.OpenAPITools.Client; +using Org.OpenAPITools.Model; + +namespace Example +{ + public class performAccessControlExample + { + public void main() + { + + // Create an instance of the API class + var apiInstance = new AuthorizationAPIApi(); + var policyAuthorization = new PolicyAuthorization(); // PolicyAuthorization | + + try { + // Request for access authorization. + authorization_result result = apiInstance.performAccessControl(policyAuthorization); + Debug.WriteLine(result); + } catch (Exception e) { + Debug.Print("Exception when calling AuthorizationAPIApi.performAccessControl: " + e.Message ); + } + } + } +} +</code></pre> + </div> + + <div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-php"> + <pre class="prettyprint"><code class="language-php"><?php +require_once(__DIR__ . '/vendor/autoload.php'); + +// Create an instance of the API class +$api_instance = new OpenAPITools\Client\Api\AuthorizationAPIApi(); +$policyAuthorization = ; // PolicyAuthorization | + +try { + $result = $api_instance->performAccessControl($policyAuthorization); + print_r($result); +} catch (Exception $e) { + echo 'Exception when calling AuthorizationAPIApi->performAccessControl: ', $e->getMessage(), PHP_EOL; +} +?></code></pre> + </div> + + <div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-perl"> + <pre class="prettyprint"><code class="language-perl">use Data::Dumper; +use WWW::OPenAPIClient::Configuration; +use WWW::OPenAPIClient::AuthorizationAPIApi; + +# Create an instance of the API class +my $api_instance = WWW::OPenAPIClient::AuthorizationAPIApi->new(); +my $policyAuthorization = WWW::OPenAPIClient::Object::PolicyAuthorization->new(); # PolicyAuthorization | + +eval { + my $result = $api_instance->performAccessControl(policyAuthorization => $policyAuthorization); + print Dumper($result); +}; +if ($@) { + warn "Exception when calling AuthorizationAPIApi->performAccessControl: $@\n"; +}</code></pre> + </div> + + <div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-python"> + <pre class="prettyprint"><code class="language-python">from __future__ import print_statement +import time +import openapi_client +from openapi_client.rest import ApiException +from pprint import pprint + +# Create an instance of the API class +api_instance = openapi_client.AuthorizationAPIApi() +policyAuthorization = # PolicyAuthorization | + +try: + # Request for access authorization. + api_response = api_instance.perform_access_control(policyAuthorization) + pprint(api_response) +except ApiException as e: + print("Exception when calling AuthorizationAPIApi->performAccessControl: %s\n" % e)</code></pre> + </div> + + <div class="tab-pane" id="examples-AuthorizationAPI-performAccessControl-0-rust"> + <pre class="prettyprint"><code class="language-rust">extern crate AuthorizationAPIApi; + +pub fn main() { + let policyAuthorization = ; // PolicyAuthorization + + let mut context = AuthorizationAPIApi::Context::default(); + let result = client.performAccessControl(policyAuthorization, &context).wait(); + + println!("{:?}", result); +} +</code></pre> + </div> + </div> + + <h2>Scopes</h2> + <table> + + </table> + + <h2>Parameters</h2> + + + + <div class="methodsubtabletitle">Body parameters</div> + <table id="methodsubtable"> + <tr> + <th width="150px">Name</th> + <th>Description</th> + </tr> + <tr><td style="width:150px;">policyAuthorization <span style="color:red;">*</span></td> +<td> +<p class="marked"></p> +<script> +$(document).ready(function() { + var schemaWrapper = { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/policy_authorization" + } + } + }, + "required" : true +}; + + var schema = findNode('schema',schemaWrapper).schema; + if (!schema) { + schema = schemaWrapper.schema; + } + if (schema.$ref != null) { + schema = defsParser.$refs.get(schema.$ref); + } else { + schemaWrapper.definitions = Object.assign({}, defs); + $RefParser.dereference(schemaWrapper).catch(function(err) { + console.log(err); + }); + } + + var view = new JSONSchemaView(schema,2,{isBodyParam: true}); + var result = $('#d2e199_performAccessControl_policyAuthorization'); + result.empty(); + result.append(view.render()); +}); +</script> +<div id="d2e199_performAccessControl_policyAuthorization"></div> +</td> +</tr> + + </table> + + + + <h2>Responses</h2> + <h3 id="examples-AuthorizationAPI-performAccessControl-title-200"></h3> + <p id="examples-AuthorizationAPI-performAccessControl-description-200" class="marked"></p> + <script> + var responseAuthorizationAPI200_description = `OK`; + var responseAuthorizationAPI200_description_break = responseAuthorizationAPI200_description.indexOf('\n'); + if (responseAuthorizationAPI200_description_break == -1) { + $("#examples-AuthorizationAPI-performAccessControl-title-200").text("Status: 200 - " + responseAuthorizationAPI200_description); + } else { + $("#examples-AuthorizationAPI-performAccessControl-title-200").text("Status: 200 - " + responseAuthorizationAPI200_description.substring(0, responseAuthorizationAPI200_description_break)); + $("#examples-AuthorizationAPI-performAccessControl-description-200").html(responseAuthorizationAPI200_description.substring(responseAuthorizationAPI200_description_break)); + } + </script> + + + <ul id="responses-detail-AuthorizationAPI-performAccessControl-200" class="nav nav-tabs nav-tabs-examples" > + <li class="active"> + <a data-toggle="tab" href="#responses-AuthorizationAPI-performAccessControl-200-schema">Schema</a> + </li> + + + + + </ul> + + + <div class="tab-content" id="responses-AuthorizationAPI-performAccessControl-200-wrapper" style='margin-bottom: 10px;'> + <div class="tab-pane active" id="responses-AuthorizationAPI-performAccessControl-200-schema"> + <div id="responses-AuthorizationAPI-performAccessControl-schema-200" class="exampleStyle"> + <script> + $(document).ready(function() { + var schemaWrapper = { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/authorization_result" + } + } + } +}; + var schema = findNode('schema',schemaWrapper).schema; + if (!schema) { + schema = schemaWrapper.schema; + } + if (schema.$ref != null) { + schema = defsParser.$refs.get(schema.$ref); + } else if (schema.items != null && schema.items.$ref != null) { + schema.items = defsParser.$refs.get(schema.items.$ref); + } else { + schemaWrapper.definitions = Object.assign({}, defs); + $RefParser.dereference(schemaWrapper).catch(function(err) { + console.log(err); + }); + } + + var view = new JSONSchemaView(schema, 3); + $('#responses-AuthorizationAPI-performAccessControl-200-schema-data').val(JSON.stringify(schema)); + var result = $('#responses-AuthorizationAPI-performAccessControl-schema-200'); + result.empty(); + result.append(view.render()); + }); + </script> + </div> + <input id='responses-AuthorizationAPI-performAccessControl-200-schema-data' type='hidden' value=''></input> + </div> + </div> + </article> + </div> + <hr> + </section> <section id="api-Callbacks"> <h1>Callbacks</h1> <div id="api-Callbacks-serviceCallback"> diff --git a/a1-policy-management/config/application.yaml b/a1-policy-management/config/application.yaml index 44e0b07c..4f80d2e3 100644 --- a/a1-policy-management/config/application.yaml +++ b/a1-policy-management/config/application.yaml @@ -46,7 +46,7 @@ logging: org.springframework: ERROR org.springframework.data: ERROR org.springframework.web.reactive.function.client.ExchangeFunctions: ERROR - org.springframework.web.servlet.DispatcherServlet: INFO + org.springframework.web.servlet.DispatcherServlet: ERROR org.onap.ccsdk.oran.a1policymanagementservice: INFO pattern: console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] %logger{20} - %msg%n" @@ -91,6 +91,9 @@ app: # A file containing an authorization token, which shall be inserted in each HTTP header (authorization). # If the file name is empty, no authorization token is sent. auth-token-file: + # A URL to authorization provider such as OPA. Each time an A1 Policy is accessed, a call to this + # authorization provider is done for access control. If this is empty, no fine grained access control is done. + authorization-provider: # 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/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/configuration/ApplicationConfig.java b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/configuration/ApplicationConfig.java index eea96927..3de674bd 100644 --- a/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/configuration/ApplicationConfig.java +++ b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/configuration/ApplicationConfig.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.Map; import lombok.Getter; +import lombok.Setter; import org.onap.ccsdk.oran.a1policymanagementservice.configuration.WebClientConfig.HttpProxyConfig; import org.onap.ccsdk.oran.a1policymanagementservice.exceptions.ServiceException; @@ -98,6 +99,11 @@ public class ApplicationConfig { @Value("${app.s3.bucket:}") private String s3Bucket; + @Getter + @Setter + @Value("${app.authorization-provider:}") + private String authProviderUrl; + private Map<String, RicConfig> ricConfigs = new HashMap<>(); private WebClientConfig webClientConfig = null; diff --git a/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/AuthorizationCheck.java b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/AuthorizationCheck.java new file mode 100644 index 00000000..0f376c06 --- /dev/null +++ b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/AuthorizationCheck.java @@ -0,0 +1,109 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * Copyright (C) 2023 Nordix Foundation. All rights reserved. + * ====================================================================== + * 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.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.lang.invoke.MethodHandles; +import java.util.Map; + +import org.onap.ccsdk.oran.a1policymanagementservice.clients.AsyncRestClient; +import org.onap.ccsdk.oran.a1policymanagementservice.clients.AsyncRestClientFactory; +import org.onap.ccsdk.oran.a1policymanagementservice.clients.SecurityContext; +import org.onap.ccsdk.oran.a1policymanagementservice.configuration.ApplicationConfig; +import org.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization.PolicyAuthorizationRequest.Input.AccessType; +import org.onap.ccsdk.oran.a1policymanagementservice.exceptions.ServiceException; +import org.onap.ccsdk.oran.a1policymanagementservice.repository.Policy; +import org.onap.ccsdk.oran.a1policymanagementservice.repository.PolicyType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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(); + + public AuthorizationCheck(ApplicationConfig applicationConfig, SecurityContext securityContext) { + + this.applicationConfig = applicationConfig; + AsyncRestClientFactory restClientFactory = + new AsyncRestClientFactory(applicationConfig.getWebClientConfig(), securityContext); + this.restClient = restClientFactory.createRestClientUseHttpProxy(""); + } + + public Mono<Policy> doAccessControl(Map<String, String> receivedHttpHeaders, Policy policy, AccessType accessType) { + return doAccessControl(receivedHttpHeaders, policy.getType(), accessType) // + .map(x -> policy); + } + + public Mono<PolicyType> doAccessControl(Map<String, String> receivedHttpHeaders, PolicyType type, + AccessType accessType) { + if (this.applicationConfig.getAuthProviderUrl().isEmpty()) { + return Mono.just(type); + } + + String tkn = getAuthToken(receivedHttpHeaders); + PolicyAuthorizationRequest.Input input = PolicyAuthorizationRequest.Input.builder() // + .authToken(tkn) // + .policyTypeId(type.getId()) // + .accessType(accessType).build(); + + PolicyAuthorizationRequest req = PolicyAuthorizationRequest.builder().input(input).build(); + + String url = this.applicationConfig.getAuthProviderUrl(); + 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/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/AuthorizationConsts.java b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/AuthorizationConsts.java new file mode 100644 index 00000000..a905b9d1 --- /dev/null +++ b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/AuthorizationConsts.java @@ -0,0 +1,37 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * Copyright (C) 2023 Nordix Foundation. All rights reserved. + * ====================================================================== + * 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.onap.ccsdk.oran.a1policymanagementservice.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 A1 policy access (this is provided by an authorization producer such as OPA). + Note that this API is called by PMS, it is not provided. + """; + + 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/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/PolicyAuthorizationRequest.java b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/AuthorizationResult.java index 63a53109..796c44e9 100644 --- a/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/PolicyAuthorizationRequest.java +++ b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/AuthorizationResult.java @@ -2,7 +2,7 @@ * ========================LICENSE_START================================= * ONAP : ccsdk oran * ====================================================================== - * Copyright (C) 2022 Nordix Foundation. All rights reserved. + * Copyright (C) 2023 Nordix Foundation. All rights reserved. * ====================================================================== * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ * ========================LICENSE_END=================================== */ -package org.onap.ccsdk.oran.a1policymanagementservice.controllers.v2; +package org.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; @@ -28,20 +28,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; - -@Schema(name = "policy_authorization", description = "Authorization request for A1 policy requests") +@Schema(name = "authorization_result", description = "Result of authorization") @Builder -public class PolicyAuthorizationRequest { - - @Schema(name = "acces_type", description = "Access type") - public enum AccessType { - READ, WRITE, DELETE - } +public class AuthorizationResult { - @Schema(name = "access_type", description = "Access type", required = true) - @JsonProperty(value = "access_type", required = true) - @SerializedName("access_type") + @Schema(name = "result", description = "If true, the access is granted", required = true) + @JsonProperty(value = "result", required = true) + @SerializedName("result") @Getter - private String accessType; + private boolean result; } diff --git a/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/PolicyAuthorizationRequest.java b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/PolicyAuthorizationRequest.java new file mode 100644 index 00000000..8dd4e7bb --- /dev/null +++ b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/authorization/PolicyAuthorizationRequest.java @@ -0,0 +1,77 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * Copyright (C) 2023 Nordix Foundation. All rights reserved. + * ====================================================================== + * 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.onap.ccsdk.oran.a1policymanagementservice.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 = "policy_authorization", description = "Authorization request for A1 policy requests") +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class PolicyAuthorizationRequest { + + @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, DELETE + } + + @Schema(name = "access_type", description = "Access type", required = true) + @JsonProperty(value = "access_type", required = true) + @SerializedName("access_type") + @Getter + private AccessType accessType; + + @Schema(name = "policy_type_id", description = "Policy type identifier", required = true) + @SerializedName("policy_type_id") + @JsonProperty(value = "policy_type_id", required = true) + private String policyTypeId; + + @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/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/PolicyController.java b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/PolicyController.java index 395daa30..64905f44 100644 --- a/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/PolicyController.java +++ b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/PolicyController.java @@ -36,11 +36,14 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import lombok.Getter; import org.onap.ccsdk.oran.a1policymanagementservice.clients.A1ClientFactory; import org.onap.ccsdk.oran.a1policymanagementservice.controllers.VoidResponse; +import org.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization.AuthorizationCheck; +import org.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization.PolicyAuthorizationRequest.Input.AccessType; import org.onap.ccsdk.oran.a1policymanagementservice.exceptions.EntityNotFoundException; import org.onap.ccsdk.oran.a1policymanagementservice.exceptions.ServiceException; import org.onap.ccsdk.oran.a1policymanagementservice.repository.Lock; @@ -64,11 +67,13 @@ 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.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.function.client.WebClientException; import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @RestController("PolicyControllerV2") @@ -104,6 +109,9 @@ public class PolicyController { @Autowired private Services services; + @Autowired + private AuthorizationCheck authorization; + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static Gson gson = new GsonBuilder() // .create(); // @@ -175,10 +183,13 @@ public class PolicyController { description = "Policy is not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public ResponseEntity<Object> getPolicy( // - @PathVariable(name = Consts.POLICY_ID_PARAM, required = true) String id) throws EntityNotFoundException { - Policy p = policies.getPolicy(id); - return new ResponseEntity<>(gson.toJson(toPolicyInfo(p)), HttpStatus.OK); + public Mono<ResponseEntity<Object>> getPolicy( // + @PathVariable(name = Consts.POLICY_ID_PARAM, required = true) String id, + @RequestHeader Map<String, String> headers) throws EntityNotFoundException { + Policy policy = policies.getPolicy(id); + return authorization.doAccessControl(headers, policy, AccessType.READ) // + .map(x -> new ResponseEntity<>((Object) gson.toJson(toPolicyInfo(policy)), HttpStatus.OK)) // + .onErrorResume(this::handleException); } @DeleteMapping(Consts.V2_API_ROOT + "/policies/{policy_id:.+}") @@ -198,12 +209,15 @@ public class PolicyController { content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) public Mono<ResponseEntity<Object>> deletePolicy( // - @PathVariable(Consts.POLICY_ID_PARAM) String policyId) throws EntityNotFoundException { + @PathVariable(Consts.POLICY_ID_PARAM) String policyId, @RequestHeader Map<String, String> headers) + throws EntityNotFoundException { Policy policy = policies.getPolicy(policyId); keepServiceAlive(policy.getOwnerServiceId()); - return policy.getRic().getLock().lock(LockType.SHARED, "deletePolicy") // - .flatMap(grant -> deletePolicy(grant, policy)); + return authorization.doAccessControl(headers, policy, AccessType.WRITE) + .flatMap(x -> policy.getRic().getLock().lock(LockType.SHARED, "deletePolicy")) // + .flatMap(grant -> deletePolicy(grant, policy)) // + .onErrorResume(this::handleException); } Mono<ResponseEntity<Object>> deletePolicy(Lock.Grant grant, Policy policy) { @@ -232,7 +246,8 @@ public class PolicyController { description = "Near-RT RIC or policy type is not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public Mono<ResponseEntity<Object>> putPolicy(@RequestBody PolicyInfo policyInfo) throws EntityNotFoundException { + public Mono<ResponseEntity<Object>> putPolicy(@RequestBody PolicyInfo policyInfo, + @RequestHeader Map<String, String> headers) throws EntityNotFoundException { if (!policyInfo.validate()) { return ErrorResponse.createMono("Missing required parameter in body", HttpStatus.BAD_REQUEST); @@ -255,8 +270,10 @@ public class PolicyController { .statusNotificationUri(policyInfo.statusNotificationUri == null ? "" : policyInfo.statusNotificationUri) // .build(); - return ric.getLock().lock(LockType.SHARED, "putPolicy") // - .flatMap(grant -> putPolicy(grant, policy)); + return authorization.doAccessControl(headers, policy, AccessType.WRITE) // + .flatMap(x -> ric.getLock().lock(LockType.SHARED, "putPolicy")) // + .flatMap(grant -> putPolicy(grant, policy)) // + .onErrorResume(this::handleException); } private Mono<ResponseEntity<Object>> putPolicy(Lock.Grant grant, Policy policy) { @@ -285,6 +302,9 @@ public class PolicyController { } else if (throwable instanceof RejectionException) { RejectionException e = (RejectionException) throwable; return ErrorResponse.createMono(e.getMessage(), e.getStatus()); + } else if (throwable instanceof ServiceException) { + ServiceException e = (ServiceException) throwable; + return ErrorResponse.createMono(e.getMessage(), e.getHttpStatus()); } else { return ErrorResponse.createMono(throwable.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } @@ -339,7 +359,7 @@ public class PolicyController { description = "Near-RT RIC, policy type or service not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public ResponseEntity<Object> getPolicyInstances( // + public Mono<ResponseEntity<Object>> getPolicyInstances( // @Parameter(name = Consts.POLICY_TYPE_ID_PARAM, required = false, description = "Select policies with a given type identity.") // @RequestParam(name = Consts.POLICY_TYPE_ID_PARAM, required = false) String typeId, // @@ -351,8 +371,8 @@ public class PolicyController { @RequestParam(name = Consts.SERVICE_ID_PARAM, required = false) String service, @Parameter(name = Consts.TYPE_NAME_PARAM, required = false, // description = "Select policies of a given type name (type identity has the format <typename_version>)") // - @RequestParam(name = Consts.TYPE_NAME_PARAM, required = false) String typeName) - throws EntityNotFoundException // + @RequestParam(name = Consts.TYPE_NAME_PARAM, required = false) String typeName, + @RequestHeader Map<String, String> headers) throws EntityNotFoundException // { if ((typeId != null && this.policyTypes.get(typeId) == null)) { throw new EntityNotFoundException("Policy type identity not found"); @@ -361,8 +381,14 @@ public class PolicyController { throw new EntityNotFoundException("Near-RT RIC not found"); } - String filteredPolicies = policiesToJson(policies.filterPolicies(typeId, ric, service, typeName)); - return new ResponseEntity<>(filteredPolicies, HttpStatus.OK); + Collection<Policy> filtered = policies.filterPolicies(typeId, ric, service, typeName); + return Flux.fromIterable(filtered) // + .flatMap(policy -> authorization.doAccessControl(headers, policy, AccessType.READ)) // + .doOnError(e -> logger.debug("Unauthorized to read policy: {}", e.getMessage())) // + .onErrorResume(e -> Mono.empty()) // + .collectList() // + .map(authPolicies -> policiesToJson(authPolicies)) // + .map(str -> new ResponseEntity<>(str, HttpStatus.OK)); } @GetMapping(path = Consts.V2_API_ROOT + "/policies", produces = MediaType.APPLICATION_JSON_VALUE) // @@ -375,7 +401,7 @@ public class PolicyController { description = "Near-RT RIC or type not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public ResponseEntity<Object> getPolicyIds( // + public Mono<ResponseEntity<Object>> getPolicyIds( // @Parameter(name = Consts.POLICY_TYPE_ID_PARAM, required = false, // description = "Select policies of a given policy type identity.") // @RequestParam(name = Consts.POLICY_TYPE_ID_PARAM, required = false) String policyTypeId, // @@ -387,8 +413,8 @@ public class PolicyController { @RequestParam(name = Consts.SERVICE_ID_PARAM, required = false) String serviceId, @Parameter(name = Consts.TYPE_NAME_PARAM, required = false, // description = "Select policies of types with the given type name (type identity has the format <typename_version>)") // - @RequestParam(name = Consts.TYPE_NAME_PARAM, required = false) String typeName) - throws EntityNotFoundException // + @RequestParam(name = Consts.TYPE_NAME_PARAM, required = false) String typeName, + @RequestHeader Map<String, String> headers) throws EntityNotFoundException // { if ((policyTypeId != null && this.policyTypes.get(policyTypeId) == null)) { throw new EntityNotFoundException("Policy type not found"); @@ -397,8 +423,14 @@ public class PolicyController { throw new EntityNotFoundException("Near-RT RIC not found"); } - String policyIdsJson = toPolicyIdsJson(policies.filterPolicies(policyTypeId, ricId, serviceId, typeName)); - return new ResponseEntity<>(policyIdsJson, HttpStatus.OK); + Collection<Policy> filtered = policies.filterPolicies(policyTypeId, ricId, serviceId, typeName); + return Flux.fromIterable(filtered) // + .flatMap(policy -> authorization.doAccessControl(headers, policy, AccessType.READ)) // + .doOnError(e -> logger.debug("Unauthorized to read policy: {}", e.getMessage())) // + .onErrorResume(e -> Mono.empty()) // + .collectList() // + .map(authPolicies -> toPolicyIdsJson(authPolicies)) // + .map(policyIdsJson -> new ResponseEntity<>(policyIdsJson, HttpStatus.OK)); } @GetMapping(path = Consts.V2_API_ROOT + "/policies/{policy_id}/status", produces = MediaType.APPLICATION_JSON_VALUE) @@ -412,10 +444,12 @@ public class PolicyController { content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) public Mono<ResponseEntity<Object>> getPolicyStatus( // - @PathVariable(Consts.POLICY_ID_PARAM) String policyId) throws EntityNotFoundException { + @PathVariable(Consts.POLICY_ID_PARAM) String policyId, @RequestHeader Map<String, String> headers) + throws EntityNotFoundException { Policy policy = policies.getPolicy(policyId); - return a1ClientFactory.createA1Client(policy.getRic()) // + return authorization.doAccessControl(headers, policy, AccessType.READ) // + .flatMap(notUsed -> a1ClientFactory.createA1Client(policy.getRic())) // .flatMap(client -> client.getPolicyStatus(policy).onErrorResume(e -> Mono.just("{}"))) // .flatMap(status -> createPolicyStatus(policy, status)) // .onErrorResume(this::handleException); diff --git a/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/ServiceController.java b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/ServiceController.java index c3f41768..ed97820f 100644 --- a/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/ServiceController.java +++ b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/ServiceController.java @@ -43,7 +43,6 @@ import org.onap.ccsdk.oran.a1policymanagementservice.repository.Policies; import org.onap.ccsdk.oran.a1policymanagementservice.repository.Policy; import org.onap.ccsdk.oran.a1policymanagementservice.repository.Service; import org.onap.ccsdk.oran.a1policymanagementservice.repository.Services; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -71,7 +70,6 @@ public class ServiceController { private static Gson gson = new GsonBuilder().create(); - @Autowired ServiceController(Services services, Policies policies) { this.services = services; this.policies = policies; diff --git a/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/repository/Policies.java b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/repository/Policies.java index 77ac0f4a..7eddeae1 100644 --- a/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/repository/Policies.java +++ b/a1-policy-management/src/main/java/org/onap/ccsdk/oran/a1policymanagementservice/repository/Policies.java @@ -197,7 +197,7 @@ public class Policies { byte[] bytes = gson.toJson(toStorageObject(policy)).getBytes(); this.dataStore.writeObject(this.getPath(policy), bytes) // - .doOnError(t -> logger.error("Could not store job in S3, reason: {}", t.getMessage())) // + .doOnError(t -> logger.error("Could not store policy in S3, reason: {}", t.getMessage())) // .subscribe(); } diff --git a/a1-policy-management/src/test/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/OpenPolicyAgentSimulatorController.java b/a1-policy-management/src/test/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/OpenPolicyAgentSimulatorController.java new file mode 100644 index 00000000..236c31b8 --- /dev/null +++ b/a1-policy-management/src/test/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/OpenPolicyAgentSimulatorController.java @@ -0,0 +1,112 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * Copyright (C) 2023 Nordix Foundation. All rights reserved. + * ====================================================================== + * 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.onap.ccsdk.oran.a1policymanagementservice.controllers; + +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.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization.AuthorizationConsts; +import org.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization.AuthorizationResult; +import org.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization.PolicyAuthorizationRequest; +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 ACCESS_CONTROL_URL = "/example-authz-check"; + public static final String ACCESS_CONTROL_URL_REJECT = "/example-authz-check-reject"; + + public static class TestResults { + + public List<PolicyAuthorizationRequest> receivedRequests = + Collections.synchronizedList(new ArrayList<PolicyAuthorizationRequest>()); + + public TestResults() {} + + public void reset() { + receivedRequests.clear(); + + } + } + + @Getter + private TestResults testResults = new TestResults(); + + @PostMapping(path = ACCESS_CONTROL_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> performAccessControl( // + @RequestHeader Map<String, String> headers, // + @RequestBody PolicyAuthorizationRequest request) { + logger.debug("Auth {}", request); + testResults.receivedRequests.add(request); + + String res = gson.toJson(AuthorizationResult.builder().result(true).build()); + return new ResponseEntity<>(res, HttpStatus.OK); + } + + @PostMapping(path = ACCESS_CONTROL_URL_REJECT, 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> performAccessControlReject( // + @RequestHeader Map<String, String> headers, // + @RequestBody PolicyAuthorizationRequest request) { + logger.debug("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/a1-policy-management/src/test/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/ApplicationTest.java b/a1-policy-management/src/test/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/ApplicationTest.java index 9f0473df..066adc48 100644 --- a/a1-policy-management/src/test/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/ApplicationTest.java +++ b/a1-policy-management/src/test/java/org/onap/ccsdk/oran/a1policymanagementservice/controllers/v2/ApplicationTest.java @@ -46,6 +46,7 @@ import java.util.List; import org.json.JSONObject; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; @@ -58,7 +59,10 @@ import org.onap.ccsdk.oran.a1policymanagementservice.configuration.ApplicationCo import org.onap.ccsdk.oran.a1policymanagementservice.configuration.ApplicationConfig.RicConfigUpdate; import org.onap.ccsdk.oran.a1policymanagementservice.configuration.RicConfig; import org.onap.ccsdk.oran.a1policymanagementservice.configuration.WebClientConfig; +import org.onap.ccsdk.oran.a1policymanagementservice.controllers.OpenPolicyAgentSimulatorController; import org.onap.ccsdk.oran.a1policymanagementservice.controllers.ServiceCallbackInfo; +import org.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization.PolicyAuthorizationRequest; +import org.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization.PolicyAuthorizationRequest.Input.AccessType; import org.onap.ccsdk.oran.a1policymanagementservice.exceptions.ServiceException; import org.onap.ccsdk.oran.a1policymanagementservice.repository.Lock; import org.onap.ccsdk.oran.a1policymanagementservice.repository.Lock.LockType; @@ -145,6 +149,9 @@ class ApplicationTest { @Autowired SecurityContext securityContext; + @Autowired + OpenPolicyAgentSimulatorController openPolicyAgentSimulatorController; + private static Gson gson = new GsonBuilder().create(); /** @@ -174,6 +181,11 @@ class ApplicationTest { @LocalServerPort private int port; + @BeforeEach + void init() { + this.applicationConfig.setAuthProviderUrl(baseUrl() + OpenPolicyAgentSimulatorController.ACCESS_CONTROL_URL); + } + @AfterEach void reset() { rics.clear(); @@ -184,6 +196,7 @@ class ApplicationTest { this.rAppSimulator.getTestResults().clear(); this.a1ClientFactory.setPolicyTypes(policyTypes); // Default same types in RIC and in this app this.securityContext.setAuthTokenFilePath(null); + this.openPolicyAgentSimulatorController.getTestResults().reset(); } @AfterAll @@ -513,6 +526,15 @@ class ApplicationTest { this.rics.getRic(ricId).setState(Ric.RicState.AVAILABLE); restClient().put(url, policyBody).block(); + { + // Check the authorization request + OpenPolicyAgentSimulatorController.TestResults res = + this.openPolicyAgentSimulatorController.getTestResults(); + assertThat(res.receivedRequests).hasSize(1); + PolicyAuthorizationRequest req = res.receivedRequests.get(0); + assertThat(req.getInput().getAccessType()).isEqualTo(AccessType.WRITE); + assertThat(req.getInput().getPolicyTypeId()).isEqualTo(policyTypeName); + } Policy policy = policies.getPolicy(policyInstanceId); assertThat(policy).isNotNull(); @@ -551,6 +573,57 @@ class ApplicationTest { } @Test + void testFineGrainedAuth() throws Exception { + final String POLICY_ID = "policyId"; + final String RIC_ID = "ric1"; + final String TYPE_ID = "typeName"; + addPolicy(POLICY_ID, TYPE_ID, null, RIC_ID); + assertThat(policies.size()).isEqualTo(1); + + this.applicationConfig + .setAuthProviderUrl(baseUrl() + OpenPolicyAgentSimulatorController.ACCESS_CONTROL_URL_REJECT); + + String url = "/policy-instances"; + String rsp = restClient().get(url).block(); + assertThat(rsp).as("Response contains no policy instance ID.").contains("[]"); + + url = "/policies/" + POLICY_ID; + testErrorCode(restClient().delete(url), HttpStatus.UNAUTHORIZED, "Not authorized"); + + url = "/policies"; + String policyBody = putPolicyBody(null, RIC_ID, TYPE_ID, POLICY_ID, false, null); + testErrorCode(restClient().put(url, policyBody), HttpStatus.UNAUTHORIZED, "Not authorized"); + + rsp = restClient().get(url).block(); + assertThat(rsp).as("Response contains no policy instance ID.").contains("[]"); + } + + @Test + void testFineGrainedAuth_OPA_UNAVALIABLE() throws Exception { + final String POLICY_ID = "policyId"; + final String RIC_ID = "ric1"; + final String TYPE_ID = "typeName"; + addPolicy(POLICY_ID, TYPE_ID, null, RIC_ID); + assertThat(policies.size()).isEqualTo(1); + + this.applicationConfig.setAuthProviderUrl("junk"); + + String url = "/policy-instances"; + String rsp = restClient().get(url).block(); + assertThat(rsp).as("Response contains no policy instance ID.").contains("[]"); + + url = "/policies/" + POLICY_ID; + testErrorCode(restClient().delete(url), HttpStatus.UNAUTHORIZED, "Not authorized"); + + url = "/policies"; + String policyBody = putPolicyBody(null, RIC_ID, TYPE_ID, POLICY_ID, false, null); + testErrorCode(restClient().put(url, policyBody), HttpStatus.UNAUTHORIZED, "Not authorized"); + + rsp = restClient().get(url).block(); + assertThat(rsp).as("Response contains no policy instance ID.").contains("[]"); + } + + @Test @DisplayName("test Put Policy No Service No Status Uri") void testPutPolicy_NoServiceNoStatusUri() throws Exception { String ricId = "ric.1"; @@ -1042,6 +1115,7 @@ class ApplicationTest { @Test @DisplayName("test Concurrency") void testConcurrency() throws Exception { + this.applicationConfig.setAuthProviderUrl(""); logger.info("Concurrency test starting"); final Instant startTime = Instant.now(); List<Thread> threads = new ArrayList<>(); @@ -1138,8 +1212,10 @@ class ApplicationTest { boolean expectApplicationProblemJsonMediaType) { assertTrue(throwable instanceof WebClientResponseException); WebClientResponseException responseException = (WebClientResponseException) throwable; + String body = responseException.getResponseBodyAsString(); + assertThat(body).contains(responseContains); assertThat(responseException.getStatusCode()).isEqualTo(expStatus); - assertThat(responseException.getResponseBodyAsString()).contains(responseContains); + if (expectApplicationProblemJsonMediaType) { assertThat(responseException.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PROBLEM_JSON); } diff --git a/docs/offeredapis/swagger/pms-api.json b/docs/offeredapis/swagger/pms-api.json index 7574032c..9efa7b75 100644 --- a/docs/offeredapis/swagger/pms-api.json +++ b/docs/offeredapis/swagger/pms-api.json @@ -28,6 +28,15 @@ "type": "string" }} }, + "authorization_result": { + "description": "Result of authorization", + "type": "object", + "required": ["result"], + "properties": {"result": { + "description": "If true, the access is granted", + "type": "boolean" + }} + }, "ric_info_v2": { "description": "Information for a Near-RT RIC", "type": "object", @@ -148,6 +157,40 @@ "type": "object" }} }, + "input": { + "description": "input", + "type": "object", + "required": [ + "access_type", + "auth_token", + "policy_type_id" + ], + "properties": { + "access_type": { + "description": "Access type", + "type": "string", + "enum": [ + "READ", + "WRITE", + "DELETE" + ] + }, + "auth_token": { + "description": "Authorization token", + "type": "string" + }, + "policy_type_id": { + "description": "Policy type identifier", + "type": "string" + } + } + }, + "policy_authorization": { + "description": "Authorization request for A1 policy requests", + "type": "object", + "required": ["input"], + "properties": {"input": {"$ref": "#/components/schemas/input"}} + }, "policytype_id_list_v2": { "description": "Information about policy types", "type": "object", @@ -298,6 +341,20 @@ ], "tags": ["A1 Policy Management"] }}, + "/example-authz-check": {"post": { + "summary": "Request for access authorization.", + "requestBody": { + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/policy_authorization"}}}, + "required": true + }, + "description": "The authorization function decides if access is granted.", + "operationId": "performAccessControl", + "responses": {"200": { + "description": "OK", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/authorization_result"}}} + }}, + "tags": ["Authorization API"] + }}, "/actuator/threaddump": {"get": { "summary": "Actuator web endpoint 'threaddump'", "operationId": "threaddump", @@ -946,12 +1003,18 @@ "title": "A1 Policy Management Service", "version": "1.1.0" }, - "tags": [{ - "name": "Actuator", - "description": "Monitor and interact", - "externalDocs": { - "description": "Spring Boot Actuator Web API Documentation", - "url": "https://docs.spring.io/spring-boot/docs/current/actuator-api/html/" + "tags": [ + { + "name": "Authorization API", + "description": "API used for authorization of information A1 policy access (this is provided by an authorization producer such as OPA).\nNote that this API is called by PMS, it is not provided.\n" + }, + { + "name": "Actuator", + "description": "Monitor and interact", + "externalDocs": { + "description": "Spring Boot Actuator Web API Documentation", + "url": "https://docs.spring.io/spring-boot/docs/current/actuator-api/html/" + } } - }] + ] }
\ No newline at end of file |