Skip to content

Commit 82e3d59

Browse files
authored
Merge pull request ceph#58456 from rhcs-dashboard/auth2-sso
mgr/dashboard: Add SSO through oauth2 protocol Reviewed-by: afreen23 <NOT@FOUND> Reviewed-by: Ernesto Puerta <[email protected]> Reviewed-by: Nizamudeen A <[email protected]> Reviewed-by: Redouane Kachach <[email protected]>
2 parents 421e7c5 + a376752 commit 82e3d59

File tree

14 files changed

+431
-86
lines changed

14 files changed

+431
-86
lines changed

qa/tasks/mgr/dashboard/test_auth.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ def test_logout(self):
152152
self._post("/api/auth/logout")
153153
self.assertStatus(200)
154154
self.assertJsonBody({
155-
"redirect_url": "#/login"
155+
"redirect_url": "#/login",
156+
"protocol": 'local'
156157
})
157158
self._get("/api/host", version='1.1')
158159
self.assertStatus(401)
@@ -167,7 +168,8 @@ def test_logout(self):
167168
self._post("/api/auth/logout", set_cookies=True)
168169
self.assertStatus(200)
169170
self.assertJsonBody({
170-
"redirect_url": "#/login"
171+
"redirect_url": "#/login",
172+
"protocol": 'local'
171173
})
172174
self._get("/api/host", set_cookies=True, version='1.1')
173175
self.assertStatus(401)

src/pybind/mgr/dashboard/controllers/auth.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from .. import mgr
1212
from ..exceptions import InvalidCredentialsError, UserDoesNotExist
13-
from ..services.auth import AuthManager, JwtManager
13+
from ..services.auth import AuthManager, AuthType, BaseAuth, JwtManager, OAuth2
1414
from ..services.cluster import ClusterModel
1515
from ..settings import Settings
1616
from . import APIDoc, APIRouter, ControllerAuthMixin, EndpointDoc, RESTController, allow_empty_body
@@ -132,7 +132,7 @@ def create(self, username, password, ttl: Optional[int] = None):
132132
'username': username,
133133
'permissions': user_perms,
134134
'pwdExpirationDate': pwd_expiration_date,
135-
'sso': mgr.SSO_DB.protocol == 'saml2',
135+
'sso': BaseAuth.from_protocol(mgr.SSO_DB.protocol).sso,
136136
'pwdUpdateRequired': pwd_update_required
137137
}
138138
mgr.ACCESS_CTRL_DB.increment_attempt(username)
@@ -156,37 +156,33 @@ def create(self, username, password, ttl: Optional[int] = None):
156156
@RESTController.Collection('POST')
157157
@allow_empty_body
158158
def logout(self):
159-
logger.debug('Logout successful')
160-
token = JwtManager.get_token_from_header()
159+
logger.debug('Logout started')
160+
token = JwtManager.get_token(cherrypy.request)
161161
JwtManager.blocklist_token(token)
162162
self._delete_token_cookie(token)
163-
redirect_url = '#/login'
164-
if mgr.SSO_DB.protocol == 'saml2':
165-
redirect_url = 'auth/saml2/slo'
166163
return {
167-
'redirect_url': redirect_url
164+
'redirect_url': BaseAuth.from_db(mgr.SSO_DB).LOGOUT_URL,
165+
'protocol': BaseAuth.from_db(mgr.SSO_DB).get_auth_name()
168166
}
169167

170-
def _get_login_url(self):
171-
if mgr.SSO_DB.protocol == 'saml2':
172-
return 'auth/saml2/login'
173-
return '#/login'
174-
175168
@RESTController.Collection('POST', query_params=['token'])
176169
@EndpointDoc("Check token Authentication",
177170
parameters={'token': (str, 'Authentication Token')},
178171
responses={201: AUTH_CHECK_SCHEMA})
179172
def check(self, token):
180173
if token:
181-
user = JwtManager.get_user(token)
174+
if mgr.SSO_DB.protocol == AuthType.OAUTH2:
175+
user = OAuth2.get_user(token)
176+
else:
177+
user = JwtManager.get_user(token)
182178
if user:
183179
return {
184180
'username': user.username,
185181
'permissions': user.permissions_dict(),
186-
'sso': mgr.SSO_DB.protocol == 'saml2',
182+
'sso': BaseAuth.from_db(mgr.SSO_DB).sso,
187183
'pwdUpdateRequired': user.pwd_update_required
188184
}
189185
return {
190-
'login_url': self._get_login_url(),
186+
'login_url': BaseAuth.from_db(mgr.SSO_DB).LOGIN_URL,
191187
'cluster_status': ClusterModel.from_db().dict()['status']
192188
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import cherrypy
2+
3+
from dashboard.exceptions import DashboardException
4+
from dashboard.services.auth.oauth2 import OAuth2
5+
6+
from . import Endpoint, RESTController, Router
7+
8+
9+
@Router('/auth/oauth2', secure=False)
10+
class Oauth2(RESTController):
11+
12+
@Endpoint(json_response=False, version=None)
13+
def login(self):
14+
if not OAuth2.enabled():
15+
raise DashboardException(500, msg='Failed to login: SSO OAuth2 is not enabled')
16+
17+
token = OAuth2.get_token(cherrypy.request)
18+
if not token:
19+
raise cherrypy.HTTPError()
20+
21+
raise cherrypy.HTTPRedirect(OAuth2.get_login_redirect_url(token))
22+
23+
@Endpoint(json_response=False, version=None)
24+
def logout(self):
25+
if not OAuth2.enabled():
26+
raise DashboardException(500, msg='Failed to logout: SSO OAuth2 is not enabled')
27+
28+
token = OAuth2.get_token(cherrypy.request)
29+
if not token:
30+
raise cherrypy.HTTPError()
31+
32+
raise cherrypy.HTTPRedirect(OAuth2.get_logout_redirect_url(token))

src/pybind/mgr/dashboard/controllers/saml2.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def _check_python_saml():
3737
if not python_saml_imported:
3838
raise cherrypy.HTTPError(400, 'Required library not found: `python3-saml`')
3939
try:
40-
OneLogin_Saml2_Settings(mgr.SSO_DB.saml2.onelogin_settings)
40+
OneLogin_Saml2_Settings(mgr.SSO_DB.config.onelogin_settings)
4141
except OneLogin_Saml2_Error:
4242
raise cherrypy.HTTPError(400, 'Single Sign-On is not configured.')
4343

@@ -46,19 +46,19 @@ def _check_python_saml():
4646
def auth_response(self, **kwargs):
4747
Saml2._check_python_saml()
4848
req = Saml2._build_req(self._request, kwargs)
49-
auth = OneLogin_Saml2_Auth(req, mgr.SSO_DB.saml2.onelogin_settings)
49+
auth = OneLogin_Saml2_Auth(req, mgr.SSO_DB.config.onelogin_settings)
5050
auth.process_response()
5151
errors = auth.get_errors()
5252

5353
if auth.is_authenticated():
5454
JwtManager.reset_user()
55-
username_attribute = auth.get_attribute(mgr.SSO_DB.saml2.get_username_attribute())
55+
username_attribute = auth.get_attribute(mgr.SSO_DB.config.get_username_attribute())
5656
if username_attribute is None:
5757
raise cherrypy.HTTPError(400,
5858
'SSO error - `{}` not found in auth attributes. '
5959
'Received attributes: {}'
6060
.format(
61-
mgr.SSO_DB.saml2.get_username_attribute(),
61+
mgr.SSO_DB.config.get_username_attribute(),
6262
auth.get_attributes()))
6363
username = username_attribute[0]
6464
url_prefix = prepare_url_prefix(mgr.get_module_option('url_prefix', default=''))
@@ -85,29 +85,29 @@ def auth_response(self, **kwargs):
8585
@Endpoint(xml=True, version=None)
8686
def metadata(self):
8787
Saml2._check_python_saml()
88-
saml_settings = OneLogin_Saml2_Settings(mgr.SSO_DB.saml2.onelogin_settings)
88+
saml_settings = OneLogin_Saml2_Settings(mgr.SSO_DB.config.onelogin_settings)
8989
return saml_settings.get_sp_metadata()
9090

9191
@Endpoint(json_response=False, version=None)
9292
def login(self):
9393
Saml2._check_python_saml()
9494
req = Saml2._build_req(self._request, {})
95-
auth = OneLogin_Saml2_Auth(req, mgr.SSO_DB.saml2.onelogin_settings)
95+
auth = OneLogin_Saml2_Auth(req, mgr.SSO_DB.config.onelogin_settings)
9696
raise cherrypy.HTTPRedirect(auth.login())
9797

9898
@Endpoint(json_response=False, version=None)
9999
def slo(self):
100100
Saml2._check_python_saml()
101101
req = Saml2._build_req(self._request, {})
102-
auth = OneLogin_Saml2_Auth(req, mgr.SSO_DB.saml2.onelogin_settings)
102+
auth = OneLogin_Saml2_Auth(req, mgr.SSO_DB.config.onelogin_settings)
103103
raise cherrypy.HTTPRedirect(auth.logout())
104104

105105
@Endpoint(json_response=False, version=None)
106106
def logout(self, **kwargs):
107107
# pylint: disable=unused-argument
108108
Saml2._check_python_saml()
109109
JwtManager.reset_user()
110-
token = JwtManager.get_token_from_header()
110+
token = JwtManager.get_token(cherrypy.request)
111111
self._delete_token_cookie(token)
112112
url_prefix = prepare_url_prefix(mgr.get_module_option('url_prefix', default=''))
113113
raise cherrypy.HTTPRedirect("{}/#/login".format(url_prefix))

src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export class AuthService {
4242
logout(callback: Function = null) {
4343
return this.http.post('api/auth/logout', null).subscribe((resp: any) => {
4444
this.authStorageService.remove();
45+
if (resp.protocol == 'oauth2') {
46+
return window.location.replace(resp.redirect_url);
47+
}
4548
const url = _.get(this.route.snapshot.queryParams, 'returnUrl', '/login');
4649
this.router.navigate([url], { skipLocationChange: true });
4750
if (callback) {

src/pybind/mgr/dashboard/module.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ class Module(MgrModule, CherryPyConfig):
275275
min=400, max=599),
276276
Option(name='redirect_resolve_ip_addr', type='bool', default=False),
277277
Option(name='cross_origin_url', type='str', default=''),
278+
Option(name='sso_oauth2', type='bool', default=False),
278279
]
279280
MODULE_OPTIONS.extend(options_schema_list())
280281
for options in PLUGIN_MANAGER.hook.get_options() or []:

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ def from_dict(cls, r_dict):
193193
return Role(r_dict['name'], r_dict['description'],
194194
r_dict['scopes_permissions'])
195195

196+
@classmethod
197+
def map_to_system_roles(cls, roles) -> List['Role']:
198+
matches = []
199+
for rn in SYSTEM_ROLES_NAMES:
200+
for role in roles:
201+
if role in SYSTEM_ROLES_NAMES[rn]:
202+
matches.append(rn)
203+
return matches
204+
196205

197206
# static pre-defined system roles
198207
# this roles cannot be deleted nor updated
@@ -283,6 +292,12 @@ def from_dict(cls, r_dict):
283292
GANESHA_MGR_ROLE.name: GANESHA_MGR_ROLE,
284293
}
285294

295+
# static name-like roles list for role mapping
296+
SYSTEM_ROLES_NAMES = {
297+
ADMIN_ROLE: [ADMIN_ROLE.name, 'admin'],
298+
READ_ONLY_ROLE: [READ_ONLY_ROLE.name, 'read', 'guest', 'monitor']
299+
}
300+
286301

287302
class User(object):
288303
def __init__(self, username, password, name=None, email=None, roles=None,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from .auth import AuthManager, AuthManagerTool, AuthType, BaseAuth, \
2+
JwtManager, SSOAuth, decode_jwt_segment
3+
from .oauth2 import OAuth2
4+
from .saml2 import Saml2
5+
6+
__all__ = [
7+
'AuthManager',
8+
'AuthManagerTool',
9+
'AuthType',
10+
'BaseAuth',
11+
'SSOAuth',
12+
'JwtManager',
13+
'decode_jwt_segment',
14+
'Saml2',
15+
'OAuth2'
16+
]

0 commit comments

Comments
 (0)