mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-15 06:45:23 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user