8000 feat(auth): Adding SAMLProviderConfig type and the getter method (#437) · brianrodri/firebase-admin-python@185771a · GitHub
[go: up one dir, main page]

Skip to content

Commit 185771a

Browse files
authored
feat(auth): Adding SAMLProviderConfig type and the getter method (firebase#437)
* feat(auth): Adding SAMLProviderConfig type and the getter method * Added ConfigurationNotFoundError type * Fixing a lint error related to super delegation
1 parent 2d333fa commit 185771a

File tree

10 files changed

+387
-96
lines changed

10 files changed

+387
-96
lines changed

firebase_admin/_auth_providers.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Copyright 2020 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Firebase auth providers management sub module."""
16+
17+
import requests
18+
19+
from firebase_admin import _auth_utils
20+
21+
22+
class ProviderConfig:
23+
"""Parent type for all authentication provider config types."""
24+
25+
def __init__(self, data):
26+
self._data = data
27+
28+
@property
29+
def provider_id(self):
30+
name = self._data['name']
31+
return name.split('/')[-1]
32+
33+
@property
34+
def display_name(self):
35+
return self._data.get('displayName')
36+
37+
@property
38+
def enabled(self):
39+
return self._data['enabled']
40+
41+
42+
class SAMLProviderConfig(ProviderConfig):
43+
"""Represents he SAML auth provider configuration.
44+
45+
See http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html."""
46+
47+
@property
48+
def idp_entity_id(self):
49+
return self._data.get('idpConfig', {})['idpEntityId']
50+
51+
@property
52+
def sso_url(self):
53+
return self._data.get('idpConfig', {})['ssoUrl']
54+
55+
@property
56+
def x509_certificates(self):
57+
certs = self._data.get('idpConfig', {})['idpCertificates']
58+
return [c['x509Certificate'] for c in certs]
59+
60+
@property
61+
def request_signing_enabled(self):
62+
return self._data.get('idpConfig', {})['signRequest']
63+
64+
@property
65+
def callback_url(self):
66+
return self._data.get('spConfig', {})['callbackUri']
67+
68+
@property
69+
def rp_entity_id(self):
70+
return self._data.get('spConfig', {})['spEntityId']
71+
72+
73+
class ProviderConfigClient:
74+
"""Client for managing Auth provider configurations."""
75+
76+
PROVIDER_CONFIG_URL = 'https://identitytoolkit.googleapis.com/v2beta1'
77+
78+
def __init__(self, http_client, project_id, tenant_id=None):
79+
self.http_client = http_client
80+
self.base_url = '{0}/projects/{1}'.format(self.PROVIDER_CONFIG_URL, project_id)
81+
if tenant_id:
82+
self.base_url += '/tenants/{0}'.format(tenant_id)
83+
84+
def get_saml_provider_config(self, provider_id):
85+
if not isinstance(provider_id, str):
86+
raise ValueError(
87+
'Invalid SAML provider ID: {0}. Provider ID must be a non-empty string.'.format(
88+
provider_id))
89+
if not provider_id.startswith('saml.'):
90+
raise ValueError('Invalid SAML provider ID: {0}.'.format(provider_id))
91+
92+
body = self._make_request('get', '/inboundSamlConfigs/{0}'.format(provider_id))
93+
return SAMLProviderConfig(body)
94+
95+
def _make_request(self, method, path, body=None):
96+
url = '{0}{1}'.format(self.base_url, path)
97+
try:
98+
return self.http_client.body(method, url, json=body)
99+
except requests.exceptions.RequestException as error:
100+
raise _auth_utils.handle_auth_backend_error(error)

firebase_admin/_auth_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,17 @@ def __init__(self, message):
307307
exceptions.InvalidArgumentError.__init__(self, message)
308308

309309

310+
class ConfigurationNotFoundError(exceptions.NotFoundError):
311+
"""No auth provider found for the specified identifier."""
312+
313+
default_message = 'No auth provider found for the given identifier'
314+
315+
def __init__(self, message, cause=None, http_response=None):
316+
exceptions.NotFoundError.__init__(self, message, cause, http_response)
317+
318+
310319
_CODE_TO_EXC_TYPE = {
320+
'CONFIGURATION_NOT_FOUND': ConfigurationNotFoundError,
311321
'DUPLICATE_EMAIL': EmailAlreadyExistsError,
312322
'DUPLICATE_LOCAL_ID': UidAlreadyExistsError,
313323
'EMAIL_EXISTS': EmailAlreadyExistsError,

firebase_admin/_token_gen.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,13 @@ def from_iam(cls, request, google_cred, service_account):
8282
class TokenGenerator:
8383
"""Generates custom tokens and session cookies."""
8484

85-
def __init__(self, app, client):
85+
ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v1'
86+
87+
def __init__(self, app, http_client):
8688
self.app = app
87-
self.client = client
89+
self.http_client = http_client
8890
self.request = transport.requests.Request()
91+
self.base_url = '{0}/projects/{1}'.format(self.ID_TOOLKIT_URL, app.project_id)
8992
self._signing_provider = None
9093

9194
def _init_signing_provider(self):
@@ -192,13 +195,13 @@ def create_session_cookie(self, id_token, expires_in):
192195
raise ValueError('Illegal expiry duration: {0}. Duration must be at most {1} '
193196
'seconds.'.format(expires_in, MAX_SESSION_COOKIE_DURATION_SECONDS))
194197

198+
url = '{0}:createSessionCookie'.format(self.base_url)
195199
payload = {
196200
'idToken': id_token,
197201
'validDuration': expires_in,
198202
}
199203
try:
200-
body, http_resp = self.client.body_and_response(
201-
'post', ':createSessionCookie', json=payload)
204+
body, http_resp = self.http_client.body_and_response('post', url, json=payload)
202205
except requests.exceptions.RequestException as error:
203206
raise _auth_utils.handle_auth_backend_error(error)
204207
else:

firebase_admin/_user_mgt.py

Lines changed: 43 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -454,8 +454,13 @@ def encode_action_code_settings(settings):
454454
class UserManager:
455455
"""Provides methods for interacting with the Google Identity Toolkit."""
456456

457-
def __init__(self, client):
458-
self._client = client
457+
ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v1'
458+
459+
def __init__(self, http_client, project_id, tenant_id=None):
460+
self.http_client = http_client
461+
self.base_url = '{0}/projects/{1}'.format(self.ID_TOOLKIT_URL, project_id)
462+
if tenant_id:
463+
self.base_url += '/tenants/{0}'.format(tenant_id)
459464

460465
def get_user(self, **kwargs):
461466
"""Gets the user data corresponding to the provided key."""
@@ -471,17 +476,12 @@ def get_user(self, **kwargs):
471476
else:
472477
raise TypeError('Unsupported keyword arguments: {0}.'.format(kwargs))
473478

474-
try:
475-
body, http_resp = self._client.body_and_response(
476-
'post', '/accounts:lookup', json=payload)
477-
except requests.exceptions.RequestException as error:
478-
raise _auth_utils.handle_auth_backend_error(error)
479-
else:
480-
if not body or not body.get('users'):
481-
raise _auth_utils.UserNotFoundError(
482-
'No user record found for the provided {0}: {1}.'.format(key_type, key),
483-
http_response=http_resp)
484-
return body['users'][0]
479+
body, http_resp = self._make_request('post', '/accounts:lookup', json=payload)
480+
if not body or not body.get('users'):
481+
raise _auth_utils.UserNotFoundError(
482+
'No user record found for the provided {0}: {1}.'.format(key_type, key),
483+
http_response=http_resp)
484+
return body['users'][0]
485485

486486
def list_users(self, page_token=None, max_results=MAX_LIST_USERS_RESULTS):
487487
"""Retrieves a batch of users."""
@@ -498,10 +498,8 @@ def list_users(self, page_token=None, max_results=MAX_LIST_USERS_RESULTS):
498498
payload = {'maxResults': max_results}
499499
if page_token:
500500
payload['nextPageToken'] = page_token
501-
try:
502-
return self._client.body('get', '/accounts:batchGet', params=payload)
503-
except requests.exceptions.RequestException as error:
504-
raise _auth_utils.handle_auth_backend_error(error)
501+
body, _ = self._make_request('get', '/accounts:batchGet', params=payload)
502+
return body
505503

506504
def create_user(self, uid=None, display_name=None, email=None, phone_number=None,
507505
photo_url=None, password=None, disabled=None, email_verified=None):
@@ -517,15 +515,11 @@ def create_user(self, uid=None, display_name=None, email=None, phone_number=None
517515
'disabled': bool(disabled) if disabled is not None else None,
518516
}
519517
payload = {k: v for k, v in payload.items() if v is not None}
520-
try:
521-
body, http_resp = self._client.body_and_response('post', '/accounts', json=payload)
522-
except requests.exceptions.RequestException as error:
523-
raise _auth_utils.handle_auth_backend_error(error)
524-
else:
525-
if not body or not body.get('localId'):
526-
raise _auth_utils.UnexpectedResponseError(
527-
'Failed to create new user.', http_response=http_resp)
528-
return body.get('localId')
518+
body, http_resp = self._make_request('post', '/accounts', json=payload)
519+
if not body or not body.get('localId'):
520+
raise _auth_utils.UnexpectedResponseError(
521+
'Failed to create new user.', http_response=http_resp)
522+
return body.get('localId')
529523

530524
def update_user(self, uid, display_name=None, email=None, phone_number=None,
531525
photo_url=None, password=None, disabled=None, email_verified=None,
@@ -568,29 +562,19 @@ def update_user(self, uid, display_name=None, email=None, phone_number=None,
568562
payload['customAttributes'] = _auth_utils.validate_custom_claims(json_claims)
569563

570564
payload = {k: v for k, v in payload.items() if v is not None}
571-
try:
572-
body, http_resp = self._client.body_and_response(
573-
'post', '/accounts:update', json=payload)
574-
except requests.exceptions.RequestException as error:
575-
raise _auth_utils.handle_auth_backend_error(error)
576-
else:
577-
if not body or not body.get('localId'):
578-
raise _auth_utils.UnexpectedResponseError(
579-
'Failed to update user: {0}.'.format(uid), http_response=http_resp)
580-
return body.get('localId')
565+
body, http_resp = self._make_request('post', '/accounts:update', json=payload)
566+
if not body or not body.get('localId'):
567+
raise _auth_utils.UnexpectedResponseError(
568+
'Failed to update user: {0}.'.format(uid), http_response=http_resp)
569+
return body.get('localId')
581570

582571
def delete_user(self, uid):
583572
"""Deletes the user identified by the specified user ID."""
584573
_auth_utils.validate_uid(uid, required=True)
585-
try:
586-
body, http_resp = self._client.body_and_response(
587-
'post', '/accounts:delete', json={'localId' : uid})
588-
except requests.exceptions.RequestException as error:
589-
raise _auth_utils.handle_auth_backend_error(error)
590-
else:
591-
if not body or not body.get('kind'):
592-
raise _auth_utils.UnexpectedResponseError(
593-
'Failed to delete user: {0}.'.format(uid), http_response=http_resp)
574+
body, http_resp = self._make_request('post', '/accounts:delete', json={'localId' : uid})
575+
if not body or not body.get('kind'):
576+
raise _auth_utils.UnexpectedResponseError(
577+
'Failed to delete user: {0}.'.format(uid), http_response=http_resp)
594578

595579
def import_users(self, users, hash_alg=None):
596580
"""Imports the given list of users to Firebase Auth."""
@@ -609,16 +593,11 @@ def import_users(self, users, hash_alg=None):
609593
if not isinstance(hash_alg, _user_import.UserImportHash):
610594
raise ValueError('A UserImportHash is required to import users with passwords.')
611595
payload.update(hash_alg.to_dict())
612-
try:
613-
body, http_resp = self._client.body_and_response(
614-
'post', '/accounts:batchCreate', json=payload)
615-
except requests.exceptions.RequestException as error:
616-
raise _auth_utils.handle_auth_backend_error(error)
617-
else:
618-
if not isinstance(body, dict):
619-
raise _auth_utils.UnexpectedResponseError(
620-
'Failed to import users.', http_response=http_resp)
621-
return body
596+
body, http_resp = self._make_request('post', '/accounts:batchCreate', json=payload)
597+
if not isinstance(body, dict):
598+
raise _auth_utils.UnexpectedResponseError(
599+
'Failed to import users.', http_response=http_resp)
600+
return body
622601

623602
def generate_email_action_link(self, action_type, email, action_code_settings=None):
624603
"""Fetches the email action links for types
@@ -646,16 +625,18 @@ def generate_email_action_link(self, action_type, email, action_code_settings=No
646625
if action_code_settings:
647626
payload.update(encode_action_code_settings(action_code_settings))
648627

628+
body, http_resp = self._make_request('post', '/accounts:sendOobCode', json=payload)
629+
if not body or not body.get('oobLink'):
630+
raise _auth_utils.UnexpectedResponseError(
631+
'Failed to generate email action link.', http_response=http_resp)
632+
return body.get('oobLink')
633+
634+
def _make_request(self, method, path, **kwargs):
635+
url = '{0}{1}'.format(self.base_url, path)
649636
try:
650-
body, http_resp = self._client.body_and_response(
651-
'post', '/accounts:sendOobCode', json=payload)
637+
return self.http_client.body_and_response(method, url, **kwargs)
652638
except requests.exceptions.RequestException as error:
653639
raise _auth_utils.handle_auth_backend_error(error)
654-
else:
655-
if not body or not body.get('oobLink'):
656-
raise _auth_utils.UnexpectedResponseError(
657-
'Failed to generate email action link.', http_response=http_resp)
658-
return body.get('oobLink')
659640

660641

661642
class _UserIterator:

0 commit comments

Comments
 (0)
0