]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/services/sso.py
import ceph 15.2.10
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / sso.py
CommitLineData
11fdf7f2
TL
1# -*- coding: utf-8 -*-
2# pylint: disable=too-many-return-statements,too-many-branches
3from __future__ import absolute_import
4
9f95a23c 5import os
11fdf7f2
TL
6import errno
7import json
9f95a23c 8import logging
11fdf7f2 9import threading
9f95a23c
TL
10import warnings
11
12import six
13from six.moves.urllib import parse
14
15from .. import mgr
16from ..tools import prepare_url_prefix
17
18
19if six.PY2:
20 FileNotFoundError = IOError # pylint: disable=redefined-builtin
21
22logger = logging.getLogger('sso')
11fdf7f2
TL
23
24try:
9f95a23c
TL
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
11fdf7f2
TL
28
29 python_saml_imported = True
30except ImportError:
31 python_saml_imported = False
32
33
11fdf7f2
TL
34class 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
52class 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):
9f95a23c 78 logger.debug("Checking for previous DB versions")
11fdf7f2
TL
79 if self.VERSION != 1:
80 raise NotImplementedError()
81
82 @classmethod
83 def load(cls):
9f95a23c 84 logger.info("Loading SSO DB version=%s", cls.VERSION)
11fdf7f2
TL
85
86 json_db = mgr.get_store(cls.ssodb_config_key(), None)
87 if json_db is None:
9f95a23c 88 logger.debug("No DB v%s found, creating new...", cls.VERSION)
11fdf7f2
TL
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
9f95a23c
TL
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')))
11fdf7f2
TL
97
98
99def load_sso_db():
100 mgr.SSO_DB = SsoDB.load()
101
102
103SSO_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 '
9f95a23c
TL
130 'name=sp_x_509_cert,type=CephFilepath,req=false '
131 'name=sp_private_key,type=CephFilepath,req=false',
11fdf7f2
TL
132 'desc': 'Setup SAML2 Single Sign-On',
133 'perm': 'w'
134 }
135]
136
137
138def _get_optional_attr(cmd, attr, default):
139 if attr in cmd:
140 if cmd[attr] != '':
141 return cmd[attr]
142 return default
143
144
145def 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
f91f0fd5
TL
153 if cmd['prefix'] == 'dashboard sso disable':
154 mgr.SSO_DB.protocol = ''
155 mgr.SSO_DB.save()
156 return 0, 'SSO is "disabled".', ''
157
11fdf7f2 158 if not python_saml_imported:
9f95a23c 159 return -errno.EPERM, '', 'Required library not found: `python3-saml`'
11fdf7f2
TL
160
161 if cmd['prefix'] == 'dashboard sso enable saml2':
162 try:
9f95a23c
TL
163 Saml2Settings(mgr.SSO_DB.saml2.onelogin_settings)
164 except Saml2Error:
11fdf7f2
TL
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
11fdf7f2
TL
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)
9f95a23c
TL
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:
11fdf7f2 188 return -errno.EINVAL, '', 'Missing parameter `sp_private_key`.'
9f95a23c 189 if not sp_x_509_cert_path and sp_private_key_path:
11fdf7f2 190 return -errno.EINVAL, '', 'Missing parameter `sp_x_509_cert`.'
9f95a23c
TL
191 has_sp_cert = sp_x_509_cert_path != "" and sp_private_key_path != ""
192 if has_sp_cert:
11fdf7f2 193 try:
801d1391 194 with open(sp_x_509_cert_path, 'r', encoding='utf-8') as f:
9f95a23c 195 sp_x_509_cert = f.read()
11fdf7f2 196 except FileNotFoundError:
9f95a23c 197 return -errno.EINVAL, '', '`{}` not found.'.format(sp_x_509_cert_path)
11fdf7f2 198 try:
801d1391 199 with open(sp_private_key_path, 'r', encoding='utf-8') as f:
9f95a23c
TL
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)
801d1391 210 with open(idp_metadata, 'r', encoding='utf-8') as f:
9f95a23c
TL
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)
11fdf7f2
TL
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,
801d1391 252 "wantNameIdEncrypted": False, # Not all Identity Providers support this.
11fdf7f2
TL
253 "metadataValidUntil": '',
254 "wantAttributeStatement": False
255 }
256 }
9f95a23c 257 settings = Saml2Parser.merge_settings(settings, idp_settings)
11fdf7f2
TL
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, '', ''