Skip to content

Commit 8cfd833

Browse files
committed
Password Policies Config Python
1 parent 9722097 commit 8cfd833

File tree

4 files changed

+357
-18
lines changed

4 files changed

+357
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# Copyright 2023 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+
"""Firebase multifactor configuration management module.
15+
16+
This module contains functions for managing various multifactor configurations at
17+
the project and tenant level.
18+
"""
19+
from enum import Enum
20+
21+
__all__ = [
22+
'validate_keys',
23+
'PasswordPolicyServerConfig',
24+
'PasswordPolicyConfig',
25+
'CustomStrengthOptionsConfig',
26+
]
27+
28+
class PasswordPolicyServerConfig:
29+
"""Represents password policy configuration response received from the server and
30+
converts it to user format.
31+
"""
32+
33+
def __init__(self, data):
34+
if not isinstance(data, dict):
35+
raise ValueError(
36+
'Invalid data argument in PasswordPolicyConfig constructor: {0}'.format(data))
37+
self._data = data
38+
39+
@property
40+
def enforcement_state(self):
41+
return self._data.get('enforcementState', None)
42+
43+
@property
44+
def force_upgrade_on_signin(self):
45+
return self._data.get('forceUpgradeOnSignin', None)
46+
47+
@property
48+
def constraints(self):
49+
data = self._data.get('passwordPolicyVersions')
50+
if data is not None:
51+
return self.CustomStrengthOptionsServerConfig(data[0].get('customStrengthOptions'))
52+
return None
53+
54+
class CustomStrengthOptionsServerConfig:
55+
"""Represents custom strength options configuration response received from the server and
56+
converts it to user format.
57+
"""
58+
59+
def __init__(self, data):
60+
if not isinstance(data, dict):
61+
raise ValueError(
62+
'Invalid data argument in CustomStrengthOptionsServerConfig constructor: {0}'.format(data))
63+
self._data = data
64+
65+
@property
66+
def require_uppercase(self):
67+
return self._data.get('containsUppercaseCharacter', None)
68+
69+
@property
70+
def require_lowercase(self):
71+
return self._data.get('containsLowercaseCharacter', None)
72+
73+
@property
74+
def require_non_alphanumeric(self):
75+
return self._data.get('containsNonAlphanumericCharacter', None)
76+
77+
@property
78+
def require_numeric(self):
79+
return self._data.get('containsNumericCharacter', None)
80+
81+
@property
82+
def min_length(self):
83+
return self._data.get('minPasswordLength', None)
84+
85+
@property
86+
def max_length(self):
87+
return self._data.get('maxPasswordLength', None)
88+
89+
90+
def validate_keys(keys, valid_keys, config_name):
91+
for key in keys:
92+
if key not in valid_keys:
93+
raise ValueError(
94+
'"{0}" is not a valid "{1}" parameter.'.format(
95+
key, config_name))
96+
97+
98+
class CustomStrengthOptionsConfig:
99+
"""Represents the strength attributes for the password policy"""
100+
101+
def __init__(
102+
self,
103+
min_length: int = 6,
104+
max_length: int = 4096,
105+
require_uppercase: bool = False,
106+
require_lowercase: bool = False,
107+
require_non_alphanumeric: bool = False,
108+
require_numeric: bool = False,
109+
):
110+
self.min_length: int = min_length
111+
self.max_length: int = max_length
112+
self.require_uppercase: bool = require_uppercase
113+
self.require_lowercase: bool = require_lowercase
114+
self.require_non_alphanumeric: bool = require_non_alphanumeric
115+
self.require_numeric: bool = require_numeric
116+
117+
def to_dict(self) -> dict:
118+
data = {}
119+
constraints_request = {};
120+
if self.max_length is not None:
121+
constraints_request['maxPasswordLength'] = self.max_length
122+
if self.min_length is not None:
123+
constraints_request['minPasswordLength'] = self.min_length
124+
if self.require_lowercase is not None:
125+
constraints_request['containsLowercaseCharacter'] = self.require_lowercase
126+
if self.require_uppercase is not None:
127+
constraints_request['containsUppercaseCharacter'] = self.require_uppercase
128+
if self.require_non_alphanumeric is not None:
129+
constraints_request['containsNonAlphanumericCharacter'] = self.require_non_alphanumeric
130+
if self.require_numeric is not None:
131+
constraints_request['containsNumericCharacter'] = self.require_numeric
132+
data['customStrengthOptions'] = constraints_request
133+
return data
134+
135+
def validate(self):
136+
"""Validates a constraints object.
137+
138+
Raises:
139+
ValueError: In case of an unsuccessful validation.
140+
"""
141+
validate_keys(keys=vars(self).keys(),
142+
valid_keys={
143+
'require_numeric',
144+
'require_uppercase',
145+
'require_lowercase',
146+
'require_non_alphanumeric',
147+
'min_length',
148+
'max_length'},
149+
config_name='CustomStrengthOptionsConfig')
150+
if not isinstance(self.require_lowercase, bool):
151+
raise ValueError('constraints.require_lowercase must be a boolean')
152+
if not isinstance(self.require_uppercase, bool):
153+
raise ValueError('constraints.require_uppercase must be a boolean')
154+
if not isinstance(self.require_non_alphanumeric, bool):
155+
raise ValueError('constraints.require_non_alphanumeric must be a boolean')
156+
if not isinstance(self.require_numeric, bool):
157+
raise ValueError('constraints.require_numeric must be a boolean')
158+
if not isinstance(self.min_length, int):
159+
raise ValueError('constraints.min_length must be an integer')
160+
if not isinstance(self.max_length, int):
161+
raise ValueError('constraints.max_length must be an integer')
162+
if not (self.min_length >= 6 and self.min_length <= 30):
163+
raise ValueError('constraints.min_length must be between 6 and 30')
164+
if not (self.max_length >= 0 and self.max_length <= 4096 ):
165+
raise ValueError('constraints.max_length can be atmost 4096')
166+
if self.min_length > self.max_length:
167+
raise ValueError('min_length must be less than or equal to max_length')
168+
169+
170+
def build_server_request(self):
171+
self.validate()
172+
return self.to_dict()
173+
174+
class PasswordPolicyConfig:
175+
"""Represents the configuration for the password policy on the project"""
176+
177+
class EnforcementState(Enum):
178+
ENFORCE = 'ENFORCE'
179+
OFF = 'OFF'
180+
181+
def __init__(
182+
self,
183+
enforcement_state: EnforcementState = None,
184+
force_upgrade_on_signin: bool = False,
185+
constraints: CustomStrengthOptionsConfig = None,
186+
):
187+
self.enforcement_state: self.EnforcementState = enforcement_state
188+
self.force_upgrade_on_signin: bool = force_upgrade_on_signin
189+
self.constraints: CustomStrengthOptionsConfig = constraints
190+
191+
def to_dict(self) -> dict:
192+
data = {}
193+
if self.enforcement_state:
194+
data['enforcementState'] = self.enforcement_state.value
195+
if self.force_upgrade_on_signin:
196+
data['forceUpgradeOnSignin'] = self.force_upgrade_on_signin
197+
if self.constraints:
198+
data['passwordPolicyVersions'] = [self.constraints.to_dict()]
199+
return data
200+
201+
def validate(self):
202+
"""Validates a password_policy_config object.
203+
204+
Raises:
205+
ValueError: In case of an unsuccessful validation.
206+
"""
207+
validate_keys(
208+
keys=vars(self).keys(),
209+
valid_keys={
210+
'enforcement_state',
211+
'force_upgrade_on_signin',
212+
'constraints'},
213+
config_name='PasswordPolicyConfig')
214+
if self.enforcement_state is None:
215+
raise ValueError('password_policy_config.enforcement_state must be defined.')
216+
if not isinstance(self.enforcement_state, PasswordPolicyConfig.EnforcementState):
217+
raise ValueError(
218+
'password_policy_config.enforcement_state must be of type PasswordPolicyConfig.EnforcementState')
219+
if not isinstance(self.force_upgrade_on_signin, bool):
220+
raise ValueError(
221+
'password_policy_config.force_upgrade_on_signin must be a valid boolean')
222+
if self.enforcement_state is self.EnforcementState.ENFORCE and self.constraints is None:
223+
raise ValueError('password_policy_config.constraints must be defined')
224+
if not isinstance(self.constraints, CustomStrengthOptionsConfig):
225+
raise ValueError(
226+
'password_policy_config.constraints must be of type CustomStrengthOptionsConfig')
227+
self.constraints.validate()
228+
229+
def build_server_request(self):
230+
self.validate()
231+
return self.to_dict()
232+

firebase_admin/project_config_mgt.py

+26-4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
from firebase_admin import _utils
2525
from firebase_admin.multi_factor_config_mgt import MultiFactorConfig
2626
from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig
27+
from firebase_admin.password_policy_config_mgt import PasswordPolicyConfig
28+
from firebase_admin.password_policy_config_mgt import PasswordPolicyServerConfig
2729

2830
_PROJECT_CONFIG_MGT_ATTRIBUTE = '_project_config_mgt'
2931

@@ -52,11 +54,15 @@ def get_project_config(app=None):
5254
project_config_mgt_service = _get_project_config_mgt_service(app)
5355
return project_config_mgt_service.get_project_config()
5456

55-
def update_project_config(multi_factor_config: MultiFactorConfig = None, app=None):
57+
def update_project_config(
58+
multi_factor_config: MultiFactorConfig = None,
59+
password_policy_config: PasswordPolicyConfig = None,
60+
app=None):
5661
"""Update the Project Config with the given options.
5762
Args:
5863
multi_factor_config: Updated Multi Factor Authentication configuration
59-
(optional)
64+
(optional).
65+
password_policy_config: Updated Password Policy configuration (optional).
6066
app: An App instance (optional).
6167
Returns:
6268
Project: An updated ProjectConfig object.
@@ -65,7 +71,9 @@ def update_project_config(multi_factor_config: MultiFactorConfig = None, app=Non
6571
FirebaseError: If an error occurs while updating the project.
6672
"""
6773
project_config_mgt_service = _get_project_config_mgt_service(app)
68-
return project_config_mgt_service.update_project_config(multi_factor_config=multi_factor_config)
74+
return project_config_mgt_service.update_project_config(
75+
multi_factor_config=multi_factor_config,
76+
password_policy_config=password_policy_config)
6977

7078

7179
def _get_project_config_mgt_service(app):
@@ -89,6 +97,13 @@ def multi_factor_config(self):
8997
return MultiFactorServerConfig(data)
9098
return None
9199

100+
@property
101+
def password_policy_config(self):
102+
data = self._data.get('passwordPolicyConfig')
103+
if data:
104+
return PasswordPolicyServerConfig(data)
105+
return None
106+
92107
class _ProjectConfigManagementService:
93108
"""Firebase project management service."""
94109

@@ -112,14 +127,21 @@ def get_project_config(self) -> ProjectConfig:
112127
else:
113128
return ProjectConfig(body)
114129

115-
def update_project_config(self, multi_factor_config: MultiFactorConfig = None) -> ProjectConfig:
130+
def update_project_config(
131+
self,
132+
multi_factor_config: MultiFactorConfig = None,
133+
password_policy_config: PasswordPolicyConfig = None) -> ProjectConfig:
116134
"""Updates the specified project with the given parameters."""
117135

118136
payload = {}
119137
if multi_factor_config is not None:
120138
if not isinstance(multi_factor_config, MultiFactorConfig):
121139
raise ValueError('multi_factor_config must be of type MultiFactorConfig.')
122140
payload['mfa'] = multi_factor_config.build_server_request()
141+
if password_policy_config is not None:
142+
if not isinstance(password_policy_config, PasswordPolicyConfig):
143+
raise ValueError('password_policy_config must be of type PasswordPolicyConfig.')
144+
payload['passwordPolicyConfig'] = password_policy_config.build_server_request()
123145
if not payload:
124146
raise ValueError(
125147
'At least one parameter must be specified for update.')

0 commit comments

Comments
 (0)