forked from Github-Mirrors/canaille
feat: use sqlalchemy-utils PasswordType to store and hash user passwords
This commit is contained in:
parent
9463d0c52a
commit
a7e574f754
5 changed files with 73 additions and 13 deletions
|
@ -12,6 +12,7 @@ Fixed
|
||||||
- Password reset and initialization mails were not sent at all the user
|
- Password reset and initialization mails were not sent at all the user
|
||||||
addresses if one email address could not be reached.
|
addresses if one email address could not be reached.
|
||||||
- Password comparision was too permissive on login.
|
- Password comparision was too permissive on login.
|
||||||
|
- Encrypt passwords in the SQL backend.
|
||||||
|
|
||||||
[0.0.35] - 2023-11-25
|
[0.0.35] - 2023-11-25
|
||||||
=====================
|
=====================
|
||||||
|
|
|
@ -21,11 +21,15 @@ from sqlalchemy.orm import mapped_column
|
||||||
from sqlalchemy.orm import reconstructor
|
from sqlalchemy.orm import reconstructor
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy_json import MutableJson
|
from sqlalchemy_json import MutableJson
|
||||||
|
from sqlalchemy_utils import force_auto_coercion
|
||||||
|
from sqlalchemy_utils import PasswordType
|
||||||
|
|
||||||
from .backend import Backend
|
from .backend import Backend
|
||||||
from .backend import Base
|
from .backend import Base
|
||||||
from .utils import TZDateTime
|
from .utils import TZDateTime
|
||||||
|
|
||||||
|
force_auto_coercion()
|
||||||
|
|
||||||
|
|
||||||
class SqlAlchemyModel(Model):
|
class SqlAlchemyModel(Model):
|
||||||
def __html__(self):
|
def __html__(self):
|
||||||
|
@ -120,7 +124,9 @@ class User(canaille.core.models.User, Base, SqlAlchemyModel):
|
||||||
String, primary_key=True, default=lambda: str(uuid.uuid4())
|
String, primary_key=True, default=lambda: str(uuid.uuid4())
|
||||||
)
|
)
|
||||||
user_name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
user_name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
||||||
password: Mapped[str] = mapped_column(String, nullable=True)
|
password: Mapped[str] = mapped_column(
|
||||||
|
PasswordType(schemes=["pbkdf2_sha512"]), nullable=True
|
||||||
|
)
|
||||||
preferred_language: Mapped[str] = mapped_column(String, nullable=True)
|
preferred_language: Mapped[str] = mapped_column(String, nullable=True)
|
||||||
family_name: Mapped[str] = mapped_column(String, nullable=True)
|
family_name: Mapped[str] = mapped_column(String, nullable=True)
|
||||||
given_name: Mapped[str] = mapped_column(String, nullable=True)
|
given_name: Mapped[str] = mapped_column(String, nullable=True)
|
||||||
|
@ -199,7 +205,7 @@ class User(canaille.core.models.User, Base, SqlAlchemyModel):
|
||||||
return User.get(user_name=login)
|
return User.get(user_name=login)
|
||||||
|
|
||||||
def has_password(self):
|
def has_password(self):
|
||||||
return bool(self.password)
|
return self.password is not None
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
if password != self.password:
|
if password != self.password:
|
||||||
|
|
51
poetry.lock
generated
51
poetry.lock
generated
|
@ -1051,6 +1051,23 @@ files = [
|
||||||
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "passlib"
|
||||||
|
version = "1.7.4"
|
||||||
|
description = "comprehensive password hashing framework supporting over 30 schemes"
|
||||||
|
optional = true
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
|
||||||
|
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
argon2 = ["argon2-cffi (>=18.2.0)"]
|
||||||
|
bcrypt = ["bcrypt (>=3.1.0)"]
|
||||||
|
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
|
||||||
|
totp = ["cryptography"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
|
@ -1886,6 +1903,34 @@ sqlalchemy = ">=0.7"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["pytest"]
|
dev = ["pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlalchemy-utils"
|
||||||
|
version = "0.41.1"
|
||||||
|
description = "Various utility functions for SQLAlchemy."
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "SQLAlchemy-Utils-0.41.1.tar.gz", hash = "sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74"},
|
||||||
|
{file = "SQLAlchemy_Utils-0.41.1-py3-none-any.whl", hash = "sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
SQLAlchemy = ">=1.3"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
arrow = ["arrow (>=0.3.4)"]
|
||||||
|
babel = ["Babel (>=1.3)"]
|
||||||
|
color = ["colour (>=0.0.4)"]
|
||||||
|
encrypted = ["cryptography (>=0.6)"]
|
||||||
|
intervals = ["intervals (>=0.7.1)"]
|
||||||
|
password = ["passlib (>=1.6,<2.0)"]
|
||||||
|
pendulum = ["pendulum (>=2.0.5)"]
|
||||||
|
phone = ["phonenumbers (>=5.9.2)"]
|
||||||
|
test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
|
||||||
|
test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
|
||||||
|
timezone = ["python-dateutil"]
|
||||||
|
url = ["furl (>=0.4.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
|
@ -2055,14 +2100,14 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link
|
||||||
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
|
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
|
||||||
|
|
||||||
[extras]
|
[extras]
|
||||||
all = ["authlib", "email_validator", "flask-babel", "flask-themer", "pycountry", "python-ldap", "pytz", "sentry-sdk", "sqlalchemy", "sqlalchemy-json", "toml"]
|
all = ["authlib", "email_validator", "flask-babel", "flask-themer", "passlib", "pycountry", "python-ldap", "pytz", "sentry-sdk", "sqlalchemy", "sqlalchemy-json", "sqlalchemy-utils", "toml"]
|
||||||
front = ["email_validator", "flask-babel", "flask-themer", "pycountry", "pytz", "toml"]
|
front = ["email_validator", "flask-babel", "flask-themer", "pycountry", "pytz", "toml"]
|
||||||
ldap = ["python-ldap"]
|
ldap = ["python-ldap"]
|
||||||
oidc = ["authlib"]
|
oidc = ["authlib"]
|
||||||
sentry = ["sentry-sdk"]
|
sentry = ["sentry-sdk"]
|
||||||
sql = ["sqlalchemy", "sqlalchemy-json"]
|
sql = ["passlib", "sqlalchemy", "sqlalchemy-json", "sqlalchemy-utils"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
content-hash = "b3561cb0465972869503d18840bb7a3da62a75c447b827b0db79183f49c4e31f"
|
content-hash = "7372f06aa097a115c189932bb0afea25c21cfdbdefd81da5b95fab228da50217"
|
||||||
|
|
|
@ -61,8 +61,10 @@ python-ldap = {version = "^3.4.0", optional=true}
|
||||||
sentry-sdk = {version = "<2", optional=true, extras=["flask"]}
|
sentry-sdk = {version = "<2", optional=true, extras=["flask"]}
|
||||||
|
|
||||||
# extra : sql
|
# extra : sql
|
||||||
sqlalchemy-json = {version = "^0.7.0", optional=true}
|
passlib = {version = "^1.7.4", optional=true}
|
||||||
sqlalchemy = {version = "^2.0.23", optional=true}
|
sqlalchemy = {version = "^2.0.23", optional=true}
|
||||||
|
sqlalchemy-json = {version = "^0.7.0", optional=true}
|
||||||
|
sqlalchemy-utils = {version = "^0.41.1", optional=true}
|
||||||
|
|
||||||
[tool.poetry.group.doc]
|
[tool.poetry.group.doc]
|
||||||
optional = true
|
optional = true
|
||||||
|
@ -117,14 +119,17 @@ sentry = [
|
||||||
"sentry-sdk",
|
"sentry-sdk",
|
||||||
]
|
]
|
||||||
sql = [
|
sql = [
|
||||||
|
"passlib",
|
||||||
"sqlalchemy",
|
"sqlalchemy",
|
||||||
"sqlalchemy-json",
|
"sqlalchemy-json",
|
||||||
|
"sqlalchemy-utils",
|
||||||
]
|
]
|
||||||
all = [
|
all = [
|
||||||
"click",
|
"click",
|
||||||
"email_validator",
|
"email_validator",
|
||||||
"flask-babel",
|
"flask-babel",
|
||||||
"flask-themer",
|
"flask-themer",
|
||||||
|
"passlib",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pytz",
|
"pytz",
|
||||||
"toml",
|
"toml",
|
||||||
|
@ -133,6 +138,7 @@ all = [
|
||||||
"sentry-sdk",
|
"sentry-sdk",
|
||||||
"sqlalchemy",
|
"sqlalchemy",
|
||||||
"sqlalchemy-json",
|
"sqlalchemy-json",
|
||||||
|
"sqlalchemy-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
|
|
|
@ -28,22 +28,24 @@ def test_user_get_from_login(testclient, user, backend):
|
||||||
|
|
||||||
|
|
||||||
def test_user_has_password(testclient, backend):
|
def test_user_has_password(testclient, backend):
|
||||||
u = models.User(
|
user = models.User(
|
||||||
formatted_name="Temp User",
|
formatted_name="Temp User",
|
||||||
family_name="Temp",
|
family_name="Temp",
|
||||||
user_name="temp",
|
user_name="temp",
|
||||||
emails=["john@doe.com"],
|
emails=["john@doe.com"],
|
||||||
)
|
)
|
||||||
u.save()
|
user.save()
|
||||||
|
|
||||||
assert not u.has_password()
|
assert user.password is None
|
||||||
|
assert not user.has_password()
|
||||||
|
|
||||||
u.password = "foobar"
|
user.password = "foobar"
|
||||||
u.save()
|
user.save()
|
||||||
|
|
||||||
assert u.has_password()
|
assert user.password is not None
|
||||||
|
assert user.has_password()
|
||||||
|
|
||||||
u.delete()
|
user.delete()
|
||||||
|
|
||||||
|
|
||||||
def test_user_set_and_check_password(testclient, user, backend):
|
def test_user_set_and_check_password(testclient, user, backend):
|
||||||
|
|
Loading…
Reference in a new issue