1 # -*- coding: utf-8 -*-
2 # pylint: disable=too-many-return-statements,too-many-branches
3 from __future__
import absolute_import
13 from six
.moves
.urllib
import parse
16 from ..tools
import prepare_url_prefix
20 FileNotFoundError
= IOError # pylint: disable=redefined-builtin
22 logger
= logging
.getLogger('sso')
25 from onelogin
.saml2
.settings
import OneLogin_Saml2_Settings
as Saml2Settings
26 from onelogin
.saml2
.errors
import OneLogin_Saml2_Error
as Saml2Error
27 from onelogin
.saml2
.idp_metadata_parser
import OneLogin_Saml2_IdPMetadataParser
as Saml2Parser
29 python_saml_imported
= True
31 python_saml_imported
= False
35 def __init__(self
, onelogin_settings
):
36 self
.onelogin_settings
= onelogin_settings
38 def get_username_attribute(self
):
39 return self
.onelogin_settings
['sp']['attributeConsumingService']['requestedAttributes'][0][
44 'onelogin_settings': self
.onelogin_settings
48 def from_dict(cls
, s_dict
):
49 return Saml2(s_dict
['onelogin_settings'])
54 SSODB_CONFIG_KEY
= "ssodb_v"
56 def __init__(self
, version
, protocol
, saml2
):
57 self
.version
= version
58 self
.protocol
= protocol
60 self
.lock
= threading
.RLock()
65 'protocol': self
.protocol
,
66 'saml2': self
.saml2
.to_dict(),
67 'version': self
.version
69 mgr
.set_store(self
.ssodb_config_key(), json
.dumps(db
))
72 def ssodb_config_key(cls
, version
=None):
75 return "{}{}".format(cls
.SSODB_CONFIG_KEY
, version
)
77 def check_and_update_db(self
):
78 logger
.debug("Checking for previous DB versions")
80 raise NotImplementedError()
84 logger
.info("Loading SSO DB version=%s", cls
.VERSION
)
86 json_db
= mgr
.get_store(cls
.ssodb_config_key(), None)
88 logger
.debug("No DB v%s found, creating new...", cls
.VERSION
)
89 db
= cls(cls
.VERSION
, '', Saml2({}))
90 # check if we can update from a previous version database
91 db
.check_and_update_db()
94 dict_db
= json
.loads(json_db
) # type: dict
95 return cls(dict_db
['version'], dict_db
.get('protocol'),
96 Saml2
.from_dict(dict_db
.get('saml2')))
100 mgr
.SSO_DB
= SsoDB
.load()
105 'cmd': 'dashboard sso enable saml2',
106 'desc': 'Enable SAML2 Single Sign-On',
110 'cmd': 'dashboard sso disable',
111 'desc': 'Disable Single Sign-On',
115 'cmd': 'dashboard sso status',
116 'desc': 'Get Single Sign-On status',
120 'cmd': 'dashboard sso show saml2',
121 'desc': 'Show SAML2 configuration',
125 'cmd': 'dashboard sso setup saml2 '
126 'name=ceph_dashboard_base_url,type=CephString '
127 'name=idp_metadata,type=CephString '
128 'name=idp_username_attribute,type=CephString,req=false '
129 'name=idp_entity_id,type=CephString,req=false '
130 'name=sp_x_509_cert,type=CephFilepath,req=false '
131 'name=sp_private_key,type=CephFilepath,req=false',
132 'desc': 'Setup SAML2 Single Sign-On',
138 def _get_optional_attr(cmd
, attr
, default
):
145 def handle_sso_command(cmd
):
146 if cmd
['prefix'] not in ['dashboard sso enable saml2',
147 'dashboard sso disable',
148 'dashboard sso status',
149 'dashboard sso show saml2',
150 'dashboard sso setup saml2']:
151 return -errno
.ENOSYS
, '', ''
153 if cmd
['prefix'] == 'dashboard sso disable':
154 mgr
.SSO_DB
.protocol
= ''
156 return 0, 'SSO is "disabled".', ''
158 if not python_saml_imported
:
159 return -errno
.EPERM
, '', 'Required library not found: `python3-saml`'
161 if cmd
['prefix'] == 'dashboard sso enable saml2':
163 Saml2Settings(mgr
.SSO_DB
.saml2
.onelogin_settings
)
165 return -errno
.EPERM
, '', 'Single Sign-On is not configured: ' \
166 'use `ceph dashboard sso setup saml2`'
167 mgr
.SSO_DB
.protocol
= 'saml2'
169 return 0, 'SSO is "enabled" with "SAML2" protocol.', ''
171 if cmd
['prefix'] == 'dashboard sso status':
172 if mgr
.SSO_DB
.protocol
== 'saml2':
173 return 0, 'SSO is "enabled" with "SAML2" protocol.', ''
175 return 0, 'SSO is "disabled".', ''
177 if cmd
['prefix'] == 'dashboard sso show saml2':
178 return 0, json
.dumps(mgr
.SSO_DB
.saml2
.to_dict()), ''
180 if cmd
['prefix'] == 'dashboard sso setup saml2':
181 ceph_dashboard_base_url
= cmd
['ceph_dashboard_base_url']
182 idp_metadata
= cmd
['idp_metadata']
183 idp_username_attribute
= _get_optional_attr(cmd
, 'idp_username_attribute', 'uid')
184 idp_entity_id
= _get_optional_attr(cmd
, 'idp_entity_id', None)
185 sp_x_509_cert_path
= _get_optional_attr(cmd
, 'sp_x_509_cert', '')
186 sp_private_key_path
= _get_optional_attr(cmd
, 'sp_private_key', '')
187 if sp_x_509_cert_path
and not sp_private_key_path
:
188 return -errno
.EINVAL
, '', 'Missing parameter `sp_private_key`.'
189 if not sp_x_509_cert_path
and sp_private_key_path
:
190 return -errno
.EINVAL
, '', 'Missing parameter `sp_x_509_cert`.'
191 has_sp_cert
= sp_x_509_cert_path
!= "" and sp_private_key_path
!= ""
194 with
open(sp_x_509_cert_path
, 'r', encoding
='utf-8') as f
:
195 sp_x_509_cert
= f
.read()
196 except FileNotFoundError
:
197 return -errno
.EINVAL
, '', '`{}` not found.'.format(sp_x_509_cert_path
)
199 with
open(sp_private_key_path
, 'r', encoding
='utf-8') as f
:
200 sp_private_key
= f
.read()
201 except FileNotFoundError
:
202 return -errno
.EINVAL
, '', '`{}` not found.'.format(sp_private_key_path
)
207 if os
.path
.isfile(idp_metadata
):
209 "Please prepend 'file://' to indicate a local SAML2 IdP file", DeprecationWarning)
210 with
open(idp_metadata
, 'r', encoding
='utf-8') as f
:
211 idp_settings
= Saml2Parser
.parse(f
.read(), entity_id
=idp_entity_id
)
212 elif parse
.urlparse(idp_metadata
)[0] in ('http', 'https', 'file'):
213 idp_settings
= Saml2Parser
.parse_remote(
214 url
=idp_metadata
, validate_cert
=False, entity_id
=idp_entity_id
)
216 idp_settings
= Saml2Parser
.parse(idp_metadata
, entity_id
=idp_entity_id
)
218 url_prefix
= prepare_url_prefix(mgr
.get_module_option('url_prefix', default
=''))
221 'entityId': '{}{}/auth/saml2/metadata'.format(ceph_dashboard_base_url
, url_prefix
),
222 'assertionConsumerService': {
223 'url': '{}{}/auth/saml2'.format(ceph_dashboard_base_url
, url_prefix
),
224 'binding': "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
226 'attributeConsumingService': {
227 'serviceName': "Ceph Dashboard",
228 "serviceDescription": "Ceph Dashboard Service",
229 "requestedAttributes": [
231 "name": idp_username_attribute
,
236 'singleLogoutService': {
237 'url': '{}{}/auth/saml2/logout'.format(ceph_dashboard_base_url
, url_prefix
),
238 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
240 "x509cert": sp_x_509_cert
,
241 "privateKey": sp_private_key
244 "nameIdEncrypted": has_sp_cert
,
245 "authnRequestsSigned": has_sp_cert
,
246 "logoutRequestSigned": has_sp_cert
,
247 "logoutResponseSigned": has_sp_cert
,
248 "signMetadata": has_sp_cert
,
249 "wantMessagesSigned": has_sp_cert
,
250 "wantAssertionsSigned": has_sp_cert
,
251 "wantAssertionsEncrypted": has_sp_cert
,
252 "wantNameIdEncrypted": False, # Not all Identity Providers support this.
253 "metadataValidUntil": '',
254 "wantAttributeStatement": False
257 settings
= Saml2Parser
.merge_settings(settings
, idp_settings
)
258 mgr
.SSO_DB
.saml2
.onelogin_settings
= settings
259 mgr
.SSO_DB
.protocol
= 'saml2'
261 return 0, json
.dumps(mgr
.SSO_DB
.saml2
.onelogin_settings
), ''
263 return -errno
.ENOSYS
, '', ''