Skip to content

Commit 4d8d39a

Browse files
authored
Merge pull request ceph#62085 from afreen23/wip-sso
mgr/dashboard: Improve sso role mapping
2 parents c8a5017 + f65b00e commit 4d8d39a

File tree

7 files changed

+77
-27
lines changed

7 files changed

+77
-27
lines changed

ceph.spec.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,7 @@ Requires: ceph-prometheus-alerts = %{_epoch_prefix}%{version}-%{release}
694694
%if 0%{?fedora} || 0%{?rhel} >= 9
695695
Requires: python%{python3_pkgversion}-grpcio
696696
Requires: python%{python3_pkgversion}-grpcio-tools
697+
Requires: python%{python3_pkgversion}-jmespath
697698
%endif
698699
%if 0%{?fedora} || 0%{?rhel} || 0%{?openEuler}
699700
Requires: python%{python3_pkgversion}-cherrypy

debian/control

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Build-Depends: automake,
9999
python3-coverage <pkg.ceph.check>,
100100
python3-dateutil <pkg.ceph.check>,
101101
python3-grpcio <pkg.ceph.check>,
102+
python3-jmespath (>=0.10) <pkg.ceph.check>,
102103
python3-openssl <pkg.ceph.check>,
103104
python3-prettytable <pkg.ceph.check>,
104105
python3-requests <pkg.ceph.check>,

src/pybind/mgr/dashboard/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ setuptools
1313
jsonpatch
1414
grpcio==1.46.5
1515
grpcio-tools==1.46.5
16+
jmespath

src/pybind/mgr/dashboard/services/access_control.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,12 @@ def from_dict(cls, r_dict):
194194
r_dict['scopes_permissions'])
195195

196196
@classmethod
197-
def map_to_system_roles(cls, roles) -> List['Role']:
197+
def map_to_system_roles(cls, roles: List[str]) -> List['Role']:
198198
matches = []
199-
for rn in SYSTEM_ROLES_NAMES:
199+
for sys_role in ROLE_MAPPER:
200200
for role in roles:
201-
if role in SYSTEM_ROLES_NAMES[rn]:
202-
matches.append(rn)
201+
if role in ROLE_MAPPER[sys_role]:
202+
matches.append(sys_role)
203203
return matches
204204

205205

@@ -304,9 +304,16 @@ def map_to_system_roles(cls, roles) -> List['Role']:
304304
}
305305

306306
# static name-like roles list for role mapping
307-
SYSTEM_ROLES_NAMES = {
307+
ROLE_MAPPER = {
308308
ADMIN_ROLE: [ADMIN_ROLE.name, 'admin'],
309-
READ_ONLY_ROLE: [READ_ONLY_ROLE.name, 'read', 'guest', 'monitor']
309+
READ_ONLY_ROLE: [READ_ONLY_ROLE.name, 'read', 'guest', 'monitor'],
310+
BLOCK_MGR_ROLE: [BLOCK_MGR_ROLE.name, 'block', 'rbd'],
311+
RGW_MGR_ROLE: [RGW_MGR_ROLE.name, 'object', 'rgw'],
312+
CLUSTER_MGR_ROLE: [CLUSTER_MGR_ROLE.name, 'cluster'],
313+
POOL_MGR_ROLE: [POOL_MGR_ROLE.name, 'pool'],
314+
CEPHFS_MGR_ROLE: [CEPHFS_MGR_ROLE.name, 'cephfs'],
315+
GANESHA_MGR_ROLE: [GANESHA_MGR_ROLE.name, 'ganesha', 'nfs'],
316+
SMB_MGR_ROLE: [SMB_MGR_ROLE.name, 'smb']
310317
}
311318

312319

src/pybind/mgr/dashboard/services/auth/oauth2.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
2+
import importlib
13
import json
4+
import logging
25
from typing import Dict, List
36
from urllib.parse import quote
47

@@ -10,26 +13,43 @@
1013
from ...tools import prepare_url_prefix
1114
from ..access_control import Role, User, UserAlreadyExists
1215

16+
try:
17+
jmespath = importlib.import_module("jmespath")
18+
except ModuleNotFoundError:
19+
logging.error("Module 'jmespath' is not installed.")
20+
21+
logger = logging.getLogger('services.oauth2')
22+
1323

1424
class OAuth2(SSOAuth):
1525
LOGIN_URL = 'auth/oauth2/login'
1626
LOGOUT_URL = 'auth/oauth2/logout'
1727
sso = True
1828

1929
class OAuth2Config(BaseAuth.Config):
20-
pass
30+
roles_path: str
31+
32+
def __init__(self, roles_path=None):
33+
self.roles_path = roles_path
34+
35+
def get_roles_path(self):
36+
return self.roles_path
2137

2238
@staticmethod
2339
def enabled():
2440
return mgr.get_module_option('sso_oauth2')
2541

26-
def to_dict(self) -> 'BaseAuth.Config':
27-
return self.OAuth2Config()
42+
def to_dict(self) -> 'OAuth2Config':
43+
return {
44+
'roles_path': self.roles_path
45+
}
2846

2947
@classmethod
3048
def from_dict(cls, s_dict: OAuth2Config) -> 'OAuth2':
31-
# pylint: disable=unused-argument
32-
return OAuth2()
49+
try:
50+
return OAuth2(s_dict['roles_path'])
51+
except KeyError:
52+
return OAuth2({})
3353

3454
@classmethod
3555
def get_auth_name(cls):
@@ -66,25 +86,30 @@ def set_token_payload(cls, token):
6686

6787
@classmethod
6888
def get_user_roles(cls):
69-
roles: List[Role] = []
89+
roles: List[str] = []
7090
user_roles: List[Role] = []
7191
try:
7292
jwt_payload = cherrypy.request.jwt_payload
7393
except AttributeError:
74-
raise cherrypy.HTTPError()
75-
76-
# check for client roes
77-
if 'resource_access' in jwt_payload:
78-
# Find the first value where the key is not 'account'
79-
roles = next((value['roles'] for key, value in jwt_payload['resource_access'].items()
80-
if key != "account"), user_roles)
81-
# check for global roles
82-
elif 'realm_access' in jwt_payload:
83-
roles = next((value['roles'] for _, value in jwt_payload['realm_access'].items()),
84-
user_roles)
94+
raise cherrypy.HTTPError(401)
95+
96+
if jmespath and hasattr(mgr.SSO_DB.config, 'roles_path'):
97+
logger.debug("Using 'roles_path' to fetch roles")
98+
roles = jmespath.search(mgr.SSO_DB.config.roles_path, jwt_payload)
99+
# e.g Keycloak
100+
elif 'resource_access' in jwt_payload or 'realm_access' in jwt_payload:
101+
logger.debug("Using 'resource_access' or 'realm_access' to fetch roles")
102+
roles = jmespath.search(
103+
"resource_access.*[?@!='account'].roles[] || realm_access.roles[]",
104+
jwt_payload)
105+
elif 'roles' in jwt_payload:
106+
logger.debug("Using 'roles' to fetch roles")
107+
roles = jwt_payload['roles']
108+
if isinstance(roles, str):
109+
roles = [roles]
85110
else:
86-
raise cherrypy.HTTPError()
87-
user_roles = Role.map_to_system_roles(roles)
111+
raise cherrypy.HTTPError(403)
112+
user_roles = Role.map_to_system_roles(roles or [])
88113
return user_roles
89114

90115
@classmethod
@@ -106,6 +131,7 @@ def _create_user(cls):
106131
user = mgr.ACCESS_CTRL_DB.create_user(
107132
jwt_payload['sub'], None, jwt_payload['name'], jwt_payload['email'])
108133
except UserAlreadyExists:
134+
logger.debug("User already exists")
109135
user = mgr.ACCESS_CTRL_DB.get_user(jwt_payload['sub'])
110136
user.set_roles(cls.get_user_roles())
111137
# set user last update to token time issued

src/pybind/mgr/dashboard/services/sso.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
# pylint: disable=too-many-return-statements,too-many-branches
33

44
import errno
5+
import importlib
56
import json
67
import logging
78
import os
89
import threading
910
import warnings
10-
from typing import Dict
11+
from typing import Dict, Optional
1112
from urllib import parse
1213

1314
from mgr_module import CLIWriteCommand, HandleCommandResult
@@ -20,6 +21,12 @@
2021

2122
logger = logging.getLogger('sso')
2223

24+
try:
25+
jmespath = importlib.import_module('jmespath')
26+
JMESPathError = getattr(jmespath.exceptions, "JMESPathError")
27+
except ModuleNotFoundError:
28+
logger.error("Module 'jmespath' is not installed.")
29+
2330
try:
2431
from onelogin.saml2.errors import OneLogin_Saml2_Error as Saml2Error
2532
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser as Saml2Parser
@@ -88,8 +95,14 @@ def load_sso_db():
8895

8996

9097
@CLIWriteCommand("dashboard sso enable oauth2")
91-
def enable_sso(_):
98+
def enable_sso(_, roles_path: Optional[str] = None):
9299
mgr.SSO_DB.protocol = AuthType.OAUTH2
100+
if jmespath and roles_path:
101+
try:
102+
jmespath.compile(roles_path)
103+
mgr.SSO_DB.config.roles_path = roles_path
104+
except (JMESPathError, SyntaxError):
105+
return HandleCommandResult(stdout='Syntax invalid for "roles_path"')
93106
mgr.SSO_DB.save()
94107
mgr.set_module_option('sso_oauth2', True)
95108
return HandleCommandResult(stdout='SSO is "enabled" with "OAuth2" protocol.')

src/pybind/mgr/tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ deps =
8181
types-requests
8282
types-PyYAML
8383
types-jwt
84+
types-jmespath
8485
commands =
8586
mypy --config-file=../../mypy.ini \
8687
-m alerts \

0 commit comments

Comments
 (0)