mirror of
https://github.com/apache/superset.git
synced 2026-07-10 08:45:41 +00:00
126 lines
4.3 KiB
Python
126 lines
4.3 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
"""Superset password-complexity validator.
|
|
|
|
Wired in via ``FAB_PASSWORD_COMPLEXITY_VALIDATOR`` (with
|
|
``FAB_PASSWORD_COMPLEXITY_ENABLED``). Flask-AppBuilder runs this callable from
|
|
both the WTForms password fields (self-registration, user edit, reset password)
|
|
and the User REST API, so a single function enforces the policy across all
|
|
password-setting flows.
|
|
|
|
The default policy is a minimum length plus a common-password blocklist —
|
|
intentionally less draconian than FAB's built-in ``default_password_complexity``
|
|
(which requires 2 uppercase, 1 special, 2 digits, 3 lowercase and length 10).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from flask import current_app
|
|
from flask_appbuilder.exceptions import PasswordComplexityValidationError
|
|
from flask_babel import gettext as __
|
|
|
|
# A small built-in blocklist of the most common/guessable passwords. Operators
|
|
# can extend it with AUTH_PASSWORD_COMMON_BLOCKLIST. (A fuller list or a
|
|
# Have-I-Been-Pwned k-anonymity check is a possible follow-up.)
|
|
COMMON_PASSWORDS: frozenset[str] = frozenset(
|
|
{
|
|
"123456",
|
|
"123456789",
|
|
"12345678",
|
|
"1234567890",
|
|
"12345",
|
|
"111111",
|
|
"123123",
|
|
"000000",
|
|
"password",
|
|
"password1",
|
|
"password123",
|
|
"passw0rd",
|
|
"qwerty",
|
|
"qwerty123",
|
|
"qwertyuiop",
|
|
"abc123",
|
|
"letmein",
|
|
"welcome",
|
|
"welcome1",
|
|
"admin",
|
|
"admin123",
|
|
"administrator",
|
|
"root",
|
|
"superset",
|
|
"changeme",
|
|
"iloveyou",
|
|
"monkey",
|
|
"dragon",
|
|
"sunshine",
|
|
"princess",
|
|
"football",
|
|
"baseball",
|
|
"trustno1",
|
|
"login",
|
|
"master",
|
|
"hello123",
|
|
"secret",
|
|
"default",
|
|
}
|
|
)
|
|
|
|
DEFAULT_MIN_LENGTH = 8
|
|
|
|
|
|
def validate_password_complexity(password: str) -> None:
|
|
"""Validate a plaintext password against the configured policy.
|
|
|
|
:raises PasswordComplexityValidationError: if the password is too short or
|
|
appears in the common-password blocklist.
|
|
"""
|
|
raw_min_length = current_app.config.get(
|
|
"AUTH_PASSWORD_MIN_LENGTH", DEFAULT_MIN_LENGTH
|
|
)
|
|
# Operators commonly wire config via env vars, so AUTH_PASSWORD_MIN_LENGTH can
|
|
# arrive as a string (or be left unset/None). Coerce defensively and fall back
|
|
# to the default rather than blowing up every password-setting flow with a
|
|
# TypeError on the length comparison.
|
|
try:
|
|
min_length = int(raw_min_length)
|
|
except (TypeError, ValueError):
|
|
min_length = DEFAULT_MIN_LENGTH
|
|
# A zero or negative value would silently disable the length check, so
|
|
# treat non-positive values as misconfiguration and use the default.
|
|
if min_length < 1:
|
|
min_length = DEFAULT_MIN_LENGTH
|
|
|
|
if len(password) < min_length:
|
|
raise PasswordComplexityValidationError(
|
|
__(
|
|
"Password must be at least %(min_length)s characters long.",
|
|
min_length=min_length,
|
|
)
|
|
)
|
|
|
|
extra = current_app.config.get("AUTH_PASSWORD_COMMON_BLOCKLIST") or []
|
|
# A bare string is iterable but would be split into characters, so treat a
|
|
# misconfigured string as a single entry. casefold() gives correct
|
|
# case-insensitive matching for non-ASCII passwords too.
|
|
if isinstance(extra, str):
|
|
extra = [extra]
|
|
blocklist = COMMON_PASSWORDS | {str(item).casefold() for item in extra}
|
|
if password.casefold() in blocklist:
|
|
raise PasswordComplexityValidationError(
|
|
__("This password is too common; please choose a less guessable one.")
|
|
)
|