]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Improve sso role mapping
authorAfreen Misbah <afreen@ibm.com>
Mon, 3 Mar 2025 08:13:11 +0000 (13:43 +0530)
committerAfreen Misbah <afreen@ibm.com>
Mon, 10 Mar 2025 11:12:03 +0000 (16:42 +0530)
Fixes https://tracker.ceph.com/issues/70366

- adds support for ISV
- using jmespath expression for fetching roles from payload
- added roles_path parameter in sso enable command as optional argument
- modified roles mapper for sso login

Signed-off-by: Afreen Misbah <afreen@ibm.com>
ceph.spec.in
debian/control
src/pybind/mgr/dashboard/requirements.txt
src/pybind/mgr/dashboard/services/access_control.py
src/pybind/mgr/dashboard/services/auth/oauth2.py
src/pybind/mgr/dashboard/services/sso.py
src/pybind/mgr/tox.ini

index 6276dd0878a6ece1937b29669d14ea7201fa152e..d72b5abcc62228f7ce7e8e22aff0ac06e28cde3f 100644 (file)
@@ -694,6 +694,7 @@ Requires:       ceph-prometheus-alerts = %{_epoch_prefix}%{version}-%{release}
 %if 0%{?fedora} || 0%{?rhel} >= 9
 Requires:       python%{python3_pkgversion}-grpcio
 Requires:       python%{python3_pkgversion}-grpcio-tools
+Requires:       python%{python3_pkgversion}-jmespath
 %endif
 %if 0%{?fedora} || 0%{?rhel} || 0%{?openEuler}
 Requires:       python%{python3_pkgversion}-cherrypy
index a8c79f7a731a7777add44224b280fc84cdb8d335..88c4c92238f0f1cea70b86a74c3d277a64dd56ce 100644 (file)
@@ -99,6 +99,7 @@ Build-Depends: automake,
                python3-coverage <pkg.ceph.check>,
                python3-dateutil <pkg.ceph.check>,
                python3-grpcio <pkg.ceph.check>,
+               python3-jmespath (>=0.10) <pkg.ceph.check>,
                python3-openssl <pkg.ceph.check>,
                python3-prettytable <pkg.ceph.check>,
                python3-requests <pkg.ceph.check>,
index b5c78ac8bec405ea7704af45113db1e0acdb7343..8cf24eb6ae914130a2d804d548d4e84fbedc94f5 100644 (file)
@@ -13,3 +13,4 @@ setuptools
 jsonpatch
 grpcio==1.46.5
 grpcio-tools==1.46.5
+jmespath
index 6319802b6cc7ba0e309b01154d68f8efb2f67f6d..e6b714af9e7221984ea2d056768ba716c0aa1f5e 100644 (file)
@@ -194,12 +194,12 @@ class Role(object):
                     r_dict['scopes_permissions'])
 
     @classmethod
-    def map_to_system_roles(cls, roles) -> List['Role']:
+    def map_to_system_roles(cls, roles: List[str]) -> List['Role']:
         matches = []
-        for rn in SYSTEM_ROLES_NAMES:
+        for sys_role in ROLE_MAPPER:
             for role in roles:
-                if role in SYSTEM_ROLES_NAMES[rn]:
-                    matches.append(rn)
+                if role in ROLE_MAPPER[sys_role]:
+                    matches.append(sys_role)
         return matches
 
 
@@ -304,9 +304,16 @@ SYSTEM_ROLES = {
 }
 
 # static name-like roles list for role mapping
-SYSTEM_ROLES_NAMES = {
+ROLE_MAPPER = {
     ADMIN_ROLE: [ADMIN_ROLE.name, 'admin'],
-    READ_ONLY_ROLE: [READ_ONLY_ROLE.name, 'read', 'guest', 'monitor']
+    READ_ONLY_ROLE: [READ_ONLY_ROLE.name, 'read', 'guest', 'monitor'],
+    BLOCK_MGR_ROLE: [BLOCK_MGR_ROLE.name, 'block', 'rbd'],
+    RGW_MGR_ROLE: [RGW_MGR_ROLE.name, 'object', 'rgw'],
+    CLUSTER_MGR_ROLE: [CLUSTER_MGR_ROLE.name, 'cluster'],
+    POOL_MGR_ROLE: [POOL_MGR_ROLE.name, 'pool'],
+    CEPHFS_MGR_ROLE: [CEPHFS_MGR_ROLE.name, 'cephfs'],
+    GANESHA_MGR_ROLE: [GANESHA_MGR_ROLE.name, 'ganesha', 'nfs'],
+    SMB_MGR_ROLE: [SMB_MGR_ROLE.name, 'smb']
 }
 
 
index 5376107667e0040fa57b69f464b00c6f1adef8d8..26027adf603d99366a615823e4f961ff88870c27 100644 (file)
@@ -1,4 +1,7 @@
+
+import importlib
 import json
+import logging
 from typing import Dict, List
 from urllib.parse import quote
 
@@ -10,6 +13,13 @@ from ...services.auth import BaseAuth, SSOAuth, decode_jwt_segment
 from ...tools import prepare_url_prefix
 from ..access_control import Role, User, UserAlreadyExists
 
+try:
+    jmespath = importlib.import_module("jmespath")
+except ModuleNotFoundError:
+    logging.error("Module 'jmespath' is not installed.")
+
+logger = logging.getLogger('services.oauth2')
+
 
 class OAuth2(SSOAuth):
     LOGIN_URL = 'auth/oauth2/login'
@@ -17,19 +27,29 @@ class OAuth2(SSOAuth):
     sso = True
 
     class OAuth2Config(BaseAuth.Config):
-        pass
+        roles_path: str
+
+    def __init__(self, roles_path=None):
+        self.roles_path = roles_path
+
+    def get_roles_path(self):
+        return self.roles_path
 
     @staticmethod
     def enabled():
         return mgr.get_module_option('sso_oauth2')
 
-    def to_dict(self) -> 'BaseAuth.Config':
-        return self.OAuth2Config()
+    def to_dict(self) -> 'OAuth2Config':
+        return {
+            'roles_path': self.roles_path
+        }
 
     @classmethod
     def from_dict(cls, s_dict: OAuth2Config) -> 'OAuth2':
-        # pylint: disable=unused-argument
-        return OAuth2()
+        try:
+            return OAuth2(s_dict['roles_path'])
+        except KeyError:
+            return OAuth2({})
 
     @classmethod
     def get_auth_name(cls):
@@ -66,25 +86,30 @@ class OAuth2(SSOAuth):
 
     @classmethod
     def get_user_roles(cls):
-        roles: List[Role] = []
+        roles: List[str] = []
         user_roles: List[Role] = []
         try:
             jwt_payload = cherrypy.request.jwt_payload
         except AttributeError:
-            raise cherrypy.HTTPError()
-
-        # check for client roes
-        if 'resource_access' in jwt_payload:
-            # Find the first value where the key is not 'account'
-            roles = next((value['roles'] for key, value in jwt_payload['resource_access'].items()
-                          if key != "account"), user_roles)
-        # check for global roles
-        elif 'realm_access' in jwt_payload:
-            roles = next((value['roles'] for _, value in jwt_payload['realm_access'].items()),
-                         user_roles)
+            raise cherrypy.HTTPError(401)
+
+        if jmespath and hasattr(mgr.SSO_DB.config, 'roles_path'):
+            logger.debug("Using 'roles_path' to fetch roles")
+            roles = jmespath.search(mgr.SSO_DB.config.roles_path, jwt_payload)
+        # e.g Keycloak
+        elif 'resource_access' in jwt_payload or 'realm_access' in jwt_payload:
+            logger.debug("Using 'resource_access' or 'realm_access' to fetch roles")
+            roles = jmespath.search(
+                "resource_access.*[?@!='account'].roles[] || realm_access.roles[]",
+                jwt_payload)
+        elif 'roles' in jwt_payload:
+            logger.debug("Using 'roles' to fetch roles")
+            roles = jwt_payload['roles']
+            if isinstance(roles, str):
+                roles = [roles]
         else:
-            raise cherrypy.HTTPError()
-        user_roles = Role.map_to_system_roles(roles)
+            raise cherrypy.HTTPError(403)
+        user_roles = Role.map_to_system_roles(roles or [])
         return user_roles
 
     @classmethod
@@ -106,6 +131,7 @@ class OAuth2(SSOAuth):
             user = mgr.ACCESS_CTRL_DB.create_user(
                 jwt_payload['sub'], None, jwt_payload['name'], jwt_payload['email'])
         except UserAlreadyExists:
+            logger.debug("User already exists")
             user = mgr.ACCESS_CTRL_DB.get_user(jwt_payload['sub'])
         user.set_roles(cls.get_user_roles())
         # set user last update to token time issued
index 0b607e217df76c149aad391e3ca77455d7f75a9a..29ec62d16990580067b64b6f13386b8b66104b3b 100644 (file)
@@ -2,12 +2,13 @@
 # pylint: disable=too-many-return-statements,too-many-branches
 
 import errno
+import importlib
 import json
 import logging
 import os
 import threading
 import warnings
-from typing import Dict
+from typing import Dict, Optional
 from urllib import parse
 
 from mgr_module import CLIWriteCommand, HandleCommandResult
@@ -20,6 +21,12 @@ from ..tools import prepare_url_prefix
 
 logger = logging.getLogger('sso')
 
+try:
+    jmespath = importlib.import_module('jmespath')
+    JMESPathError = getattr(jmespath.exceptions, "JMESPathError")
+except ModuleNotFoundError:
+    logger.error("Module 'jmespath' is not installed.")
+
 try:
     from onelogin.saml2.errors import OneLogin_Saml2_Error as Saml2Error
     from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser as Saml2Parser
@@ -88,8 +95,14 @@ def load_sso_db():
 
 
 @CLIWriteCommand("dashboard sso enable oauth2")
-def enable_sso(_):
+def enable_sso(_, roles_path: Optional[str] = None):
     mgr.SSO_DB.protocol = AuthType.OAUTH2
+    if jmespath and roles_path:
+        try:
+            jmespath.compile(roles_path)
+            mgr.SSO_DB.config.roles_path = roles_path
+        except (JMESPathError, SyntaxError):
+            return HandleCommandResult(stdout='Syntax invalid for "roles_path"')
     mgr.SSO_DB.save()
     mgr.set_module_option('sso_oauth2', True)
     return HandleCommandResult(stdout='SSO is "enabled" with "OAuth2" protocol.')
index 5afbe93ace004905cee391feececf20a29118c42..366dc1ff87121597591bd8c569e0e442c5650e8c 100644 (file)
@@ -81,6 +81,7 @@ deps =
     types-requests
     types-PyYAML
     types-jwt
+    types-jmespath
 commands =
     mypy --config-file=../../mypy.ini \
            -m alerts \