]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/services/auth.py
import ceph quincy 17.2.4
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / auth.py
CommitLineData
11fdf7f2 1# -*- coding: utf-8 -*-
11fdf7f2 2
11fdf7f2 3import json
9f95a23c 4import logging
11fdf7f2
TL
5import os
6import threading
7import time
8import uuid
f67539c2 9from base64 import b64encode
11fdf7f2
TL
10
11import cherrypy
12import jwt
13
9f95a23c 14from .. import mgr
f67539c2 15from .access_control import LocalAuthenticator, UserDoesNotExist
11fdf7f2 16
cd265ab1
TL
17cherrypy.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
11fdf7f2
TL
24
25class JwtManager(object):
f67539c2 26 JWT_TOKEN_BLOCKLIST_KEY = "jwt_token_block_list"
11fdf7f2
TL
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):
9f95a23c 40 cls.logger = logging.getLogger('jwt') # type: ignore
11fdf7f2
TL
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 }
9f95a23c 62 return jwt.encode(payload, cls._secret, algorithm=cls.JWT_ALGORITHM) # type: ignore
11fdf7f2
TL
63
64 @classmethod
65 def decode_token(cls, token):
66 if not cls._secret:
67 cls.init()
9f95a23c 68 return jwt.decode(token, cls._secret, algorithms=cls.JWT_ALGORITHM) # type: ignore
11fdf7f2
TL
69
70 @classmethod
71 def get_token_from_header(cls):
adb31ebb
TL
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
11fdf7f2
TL
86
87 @classmethod
9f95a23c
TL
88 def set_user(cls, username):
89 cls.LOCAL_USER.username = username
11fdf7f2
TL
90
91 @classmethod
92 def reset_user(cls):
9f95a23c 93 cls.set_user(None)
11fdf7f2
TL
94
95 @classmethod
96 def get_username(cls):
97 return getattr(cls.LOCAL_USER, 'username', None)
98
9f95a23c
TL
99 @classmethod
100 def get_user(cls, token):
101 try:
102 dtoken = JwtManager.decode_token(token)
f67539c2 103 if not JwtManager.is_blocklisted(dtoken['jti']):
9f95a23c
TL
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:
f67539c2 112 cls.logger.debug('Token is block-listed') # type: ignore
9f95a23c
TL
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
11fdf7f2 123 @classmethod
f67539c2 124 def blocklist_token(cls, token):
cd265ab1 125 token = cls.decode_token(token)
f67539c2
TL
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)
11fdf7f2
TL
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']
f67539c2 141 mgr.set_store(cls.JWT_TOKEN_BLOCKLIST_KEY, json.dumps(bl_dict))
11fdf7f2
TL
142
143 @classmethod
f67539c2
TL
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)
11fdf7f2
TL
149 return jti in bl_dict
150
151
152class 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):
9f95a23c 161 return cls.AUTH_PROVIDER.get_user(username) # type: ignore
11fdf7f2
TL
162
163 @classmethod
164 def authenticate(cls, username, password):
9f95a23c 165 return cls.AUTH_PROVIDER.authenticate(username, password) # type: ignore
11fdf7f2
TL
166
167 @classmethod
168 def authorize(cls, username, scope, permissions):
9f95a23c 169 return cls.AUTH_PROVIDER.authorize(username, scope, permissions) # type: ignore
11fdf7f2
TL
170
171
172class AuthManagerTool(cherrypy.Tool):
173 def __init__(self):
174 super(AuthManagerTool, self).__init__(
175 'before_handler', self._check_authentication, priority=20)
9f95a23c 176 self.logger = logging.getLogger('auth')
11fdf7f2
TL
177
178 def _check_authentication(self):
179 JwtManager.reset_user()
180 token = JwtManager.get_token_from_header()
11fdf7f2 181 if token:
9f95a23c
TL
182 user = JwtManager.get_user(token)
183 if user:
184 self._check_authorization(user.username)
185 return
186 self.logger.debug('Unauthorized access to %s',
187 cherrypy.url(relative='server'))
11fdf7f2
TL
188 raise cherrypy.HTTPError(401, 'You are not authorized to access '
189 'that resource')
190
9f95a23c
TL
191 def _check_authorization(self, username):
192 self.logger.debug("checking authorization...")
11fdf7f2
TL
193 handler = cherrypy.request.handler.callable
194 controller = handler.__self__
195 sec_scope = getattr(controller, '_security_scope', None)
196 sec_perms = getattr(handler, '_security_permissions', None)
9f95a23c 197 JwtManager.set_user(username)
11fdf7f2
TL
198
199 if not sec_scope:
200 # controller does not define any authorization restrictions
201 return
202
9f95a23c
TL
203 self.logger.debug("checking '%s' access to '%s' scope", sec_perms,
204 sec_scope)
11fdf7f2
TL
205
206 if not sec_perms:
9f95a23c
TL
207 self.logger.debug("Fail to check permission on: %s:%s", controller,
208 handler)
11fdf7f2
TL
209 raise cherrypy.HTTPError(403, "You don't have permissions to "
210 "access that resource")
211
212 if not AuthManager.authorize(username, sec_scope, sec_perms):
213 raise cherrypy.HTTPError(403, "You don't have permissions to "
214 "access that resource")