Feature: Add "Authentication Method" to allow existing users to sign in with LDAP (#2143)

* adds authentication method for users

* fix db migration with postgres

* tests for auth method

* update migration ids

* hide auth method on user creation form

* (docs): Added documentation for the new authentication method

* update migration

* add  to auto-form instead of having hidden fields
This commit is contained in:
Carter
2023-02-26 13:12:16 -06:00
committed by GitHub
parent 39012adcc1
commit 2e6ad5da8e
24 changed files with 213 additions and 24 deletions

View File

@@ -6,6 +6,7 @@ from jose import jwt
from mealie.core.config import get_app_settings
from mealie.core.security.hasher import get_hasher
from mealie.db.models.users.users import AuthMethod
from mealie.repos.all_repositories import get_repositories
from mealie.repos.repository_factory import AllRepositories
from mealie.schema.user import PrivateUser
@@ -115,6 +116,7 @@ def user_from_ldap(db: AllRepositories, username: str, password: str) -> Private
"full_name": full_name,
"email": email,
"admin": False,
"auth_method": AuthMethod.LDAP,
},
)
@@ -134,7 +136,7 @@ def authenticate_user(session, email: str, password: str) -> PrivateUser | bool:
if not user:
user = db.users.get_one(email, "username", any_case=True)
if settings.LDAP_AUTH_ENABLED and (not user or user.password == "LDAP"):
if settings.LDAP_AUTH_ENABLED and (not user or user.password == "LDAP" or user.auth_method == AuthMethod.LDAP):
return user_from_ldap(db, email, password)
if not user:
# To prevent user enumeration we perform the verify_password computation to ensure

View File

@@ -1,7 +1,8 @@
import enum
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, orm
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from mealie.core.config import get_app_settings
@@ -32,6 +33,11 @@ class LongLiveToken(SqlAlchemyBase, BaseMixins):
self.user_id = user_id
class AuthMethod(enum.Enum):
MEALIE = "Mealie"
LDAP = "LDAP"
class User(SqlAlchemyBase, BaseMixins):
__tablename__ = "users"
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
@@ -39,6 +45,7 @@ class User(SqlAlchemyBase, BaseMixins):
username: Mapped[str | None] = mapped_column(String, index=True, unique=True)
email: Mapped[str | None] = mapped_column(String, unique=True, index=True)
password: Mapped[str | None] = mapped_column(String)
auth_method: Mapped[Enum(AuthMethod)] = mapped_column(Enum(AuthMethod), default=AuthMethod.MEALIE)
admin: Mapped[bool | None] = mapped_column(Boolean, default=False)
advanced: Mapped[bool | None] = mapped_column(Boolean, default=False)

View File

@@ -11,10 +11,11 @@
"user": {
"user-updated": "User updated",
"password-updated": "Password updated",
"invalid-current-password": "Invalid current password"
"invalid-current-password": "Invalid current password",
"ldap-update-password-unavailable": "Unable to update password, user is controlled by LDAP"
},
"group": {
"report-deleted": "Report deleted."
"report-deleted": "Report deleted."
},
"exceptions": {
"permission_denied": "You do not have permission to perform this action",

View File

@@ -2,6 +2,7 @@ from fastapi import Depends, HTTPException, status
from pydantic import UUID4
from mealie.core.security import hash_password, verify_password
from mealie.db.models.users.users import AuthMethod
from mealie.routes._base import BaseAdminController, BaseUserController, controller
from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter
@@ -61,6 +62,10 @@ class UserController(BaseUserController):
@user_router.put("/password")
def update_password(self, password_change: ChangePassword):
"""Resets the User Password"""
if self.user.password == "LDAP" or self.user.auth_method == AuthMethod.LDAP:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, ErrorResponse.respond(self.t("user.ldap-update-password-unavailable"))
)
if not verify_password(password_change.current_password, self.user.password):
raise HTTPException(
status.HTTP_400_BAD_REQUEST, ErrorResponse.respond(self.t("user.invalid-current-password"))

View File

@@ -9,6 +9,7 @@ from pydantic.utils import GetterDict
from mealie.core.config import get_app_dirs, get_app_settings
from mealie.db.models.users import User
from mealie.db.models.users.users import AuthMethod
from mealie.schema._mealie import MealieModel
from mealie.schema.group.group_preferences import ReadGroupPreferences
from mealie.schema.recipe import RecipeSummary
@@ -66,6 +67,7 @@ class UserBase(MealieModel):
username: str | None
full_name: str | None = None
email: constr(to_lower=True, strip_whitespace=True) # type: ignore
auth_method: AuthMethod = AuthMethod.MEALIE
admin: bool = False
group: str | None
advanced: bool = False

View File

@@ -2,6 +2,7 @@ from fastapi import HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.security import hash_password, url_safe_token
from mealie.db.models.users.users import AuthMethod
from mealie.repos.all_repositories import get_repositories
from mealie.schema.user.user_passwords import SavePasswordResetToken
from mealie.services._base_service import BaseService
@@ -20,6 +21,9 @@ class PasswordResetService(BaseService):
self.logger.error(f"failed to create password reset for {email=}: user doesn't exists")
# Do not raise exception here as we don't want to confirm to the client that the Email doesn't exists
return None
elif user.password == "LDAP" or user.auth_method == AuthMethod.LDAP:
self.logger.error(f"failed to create password reset for {email=}: user controlled by LDAP")
return None
# Create Reset Token
token = url_safe_token()