]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/services/auth.py
bbb8a2ecfe11c664663f3943982d383691c0c456
[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_cookie_name = 'token'
67 try:
68 # use cookie
69 return cherrypy.request.cookie[auth_cookie_name].value
70 except KeyError:
71 try:
72 # fall-back: use Authorization header
73 auth_header = cherrypy.request.headers.get('authorization')
74 if auth_header is not None:
75 scheme, params = auth_header.split(' ', 1)
76 if scheme.lower() == 'bearer':
77 return params
78 except IndexError:
79 return None
80
81 @classmethod
82 def set_user(cls, username):
83 cls.LOCAL_USER.username = username
84
85 @classmethod
86 def reset_user(cls):
87 cls.set_user(None)
88
89 @classmethod
90 def get_username(cls):
91 return getattr(cls.LOCAL_USER, 'username', None)
92
93 @classmethod
94 def get_user(cls, token):
95 try:
96 dtoken = JwtManager.decode_token(token)
97 if not JwtManager.is_blacklisted(dtoken['jti']):
98 user = AuthManager.get_user(dtoken['username'])
99 if user.last_update <= dtoken['iat']:
100 return user
101 cls.logger.debug( # type: ignore
102 "user info changed after token was issued, iat=%s last_update=%s",
103 dtoken['iat'], user.last_update
104 )
105 else:
106 cls.logger.debug('Token is black-listed') # type: ignore
107 except jwt.ExpiredSignatureError:
108 cls.logger.debug("Token has expired") # type: ignore
109 except jwt.InvalidTokenError:
110 cls.logger.debug("Failed to decode token") # type: ignore
111 except UserDoesNotExist:
112 cls.logger.debug( # type: ignore
113 "Invalid token: user %s does not exist", dtoken['username']
114 )
115 return None
116
117 @classmethod
118 def blacklist_token(cls, token):
119 token = jwt.decode(token, verify=False)
120 blacklist_json = mgr.get_store(cls.JWT_TOKEN_BLACKLIST_KEY)
121 if not blacklist_json:
122 blacklist_json = "{}"
123 bl_dict = json.loads(blacklist_json)
124 now = time.time()
125
126 # remove expired tokens
127 to_delete = []
128 for jti, exp in bl_dict.items():
129 if exp < now:
130 to_delete.append(jti)
131 for jti in to_delete:
132 del bl_dict[jti]
133
134 bl_dict[token['jti']] = token['exp']
135 mgr.set_store(cls.JWT_TOKEN_BLACKLIST_KEY, json.dumps(bl_dict))
136
137 @classmethod
138 def is_blacklisted(cls, jti):
139 blacklist_json = mgr.get_store(cls.JWT_TOKEN_BLACKLIST_KEY)
140 if not blacklist_json:
141 blacklist_json = "{}"
142 bl_dict = json.loads(blacklist_json)
143 return jti in bl_dict
144
145
146 class AuthManager(object):
147 AUTH_PROVIDER = None
148
149 @classmethod
150 def initialize(cls):
151 cls.AUTH_PROVIDER = LocalAuthenticator()
152
153 @classmethod
154 def get_user(cls, username):
155 return cls.AUTH_PROVIDER.get_user(username) # type: ignore
156
157 @classmethod
158 def authenticate(cls, username, password):
159 return cls.AUTH_PROVIDER.authenticate(username, password) # type: ignore
160
161 @classmethod
162 def authorize(cls, username, scope, permissions):
163 return cls.AUTH_PROVIDER.authorize(username, scope, permissions) # type: ignore
164
165
166 class AuthManagerTool(cherrypy.Tool):
167 def __init__(self):
168 super(AuthManagerTool, self).__init__(
169 'before_handler', self._check_authentication, priority=20)
170 self.logger = logging.getLogger('auth')
171
172 def _check_authentication(self):
173 JwtManager.reset_user()
174 token = JwtManager.get_token_from_header()
175 self.logger.debug("token: %s", token)
176 if token:
177 user = JwtManager.get_user(token)
178 if user:
179 self._check_authorization(user.username)
180 return
181 self.logger.debug('Unauthorized access to %s',
182 cherrypy.url(relative='server'))
183 raise cherrypy.HTTPError(401, 'You are not authorized to access '
184 'that resource')
185
186 def _check_authorization(self, username):
187 self.logger.debug("checking authorization...")
188 username = username
189 handler = cherrypy.request.handler.callable
190 controller = handler.__self__
191 sec_scope = getattr(controller, '_security_scope', None)
192 sec_perms = getattr(handler, '_security_permissions', None)
193 JwtManager.set_user(username)
194
195 if not sec_scope:
196 # controller does not define any authorization restrictions
197 return
198
199 self.logger.debug("checking '%s' access to '%s' scope", sec_perms,
200 sec_scope)
201
202 if not sec_perms:
203 self.logger.debug("Fail to check permission on: %s:%s", controller,
204 handler)
205 raise cherrypy.HTTPError(403, "You don't have permissions to "
206 "access that resource")
207
208 if not AuthManager.authorize(username, sec_scope, sec_perms):
209 raise cherrypy.HTTPError(403, "You don't have permissions to "
210 "access that resource")