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