]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/services/sso.py
Import ceph 15.2.8
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / sso.py
1 # -*- coding: utf-8 -*-
2 # pylint: disable=too-many-return-statements,too-many-branches
3 from __future__ import absolute_import
4
5 import os
6 import errno
7 import json
8 import logging
9 import threading
10 import warnings
11
12 import six
13 from six.moves.urllib import parse
14
15 from .. import mgr
16 from ..tools import prepare_url_prefix
17
18
19 if six.PY2:
20 FileNotFoundError = IOError # pylint: disable=redefined-builtin
21
22 logger = logging.getLogger('sso')
23
24 try:
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
28
29 python_saml_imported = True
30 except ImportError:
31 python_saml_imported = False
32
33
34 class Saml2(object):
35 def __init__(self, onelogin_settings):
36 self.onelogin_settings = onelogin_settings
37
38 def get_username_attribute(self):
39 return self.onelogin_settings['sp']['attributeConsumingService']['requestedAttributes'][0][
40 'name']
41
42 def to_dict(self):
43 return {
44 'onelogin_settings': self.onelogin_settings
45 }
46
47 @classmethod
48 def from_dict(cls, s_dict):
49 return Saml2(s_dict['onelogin_settings'])
50
51
52 class SsoDB(object):
53 VERSION = 1
54 SSODB_CONFIG_KEY = "ssodb_v"
55
56 def __init__(self, version, protocol, saml2):
57 self.version = version
58 self.protocol = protocol
59 self.saml2 = saml2
60 self.lock = threading.RLock()
61
62 def save(self):
63 with self.lock:
64 db = {
65 'protocol': self.protocol,
66 'saml2': self.saml2.to_dict(),
67 'version': self.version
68 }
69 mgr.set_store(self.ssodb_config_key(), json.dumps(db))
70
71 @classmethod
72 def ssodb_config_key(cls, version=None):
73 if version is None:
74 version = cls.VERSION
75 return "{}{}".format(cls.SSODB_CONFIG_KEY, version)
76
77 def check_and_update_db(self):
78 logger.debug("Checking for previous DB versions")
79 if self.VERSION != 1:
80 raise NotImplementedError()
81
82 @classmethod
83 def load(cls):
84 logger.info("Loading SSO DB version=%s", cls.VERSION)
85
86 json_db = mgr.get_store(cls.ssodb_config_key(), None)
87 if json_db is 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()
92 return db
93
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')))
97
98
99 def load_sso_db():
100 mgr.SSO_DB = SsoDB.load()
101
102
103 SSO_COMMANDS = [
104 {
105 'cmd': 'dashboard sso enable saml2',
106 'desc': 'Enable SAML2 Single Sign-On',
107 'perm': 'w'
108 },
109 {
110 'cmd': 'dashboard sso disable',
111 'desc': 'Disable Single Sign-On',
112 'perm': 'w'
113 },
114 {
115 'cmd': 'dashboard sso status',
116 'desc': 'Get Single Sign-On status',
117 'perm': 'r'
118 },
119 {
120 'cmd': 'dashboard sso show saml2',
121 'desc': 'Show SAML2 configuration',
122 'perm': 'r'
123 },
124 {
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',
133 'perm': 'w'
134 }
135 ]
136
137
138 def _get_optional_attr(cmd, attr, default):
139 if attr in cmd:
140 if cmd[attr] != '':
141 return cmd[attr]
142 return default
143
144
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, '', ''
152
153 if cmd['prefix'] == 'dashboard sso disable':
154 mgr.SSO_DB.protocol = ''
155 mgr.SSO_DB.save()
156 return 0, 'SSO is "disabled".', ''
157
158 if not python_saml_imported:
159 return -errno.EPERM, '', 'Required library not found: `python3-saml`'
160
161 if cmd['prefix'] == 'dashboard sso enable saml2':
162 try:
163 Saml2Settings(mgr.SSO_DB.saml2.onelogin_settings)
164 except Saml2Error:
165 return -errno.EPERM, '', 'Single Sign-On is not configured: ' \
166 'use `ceph dashboard sso setup saml2`'
167 mgr.SSO_DB.protocol = 'saml2'
168 mgr.SSO_DB.save()
169 return 0, 'SSO is "enabled" with "SAML2" protocol.', ''
170
171 if cmd['prefix'] == 'dashboard sso status':
172 if mgr.SSO_DB.protocol == 'saml2':
173 return 0, 'SSO is "enabled" with "SAML2" protocol.', ''
174
175 return 0, 'SSO is "disabled".', ''
176
177 if cmd['prefix'] == 'dashboard sso show saml2':
178 return 0, json.dumps(mgr.SSO_DB.saml2.to_dict()), ''
179
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 != ""
192 if has_sp_cert:
193 try:
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)
198 try:
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)
203 else:
204 sp_x_509_cert = ''
205 sp_private_key = ''
206
207 if os.path.isfile(idp_metadata):
208 warnings.warn(
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)
215 else:
216 idp_settings = Saml2Parser.parse(idp_metadata, entity_id=idp_entity_id)
217
218 url_prefix = prepare_url_prefix(mgr.get_module_option('url_prefix', default=''))
219 settings = {
220 'sp': {
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"
225 },
226 'attributeConsumingService': {
227 'serviceName': "Ceph Dashboard",
228 "serviceDescription": "Ceph Dashboard Service",
229 "requestedAttributes": [
230 {
231 "name": idp_username_attribute,
232 "isRequired": True
233 }
234 ]
235 },
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'
239 },
240 "x509cert": sp_x_509_cert,
241 "privateKey": sp_private_key
242 },
243 'security': {
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
255 }
256 }
257 settings = Saml2Parser.merge_settings(settings, idp_settings)
258 mgr.SSO_DB.saml2.onelogin_settings = settings
259 mgr.SSO_DB.protocol = 'saml2'
260 mgr.SSO_DB.save()
261 return 0, json.dumps(mgr.SSO_DB.saml2.onelogin_settings), ''
262
263 return -errno.ENOSYS, '', ''