]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # -*- coding: utf-8 -*- |
2 | from __future__ import absolute_import | |
3 | ||
4 | from base64 import b64encode | |
5 | import json | |
9f95a23c | 6 | import logging |
11fdf7f2 TL |
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 | |
9f95a23c | 16 | from .. import mgr |
11fdf7f2 TL |
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): | |
9f95a23c | 34 | cls.logger = logging.getLogger('jwt') # type: ignore |
11fdf7f2 TL |
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 | } | |
9f95a23c | 56 | return jwt.encode(payload, cls._secret, algorithm=cls.JWT_ALGORITHM) # type: ignore |
11fdf7f2 TL |
57 | |
58 | @classmethod | |
59 | def decode_token(cls, token): | |
60 | if not cls._secret: | |
61 | cls.init() | |
9f95a23c | 62 | return jwt.decode(token, cls._secret, algorithms=cls.JWT_ALGORITHM) # type: ignore |
11fdf7f2 TL |
63 | |
64 | @classmethod | |
65 | def get_token_from_header(cls): | |
adb31ebb TL |
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 | |
11fdf7f2 TL |
80 | |
81 | @classmethod | |
9f95a23c TL |
82 | def set_user(cls, username): |
83 | cls.LOCAL_USER.username = username | |
11fdf7f2 TL |
84 | |
85 | @classmethod | |
86 | def reset_user(cls): | |
9f95a23c | 87 | cls.set_user(None) |
11fdf7f2 TL |
88 | |
89 | @classmethod | |
90 | def get_username(cls): | |
91 | return getattr(cls.LOCAL_USER, 'username', None) | |
92 | ||
9f95a23c TL |
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 | ||
11fdf7f2 TL |
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): | |
9f95a23c | 155 | return cls.AUTH_PROVIDER.get_user(username) # type: ignore |
11fdf7f2 TL |
156 | |
157 | @classmethod | |
158 | def authenticate(cls, username, password): | |
9f95a23c | 159 | return cls.AUTH_PROVIDER.authenticate(username, password) # type: ignore |
11fdf7f2 TL |
160 | |
161 | @classmethod | |
162 | def authorize(cls, username, scope, permissions): | |
9f95a23c | 163 | return cls.AUTH_PROVIDER.authorize(username, scope, permissions) # type: ignore |
11fdf7f2 TL |
164 | |
165 | ||
166 | class AuthManagerTool(cherrypy.Tool): | |
167 | def __init__(self): | |
168 | super(AuthManagerTool, self).__init__( | |
169 | 'before_handler', self._check_authentication, priority=20) | |
9f95a23c | 170 | self.logger = logging.getLogger('auth') |
11fdf7f2 TL |
171 | |
172 | def _check_authentication(self): | |
173 | JwtManager.reset_user() | |
174 | token = JwtManager.get_token_from_header() | |
9f95a23c | 175 | self.logger.debug("token: %s", token) |
11fdf7f2 | 176 | if token: |
9f95a23c TL |
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')) | |
11fdf7f2 TL |
183 | raise cherrypy.HTTPError(401, 'You are not authorized to access ' |
184 | 'that resource') | |
185 | ||
9f95a23c TL |
186 | def _check_authorization(self, username): |
187 | self.logger.debug("checking authorization...") | |
188 | username = username | |
11fdf7f2 TL |
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) | |
9f95a23c | 193 | JwtManager.set_user(username) |
11fdf7f2 TL |
194 | |
195 | if not sec_scope: | |
196 | # controller does not define any authorization restrictions | |
197 | return | |
198 | ||
9f95a23c TL |
199 | self.logger.debug("checking '%s' access to '%s' scope", sec_perms, |
200 | sec_scope) | |
11fdf7f2 TL |
201 | |
202 | if not sec_perms: | |
9f95a23c TL |
203 | self.logger.debug("Fail to check permission on: %s:%s", controller, |
204 | handler) | |
11fdf7f2 TL |
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") |