]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # -*- coding: utf-8 -*- |
2 | from __future__ import absolute_import | |
3 | ||
11fdf7f2 | 4 | import json |
9f95a23c | 5 | import logging |
11fdf7f2 TL |
6 | import os |
7 | import threading | |
8 | import time | |
9 | import uuid | |
f67539c2 | 10 | from base64 import b64encode |
11fdf7f2 TL |
11 | |
12 | import cherrypy | |
13 | import jwt | |
14 | ||
9f95a23c | 15 | from .. import mgr |
f67539c2 | 16 | from .access_control import LocalAuthenticator, UserDoesNotExist |
11fdf7f2 | 17 | |
cd265ab1 TL |
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 | ||
11fdf7f2 TL |
25 | |
26 | class 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 | ||
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): | |
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 | ||
173 | class 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") |