|
14 | 14 | #
|
15 | 15 | # You should have received a copy of the GNU Lesser General Public License
|
16 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17 |
| -from .authorization import Authorization |
18 |
| -from .exceptions import raise_error_from_response, KeycloakGetError, KeycloakSecretNotFound, \ |
19 |
| - KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError |
20 |
| -from .urls_patterns import ( |
21 |
| - URL_AUTH, |
22 |
| - URL_TOKEN, |
23 |
| - URL_USERINFO, |
24 |
| - URL_WELL_KNOWN, |
25 |
| - URL_LOGOUT, |
26 |
| - URL_CERTS, |
27 |
| - URL_ENTITLEMENT, |
28 |
| - URL_INTROSPECT |
29 |
| -) |
30 |
| -from .connection import ConnectionManager |
31 |
| -from jose import jwt |
32 |
| -import json |
33 |
| - |
34 |
| - |
35 |
| -class Keycloak: |
36 |
| - |
37 |
| - def __init__(self, server_url, client_id, realm_name, client_secret_key=None): |
38 |
| - self._client_id = client_id |
39 |
| - self._client_secret_key = client_secret_key |
40 |
| - self._realm_name = realm_name |
41 |
| - |
42 |
| - self._connection = ConnectionManager(base_url=server_url, |
43 |
| - headers={}, |
44 |
| - timeout=60) |
45 |
| - |
46 |
| - self._authorization = Authorization() |
47 |
| - |
48 |
| - @property |
49 |
| - def client_id(self): |
50 |
| - return self._client_id |
51 |
| - |
52 |
| - @client_id.setter |
53 |
| - def client_id(self, value): |
54 |
| - self._client_id = value |
55 |
| - |
56 |
| - @property |
57 |
| - def client_secret_key(self): |
58 |
| - return self._client_secret_key |
59 |
| - |
60 |
| - @client_secret_key.setter |
61 |
| - def client_secret_key(self, value): |
62 |
| - self._client_secret_key = value |
63 |
| - |
64 |
| - @property |
65 |
| - def realm_name(self): |
66 |
| - return self._realm_name |
67 |
| - |
68 |
| - @realm_name.setter |
69 |
| - def realm_name(self, value): |
70 |
| - self._realm_name = value |
71 |
| - |
72 |
| - @property |
73 |
| - def connection(self): |
74 |
| - return self._connection |
75 |
| - |
76 |
| - @connection.setter |
77 |
| - def connection(self, value): |
78 |
| - self._connection = value |
79 |
| - |
80 |
| - @property |
81 |
| - def authorization(self): |
82 |
| - return self._authorization |
83 |
| - |
84 |
| - @authorization.setter |
85 |
| - def authorization(self, value): |
86 |
| - self._authorization = value |
87 |
| - |
88 |
| - def _add_secret_key(self, payload): |
89 |
| - """ |
90 |
| - Add secret key if exist. |
91 |
| -
|
92 |
| - :param payload: |
93 |
| - :return: |
94 |
| - """ |
95 |
| - if self.client_secret_key: |
96 |
| - payload.update({"client_secret": self.client_secret_key}) |
97 |
| - |
98 |
| - return payload |
99 |
| - |
100 |
| - def _build_name_role(self, role): |
101 |
| - """ |
102 |
| -
|
103 |
| - :param role: |
104 |
| - :return: |
105 |
| - """ |
106 |
| - return self.client_id + "/" + role |
107 |
| - |
108 |
| - def _token_info(self, token, method_token_info, **kwargs): |
109 |
| - """ |
110 |
| -
|
111 |
| - :param token: |
112 |
| - :param method_token_info: |
113 |
| - :param kwargs: |
114 |
| - :return: |
115 |
| - """ |
116 |
| - if method_token_info == 'introspect': |
117 |
| - token_info = self.introspect(token) |
118 |
| - else: |
119 |
| - token_info = self.decode_token(token, **kwargs) |
120 |
| - |
121 |
| - return token_info |
122 |
| - |
123 |
| - def well_know(self): |
124 |
| - """ The most important endpoint to understand is the well-known configuration |
125 |
| - endpoint. It lists endpoints and other configuration options relevant to |
126 |
| - the OpenID Connect implementation in Keycloak. |
127 |
| -
|
128 |
| - :return It lists endpoints and other configuration options relevant. |
129 |
| - """ |
130 |
| - |
131 |
| - params_path = {"realm-name": self.realm_name} |
132 |
| - data_raw = self.connection.raw_get(URL_WELL_KNOWN.format(**params_path)) |
133 |
| - |
134 |
| - return raise_error_from_response(data_raw, KeycloakGetError) |
135 |
| - |
136 |
| - def auth_url(self, redirect_uri): |
137 |
| - """ |
138 |
| -
|
139 |
| - http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint |
140 |
| -
|
141 |
| - :return: |
142 |
| - """ |
143 |
| - return NotImplemented |
144 |
| - |
145 |
| - def token(self, username, password, grant_type=["password"]): |
146 |
| - """ |
147 |
| - The token endpoint is used to obtain tokens. Tokens can either be obtained by |
148 |
| - exchanging an authorization code or by supplying credentials directly depending on |
149 |
| - what flow is used. The token endpoint is also used to obtain new access tokens |
150 |
| - when they expire. |
151 |
| -
|
152 |
| - http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint |
153 |
| -
|
154 |
| - :param username: |
155 |
| - :param password: |
156 |
| - :param grant_type: |
157 |
| - :return: |
158 |
| - """ |
159 |
| - params_path = {"realm-name": self.realm_name} |
160 |
| - payload = {"username": username, "password": password, |
161 |
| - "client_id": self.client_id, "grant_type": grant_type} |
162 |
| - |
163 |
| - payload = self._add_secret_key(payload) |
164 |
| - data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), |
165 |
| - data=payload) |
166 |
| - return raise_error_from_response(data_raw, KeycloakGetError) |
167 |
| - |
168 |
| - def userinfo(self, token): |
169 |
| - """ |
170 |
| - The userinfo endpoint returns standard claims about the authenticated user, |
171 |
| - and is protected by a bearer token. |
172 |
| -
|
173 |
| - http://openid.net/specs/openid-connect-core-1_0.html#UserInfo |
174 |
| -
|
175 |
| - :param token: |
176 |
| - :return: |
177 |
| - """ |
178 |
| - |
179 |
| - self.connection.add_param_headers("Authorization", "Bearer " + token) |
180 |
| - params_path = {"realm-name": self.realm_name} |
181 |
| - |
182 |
| - data_raw = self.connection.raw_get(URL_USERINFO.format(**params_path)) |
183 |
| - |
184 |
| - return raise_error_from_response(data_raw, KeycloakGetError) |
185 |
| - |
186 |
| - def logout(self, refresh_token): |
187 |
| - """ |
188 |
| - The logout endpoint logs out the authenticated user. |
189 |
| - :param refresh_token: |
190 |
| - :return: |
191 |
| - """ |
192 |
| - params_path = {"realm-name": self.realm_name} |
193 |
| - payload = {"client_id": self.client_id, "refresh_token": refresh_token} |
194 |
| - |
195 |
| - payload = self._add_secret_key(payload) |
196 |
| - data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), |
197 |
| - data=payload) |
198 |
| - |
199 |
| - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) |
200 |
| - |
201 |
| - def certs(self): |
202 |
| - """ |
203 |
| - The certificate endpoint returns the public keys enabled by the realm, encoded as a |
204 |
| - JSON Web Key (JWK). Depending on the realm settings there can be one or more keys enabled |
205 |
| - for verifying tokens. |
206 |
| -
|
207 |
| - https://tools.ietf.org/html/rfc7517 |
208 |
| -
|
209 |
| - :return: |
210 |
| - """ |
211 |
| - params_path = {"realm-name": self.realm_name} |
212 |
| - data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) |
213 |
| - return raise_error_from_response(data_raw, KeycloakGetError) |
214 |
| - |
215 |
| - def entitlement(self, token, resource_server_id): |
216 |
| - """ |
217 |
| - Client applications can use a specific endpoint to obtain a special security token |
218 |
| - called a requesting party token (RPT). This token consists of all the entitlements |
219 |
| - (or permissions) for a user as a result of the evaluation of the permissions and authorization |
220 |
| - policies associated with the resources being requested. With an RPT, client applications can |
221 |
| - gain access to protected resources at the resource server. |
222 |
| -
|
223 |
| - :return: |
224 |
| - """ |
225 |
| - self.connection.add_param_headers("Authorization", "Bearer " + token) |
226 |
| - params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id} |
227 |
| - data_raw = self.connection.raw_get(URL_ENTITLEMENT.format(**params_path)) |
228 |
| - |
229 |
| - return raise_error_from_response(data_raw, KeycloakGetError) |
230 |
| - |
231 |
| - def introspect(self, token, rpt=None, token_type_hint=None): |
232 |
| - """ |
233 |
| - The introspection endpoint is used to retrieve the active state of a token. It is can only be |
234 |
| - invoked by confidential clients. |
235 |
| -
|
236 |
| - https://tools.ietf.org/html/rfc7662 |
237 |
| -
|
238 |
| - :param token: |
239 |
| - :param rpt: |
240 |
| - :param token_type_hint: |
241 |
| -
|
242 |
| - :return: |
243 |
| - """ |
244 |
| - params_path = {"realm-name": self.realm_name} |
245 |
| - |
246 |
| - payload = {"client_id": self.client_id, "token": token} |
247 |
| - |
248 |
| - if token_type_hint == 'requesting_party_token': |
249 |
| - if rpt: |
250 |
| - payload.update({"token": rpt, "token_type_hint": token_type_hint}) |
251 |
| - self.connection.add_param_headers("Authorization", "Bearer " + token) |
252 |
| - else: |
253 |
| - raise KeycloakRPTNotFound("Can't found RPT.") |
254 |
| - |
255 |
| - payload = self._add_secret_key(payload) |
256 |
| - |
257 |
| - data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), |
258 |
| - data=payload) |
259 |
| - |
260 |
| - return raise_error_from_response(data_raw, KeycloakGetError) |
261 |
| - |
262 |
| - def decode_token(self, token, key, algorithms=['RS256'], **kwargs): |
263 |
| - """ |
264 |
| - A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data |
265 |
| - structure that represents a cryptographic key. This specification |
266 |
| - also defines a JWK Set JSON data structure that represents a set of |
267 |
| - JWKs. Cryptographic algorithms and identifiers for use with this |
268 |
| - specification are described in the separate JSON Web Algorithms (JWA) |
269 |
| - specification and IANA registries established by that specification. |
270 |
| -
|
271 |
| - https://tools.ietf.org/html/rfc7517 |
272 |
| -
|
273 |
| - :param token: |
274 |
| - :param key: |
275 |
| - :param algorithms: |
276 |
| - :return: |
277 |
| - """ |
278 |
| - |
279 |
| - return jwt.decode(token, key, algorithms=algorithms, |
280 |
| - audience=self.client_id, **kwargs) |
281 |
| - |
282 |
| - def load_authorization_config(self, path): |
283 |
| - """ |
284 |
| - Load Keycloak settings (authorization) |
285 |
| -
|
286 |
| - :param path: settings file (json) |
287 |
| - :return: |
288 |
| - """ |
289 |
| - authorization_file = open(path, 'r') |
290 |
| - authorization_json = json.loads(authorization_file.read()) |
291 |
| - self.authorization.load_config(authorization_json) |
292 |
| - authorization_file.close() |
293 |
| - |
294 |
| - def get_policies(self, token, method_token_info='introspect', **kwargs): |
295 |
| - """ |
296 |
| - Get policies by user token |
297 |
| -
|
298 |
| - :param token: user token |
299 |
| - :return: policies list |
300 |
| - """ |
301 |
| - |
302 |
| - if not self.authorization.policies: |
303 |
| - raise KeycloakAuthorizationConfigError( |
304 |
| - "Keycloak settings not found. Load Authorization Keycloak settings." |
305 |
| - ) |
306 |
| - |
307 |
| - token_info = self._token_info(token, method_token_info, **kwargs) |
308 |
| - |
309 |
| - if method_token_info == 'introspect' and not token_info['active']: |
310 |
| - raise KeycloakInvalidTokenError( |
311 |
| - "Token expired or invalid." |
312 |
| - ) |
313 |
| - |
314 |
| - user_resources = token_info['resource_access'].get(self.client_id) |
315 |
| - |
316 |
| - if not user_resources: |
317 |
| - return None |
318 |
| - |
319 |
| - policies = [] |
320 |
| - |
321 |
| - for policy_name, policy in self.authorization.policies.items(): |
322 |
| - for role in user_resources['roles']: |
323 |
| - if self._build_name_role(role) in policy.roles: |
324 |
| - policies.append(policy) |
325 |
| - |
326 |
| - return list(set(policies)) |
327 |
| - |
328 |
| - def get_permissions(self, token, method_token_info='introspect', **kwargs): |
329 |
| - """ |
330 |
| - Get permission by user token |
331 |
| -
|
332 |
| - :param token: user token |
333 |
| - :param method_token_info: Decode token method |
334 |
| - :param kwargs: parameters for decode |
335 |
| - :return: permissions list |
336 |
| - """ |
337 |
| - |
338 |
| - if not self.authorization.policies: |
339 |
| - raise KeycloakAuthorizationConfigError( |
340 |
| - "Keycloak settings not found. Load Authorization Keycloak settings." |
341 |
| - ) |
342 |
| - |
343 |
| - token_info = self._token_info(token, method_token_info, **kwargs) |
344 |
| - |
345 |
| - if method_token_info == 'introspect' and not token_info['active']: |
346 |
| - raise KeycloakInvalidTokenError( |
347 |
| - "Token expired or invalid." |
348 |
| - ) |
349 |
| - |
350 |
| - user_resources = token_info['resource_access'].get(self.client_id) |
351 |
| - |
352 |
| - if not user_resources: |
353 |
| - return None |
354 |
| - |
355 |
| - permissions = [] |
356 |
| - |
357 |
| - for policy_name, policy in self.authorization.policies.items(): |
358 |
| - for role in user_resources['roles']: |
359 |
| - if self._build_name_role(role) in policy.roles: |
360 |
| - permissions += policy.permissions |
361 |
| - |
362 |
| - return list(set(permissions)) |
363 |
| - |
364 |
| - |
365 | 17 |
|
| 18 | +from .keycloak_openid import * |
| 19 | +from .keycloak_admin import * |
0 commit comments