WORK IN PROGRESS & For Discussion
Background
Some A1-Policy operations require the creator (and owner) of an A1-Policy to be known.
In the pre-R1 A1-Policy API it was required to provide a 'service_id' to link A1-Policies to the client or service that 'owns' them.
- https://docs.onap.org/projects/onap-ccsdk-oran/en/latest/offeredapis/pms-api.html#tag/Service-Registry-and-Supervision
- https://docs.onap.org/projects/onap-ccsdk-oran/en/latest/offeredapis/pms-api.html#tag/A1-Policy-Management/operation/putPolicy
Where this mandatory 'service_id' is a registered 'Service', then the new A1-Policy instance is subject to the 'keep-alive' requirements for the service, and policies are deleted when the 'Service' is unregistered/deleted.
However, this mandatory 'service_id' did not need to correspond to an existing registered 'Service', and if the value corresponded to an unregistered 'Service' service registration is not performed for that service.
A "Service" cannot be registered with a null or empty (""), however an empty value "" can be passed for 'service_id' when creating a policy - to represent "no service".
O-RAN Alliance R1-GAP, R1-AP and Security specifications describe how R1 requests authentication headers should include a bearer-token, which includes the 'ClientID' of the R1-Service invoker. There is an assumption (TBC) that this is the 'rAppID' for R1 invocations by rApps. When creating A1-Polices this 'ClientID' corresponds to the 'owner' and 'creator' of those A1-Policies. ('owner' == 'creator').
When a 'ClientID' is present in a the Auth header of a request, as part of a cryptographically signed/verified JWT token, the 'ClientID' will be the actual service invoker. JWT tokens generally should not be shared, and it should not be possible for one client to masquerade as a different client.
However, there may be cases where non-rApp clients need to be 'creator' for an A1-Policy, but not the 'owner'. e.g. an admin client, a GUI, a batch or restore operation, distributed applications, etc.. So it should be possible to also include supplementary 'owner' information in an A1-Policy create request.
In the R1-aligned enhanced ('v3') API introduced for A1-PMS (link currently broken: https://docs.onap.org/projects/onap-ccsdk-oran/en/latest/offeredapis/v3/pms-api-v3.html) the Create-Policy operation retained the 'service_id' parameter as an optional non-spec parameter, with default value as empty string "". In this way we can use either/both 'ClientID' in the header's JWT token and/or the 'service_id' parameter in the create-policy operation.
This page discusses how to deal with combinations of 'ClientID' (as "creator" and/or "own
er") in the Header, and 'service_id' ( as "owner") in the request body.
Scenarios:
Not considering security here!
The discussion below does not consider whether a Create-Policy request (or any other request) should be allowed or not.
Request authorization and/or authentication is checked elsewhere :
- Firstly, outside the service (e.g. RBAC checking in an API gateway, service mesh, proxy etc)
- Secondly, using the call-outs to an optional external Authorization handler via the Authorization-API
Therefore, depending on how the service is deployed and configured, some of the scenarios below may be restricted, and the operation may fail with an 'Unauthorized' error.
Original pre-spec v2 API:
- Request 'service_id' ❌ absent (null)
Token or its 'ClientID' ❌ absent (null), or
Token's 'ClientID' ✔️ present but blank/empty (""), or
Token's 'ClientID' ✅present
Leave parameter 'service_id' as null and continue.
Operation will fail because 'service_id' is mandatory.
- Request 'service_id' ✔️ present but blank/empty ("")
Token or its 'ClientID' ❌ absent (null)
Leave parameter 'service_id' as empty ("") and continue.
This policy has no 'owner', so cannot be retrieved using a service-id filter in Get-Instances, and cannot be later inherited by a newly-registered service ("" is not a valid service name when registering a 'Service').
Policy cannot be auto-deleted/garbage-collected, since this functionality is tried to the life-cycle of registered Services only.
Token's 'ClientID' ✔️ present but blank/empty ("")
Leave parameter 'service_id' as empty ("") and continue.
This policy has no 'owner', so cannot be retrieved using a service-id filter in Get-Instances, and cannot be later inherited by a newly-registered service ("" is not a valid service name when registering a 'Service').
Policy cannot be auto-deleted/garbage-collected, since this functionality is tried to the life-cycle of registered Services only.
Token's 'ClientID' ✅present
Set parameter 'service_id' = 'ClientID' and continue.
This policy's 'owner' is set be its 'creator'.
See options 3.d and 4.d below to understand behavior when 'service_id' == 'ClientID'
- Request 'service_id' ✅present but NOT a registered 'Service'
Token or its 'ClientID' ❌ absent (null)
Leave parameter 'service_id' as is (not-registered Service) and continue.
This policy has an unregistered 'owner', but can be retrieved using that 'service-id' filter in Get-Instances
Policy cannot be auto-deleted/garbage-collected, since this functionality is tried to the life-cycle of registered Services only.
Policy can be later inherited by a newly-registered Service with same 'service_id'.Token's 'ClientID' ✔️ present but blank/empty ("")
Leave parameter 'service_id' as is (not-registered Service) and continue.
This policy has an unregistered 'owner', but can be retrieved using that 'service-id' filter in Get-Instances
Policy cannot be auto-deleted/garbage-collected, since this functionality is tried to the life-cycle of registered Services only.
Policy can be later inherited by a newly-registered Service with same 'service_id'.Token's 'ClientID' ✅present but != 'service_id'
Leave parameter 'service_id' as is (not-registered Service) and continue.
Policy has an different 'owner' than 'creator'This policy has an unregistered 'owner', but can be retrieved using that 'service-id' filter in Get-Instances
Policy cannot be auto-deleted/garbage-collected, since this functionality is tried to the life-cycle of registered Services only.
Policy can be later inherited by a newly-registered Service with same 'service_id' (see option 4.c below).To Do!
Need to extend functionality to save the additional 'creator' information.
Token's 'ClientID' ✅present and == 'service_id'
Leave parameter 'service_id' as is (not-registered Service) and continue.
This policy has an unregistered 'owner', but can be retrieved using that 'service-id' filter in Get-Instances
Policy cannot be auto-deleted/garbage-collected, since this functionality is tried to the life-cycle of registered Services only.
Policy can be later inherited by a newly-registered Service with same 'service_id' (see option 4.d below).
- Request 'service_id' ✅present but is a registered 'Service'
Token or its 'ClientID' ❌ absent (null)
Leave parameter 'service_id' as is (pre-registered Service) and continue.
This policy has a registered 'owner', and can be retrieved using that 'service-id' filter in Get-Instances
Policy may be auto-deleted/garbage-collected, depending on the life-cycle and activity registered Service.
Policy will be deleted if Service is deleted or times-out according to however the Service is configured.Token's 'ClientID' ✔️ present but blank/empty ("")
Leave parameter 'service_id' as is (pre-registered Service) and continue.
This policy has a registered 'owner', and can be retrieved using that 'service-id' filter in Get-Instances
Policy may be auto-deleted/garbage-collected, depending on the life-cycle and activity registered Service.
Policy will be deleted if Service is deleted or times-out according to however the Service is configured.Token's 'ClientID' ✅present but != 'service_id'
Leave parameter 'service_id' as is (pre-registered Service) and continue.
Policy has an different 'owner' than 'creator'This policy has a registered 'owner', and can be retrieved using that 'service-id' filter in Get-Instances
Policy may be auto-deleted/garbage-collected, depending on the life-cycle and activity registered Service.
Policy will be deleted if Service is deleted or times-out according to however the Service is configured.To Do!
Need to extend functionality to save the additional 'creator' information.
Token's 'ClientID' ✅present and == 'service_id'
Leave parameter 'service_id' as is (pre-registered Service) and continue.
Policy has same 'owner' than 'creator'This policy has a registered 'owner' - same as 'creator', and can be retrieved using that 'service-id' filter in Get-Instances
Policy may be auto-deleted/garbage-collected, depending on the life-cycle and activity registered Service.
Policy will be deleted if Service is deleted or times-out according to however the Service is configured.
New R1-spec v3 API:
The main difference between 'v2' API and new 'v3' API is that 'service_id' is now optional instead of mandatory.
So all scenarios are the same as above EXCEPT scenario #1
Request 'service_id' ❌ absent (null)
Set parameter 'service_id' as empty ("") and continue.
See option set #2 above
Demo with tokens and ClientID extraction for using it as service_id
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
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" }'
In org.onap.ccsdk.oran.a1policymanagementservice.controllers.v2.PolicyController
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
Summary:
ClientID coming from the JWT, ServiceID coming in from the body request.
v3 | ClientID NULL | ClientID EMPTY | ClientID VALID |
---|---|---|---|
ServiceID NULL | S EMPTY | C EMPTY | C |
ServiceID EMPTY | S | S EMPTY | C |
ServiceID REGISTERED | S | S | S (*) |
ServiceID UNREGISTERED | S | S | S (*) |
(*) Owner (serviceID) != Creator (clientID)