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