]>
Commit | Line | Data |
---|---|---|
11fdf7f2 | 1 | # -*- coding: utf-8 -*- |
11fdf7f2 | 2 | |
adb31ebb | 3 | import http.cookies |
9f95a23c | 4 | import logging |
adb31ebb | 5 | import sys |
11fdf7f2 | 6 | |
9f95a23c | 7 | from .. import mgr |
adb31ebb | 8 | from ..exceptions import InvalidCredentialsError, UserDoesNotExist |
11fdf7f2 | 9 | from ..services.auth import AuthManager, JwtManager |
a4b75251 | 10 | from ..services.cluster import ClusterModel |
adb31ebb | 11 | from ..settings import Settings |
a4b75251 | 12 | from . import APIDoc, APIRouter, ControllerAuthMixin, EndpointDoc, RESTController, allow_empty_body |
9f95a23c | 13 | |
adb31ebb TL |
14 | # Python 3.8 introduced `samesite` attribute: |
15 | # https://docs.python.org/3/library/http.cookies.html#morsel-objects | |
16 | if sys.version_info < (3, 8): | |
17 | http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore # pylint: disable=W0212 | |
9f95a23c TL |
18 | |
19 | logger = logging.getLogger('controllers.auth') | |
11fdf7f2 | 20 | |
f67539c2 TL |
21 | AUTH_CHECK_SCHEMA = { |
22 | "username": (str, "Username"), | |
23 | "permissions": ({ | |
24 | "cephfs": ([str], "") | |
25 | }, "List of permissions acquired"), | |
26 | "sso": (bool, "Uses single sign on?"), | |
27 | "pwdUpdateRequired": (bool, "Is password update required?") | |
28 | } | |
29 | ||
11fdf7f2 | 30 | |
a4b75251 TL |
31 | @APIRouter('/auth', secure=False) |
32 | @APIDoc("Initiate a session with Ceph", "Auth") | |
f67539c2 | 33 | class Auth(RESTController, ControllerAuthMixin): |
11fdf7f2 TL |
34 | """ |
35 | Provide authenticates and returns JWT token. | |
36 | """ | |
f67539c2 | 37 | |
11fdf7f2 | 38 | def create(self, username, password): |
9f95a23c TL |
39 | user_data = AuthManager.authenticate(username, password) |
40 | user_perms, pwd_expiration_date, pwd_update_required = None, None, None | |
adb31ebb TL |
41 | max_attempt = Settings.ACCOUNT_LOCKOUT_ATTEMPTS |
42 | if max_attempt == 0 or mgr.ACCESS_CTRL_DB.get_attempt(username) < max_attempt: | |
43 | if user_data: | |
44 | user_perms = user_data.get('permissions') | |
45 | pwd_expiration_date = user_data.get('pwdExpirationDate', None) | |
46 | pwd_update_required = user_data.get('pwdUpdateRequired', False) | |
47 | ||
48 | if user_perms is not None: | |
49 | url_prefix = 'https' if mgr.get_localized_module_option('ssl') else 'http' | |
f67539c2 | 50 | |
adb31ebb TL |
51 | logger.info('Login successful: %s', username) |
52 | mgr.ACCESS_CTRL_DB.reset_attempt(username) | |
53 | mgr.ACCESS_CTRL_DB.save() | |
54 | token = JwtManager.gen_token(username) | |
cd265ab1 TL |
55 | |
56 | # For backward-compatibility: PyJWT versions < 2.0.0 return bytes. | |
57 | token = token.decode('utf-8') if isinstance(token, bytes) else token | |
58 | ||
f67539c2 | 59 | self._set_token_cookie(url_prefix, token) |
adb31ebb TL |
60 | return { |
61 | 'token': token, | |
62 | 'username': username, | |
63 | 'permissions': user_perms, | |
64 | 'pwdExpirationDate': pwd_expiration_date, | |
65 | 'sso': mgr.SSO_DB.protocol == 'saml2', | |
66 | 'pwdUpdateRequired': pwd_update_required | |
67 | } | |
68 | mgr.ACCESS_CTRL_DB.increment_attempt(username) | |
69 | mgr.ACCESS_CTRL_DB.save() | |
70 | else: | |
71 | try: | |
72 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
73 | user.enabled = False | |
74 | mgr.ACCESS_CTRL_DB.save() | |
75 | logging.warning('Maximum number of unsuccessful log-in attempts ' | |
76 | '(%d) reached for ' | |
77 | 'username "%s" so the account was blocked. ' | |
78 | 'An administrator will need to re-enable the account', | |
79 | max_attempt, username) | |
80 | raise InvalidCredentialsError | |
81 | except UserDoesNotExist: | |
82 | raise InvalidCredentialsError | |
83 | logger.info('Login failed: %s', username) | |
84 | raise InvalidCredentialsError | |
11fdf7f2 TL |
85 | |
86 | @RESTController.Collection('POST') | |
f91f0fd5 | 87 | @allow_empty_body |
11fdf7f2 TL |
88 | def logout(self): |
89 | logger.debug('Logout successful') | |
90 | token = JwtManager.get_token_from_header() | |
f67539c2 TL |
91 | JwtManager.blocklist_token(token) |
92 | self._delete_token_cookie(token) | |
11fdf7f2 TL |
93 | redirect_url = '#/login' |
94 | if mgr.SSO_DB.protocol == 'saml2': | |
95 | redirect_url = 'auth/saml2/slo' | |
96 | return { | |
97 | 'redirect_url': redirect_url | |
98 | } | |
99 | ||
100 | def _get_login_url(self): | |
101 | if mgr.SSO_DB.protocol == 'saml2': | |
102 | return 'auth/saml2/login' | |
103 | return '#/login' | |
104 | ||
f67539c2 TL |
105 | @RESTController.Collection('POST', query_params=['token']) |
106 | @EndpointDoc("Check token Authentication", | |
107 | parameters={'token': (str, 'Authentication Token')}, | |
108 | responses={201: AUTH_CHECK_SCHEMA}) | |
11fdf7f2 TL |
109 | def check(self, token): |
110 | if token: | |
9f95a23c TL |
111 | user = JwtManager.get_user(token) |
112 | if user: | |
113 | return { | |
114 | 'username': user.username, | |
115 | 'permissions': user.permissions_dict(), | |
116 | 'sso': mgr.SSO_DB.protocol == 'saml2', | |
117 | 'pwdUpdateRequired': user.pwd_update_required | |
118 | } | |
11fdf7f2 | 119 | return { |
9f95a23c | 120 | 'login_url': self._get_login_url(), |
a4b75251 | 121 | 'cluster_status': ClusterModel.from_db().dict()['status'] |
11fdf7f2 | 122 | } |