WORK IN PROGRESS
Introduction: A1-PMS Compliance with O-RAN Specifications
The A1 Policy Management System (A1-PMS) plays a crucial role in managing policies and configurations within the O-RAN network architecture. As the O-RAN Alliance continually refines its specifications, it becomes essential to assess how well A1-PMS adheres to these evolving standards. In this discussion, I will make the comparison between the latest O-RAN API specifications and the upstream OpenAPI specifications utilized for implementing A1-PMS. Trying to examine the alignment, discrepancies, and potential areas for improvement.
Simple Comparison of OpenAPI Specifications
I identidy as
openapi-oran.yaml the specifications given by O-Ran Alliance revised on 31/03/2024
openapi.yaml the upstream openapi v3 specifications upstream in the /api folder
Both OpenAPI specifications share similar high-level structures, including keys like `openapi`, `info`, `servers`, `paths`, and `components`.
Paths Comparison
The main differnce in paths is based on the nesting pattern. There are multiple opinions on Nested Resources type of links. It helps with readability but it can lead to long URLs or redundant endpoints because of less flexibility in paths.
So and edpoint:
/policytypes/{policyTypeId}/policies/{policyId}
can become with policyTypeId in the body
/policies/{policyId}
Analysis Topics
1. Limitations of JSON Schema:
- JSON Schema Limitations:
- Pros:
- Standardized: JSON Schema provides a standardized way to define and validate the structure of JSON data.
- Widely Supported: Many tools and libraries support JSON Schema, making it easy to integrate with various technologies.
- Cons:
- Expressiveness: JSON Schema may lack the expressiveness needed for complex validation rules, such as interdependent fields.
- Readability: Large and complex schemas can become difficult to read and maintain.
2. Input/Output Parameters:
- Optional Fields:
- Pros:
- Reusability: Using common models with optional fields allows for model reuse across different API endpoints.
- Flexibility: Clients can send only the necessary data without being forced to include all optional fields.
- Cons:
- Dead Data: Unused optional fields may be sent by clients, leading to "dead" data that the server has to process or ignore.
- Validation Complexity: Additional logic is required to handle the presence or absence of optional fields, increasing validation complexity.
3. Specific Schemas for Certain Calls:
- Advantages:
- Documentation: More specific examples can be provided for each operation, improving documentation clarity.
- Generated Code: More accurate and efficient code generation, as the schemas are tailored to specific operations, reducing the need for extra checks.
- Disadvantages:
- Translation Effort: Similar objects may need to be translated between different schemas, requiring adapter/builder/transformer patterns in code.
- Maintenance: Maintaining multiple specific schemas can increase the overall maintenance effort.
4. Generated Code and Contract Compliance:
- Impact on Generated Code:
- Changes in schemas will affect the generated interfaces, potentially requiring updates to implementations to ensure they comply with new contracts.
- Specific schemas will result in more precise interfaces, reducing runtime errors but increasing development time for ensuring compliance.
Solutions Following Best Practices
1. Use Specific Schemas for Critical Operations:
- Define specific schemas for different operations to improve clarity and reduce the risk of errors.
- Use OpenAPI features such as `allOf`, `oneOf`, and `anyOf` to create flexible and reusable components without compromising specificity.
2. Implement Adapter Pattern for Code Adaptation:
- Use the Adapter pattern to translate between different but similar objects, ensuring compliance with ORAN specifications without major code changes.
Pros and Cons of Java Patterns for Code Adaptation
All these solutions are viable, but will add complexity and abstraction to the code
- Adapter Pattern: Useful for converting one interface to another.
- Builder Pattern: Useful for constructing complex objects step-by-step.
- Transformer Pattern: Useful for converting objects from one type to another while keeping transformation logic separate from business logic.
Using OpenAPI Rules and Syntax for Code Adaptation
OpenAPI provides several features that can help in defining clear and adaptable specifications, we might want to investigate those to have some flexible solution for obejct declaring:
1. Using `allOf`, `oneOf`, `anyOf`: https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/
- Combine multiple schemas using `allOf` to create a new schema that includes all properties.
- Use `oneOf` or `anyOf` to define schemas where only one or any combination of the listed schemas is valid, respectively.
components: schemas: Policy: type: object properties: id: type: string name: type: string PolicyExtended: allOf: - $ref: '#/components/schemas/Policy' - type: object properties: description: type: string
2. Discriminator for Polymorphism: https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/
- Use the `discriminator` property to define inheritance and polymorphism in your schemas, allowing you to use a base schema with multiple derived schemas.
components: schemas: BasePolicy: type: object discriminator: propertyName: type mapping: extended: '#/components/schemas/ExtendedPolicy' properties: id: type: string type: type: string ExtendedPolicy: allOf: - $ref: '#/components/schemas/BasePolicy' - type: object properties: description: type: string
3. Extending and Overriding Schemas: (using 1)
- Create new schemas by extending existing ones to avoid duplication and ensure consistency.
components: schemas: BasePolicy: type: object properties: id: type: string name: type: string ExtendedPolicy: allOf: - $ref: '#/components/schemas/BasePolicy' - type: object properties: description: type: string
4. Parameter and Response Reusability: (current)
- Define reusable parameters and responses in the components section and reference them in different paths.
components: parameters: PolicyId: name: policyId in: path required: true schema: type: string paths: /policies/{policyId}: get: parameters: - $ref: '#/components/parameters/PolicyId'
Schema Usage in Paths
Schema | Count | Paths |
---|---|---|
ErrorInformation | 4 | /policies/{policyId} , /policytypes/{policyTypeId}/policies , /policytypes/{policyTypeId}/policies/{policyId} , /policytypes/{policyTypeId}/policies/{policyId}/status |
JsonSchema | 2 | /policytypes/{policyTypeId}/policies , /policytypes/{policyTypeId}/policies/{policyId} |
NearRtRicId | 3 | /policies , /policies/{policyId} , /policytypes/{policyTypeId}/policies |
NotificationDestination | 1 | /policies/{policyId} |
PolicyId | 3 | /policies , /policies/{policyId} , /policytypes/{policyTypeId}/policies |
PolicyInformation | 2 | /policies/{policyId} , /policytypes/{policyTypeId}/policies/{policyId} |
PolicyObjectInformation | 1 | /policytypes/{policyTypeId}/policies/{policyId}/status |
PolicyStatusObject | 2 | /policies/{policyId} , /policytypes/{policyTypeId}/policies/{policyId}/status |
PolicyTypeId | 3 | /policytypes/{policyTypeId}/policies , /policytypes/{policyTypeId}/policies/{policyId} , /policytypes/{policyTypeId}/policies/{policyId}/status |
PolicyTypeInformation | 2 | /policytypes/{policyTypeId}/policies , /policytypes/{policyTypeId}/policies/{policyId} |
PolicyTypeObject | 1 |
|
Using allOf to Extend Objects
To avoid using the `required` flag directly and create more flexible schemas, we can use the allOf keyword to combine schemas. This allows you to create an extended schema from an original one without repeating required properties.
https://openapi-generator.tech/docs/generators/spring/#schema-support-feature |
---|
From the documentation of the openapi generator used in A1PMS, AllOf is not supported for spring server generator. But it still generated objects without the use of extending the parent class. For example: An extended object in the specification yaml: components: schemas: Policy: type: object properties: id: type: string name: type: string PolicyExtended: allOf: - $ref: '#/components/schemas/Policy' - type: object properties: description: type: string public class Policy { private String id; private String name; ... } public class PolicyExtended { private String id; private String name; private String description; ... } |
OpenAPI Required Properties
In OpenAPI 3 by default, all object properties are optional. Required properties can be identified in the required list:
components: schemas: Policy: type: object properties: id: type: string name: type: string required: - id
Having required parameters has only one effect of genereted code, the tool generates only one construcotr with only default parameters. So the implementer can use the setters and getters.
/** * Constructor with only required parameters */ public Policy(String id, String name) { this.id= id; }
Handling serviceId in Request Bodies and Bearer Tokens
Current Implementation
In the current implementation, the `serviceId` can be set in the body of a request and is defined as optional. The default value for `serviceId` in `PolicyObjectInformation` (PolicyApi) is a space, which accommodates cases where the `serviceId` might be missing. However, in the `ServiceApi`, the `serviceId` is required, for example, when creating a service.
Ideal Implementation
The use of a space as a default value for `serviceId` is a workaround to handle missing IDs. Ideally, the `serviceId` should be extracted from a bearer token. This can be done by decoding a JWT token using built-in Java functions.
Here’s a sample code snippet to decode a JWT token and extract the `serviceId`:
String token = getAuthToken(receivedHttpHeaders); String[] chunks = token.split("\\."); Base64.Decoder decoder = Base64.getUrlDecoder(); String payload = new String(decoder.decode(chunks[1])); JsonObject jsonObject = new JsonObject(); jsonObject.add("payload", JsonParser.parseString(payload)); String serviceId = jsonObject.getAsJsonObject("payload").get("client_id").getAsString();
Integrating with Existing Code
In the codebase, there is already a function to get the token located in the class:
org.onap.ccsdk.oran.a1policymanagementservice.controllers.authorization.AuthorizationCheck
String getAuthToken(Map<String, String> httpHeaders)
Bearer Token in Headers
The bearer token would be passed during the API call via headers as follows:
"Authorization: Bearer <bearer_token>"
By extracting the serviceId from the bearer token, we can ensure that the `serviceId` is always available and accurate, enhancing the security and reliability of the API.
Istio with Keycloak service mesh general use
When integrating Istio with Keycloak for JWT-based authorization, the typical workflow involves clients obtaining a JWT (JSON Web Token) from Keycloak, which is then used to access services secured by Istio. Keycloak acts as an identity provider (IdP), issuing tokens that contain various claims about the authenticated user or client.
A JWT from Keycloak includes three parts: the header, payload, and signature. The payload carries the claims about the user or client, and it is used by Istio to make authorization decisions.
1. Header: This part of the token contains metadata about the token itself, such as the type of token and the signing algorithm used. For Keycloak, this often looks like:
{
"alg": "RS256", // or another algorithm
"typ": "JWT",
"kid": "key-id" // key identifier, optional
}
2. Payload: The payload contains the claims, which are statements about the entity (typically, the user) and additional data. Some common claims include:
A sample payload might look like:
{ "iss": "https://keycloak.example.com/auth/realms/myrealm", #Issuer of the token, typically the Keycloak server URL. "sub": "12345678-1234-1234-1234-1234567890ab", #Subject of the token, usually the user ID. "aud": "myclient", #Audience for whom the token is intended. This is usually the client ID. "clientId": "myclientID", #Custom Parameter "exp": 1627584000, "iat": 1627576800, "nbf": 1627576800, "jti": "abcd1234efgh5678ijkl9012", #JWT ID "preferred_username": "user", "email": "user@example.com", "roles": ["user", "admin"] }
3. Signature: The signature is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way. The signature is created using the algorithm specified in the header and a private key, and it is then appended to the header and payload.
Client ID
The client_id is a common parameter in the payload of a JWT, particularly in tokens issued as part of OAuth 2.0 or OpenID Connect flows.
https://www.keycloak.org/docs-api/latest/rest-api/index.html#ApplicationRepresentation
Curl Examples:
Getting Admin Token
curl -k -sS --request POST \ --url "http://$KEYCLOAK_HOST/auth/realms/$REALM_NAME/protocol/openid-connect/token" \ --data client_id=$CLIENT_ID \ --data username=$USERNAME \ --data password=$PASSWORD \ --data grant_type=password \ --data scope=openid
Getting Client Secret
curl -k -sS -X GET "http://$KEYCLOAK_HOST/auth/admin/realms/$REALM_NAME/clients/$CLIENT_ID/client-secret" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ACCESS_TOKEN"
Demo with tokens and clientId extraction for using it as serviceId
To generate a JWT token and parse a value from it demonstration.
Below, I'll outline the steps to achieve this, including generating a JWT, sending it in a request header, and then parsing a value (like `client_id`) from the JWT payload.
Generating a JWT Token using Bash
Note that this example is for educational purposes and doesn't include proper security practices like using secure keys.
#!/bin/bash header='{"alg": "HS256", "typ": "JWT"}' payload='{"iss": "example_issuer", "sub": "1234567890", "aud": "myclient", "exp": 3000000000, "client_id": "myclient", "role": "user"}' # Base64 encode the header and payload without padding header_base64=$(echo -n "$header" | openssl base64 -e | tr -d '=' | tr '/+' '_-' ) payload_base64=$(echo -n "$payload" | openssl base64 -e -A | tr -d '=' | tr '/+' '_-') # Create a signature secret="mysecret" signature_base64=$(echo -n "${header_base64}.${payload_base64}" | openssl dgst -sha256 -hmac "${secret}" -binary | openssl base64 -e | tr -d '=' | tr '/+' '_-') # Combine to form the JWT jwt="${header_base64}.${payload_base64}.${signature_base64}" echo "$jwt"
This script generates a JWT and prints it out. Replace "your-256-bit-secret" with a proper secret key.
Note: for the expiration time
$ ([DateTime]('1970,1,1')).AddSeconds(3000000000)
24 January 2065 05:20:00
Sending the JWT in a REST Request Header
You can use curl to send the JWT in a request header:
curl -H "Authorization: Bearer $jwt" http://A1PMS/policy..
Parsing the JWT Payload in Java
Here's an example of how to parse the `client_id` from the JWT in Java:
import java.util.Base64; import com.google.gson.JsonObject; import com.google.gson.JsonParser; public class ParseJWT { public String parseServiceId(String token) { // Split token into its parts String[] chunks = token.split("\\."); Base64.Decoder decoder = Base64.getUrlDecoder(); // Decode payload String payload = new String(decoder.decode(chunks[1])); // Parse JSON using Gson JsonObject jsonObject = JsonParser.parseString(payload).getAsJsonObject(); // Extract the client_id String clientId = jsonObject.get("client_id").getAsString(); return clientId; } }
In the create policy code check if there is an header and if there is a clientId use it as serviceId, other cases are covered having default serviceId (If there is no header, if there is an header but not a clientId)
Policy Creation Scenario
In this example I want to log the Bearer Token given to the call: PUT
In Postman I use the generated token I got from the bash script as Bearer Token:
It would be equivalent to:
curl -v -X 'PUT' 'http://localhost:8081/a1-policy/v2/policies' \ -H'Content-Type: application/json' \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlX2lzc3VlciIsInN1YiI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJteWNsaWVudCIsImV4cCI6MzAwMDAwMDAwMCwiY2xpZW50X2lkIjoibXljbGllbnQiLCJyb2xlIjoidXNlciJ9.O5QN_SWN4J1mWKyXk_-PCvOA6GF3ypv1rSdg2uTb_Ls' \ -d'{ "ric_id": "ric1", "policy_id": "aa8feaa88d944d919ef0e83f2172a51001", "is_transient": true, "service_id": "service-1", "policy_data": { "scope": { "ueId": "ue5100", "qosId": "qos5100" }, "qosObjectives": { "priorityLevel": 5100.0 } }, "status_notification_uri": "http://callback-receiver:8090/callbacks/test", "policytype_id": "1" }'
private void logHeaders(ServerWebExchange exchange) { HttpHeaders headers = exchange.getRequest().getHeaders(); String authHeader = headers.getFirst(HttpHeaders.AUTHORIZATION); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); logger.info("Token: " + token); logger.info("ServiceId: " + parseServiceId(token)); } else { logger.info("Authorization header is missing or does not contain a Bearer token"); } } public String parseServiceId(String token) { String[] chunks = token.split("\\."); Base64.Decoder decoder = Base64.getUrlDecoder(); String payload = new String(decoder.decode(chunks[1])); JsonObject jsonObject = JsonParser.parseString(payload).getAsJsonObject(); String clientId = jsonObject.get("client_id").getAsString(); return clientId; }
Output of the call:
2024-07-30 13:32:55 2024-07-30 12:32:55.413 [INFO ] [http-nio-8081-exec-3] o.o.c.o.a.c.v.PolicyController - Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlX2lzc3VlciIsInN1YiI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJteWNsaWVudCIsImV4cCI6MzAwMDAwMDAwMCwiY2xpZW50X2lkIjoibXljbGllbnQiLCJyb2xlIjoidXNlciJ9.O5QN_SWN4J1mWKyXk_-PCvOA6GF3ypv1rSdg2uTb_Ls 2024-07-30 13:32:55 2024-07-30 12:32:55.415 [INFO ] [http-nio-8081-exec-3] o.o.c.o.a.c.v.PolicyController - ServiceId: myclient
TODO and topic to follow
- Evaluate the necessity of optional fields: Determine if certain optional fields can be removed or if their use can be better documented to avoid dead data.
- Consider adopting more specific schemas for critical operations: This can improve both the documentation and the generated code quality. Leverage OpenAPI Features: Use OpenAPI's advanced features like `allOf`,
- Prepare for code adaptations: Implement patterns like Adapter/Builder/Transformer to handle translations between similar objects, facilitating easier maintenance and adaptation to specification changes.
- Regular compliance checks.