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