password-validator-s 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- password_validator/__init__.py +30 -0
- password_validator/config/__init__.py +0 -0
- password_validator/config/settings.py +135 -0
- password_validator/constants.py +55 -0
- password_validator/engine/__init__.py +0 -0
- password_validator/engine/validator.py +168 -0
- password_validator/enums.py +72 -0
- password_validator/exceptions.py +50 -0
- password_validator/loaders/__init__.py +10 -0
- password_validator/loaders/env_loader.py +149 -0
- password_validator/models.py +261 -0
- password_validator/rules/__init__.py +24 -0
- password_validator/rules/base.py +91 -0
- password_validator/rules/digits.py +35 -0
- password_validator/rules/length.py +43 -0
- password_validator/rules/lowercase.py +34 -0
- password_validator/rules/registry.py +81 -0
- password_validator/rules/special.py +48 -0
- password_validator/rules/uppercase.py +34 -0
- password_validator/strength/__init__.py +8 -0
- password_validator/strength/analyzer.py +101 -0
- password_validator/strength/analyzers/__init__.py +55 -0
- password_validator/strength/analyzers/dictionary.py +347 -0
- password_validator/strength/analyzers/keyboard.py +378 -0
- password_validator/strength/analyzers/repeat.py +311 -0
- password_validator/strength/analyzers/sequential.py +281 -0
- password_validator/strength/config.py +120 -0
- password_validator/strength/scorer.py +473 -0
- password_validator/strength/suggestions.py +224 -0
- password_validator/strength/weights.py +65 -0
- password_validator/version.py +7 -0
- password_validator_s-1.0.0.dist-info/METADATA +245 -0
- password_validator_s-1.0.0.dist-info/RECORD +38 -0
- password_validator_s-1.0.0.dist-info/WHEEL +5 -0
- password_validator_s-1.0.0.dist-info/licenses/LICENSE +19 -0
- password_validator_s-1.0.0.dist-info/top_level.txt +2 -0
- plugins/__init__.py +0 -0
- plugins/manager.py +4 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry point for the password_validator package.
|
|
3
|
+
A configurable password validation library that allows users to define custom rules and
|
|
4
|
+
criteria for password strength and security.
|
|
5
|
+
"""
|
|
6
|
+
from .version import (
|
|
7
|
+
__title__,
|
|
8
|
+
__version__,
|
|
9
|
+
__author__,
|
|
10
|
+
__description__
|
|
11
|
+
)
|
|
12
|
+
from .engine.validator import PasswordValidator, ValidationResult
|
|
13
|
+
from .strength.scorer import PasswordStrengthScorer
|
|
14
|
+
|
|
15
|
+
# Package metadata
|
|
16
|
+
VERSION = __version__
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# Package Metadata
|
|
20
|
+
"__version__",
|
|
21
|
+
"__title__",
|
|
22
|
+
"__author__",
|
|
23
|
+
"__description__",
|
|
24
|
+
"VERSION",
|
|
25
|
+
|
|
26
|
+
# Main API
|
|
27
|
+
"PasswordValidator",
|
|
28
|
+
"ValidationResult",
|
|
29
|
+
"PasswordStrengthScorer",
|
|
30
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application-level configuration for password-validator.
|
|
3
|
+
This module provides the top-level package configuration.
|
|
4
|
+
Strength-specific configuration is maintained separately in:
|
|
5
|
+
password_validator.strength.config.StrengthConfig
|
|
6
|
+
Password policy configuration is maintained here and can be
|
|
7
|
+
loaded from environment variables.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from ..loaders.env_loader import EnvLoader
|
|
13
|
+
from ..strength.config import StrengthConfig
|
|
14
|
+
from password_validator.constants import *
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class PasswordRuleConfig:
|
|
19
|
+
"""
|
|
20
|
+
Configuration for password policy rules.
|
|
21
|
+
These settings determine whether a password satisfies the configured password policy.
|
|
22
|
+
This is separate from password strength scoring.
|
|
23
|
+
"""
|
|
24
|
+
min_length: int = 8
|
|
25
|
+
max_length: int = 128
|
|
26
|
+
|
|
27
|
+
require_uppercase: bool = True
|
|
28
|
+
require_lowercase: bool =True
|
|
29
|
+
require_digit: bool =True
|
|
30
|
+
require_special: bool = True
|
|
31
|
+
special_characters: str = "!@#$%^&*()-_=[]{}|/:;'<>?"
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
require_whitespace: bool
|
|
35
|
+
min_uppercase: int
|
|
36
|
+
min_lowercase: int
|
|
37
|
+
min_digit: int
|
|
38
|
+
min_special: int
|
|
39
|
+
|
|
40
|
+
allowed_special: str
|
|
41
|
+
allow_spaces: bool
|
|
42
|
+
|
|
43
|
+
max_repeat: int
|
|
44
|
+
check_sequential: bool
|
|
45
|
+
check_common_passwords: bool
|
|
46
|
+
check_dictionary: bool
|
|
47
|
+
|
|
48
|
+
custom_regex: str | None
|
|
49
|
+
min_entropy: int
|
|
50
|
+
language: str
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_env(cls, env: EnvLoader) -> "PasswordRuleConfig":
|
|
55
|
+
"""
|
|
56
|
+
Loads the password policy configuration from EnvLoader.
|
|
57
|
+
:param env: .env file. Defaults to ".env".
|
|
58
|
+
:return: password policy object
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
return cls(
|
|
62
|
+
min_length=env.get_int("PASSWORD_MIN_LENGTH", DEFAULT_MIN_LENGTH),
|
|
63
|
+
max_length=env.get_int("PASSWORD_MAX_LENGTH", DEFAULT_MAX_LENGTH),
|
|
64
|
+
|
|
65
|
+
require_uppercase=env.get_bool("PASSWORD_REQUIRE_UPPERCASE", DEFAULT_REQUIRE_UPPERCASE),
|
|
66
|
+
require_lowercase=env.get_bool("PASSWORD_REQUIRE_LOWERCASE", DEFAULT_REQUIRE_LOWERCASE),
|
|
67
|
+
require_digit=env.get_bool("PASSWORD_REQUIRE_DIGIT", DEFAULT_REQUIRE_DIGIT),
|
|
68
|
+
require_special=env.get_bool("PASSWORD_REQUIRE_SPECIAL", DEFAULT_REQUIRE_SPECIAL),
|
|
69
|
+
special_characters=env.get("PASSWORD_SPECIAL_CHARACTERS", DEFAULT_SPECIAL_CHARACTERS)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def validate(self) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Validate the configuration itself.
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If the configuration is invalid.
|
|
77
|
+
:return:
|
|
78
|
+
"""
|
|
79
|
+
if self.min_length < 1:
|
|
80
|
+
raise ValueError("PASSWORD_MIN_LENGTH must be greater than 0")
|
|
81
|
+
|
|
82
|
+
if self.max_length < self.min_length:
|
|
83
|
+
raise ValueError("PASSWORD_MAX_LENGTH must be greater than or equal to PASSWORD_MIN_LENGTH")
|
|
84
|
+
|
|
85
|
+
if self.require_special and not self.special_characters:
|
|
86
|
+
raise ValueError("PASSWORD_SPECIAL_CHARACTERS cannot be empty when PASSWORD_REQUIRE_SPECIAL=true")
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def defaults(cls) -> "PasswordRuleConfig":
|
|
90
|
+
"""
|
|
91
|
+
Return the package defaults.
|
|
92
|
+
Useful for applications that don't use .env.
|
|
93
|
+
:return:
|
|
94
|
+
"""
|
|
95
|
+
return cls()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(slots=True, frozen=True)
|
|
99
|
+
class Settings:
|
|
100
|
+
"""
|
|
101
|
+
Root configuration object for the password-validator package.
|
|
102
|
+
This object is the main configuration boundary for the package.
|
|
103
|
+
|
|
104
|
+
Environment variables are loaded once and converted into typed configuration objects.
|
|
105
|
+
"""
|
|
106
|
+
rules: PasswordRuleConfig
|
|
107
|
+
strength: StrengthConfig
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_env(cls, env_file: str = ".env") -> "Settings":
|
|
111
|
+
"""
|
|
112
|
+
Build application settings from environment file.
|
|
113
|
+
:param env_file: Path to the .env file
|
|
114
|
+
:return: Fully initialized settings object
|
|
115
|
+
"""
|
|
116
|
+
env = EnvLoader(env_file)
|
|
117
|
+
rules = PasswordRuleConfig.from_env(env)
|
|
118
|
+
rules.validate()
|
|
119
|
+
strength = StrengthConfig.from_env(env_file)
|
|
120
|
+
|
|
121
|
+
return cls(rules=rules, strength=strength)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def defaults(cls) -> "Settings":
|
|
125
|
+
"""
|
|
126
|
+
Return package defaults without reading an environment file
|
|
127
|
+
:return:
|
|
128
|
+
"""
|
|
129
|
+
rules = PasswordRuleConfig()
|
|
130
|
+
rules.validate()
|
|
131
|
+
|
|
132
|
+
return cls(
|
|
133
|
+
rules=rules,
|
|
134
|
+
strength=StrengthConfig.defaults()
|
|
135
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
All constants used in the password_validator package are defined here.
|
|
3
|
+
- DEFAULT_SPECIAL_CHARACTERS
|
|
4
|
+
- DEFAULT_BLACKLIST
|
|
5
|
+
- DEFAULT_MIN_LENGTH
|
|
6
|
+
- DEFAULT_ENTROPY
|
|
7
|
+
"""
|
|
8
|
+
# Password defaults
|
|
9
|
+
DEFAULT_MIN_LENGTH = 8
|
|
10
|
+
DEFAULT_MAX_LENGTH = 64
|
|
11
|
+
|
|
12
|
+
DEFAULT_REQUIRE_UPPERCASE = True
|
|
13
|
+
DEFAULT_REQUIRE_LOWERCASE = True
|
|
14
|
+
DEFAULT_REQUIRE_DIGIT = True
|
|
15
|
+
DEFAULT_REQUIRE_SPECIAL = True
|
|
16
|
+
|
|
17
|
+
# Character defaults
|
|
18
|
+
DEFAULT_SPECIAL_CHARACTERS = "!@#$%^&*()-_=+[]{}|;:'\",.<>?/`~"
|
|
19
|
+
|
|
20
|
+
DEFAULT_MIN_UPPERCASE = 1
|
|
21
|
+
DEFAULT_MIN_LOWERCASE = 1
|
|
22
|
+
DEFAULT_MIN_DIGITS = 1
|
|
23
|
+
DEFAULT_MIN_SPECIAL = 1
|
|
24
|
+
|
|
25
|
+
# Security defaults
|
|
26
|
+
DEFAULT_MAX_REPEAT_COUNT = 3
|
|
27
|
+
DEFAULT_MIN_ENTROPY = 40
|
|
28
|
+
|
|
29
|
+
# Password policies
|
|
30
|
+
DEFAULT_ALLOW_SPACES = False
|
|
31
|
+
DEFAULT_CHECK_SEQUENTIAL = True
|
|
32
|
+
DEFAULT_CHECK_COMMON_PASSWORDS = True
|
|
33
|
+
DEFAULT_CHECK_DICTIONARY = True
|
|
34
|
+
|
|
35
|
+
# Regex
|
|
36
|
+
DEFAULT_CUSTOM_REGEX = None
|
|
37
|
+
|
|
38
|
+
# Language defaults
|
|
39
|
+
DEFAULT_LANGUAGE = "en"
|
|
40
|
+
|
|
41
|
+
# Common sequences and patterns
|
|
42
|
+
SEQUENTIAL_PATTERNS = [
|
|
43
|
+
"abcdefghijklmnopqrstuvwxyz",
|
|
44
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
45
|
+
"0123456789"
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Common password file
|
|
49
|
+
DEFAULT_COMMON_PASSWORDS_FILE = "resources/common_passwords.txt"
|
|
50
|
+
DEFAULT_DICTIONARY_FILE = "resources/dictionary.txt"
|
|
51
|
+
|
|
52
|
+
# Error messages
|
|
53
|
+
DEFAULT_ERROR_PREFIX = (
|
|
54
|
+
"Password validation failed"
|
|
55
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Password validation engine.
|
|
3
|
+
|
|
4
|
+
The validation engine executes password policy rules and returns a
|
|
5
|
+
structured validation result.
|
|
6
|
+
|
|
7
|
+
It does not perform password strength analysis. Strength analysis is
|
|
8
|
+
handled by password_validator.strength.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
|
|
15
|
+
from ..config.settings import PasswordRuleConfig
|
|
16
|
+
from ..models import ValidationResult
|
|
17
|
+
from ..rules.base import Rule, RuleResult
|
|
18
|
+
from ..rules.digits import DigitsRule
|
|
19
|
+
from ..rules.length import LengthRule
|
|
20
|
+
from ..rules.lowercase import LowercaseRule
|
|
21
|
+
from ..rules.special import SpecialCharacterRule
|
|
22
|
+
from ..rules.uppercase import UppercaseRule
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PasswordValidator:
|
|
26
|
+
"""
|
|
27
|
+
Password policy validation engine.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
config: PasswordRuleConfig | None = None,
|
|
33
|
+
rules: Sequence[Rule] | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Initialize the password validator.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
config: Password policy configuration.
|
|
40
|
+
rules: Optional custom rule collection.
|
|
41
|
+
|
|
42
|
+
When rules are supplied, they completely replace the default
|
|
43
|
+
rule collection.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
self.config = config or PasswordRuleConfig()
|
|
47
|
+
|
|
48
|
+
if hasattr(self.config, "validate"):
|
|
49
|
+
self.config.validate()
|
|
50
|
+
|
|
51
|
+
if rules is not None:
|
|
52
|
+
self.rules = list(rules)
|
|
53
|
+
else:
|
|
54
|
+
self.rules = self._build_default_rules()
|
|
55
|
+
|
|
56
|
+
def _build_default_rules(self) -> list[Rule]:
|
|
57
|
+
"""Build the default password policy rule collection."""
|
|
58
|
+
|
|
59
|
+
rules: list[Rule] = []
|
|
60
|
+
|
|
61
|
+
rules.append(
|
|
62
|
+
LengthRule(
|
|
63
|
+
min_length=self.config.min_length,
|
|
64
|
+
max_length=self.config.max_length,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if self.config.require_uppercase:
|
|
69
|
+
rules.append(UppercaseRule())
|
|
70
|
+
|
|
71
|
+
if self.config.require_lowercase:
|
|
72
|
+
rules.append(LowercaseRule())
|
|
73
|
+
|
|
74
|
+
if self.config.require_digit:
|
|
75
|
+
rules.append(DigitsRule())
|
|
76
|
+
|
|
77
|
+
if self.config.require_special:
|
|
78
|
+
rules.append(
|
|
79
|
+
SpecialCharacterRule(
|
|
80
|
+
special_characters=self.config.special_characters,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return rules
|
|
85
|
+
|
|
86
|
+
def validate(self, password: str) -> ValidationResult:
|
|
87
|
+
"""
|
|
88
|
+
Validate a password against all configured rules.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
password: Password to validate.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
ValidationResult containing the overall status and individual
|
|
95
|
+
rule results.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
TypeError: If password is not a string.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
if not isinstance(password, str):
|
|
102
|
+
raise TypeError("Password must be a string")
|
|
103
|
+
|
|
104
|
+
rule_results: list[RuleResult] = []
|
|
105
|
+
passed: list[str] = []
|
|
106
|
+
failed: list[str] = []
|
|
107
|
+
errors: list[str] = []
|
|
108
|
+
|
|
109
|
+
for rule in self.rules:
|
|
110
|
+
result = rule.validate(password)
|
|
111
|
+
|
|
112
|
+
rule_results.append(result)
|
|
113
|
+
|
|
114
|
+
rule_name = self._rule_name(rule, result)
|
|
115
|
+
|
|
116
|
+
if result.passed:
|
|
117
|
+
passed.append(rule_name)
|
|
118
|
+
else:
|
|
119
|
+
failed.append(rule_name)
|
|
120
|
+
|
|
121
|
+
if result.message:
|
|
122
|
+
errors.append(result.message)
|
|
123
|
+
|
|
124
|
+
return ValidationResult(
|
|
125
|
+
valid=not failed,
|
|
126
|
+
passed=tuple(passed),
|
|
127
|
+
failed=tuple(failed),
|
|
128
|
+
errors=tuple(errors),
|
|
129
|
+
rule_results=tuple(rule_results),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _rule_name(
|
|
134
|
+
rule: Rule,
|
|
135
|
+
result: RuleResult,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Resolve a stable rule name.
|
|
139
|
+
|
|
140
|
+
Resolution order:
|
|
141
|
+
|
|
142
|
+
1. RuleResult.rule_name
|
|
143
|
+
2. rule.name
|
|
144
|
+
3. Rule class name with the "Rule" suffix removed
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
result_name = getattr(result, "rule_name", None)
|
|
148
|
+
|
|
149
|
+
if result_name:
|
|
150
|
+
return str(result_name)
|
|
151
|
+
|
|
152
|
+
rule_name = getattr(rule, "name", None)
|
|
153
|
+
|
|
154
|
+
if rule_name:
|
|
155
|
+
return str(rule_name)
|
|
156
|
+
|
|
157
|
+
class_name = rule.__class__.__name__
|
|
158
|
+
|
|
159
|
+
if class_name.endswith("Rule"):
|
|
160
|
+
class_name = class_name[:-4]
|
|
161
|
+
|
|
162
|
+
return class_name.lower()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
__all__ = [
|
|
166
|
+
"PasswordValidator",
|
|
167
|
+
"ValidationResult",
|
|
168
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enums used in the password_validator package are defined here.
|
|
3
|
+
- Strength
|
|
4
|
+
- RuleType
|
|
5
|
+
- ErrorCode
|
|
6
|
+
"""
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Rule(str, Enum):
|
|
11
|
+
"""
|
|
12
|
+
Supported validation rule types for password validation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
LENGTH = "length"
|
|
16
|
+
UPPERCASE = "uppercase"
|
|
17
|
+
LOWERCASE = "lowercase"
|
|
18
|
+
DIGIT = "digit"
|
|
19
|
+
SPECIAL = "special"
|
|
20
|
+
WHITESPACE = "whitespace"
|
|
21
|
+
REPEATED = "repeated"
|
|
22
|
+
SEQUENTIAL = "sequential"
|
|
23
|
+
BLACKLIST = "blacklist"
|
|
24
|
+
COMMON_PASSWORDS = "common_passwords"
|
|
25
|
+
DICTIONARY = "dictionary"
|
|
26
|
+
REGEX = "regex"
|
|
27
|
+
USERNAME = "username"
|
|
28
|
+
ENTROPY = "entropy"
|
|
29
|
+
HISTORY = "history"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StrengthLevel(str, Enum):
|
|
33
|
+
"""
|
|
34
|
+
Supported strength levels for password validation.
|
|
35
|
+
"""
|
|
36
|
+
VERY_WEAK = "very_weak"
|
|
37
|
+
WEAK = "weak"
|
|
38
|
+
FAIR = "fair"
|
|
39
|
+
GOOD = "good"
|
|
40
|
+
STRONG = "strong"
|
|
41
|
+
VERY_STRONG = "very_strong"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ErrorCode(str, Enum):
|
|
45
|
+
"""
|
|
46
|
+
Supported error codes for password validation.
|
|
47
|
+
"""
|
|
48
|
+
TOO_SHORT = "PASSWORD_TOO_SHORT"
|
|
49
|
+
TOO_LONG = "PASSWORD_TOO_LONG"
|
|
50
|
+
MISSING_UPPERCASE = "PASSWORD_MISSING_UPPERCASE"
|
|
51
|
+
MISSING_LOWERCASE = "PASSWORD_MISSING_LOWERCASE"
|
|
52
|
+
MISSING_DIGIT = "PASSWORD_MISSING_DIGIT"
|
|
53
|
+
MISSING_SPECIAL = "PASSWORD_MISSING_SPECIAL"
|
|
54
|
+
CONTAINS_SPACE = "PASSWORD_CONTAINS_SPACE"
|
|
55
|
+
TOO_MANY_REPEATS = "PASSWORD_TOO_MANY_REPEATS"
|
|
56
|
+
SEQUENTIAL_PATTERN = "PASSWORD_SEQUENTIAL_PATTERN"
|
|
57
|
+
COMMON_PASSWORD = "PASSWORD_COMMON_PASSWORD"
|
|
58
|
+
DICTIONARY_WORD = "PASSWORD_DICTIONARY_WORD"
|
|
59
|
+
INVALID_REGEX = "PASSWORD_INVALID_REGEX"
|
|
60
|
+
USERNAME_IN_PASSWORD = "USERNAME_IN_PASSWORD"
|
|
61
|
+
LOW_ENTROPY = "PASSWORD_LOW_ENTROPY"
|
|
62
|
+
PASSWORD_HISTORY_VIOLATION = "PASSWORD_HISTORY_VIOLATION"
|
|
63
|
+
BLACKLISTED_PASSWORD = "BLACKLISTED_PASSWORD"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ValidationMode(str, Enum):
|
|
67
|
+
"""
|
|
68
|
+
Supported validation modes for password validation.
|
|
69
|
+
"""
|
|
70
|
+
STRICT = "strict"
|
|
71
|
+
NORMAL = "normal"
|
|
72
|
+
LENIENT = "lenient"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
All exceptions used in the password_validator package are defined here.
|
|
3
|
+
- PasswordValidationError
|
|
4
|
+
- ConfigurationError
|
|
5
|
+
- RuleConfigurationError
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PasswordValidatorError(Exception):
|
|
10
|
+
"""Raised when a password fails validation."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfigurationError(PasswordValidatorError):
|
|
15
|
+
"""Raised when there is an issue with the configuration."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MissingConfigurationError(ConfigurationError):
|
|
20
|
+
"""Raised when a required configuration is missing."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RuleConfigurationError(ConfigurationError):
|
|
25
|
+
"""Raised when there is an issue with a specific rule's configuration."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InvalidConfigurationValue(PasswordValidatorError):
|
|
30
|
+
"""Raised when a configuration value is invalid."""
|
|
31
|
+
"""
|
|
32
|
+
Raised when an environment/configuration value
|
|
33
|
+
has an invalid type or format.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
key: str,
|
|
39
|
+
value: object,
|
|
40
|
+
expected: str,
|
|
41
|
+
) -> None:
|
|
42
|
+
super().__init__(
|
|
43
|
+
f"Invalid configuration value for "
|
|
44
|
+
f"'{key}': {value!r}. "
|
|
45
|
+
f"Expected {expected}."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self.key = key
|
|
49
|
+
self.value = value
|
|
50
|
+
self.expected = expected
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Environment configuration loader.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from ..exceptions import InvalidConfigurationValue
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EnvLoader:
|
|
12
|
+
"""
|
|
13
|
+
Loads and converts environment variables into a configuration dictionary.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, env_file: str | None = ".env") -> None:
|
|
16
|
+
"""
|
|
17
|
+
Initializes the EnvLoader.
|
|
18
|
+
|
|
19
|
+
:param env_file: Path to the .env file. Defaults to ".env".
|
|
20
|
+
"""
|
|
21
|
+
self._values: dict[str, str] = {}
|
|
22
|
+
|
|
23
|
+
if env_file:
|
|
24
|
+
self._load_env_file(env_file)
|
|
25
|
+
|
|
26
|
+
self._load_environment()
|
|
27
|
+
|
|
28
|
+
def _load_env_file(self, env_file: str) -> None:
|
|
29
|
+
path = Path(env_file)
|
|
30
|
+
|
|
31
|
+
if not path.exists():
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
35
|
+
line = raw_line.strip()
|
|
36
|
+
|
|
37
|
+
if not line:
|
|
38
|
+
continue
|
|
39
|
+
if line.startswith("#"):
|
|
40
|
+
continue
|
|
41
|
+
if "=" not in line:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
key, value = line.split("=", 1)
|
|
45
|
+
|
|
46
|
+
key = key.strip()
|
|
47
|
+
value = value.strip()
|
|
48
|
+
|
|
49
|
+
if (len(value) >= 2 and
|
|
50
|
+
value[0] == value[-1] and
|
|
51
|
+
value[0] in {'"', "'"}):
|
|
52
|
+
value = value[1:-1]
|
|
53
|
+
|
|
54
|
+
self._values[key] = value
|
|
55
|
+
|
|
56
|
+
def _load_environment(self) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Environment variables override values from .env
|
|
59
|
+
:return:
|
|
60
|
+
"""
|
|
61
|
+
for key, value in os.environ.items():
|
|
62
|
+
self._values[key] = value
|
|
63
|
+
|
|
64
|
+
def get(self, key: str, default: str = None) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Retrieves the value of an environment variable.
|
|
67
|
+
|
|
68
|
+
:param key: The environment variable key.
|
|
69
|
+
:param default: The default value if the key is not found.
|
|
70
|
+
:return: The value of the environment variable or the default.
|
|
71
|
+
"""
|
|
72
|
+
return self._values.get(key, default)
|
|
73
|
+
|
|
74
|
+
def get_bool(self, key: str, default: bool = False) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Retrieves the value of an environment variable as a boolean.
|
|
77
|
+
|
|
78
|
+
:param key: The environment variable key.
|
|
79
|
+
:param default: The default boolean value if the key is not found.
|
|
80
|
+
:return: The boolean value of the environment variable or the default.
|
|
81
|
+
"""
|
|
82
|
+
value = self.get(key)
|
|
83
|
+
|
|
84
|
+
if value is None:
|
|
85
|
+
return default
|
|
86
|
+
|
|
87
|
+
normalized = value.strip().lower()
|
|
88
|
+
|
|
89
|
+
if normalized in ['true', '1', 'yes', 'y', "on"]:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
if value.lower() in ['false', '0', 'no', 'n', "off"]:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
raise InvalidConfigurationValue(f"Invalid boolean value for {key}: {value}")
|
|
96
|
+
|
|
97
|
+
def get_int(self, key: str, default: int = 0) -> int:
|
|
98
|
+
"""
|
|
99
|
+
Retrieves the value of an environment variable as an integer.
|
|
100
|
+
|
|
101
|
+
:param key: The environment variable key.
|
|
102
|
+
:param default: The default integer value if the key is not found.
|
|
103
|
+
:return: The integer value of the environment variable or the default.
|
|
104
|
+
"""
|
|
105
|
+
value = self.get(key)
|
|
106
|
+
|
|
107
|
+
if value is None:
|
|
108
|
+
return default
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
return int(value)
|
|
112
|
+
except ValueError:
|
|
113
|
+
raise InvalidConfigurationValue(key, value, "integer") from None
|
|
114
|
+
|
|
115
|
+
def get_list(self, key: str, default: list[str] = None, delimiter: str = ',') -> list[str]:
|
|
116
|
+
"""
|
|
117
|
+
Retrieves the value of an environment variable as a list.
|
|
118
|
+
|
|
119
|
+
:param key: The environment variable key.
|
|
120
|
+
:param default: The default list value if the key is not found.
|
|
121
|
+
:param delimiter: The delimiter used to split the string into a list. Defaults to ','.
|
|
122
|
+
:return: The list value of the environment variable or the default.
|
|
123
|
+
"""
|
|
124
|
+
value = self.get(key, default)
|
|
125
|
+
|
|
126
|
+
if isinstance(value, list):
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
if isinstance(value, str):
|
|
130
|
+
return [item.strip() for item in value.split(delimiter) if item.strip()]
|
|
131
|
+
|
|
132
|
+
raise InvalidConfigurationValue(f"{key}: {value} must be a list or a string")
|
|
133
|
+
|
|
134
|
+
def get_float(self, key: str, default: float = 0.0) -> float:
|
|
135
|
+
"""
|
|
136
|
+
Retrieves the value of an environment variable as a float.
|
|
137
|
+
|
|
138
|
+
:param key: The environment variable key.
|
|
139
|
+
:param default: The default float value if the key is not found.
|
|
140
|
+
:return: The float value of the environment variable or the default.
|
|
141
|
+
"""
|
|
142
|
+
value = self.get(key)
|
|
143
|
+
if value is None:
|
|
144
|
+
return default
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
return float(value)
|
|
148
|
+
except (TypeError, ValueError) as exc:
|
|
149
|
+
raise InvalidConfigurationValue(key, value, "float") from None
|