]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/services/auth.py
update ceph source to reef 18.2.1
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / auth.py
1 # -*- coding: utf-8 -*-
2
3 import json
4 import logging
5 import os
6 import threading
7 import time
8 import uuid
9 from base64 import b64encode
10
11 import cherrypy
12 import jwt
13
14 from .. import mgr
15 from .access_control import LocalAuthenticator, UserDoesNotExist
16
17 cherrypy.config.update({
18 'response.headers.server': 'Ceph-Dashboard',
19 'response.headers.content-security-policy': "frame-ancestors 'self';",
20 'response.headers.x-content-type-options': 'nosniff',
21 'response.headers.strict-transport-security': 'max-age=63072000; includeSubDomains; preload'
22 })
23
24
25 class JwtManager(object):
26 JWT_TOKEN_BLOCKLIST_KEY = "jwt_token_block_list"
27 JWT_TOKEN_TTL = 28800 # default 8 hours
28 JWT_ALGORITHM = 'HS256'
29 _secret = None
30
31 LOCAL_USER = threading.local()
32
33 @staticmethod
34 def _gen_secret():
35 secret = os.urandom(16)
36 return b64encode(secret).decode('utf-8')
37
38 @classmethod
39 def init(cls):
40 cls.logger = logging.getLogger('jwt') # type: ignore
41 # generate a new secret if it does not exist
42 secret = mgr.get_store('jwt_secret')
43 if secret is None:
44 secret = cls._gen_secret()
45 mgr.set_store('jwt_secret', secret)
46 cls._secret = secret
47
48 @classmethod
49 def gen_token(cls, username):
50 if not cls._secret:
51 cls.init()
52 ttl = mgr.get_module_option('jwt_token_ttl', cls.JWT_TOKEN_TTL)
53 ttl = int(ttl)
54 now = int(time.time())
55 payload = {
56 'iss': 'ceph-dashboard',
57 'jti': str(uuid.uuid4()),
58 'exp': now + ttl,
59 'iat': now,
60 'username': username
61 }
62 return jwt.encode(payload, cls._secret, algorithm=cls.JWT_ALGORITHM) # type: ignore
63
64 @classmethod
65 def decode_token(cls, token):
66 if not cls._secret:
67 cls.init()
68 return jwt.decode(token, cls._secret, algorithms=cls.JWT_ALGORITHM) # type: ignore
69
70 @classmethod
71 def get_token_from_header(cls):
72 auth_cookie_name = 'token'
73 try:
74 # use cookie
75 return cherrypy.request.cookie[auth_cookie_name].value
76 except KeyError:
77 try:
78 # fall-back: use Authorization header
79 auth_header = cherrypy.request.headers.get('authorization')
80 if auth_header is not None:
81 scheme, params = auth_header.split(' ', 1)
82 if scheme.lower() == 'bearer':
83 return params
84 except IndexError:
85 return None
86
87 @classmethod
88 def set_user(cls, username):
89 cls.LOCAL_USER.username = username
90
91 @classmethod
92 def reset_user(cls):
93 cls.set_user(None)
94
95 @classmethod
96 def get_username(cls):
97 return getattr(cls.LOCAL_USER, 'username', None)
98
99 @classmethod
100 def get_user(cls, token):
101 try:
102 dtoken = JwtManager.decode_token(token)
103 if not JwtManager.is_blocklisted(dtoken['jti']):
104 user = AuthManager.get_user(dtoken['username'])
105 if user.last_update <= dtoken['iat']:
106 return user
107 cls.logger.debug( # type: ignore
108 "user info changed after token was issued, iat=%s last_update=%s",
109 dtoken['iat'], user.last_update
110 )
111 else:
112 cls.logger.debug('Token is block-listed') # type: ignore
113 except jwt.ExpiredSignatureError:
114 cls.logger.debug("Token has expired") # type: ignore
115 except jwt.InvalidTokenError:
116 cls.logger.debug("Failed to decode token") # type: ignore
117 except UserDoesNotExist:
118 cls.logger.debug( # type: ignore
119 "Invalid token: user %s does not exist", dtoken['username']
120 )
121 return None
122
123 @classmethod
124 def blocklist_token(cls, token):
125 token = cls.decode_token(token)
126 blocklist_json = mgr.get_store(cls.JWT_TOKEN_BLOCKLIST_KEY)
127 if not blocklist_json:
128 blocklist_json = "{}"
129 bl_dict = json.loads(blocklist_json)
130 now = time.time()
131
132 # remove expired tokens
133 to_delete = []
134 for jti, exp in bl_dict.items():
135 if exp < now:
136 to_delete.append(jti)
137 for jti in to_delete:
138 del bl_dict[jti]
139
140 bl_dict[token['jti']] = token['exp']
141 mgr.set_store(cls.JWT_TOKEN_BLOCKLIST_KEY, json.dumps(bl_dict))
142
143 @classmethod
144 def is_blocklisted(cls, jti):
145 blocklist_json = mgr.get_store(cls.JWT_TOKEN_BLOCKLIST_KEY)
146 if not blocklist_json:
147 blocklist_json = "{}"
148 bl_dict = json.loads(blocklist_json)
149 return jti in bl_dict
150
151
152 class AuthManager(object):
153 AUTH_PROVIDER = None
154
155 @classmethod
156 def initialize(cls):
157 cls.AUTH_PROVIDER = LocalAuthenticator()
158
159 @classmethod
160 def get_user(cls, username):
161 return cls.AUTH_PROVIDER.get_user(username) # type: ignore
162
163 @classmethod
164 def authenticate(cls, username, password):
165 return cls.AUTH_PROVIDER.authenticate(username, password) # type: ignore
166
167 @classmethod
168 def authorize(cls, username, scope, permissions):
169 return cls.AUTH_PROVIDER.authorize(username, scope, permissions) # type: ignore
170
171
172 class AuthManagerTool(cherrypy.Tool):
173 def __init__(self):
174 super(AuthManagerTool, self).__init__(
175 'before_handler', self._check_authentication, priority=20)
176 self.logger = logging.getLogger('auth')
177
178 def _check_authentication(self):
179 JwtManager.reset_user()
180 token = JwtManager.get_token_from_header()
181 if token:
182 user = JwtManager.get_user(token)
183 if user:
184 self._check_authorization(user.username)
185 return
186
187 resp_head = cherrypy.response.headers
188 req_head = cherrypy.request.headers
189 req_header_cross_origin_url = req_head.get('Access-Control-Allow-Origin')
190 cross_origin_urls = mgr.get_module_option('cross_origin_url', '')
191 cross_origin_url_list = [url.strip() for url in cross_origin_urls.split(',')]
192
193 if req_header_cross_origin_url in cross_origin_url_list:
194 resp_head['Access-Control-Allow-Origin'] = req_header_cross_origin_url
195
196 self.logger.debug('Unauthorized access to %s',
197 cherrypy.url(relative='server'))
198 raise cherrypy.HTTPError(401, 'You are not authorized to access '
199 'that resource')
200
201 def _check_authorization(self, username):
202 self.logger.debug("checking authorization...")
203 handler = cherrypy.request.handler.callable
204 controller = handler.__self__
205 sec_scope = getattr(controller, '_security_scope', None)
206 sec_perms = getattr(handler, '_security_permissions', None)
207 JwtManager.set_user(username)
208
209 if not sec_scope:
210 # controller does not define any authorization restrictions
211 return
212
213 self.logger.debug("checking '%s' access to '%s' scope", sec_perms,
214 sec_scope)
215
216 if not sec_perms:
217 self.logger.debug("Fail to check permission on: %s:%s", controller,
218 handler)
219 raise cherrypy.HTTPError(403, "You don't have permissions to "
220 "access that resource")
221
222 if not AuthManager.authorize(username, sec_scope, sec_perms):
223 raise cherrypy.HTTPError(403, "You don't have permissions to "
224 "access that resource")