From 7db70bcd0b1e8950af28d007ce097c69a6f37c16 Mon Sep 17 00:00:00 2001 From: Ravi Pendurty Date: Mon, 5 May 2025 18:08:04 +0530 Subject: [PATCH] Include grafana for PM dashboards Integrate grafana with keycloak Issue-ID: OAM-453 Change-Id: I0559cc7caf26c23c6c9bac748d6d1dfa5e678996 Signed-off-by: Ravi Pendurty --- solution/smo/common/identity/authentication.json | 20 +++- solution/smo/common/identity/config.py | 113 ++++++++++++++++++++- solution/smo/common/identity/o-ran-sc-realm.json | 95 +++++++++++++++++ .../smo/oam/pm/config/pmpr/token-cache/jwt.txt | 2 +- solution/smo/oam/pm/docker-compose-grafana.yaml | 38 +++++++ solution/smo/oam/pm/setup.sh | 9 +- solution/smo/oam/pm/tear-down.sh | 1 + 7 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 solution/smo/oam/pm/docker-compose-grafana.yaml diff --git a/solution/smo/common/identity/authentication.json b/solution/smo/common/identity/authentication.json index 7797110..9ff99b6 100644 --- a/solution/smo/common/identity/authentication.json +++ b/solution/smo/common/identity/authentication.json @@ -32,7 +32,10 @@ ], "requiredActions": [ "UPDATE_PASSWORD" - ] + ], + "clientRoles" : { + "grafana-ui.app" : [ "grafanaadmin" ] + } }, { "firstName": "Luke", @@ -49,7 +52,10 @@ ], "requiredActions": [ "UPDATE_PASSWORD" - ] + ], + "clientRoles" : { + "grafana-ui.app" : [ "editor" ] + } }, { "firstName": "Jargo", @@ -66,7 +72,10 @@ ], "requiredActions": [ "UPDATE_PASSWORD" - ] + ], + "clientRoles" : { + "grafana-ui.app" : [ "viewer" ] + } }, { "firstName": "Martin", @@ -83,7 +92,10 @@ ], "requiredActions": [ "UPDATE_PASSWORD" - ] + ], + "clientRoles" : { + "grafana-ui.app" : [ "grafanaadmin" ] + } } ], "grants": [ diff --git a/solution/smo/common/identity/config.py b/solution/smo/common/identity/config.py index 37967e1..6e2207a 100644 --- a/solution/smo/common/identity/config.py +++ b/solution/smo/common/identity/config.py @@ -198,6 +198,9 @@ def createUser(token, realmConfig, user): if response.status_code >= 200 and response.status_code < 300: print('User', user['username'], 'created!') + for client, roles in user.get("clientRoles", {}).items(): + #print(f"Adding Client Role(s): {roles} from Client: {client} to User: {user['username']}") + assignClientRoleToUser(token, realmId, user['username'], client, roles) else: print('User creation', user['username'], 'failed!\n', response.text) @@ -207,7 +210,7 @@ def createUsers(token, realmConfig, authConfig): for user in authConfig['users']: createUser(token, realmConfig, user) - # create a user based on system user + # create a user based on unix system user systemUser = { "firstName": getpass.getuser(), "lastName": "", @@ -244,6 +247,114 @@ def addUserRole(user: dict, role: list, options: dict): # catastrophic error. bail. raise SystemExit(e) +# Assigns a Client Role to User +def assignClientRoleToUser(token, realmId, userName, clientName, roles): + url = base + '/admin/realms/' + realmId + '/users' + auth = 'bearer ' + token + headers = { + 'content-type': 'application/json', + 'accept': 'application/json', + 'authorization': auth + } + # 1. Query userId by userName + userId = getUserIdbyName(token, realmId, userName) + # 2. Query clientId by clientName + clientId = getClientIdByName(token, realmId, clientName) + # 3. Iterate through roles and Query roleId for each role as follows - https://identity.smo.o-ran-sc.org/admin/realms/onap/clients/5543ba76-c3bc-42e8-ae79-63a82d6dc2ee/roles?search=grafanaadmin + for roleName in roles: + roleId = getRoleIdByName(token, realmId, clientId, roleName) + # 4. Form a JSON body as - {"id": , "name": } + data = [ + { + "id": roleId, + "name": roleName + } + ] + # 5. Form URL as - //role-mappings/clients/ + roleAssignUrl = url + '/' + userId + '/role-mappings/clients/' + clientId + # 6. Send POST request to assign role to user + try: + response = requests.post(roleAssignUrl, verify=False, json=data, headers=headers) + if response.status_code >= 200 and response.status_code < 300: + print('Successfully Assigned role', roleName, 'to user', userName) + else: + print('Failed to Assign role', roleName, 'to user', userName, response.text) + except requests.exceptions.Timeout: + sys.exit('HTTP request failed, please check you internet connection.') + except requests.exceptions.TooManyRedirects: + sys.exit('HTTP request failed, please check your proxy settings.') + except requests.exceptions.RequestException as e: + # catastrophic error. bail. + raise SystemExit(e) + +def getRoleIdByName(token, realmId, clientId, roleName) -> str: + url = base + '/admin/realms/' + realmId + '/clients/' +clientId + '/roles?search=' + roleName + auth = 'bearer ' + token + headers = { + 'content-type': 'application/json', + 'accept': 'application/json', + 'authorization': auth + } + try: + response = requests.get(url, verify=False, headers=headers) + except requests.exceptions.Timeout: + sys.exit('HTTP request failed, please check you internet connection.') + except requests.exceptions.TooManyRedirects: + sys.exit('HTTP request failed, please check your proxy settings.') + except requests.exceptions.RequestException as e: + # catastrophic error. bail. + raise SystemExit(e) + + if response.status_code >= 200 and response.status_code < 300: + role = response.json() + roleId = role[0]["id"] + return roleId + +def getClientIdByName(token, realmId, clientName) -> str: + url = base + '/admin/realms/' + realmId + '/clients?clientId=' + clientName + auth = 'bearer ' + token + headers = { + 'content-type': 'application/json', + 'accept': 'application/json', + 'authorization': auth + } + try: + response = requests.get(url, verify=False, headers=headers) + except requests.exceptions.Timeout: + sys.exit('HTTP request failed, please check you internet connection.') + except requests.exceptions.TooManyRedirects: + sys.exit('HTTP request failed, please check your proxy settings.') + except requests.exceptions.RequestException as e: + # catastrophic error. bail. + raise SystemExit(e) + + if response.status_code >= 200 and response.status_code < 300: + client = response.json() + clientId = client[0]["id"] + return clientId + +def getUserIdbyName(token, realmId, userName) -> str: + url = base + '/admin/realms/' + realmId + '/users?username=' + userName + auth = 'bearer ' + token + headers = { + 'content-type': 'application/json', + 'accept': 'application/json', + 'authorization': auth + } + try: + response = requests.get(url, verify=False, headers=headers) + except requests.exceptions.Timeout: + sys.exit('HTTP request failed, please check you internet connection.') + except requests.exceptions.TooManyRedirects: + sys.exit('HTTP request failed, please check your proxy settings.') + except requests.exceptions.RequestException as e: + # catastrophic error. bail. + raise SystemExit(e) + + if response.status_code >= 200 and response.status_code < 300: + user = response.json() + userId = user[0]["id"] + return userId # searches for the role of a given user def findRole(username: str, authConfig: dict, realmConfig: dict) -> dict: diff --git a/solution/smo/common/identity/o-ran-sc-realm.json b/solution/smo/common/identity/o-ran-sc-realm.json index 14ea1c9..27cc221 100644 --- a/solution/smo/common/identity/o-ran-sc-realm.json +++ b/solution/smo/common/identity/o-ran-sc-realm.json @@ -305,6 +305,33 @@ ], "odlux.app": [], "kafka-ui.app": [], + "grafana-ui.app": [ + { + "id" : "b072ad1a-818e-4ff9-b98c-3179bd7f4228", + "name" : "editor", + "description" : "Grafana Read Write Role", + "composite" : false, + "clientRole" : true, + "containerId" : "9fc6cecf-f3a8-48a8-8065-b2fc80b8b2f5", + "attributes" : { } + }, { + "id" : "09436bef-901c-44a5-b38d-508273d730ba", + "name" : "viewer", + "description" : "Read only access Role", + "composite" : false, + "clientRole" : true, + "containerId" : "9fc6cecf-f3a8-48a8-8065-b2fc80b8b2f5", + "attributes" : { } + }, { + "id" : "37e3d5fc-41d6-4926-a9c9-e3d96f7f4d6a", + "name" : "grafanaadmin", + "description" : "Grafana Administrator Role", + "composite" : false, + "clientRole" : true, + "containerId" : "9fc6cecf-f3a8-48a8-8065-b2fc80b8b2f5", + "attributes" : { } + } + ], "security-admin-console": [], "admin-cli": [], "account-console": [], @@ -811,6 +838,67 @@ "manage": true } }, + { + "clientId": "grafana-ui.app", + "name": "Grafana UI", + "description": "", + "rootUrl": "https://grafana.smo.o-ran-sc.org", + "adminUrl": "https://grafana.smo.o-ran-sc.org", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "lVPuFWZlOV7yAbV1FIuaM0FOodD7cLTm", + "redirectUris": [ + "https://grafana.smo.o-ran-sc.org/login/generic_oauth" + ], + "webOrigins": [ + "https://grafana.smo.o-ran-sc.org" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1745992471", + "backchannel.logout.session.required": "true", + "frontchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + } + }, { "id": "048a9bfc-077a-42a2-afe8-1ec13d3a43a3", "clientId": "realm-management", @@ -1348,6 +1436,9 @@ "protocolMapper": "oidc-usermodel-client-role-mapper", "consentRequired": false, "config": { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", "user.attribute": "foo", "access.token.claim": "true", "claim.name": "resource_access.${client_id}.roles", @@ -1362,6 +1453,10 @@ "protocolMapper": "oidc-usermodel-realm-role-mapper", "consentRequired": false, "config": { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "lightweight.claim" : "true", "user.attribute": "foo", "access.token.claim": "true", "claim.name": "realm_access.roles", diff --git a/solution/smo/oam/pm/config/pmpr/token-cache/jwt.txt b/solution/smo/oam/pm/config/pmpr/token-cache/jwt.txt index 8c6b1c8..3c71de9 100644 --- a/solution/smo/oam/pm/config/pmpr/token-cache/jwt.txt +++ b/solution/smo/oam/pm/config/pmpr/token-cache/jwt.txt @@ -1 +1 @@ -eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJQQTBmYmN1RUNvU1BkZlFCVjgxaE5kVHJwY0wtaE9LS0pGVkFDdEc5bUFrIn0.eyJleHAiOjE3NDI5NjYwODksImlhdCI6MTc0Mjk2NTc4OSwianRpIjoiMWU1OWVmY2EtYWYwZS00Zjc0LTgwMTktNGQ4NzE3YjBhYTZhIiwiaXNzIjoiaHR0cDovL2lkZW50aXR5OjgwODAvcmVhbG1zL25vbnJ0cmljLXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjQ4NTQyYmQzLWUxNmEtNGYwYS05NzY4LWM4NDVhM2Y2ZThjYiIsInR5cCI6IkJlYXJlciIsImF6cCI6InBtLXByb2R1Y2VyLWpzb24ya2Fma2EiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtbm9ucnRyaWMtcmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwiY2xpZW50SG9zdCI6IjE3Mi4xOC4wLjE5IiwiY2xpZW50SWQiOiJwbS1wcm9kdWNlci1qc29uMmthZmthIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtcG0tcHJvZHVjZXItanNvbjJrYWZrYSIsImNsaWVudEFkZHJlc3MiOiIxNzIuMTguMC4xOSJ9.Y5ynDzNr9PRhsXNRDrWyi6HiIvWiA0vS-zP53Iq0NORSqNJTTdqC8oH1yuxlJYFqbs888uQ46CfyF_w7SocuvhvOHY_fouB8H4d9ENE3VAhK8SzfFFKUlRMLrReY9cgzjy64oT3jcJIfdbUpJzRn08eaozfq2WD-9pRqJHxN_9HhZgqKwaCoJS0u22WHEom-pmp0UNRN2o_W9xjEnNcvbX79-DLTWaHL5Bn98Vih4BmKPmDHYf43nmpXuMYPe2O8pwmaYpfAAPpjGXFs_hDGm_B-dPOt-RjHm_zSnh4I4DtKMuPKm6rFbs6hD-boFQUT2SlAfd1XSMdxdQxWzUWiIw \ No newline at end of file +eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWeDlCMUJNbjBLQlZtdHJXQ0xCTVRFWXloNFhiaDNOT2NYbXpudVlZUDZBIn0.eyJleHAiOjE3NDY0NDc4NDUsImlhdCI6MTc0NjQ0NzU0NSwianRpIjoiYTViNzg4ZWUtM2RkNy00NmM3LTk4ZTktMzIxZTc5NmFjNTEyIiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5zbW8uby1yYW4tc2Mub3JnL3JlYWxtcy9ub25ydHJpYy1yZWFsbSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIwNTUyMzJjYi1lMmE1LTQ2M2QtYTQxMi03ZGYwYjQ0ODhlOTYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJwbS1wcm9kdWNlci1qc29uMmthZmthIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW5vbnJ0cmljLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImNsaWVudEhvc3QiOiIxNzIuMjAuMC4xOSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LXBtLXByb2R1Y2VyLWpzb24ya2Fma2EiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjIwLjAuMTkiLCJjbGllbnRfaWQiOiJwbS1wcm9kdWNlci1qc29uMmthZmthIn0.kC4WihXFgbs-tHNK4FmxgbCoXPnJfCvoxhg-gTObfPYWCLnakdJCn-arC-RjOYHNulJGr5broTCvh336whVc4fK6kJMNR2mZ1LcRCm8Dt9qr_bYcJCp2PFqVjzybsOcP4rxjZFGIL7BNvSmc55ST4iXYSwtGeejuQ9iarAxPnU4dWSUJsEX4cO9XgL9P9QriQuZbqFa1YP_bDUahz_Y7mwncq4k_cp2_-weZmRGIRkxF3HiQDD-sOXt7CO8_mMXTJvASQnMipjl21H8f_6etMEgIRz5m1kfsjFvyWI3dogzS3L6BVaOFOO_iqTRIvB7IhsLoIOi2rylVgRbdSh8ztw \ No newline at end of file diff --git a/solution/smo/oam/pm/docker-compose-grafana.yaml b/solution/smo/oam/pm/docker-compose-grafana.yaml new file mode 100644 index 0000000..1658a74 --- /dev/null +++ b/solution/smo/oam/pm/docker-compose-grafana.yaml @@ -0,0 +1,38 @@ +services: + grafana: + image: grafana/grafana:10.0.0 + container_name: grafana + ports: + - "3000:3000" + environment: + GF_AUTH_GENERIC_OAUTH_ENABLED: "true" + GF_AUTH_GENERIC_OAUTH_NAME: "Keycloak" + GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP: "true" + GF_AUTH_GENERIC_OAUTH_CLIENT_ID: "grafana-ui.app" + GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: "lVPuFWZlOV7yAbV1FIuaM0FOodD7cLTm" + GF_AUTH_GENERIC_OAUTH_SCOPES: "openid profile email offline_access roles" + GF_AUTH_GENERIC_OAUTH_AUTH_URL: "https://identity.${HTTP_DOMAIN}/realms/onap/protocol/openid-connect/auth" + GF_AUTH_GENERIC_OAUTH_TOKEN_URL: "https://identity.${HTTP_DOMAIN}/realms/onap/protocol/openid-connect/token" + GF_AUTH_GENERIC_OAUTH_API_URL: "https://identity.${HTTP_DOMAIN}/realms/onap/protocol/openid-connect/userinfo" + GF_SERVER_ROOT_URL: "https://grafana.${HTTP_DOMAIN}" + GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE: role + GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH: contains(resource_access."grafana-ui.app".roles[*], 'grafanaadmin') && 'GrafanaAdmin' || contains(resource_access."grafana-ui.app".roles[*], 'admin') && 'Admin' || contains(resource_access."grafana.app".roles[*], 'editor') && 'Editor' || 'Viewer' + GF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_GRAFANA_ROLES: true + GF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_GRAFANA_ADMIN: true + GF_AUTH_GENERIC_OAUTH_TLS_SKIP_VERIFY_INSECURE: true + GF_LOG_LEVEL: debug + labels: + traefik.enable: true + traefik.http.routers.grafana.entrypoints: websecure + traefik.http.routers.grafana.rule: Host(`grafana.${HTTP_DOMAIN}`) + traefik.http.routers.grafana.tls: true + traefik.http.services.grafana.loadbalancer.server.port: 3000 + app: "grafana" + deploy: "o-ran-sc-smo-oam-pm" + solution: "o-ran-sc-smo" + networks: + - dmz + +networks: + dmz: + external: true diff --git a/solution/smo/oam/pm/setup.sh b/solution/smo/oam/pm/setup.sh index 6c7a4cf..9eeb0da 100755 --- a/solution/smo/oam/pm/setup.sh +++ b/solution/smo/oam/pm/setup.sh @@ -76,6 +76,11 @@ setup_influx() { docker compose -p influx -f docker-compose-influxdb_gen.yaml up -d } +setup_grafana() { + envsubst < docker-compose-grafana.yaml > docker-compose-grafana_gen.yaml + docker compose -p grafana -f docker-compose-grafana.yaml up -d +} + create_topics() { echo "Creating topics: $TOPICS, may take a while ..." for t in $TOPICS; do @@ -101,7 +106,7 @@ for net in $DNETWORKS; do if [ $? -ne 0 ]; then docker network create $net else - echo " Network: $net exits" + echo " Network: $net exists" fi done } @@ -141,4 +146,6 @@ export INFLUXDB2_TOKEN setup_pm check_error $? +setup_grafana +check_error $? \ No newline at end of file diff --git a/solution/smo/oam/pm/tear-down.sh b/solution/smo/oam/pm/tear-down.sh index a725eb1..baad2ba 100755 --- a/solution/smo/oam/pm/tear-down.sh +++ b/solution/smo/oam/pm/tear-down.sh @@ -2,6 +2,7 @@ echo "Stop and remove all containers in the project" docker compose -p influx -f docker-compose-influxdb_gen.yaml down docker compose -p pm -f docker-compose_gen.yaml down +docker compose -p grafana -f docker-compose-grafana_gen.yaml down echo "Removing influxdb2 config..." rm -rf ./config/influxdb2 -- 2.16.6