Skip to content

Commit e569662

Browse files
committed
Password Policies Config Python
1 parent 9722097 commit e569662

File tree

4 files changed

+364
-17
lines changed

4 files changed

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

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)