diff --git a/alembic/versions/2023-02-22-21.45.52_38514b39a824_add_auth_method_to_user_table.py b/alembic/versions/2023-02-22-21.45.52_38514b39a824_add_auth_method_to_user_table.py
new file mode 100644
index 000000000000..95d92086539c
--- /dev/null
+++ b/alembic/versions/2023-02-22-21.45.52_38514b39a824_add_auth_method_to_user_table.py
@@ -0,0 +1,43 @@
+"""add auth_method to user table
+
+Revision ID: 38514b39a824
+Revises: b04a08da2108
+Create Date: 2023-02-22 21:45:52.900964
+
+"""
+import sqlalchemy as sa
+
+import mealie.db.migration_types
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "38514b39a824"
+down_revision = "b04a08da2108"
+branch_labels = None
+depends_on = None
+
+
+def is_postgres():
+ return op.get_context().dialect.name == "postgresql"
+
+
+authMethod = sa.Enum("MEALIE", "LDAP", name="authmethod")
+
+
+def upgrade():
+ if is_postgres():
+ authMethod.create(op.get_bind())
+
+ op.add_column(
+ "users",
+ sa.Column("auth_method", authMethod, nullable=False, server_default="MEALIE"),
+ )
+ op.execute("UPDATE users SET auth_method = 'LDAP' WHERE password = 'LDAP'")
+
+
+def downgrade():
+ with op.batch_alter_table("users", schema=None) as batch_op:
+ batch_op.drop_column("auth_method")
+
+ if is_postgres():
+ authMethod.drop(op.get_bind())
diff --git a/docs/docs/documentation/getting-started/installation/backend-config.md b/docs/docs/documentation/getting-started/installation/backend-config.md
index 36738700e17e..f4ba501036fb 100644
--- a/docs/docs/documentation/getting-started/installation/backend-config.md
+++ b/docs/docs/documentation/getting-started/installation/backend-config.md
@@ -17,7 +17,6 @@
| TZ | UTC | Must be set to get correct date/time on the server |
| ALLOW_SIGNUP | true | Allow user sign-up without token (should match frontend env) |
-
### Security
| Variables | Default | Description |
@@ -36,7 +35,6 @@
| POSTGRES_PORT | 5432 | Postgres database port |
| POSTGRES_DB | mealie | Postgres database name |
-
### Email
| Variables | Default | Description |
@@ -50,6 +48,7 @@
| SMTP_PASSWORD | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
### Webworker
+
Changing the webworker settings may cause unforeseen memory leak issues with Mealie. It's best to leave these at the defaults unless you begin to experience issues with multiple users. Exercise caution when changing these settings
| Variables | Default | Description |
@@ -59,9 +58,9 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
| MAX_WORKERS | 1 | Set the maximum number of workers to use. Default is not set meaning unlimited. More info [here][max_workers] |
| WEB_CONCURRENCY | 1 | Override the automatic definition of number of workers. More info [here][web_concurrency] |
-
### LDAP
+
| Variables | Default | Description |
| ------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------ |
| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth |
@@ -71,7 +70,7 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
| LDAP_BASE_DN | None | Starting point when searching for users authentication (e.g. `CN=Users,DC=xx,DC=yy,DC=de`) |
| LDAP_QUERY_BIND | None | A bind user for LDAP search queries (e.g. `cn=admin,cn=users,dc=example,dc=com`) |
| LDAP_QUERY_PASSWORD | None | The password for the bind user used in LDAP_QUERY_BIND |
-| LDAP_USER_FILTER | None | The LDAP search filter to find users (e.g. `(&( | ({id_attribute}={input})({mail_attribute}={input}))(objectClass=person))`).
**Note** `id_attribute` and `mail_attribute` will be replaced with `LDAP_ID_ATTRIBUTE` and `LDAP_MAIL_ATTRIBUTE`, respectively. `input` will be replaced with either the username or email the user logs in with. |
+| LDAP_USER_FILTER | None | The LDAP search filter to find users (e.g. `(&(|({id_attribute}={input})({mail_attribute}={input}))(objectClass=person))`).
**Note** `id_attribute` and `mail_attribute` will be replaced with `LDAP_ID_ATTRIBUTE` and `LDAP_MAIL_ATTRIBUTE`, respectively. `input` will be replaced with either the username or email the user logs in with. |
| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) |
| LDAP_ID_ATTRIBUTE | uid | The LDAP attribute that maps to the user's id |
| LDAP_NAME_ATTRIBUTE | name | The LDAP attribute that maps to the user's name |
diff --git a/docs/docs/documentation/getting-started/usage/ldap.md b/docs/docs/documentation/getting-started/usage/ldap.md
new file mode 100644
index 000000000000..365c82a5b9a7
--- /dev/null
+++ b/docs/docs/documentation/getting-started/usage/ldap.md
@@ -0,0 +1,8 @@
+# LDAP Authentication
+
+If LDAP is enabled and [configured properly](../installation/backend-config.md), users will be able to log in with their LDAP credentials. If the user does not already have an account in Mealie, then one will be created.
+
+If the user already has an account in Mealie and wants to use their LDAP credentials instead, then you can go to the **User Management** page in the admin panel and change the "Authentication Backend" from `Mealie` to `LDAP`. If for whatever reason, the user no longer wants to use LDAP authentication, then you can switch this back to `Mealie`.
+
+!!! warning "Head's Up"
+ If you switch a user from `LDAP` to `Mealie` who was initially created by LDAP, then the user will have to reset their password through the password reset flow.
diff --git a/docs/docs/overrides/api.html b/docs/docs/overrides/api.html
index 2d7e85f36633..85f0cd975a58 100644
--- a/docs/docs/overrides/api.html
+++ b/docs/docs/overrides/api.html
@@ -14,7 +14,7 @@
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index eb89182c1cc1..22f95c089d39 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -75,6 +75,7 @@ nav:
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
- Usage:
- Backup and Restoring: "documentation/getting-started/usage/backups-and-restoring.md"
+ - LDAP Authentication: "documentation/getting-started/usage/ldap.md"
- Community Guides:
- iOS Shortcuts: "documentation/community-guide/ios.md"
diff --git a/frontend/components/global/AutoForm.vue b/frontend/components/global/AutoForm.vue
index fa62387830b0..8ad0cbcd9e41 100644
--- a/frontend/components/global/AutoForm.vue
+++ b/frontend/components/global/AutoForm.vue
@@ -18,7 +18,7 @@
:label="inputField.label"
:name="inputField.varName"
:hint="inputField.hint || ''"
- :disabled="updateMode && inputField.disableUpdate"
+ :disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate)"
@change="emitBlur"
/>
@@ -26,8 +26,8 @@
diff --git a/frontend/composables/use-users/user-form.ts b/frontend/composables/use-users/user-form.ts
index 1c0f4bc5d804..317f7bfaae4b 100644
--- a/frontend/composables/use-users/user-form.ts
+++ b/frontend/composables/use-users/user-form.ts
@@ -29,6 +29,14 @@ export const useUserForm = () => {
type: fieldTypes.PASSWORD,
rules: ["required", "minLength:8"],
},
+ {
+ label: "Authentication Method",
+ varName: "authMethod",
+ type: fieldTypes.SELECT,
+ hint: "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie'",
+ disableCreate: true,
+ options: [{ text: "Mealie" }, { text: "LDAP" }],
+ },
{
section: "Permissions",
label: "Administrator",
diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json
index 61dfc0453ce1..ab2367c014a5 100644
--- a/frontend/lang/messages/en-US.json
+++ b/frontend/lang/messages/en-US.json
@@ -667,6 +667,7 @@
"admin": "Admin",
"are-you-sure-you-want-to-delete-the-link": "Are you sure you want to delete the link {link}?",
"are-you-sure-you-want-to-delete-the-user": "Are you sure you want to delete the user {activeName} ID: {activeId}?",
+ "auth-method": "Auth Method",
"confirm-link-deletion": "Confirm Link Deletion",
"confirm-password": "Confirm Password",
"confirm-user-deletion": "Confirm User Deletion",
diff --git a/frontend/lib/api/types/user.ts b/frontend/lib/api/types/user.ts
index b3987474883c..5818b049b7f7 100644
--- a/frontend/lib/api/types/user.ts
+++ b/frontend/lib/api/types/user.ts
@@ -5,6 +5,8 @@
/* Do not modify it by hand - just update the pydantic models and then re-run the script
*/
+export type AuthMethod = "Mealie" | "LDAP";
+
export interface ChangePassword {
currentPassword: string;
newPassword: string;
@@ -53,6 +55,7 @@ export interface UserOut {
username?: string;
fullName?: string;
email: string;
+ authMethod?: AuthMethod & string;
admin?: boolean;
group: string;
advanced?: boolean;
@@ -99,6 +102,7 @@ export interface PrivateUser {
username?: string;
fullName?: string;
email: string;
+ authMethod?: AuthMethod & string;
admin?: boolean;
group: string;
advanced?: boolean;
@@ -150,6 +154,7 @@ export interface UserBase {
username?: string;
fullName?: string;
email: string;
+ authMethod?: AuthMethod & string;
admin?: boolean;
group?: string;
advanced?: boolean;
@@ -162,6 +167,7 @@ export interface UserFavorites {
username?: string;
fullName?: string;
email: string;
+ authMethod?: AuthMethod & string;
admin?: boolean;
group?: string;
advanced?: boolean;
@@ -214,6 +220,7 @@ export interface UserIn {
username?: string;
fullName?: string;
email: string;
+ authMethod?: AuthMethod & string;
admin?: boolean;
group?: string;
advanced?: boolean;
diff --git a/frontend/pages/admin/manage/users/create.vue b/frontend/pages/admin/manage/users/create.vue
index 2f5953a3bb78..8033a43106e7 100644
--- a/frontend/pages/admin/manage/users/create.vue
+++ b/frontend/pages/admin/manage/users/create.vue
@@ -67,6 +67,7 @@ export default defineComponent({
canManage: false,
canOrganize: false,
password: "",
+ authMethod: "Mealie",
},
});
@@ -92,5 +93,4 @@ export default defineComponent({
});
-
+
diff --git a/frontend/pages/admin/manage/users/index.vue b/frontend/pages/admin/manage/users/index.vue
index ec0901ad9581..4a26607de25b 100644
--- a/frontend/pages/admin/manage/users/index.vue
+++ b/frontend/pages/admin/manage/users/index.vue
@@ -131,6 +131,7 @@ export default defineComponent({
{ text: i18n.t("user.full-name"), value: "fullName" },
{ text: i18n.t("user.email"), value: "email" },
{ text: i18n.t("group.group"), value: "group" },
+ { text: i18n.t("user.auth-method"), value: "authMethod" },
{ text: i18n.t("user.admin"), value: "admin" },
{ text: i18n.t("general.delete"), value: "actions", sortable: false, align: "center" },
];
diff --git a/frontend/types/auto-forms.ts b/frontend/types/auto-forms.ts
index a82ccb8fb01a..eb08aa837df3 100644
--- a/frontend/types/auto-forms.ts
+++ b/frontend/types/auto-forms.ts
@@ -1,5 +1,10 @@
type FormFieldType = "text" | "textarea" | "list" | "select" | "object" | "boolean" | "color" | "password";
+export interface FormSelectOption {
+ text: string;
+ description?: string;
+}
+
export interface FormField {
section?: string;
sectionDetails?: string;
@@ -9,6 +14,8 @@ export interface FormField {
type: FormFieldType;
rules?: string[];
disableUpdate?: boolean;
+ disableCreate?: boolean;
+ options?: FormSelectOption[];
}
export type AutoFormItems = FormField[];
diff --git a/mealie/core/security/security.py b/mealie/core/security/security.py
index f5219970a465..40276075e964 100644
--- a/mealie/core/security/security.py
+++ b/mealie/core/security/security.py
@@ -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
diff --git a/mealie/db/models/users/users.py b/mealie/db/models/users/users.py
index 069530720e4d..5252785c72c4 100644
--- a/mealie/db/models/users/users.py
+++ b/mealie/db/models/users/users.py
@@ -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)
diff --git a/mealie/lang/messages/en-US.json b/mealie/lang/messages/en-US.json
index ac67c30864ea..a4990159d5d5 100644
--- a/mealie/lang/messages/en-US.json
+++ b/mealie/lang/messages/en-US.json
@@ -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",
diff --git a/mealie/routes/users/crud.py b/mealie/routes/users/crud.py
index bc28f44c0650..a795276b9bde 100644
--- a/mealie/routes/users/crud.py
+++ b/mealie/routes/users/crud.py
@@ -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"))
diff --git a/mealie/schema/user/user.py b/mealie/schema/user/user.py
index 76829bb45c5b..15dbb6395386 100644
--- a/mealie/schema/user/user.py
+++ b/mealie/schema/user/user.py
@@ -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
diff --git a/mealie/services/user_services/password_reset_service.py b/mealie/services/user_services/password_reset_service.py
index 1fe69653f266..95fbac7c0027 100644
--- a/mealie/services/user_services/password_reset_service.py
+++ b/mealie/services/user_services/password_reset_service.py
@@ -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()
diff --git a/template.env b/template.env
index 921efebf0f45..2c346b7786d7 100644
--- a/template.env
+++ b/template.env
@@ -42,7 +42,11 @@ LDAP_AUTH_ENABLED=False
# LDAP_BASE_DN=""
# LDAP_QUERY_BIND=""
# LDAP_QUERY_PASSWORD=""
-# LDAP_USER_FILTER=""
+
+# Optionally, filter by a particular user group
+# (&(|({id_attribute}={input})({mail_attribute}={input}))(objectClass=person)(memberOf=cn=mealie_user,ou=groups,dc=example,dc=com))
+# LDAP_USER_FILTER="(&(|({id_attribute}={input})({mail_attribute}={input}))(objectClass=person))"
+
# LDAP_ADMIN_FILTER=""
# LDAP_ID_ATTRIBUTE=uid
# LDAP_NAME_ATTRIBUTE=name
diff --git a/tests/fixtures/fixture_users.py b/tests/fixtures/fixture_users.py
index d9f22905e6ee..6d938b583480 100644
--- a/tests/fixtures/fixture_users.py
+++ b/tests/fixtures/fixture_users.py
@@ -3,6 +3,9 @@ from typing import Generator
from pytest import fixture
from starlette.testclient import TestClient
+from mealie.db.db_setup import session_context
+from mealie.db.models.users.users import AuthMethod
+from mealie.repos.all_repositories import get_repositories
from tests import utils
from tests.utils import api_routes
@@ -181,3 +184,25 @@ def user_token(admin_token, api_client: TestClient):
# Log in as this user
form_data = {"username": create_data["email"], "password": "useruser"}
return utils.login(form_data, api_client)
+
+
+@fixture(scope="module")
+def ldap_user():
+ # Create an LDAP user directly instead of using TestClient since we don't have
+ # a LDAP service set up
+ with session_context() as session:
+ db = get_repositories(session)
+ user = db.users.create(
+ {
+ "username": utils.random_string(10),
+ "password": "mealie_password_not_important",
+ "full_name": utils.random_string(10),
+ "email": utils.random_string(10),
+ "admin": False,
+ "auth_method": AuthMethod.LDAP,
+ }
+ )
+ yield user
+ with session_context() as session:
+ db = get_repositories(session)
+ db.users.delete(user.id)
diff --git a/tests/integration_tests/admin_tests/test_admin_user_actions.py b/tests/integration_tests/admin_tests/test_admin_user_actions.py
index 0365ed426830..257f89870543 100644
--- a/tests/integration_tests/admin_tests/test_admin_user_actions.py
+++ b/tests/integration_tests/admin_tests/test_admin_user_actions.py
@@ -1,6 +1,7 @@
from fastapi.testclient import TestClient
from mealie.core.config import get_app_settings
+from mealie.db.models.users.users import AuthMethod
from tests import utils
from tests.utils import api_routes
from tests.utils.factories import random_email, random_string
@@ -55,6 +56,7 @@ def test_create_user(api_client: TestClient, admin_token):
assert user_data["email"] == create_data["email"]
assert user_data["group"] == create_data["group"]
assert user_data["admin"] == create_data["admin"]
+ assert user_data["authMethod"] == AuthMethod.MEALIE.value
def test_create_user_as_non_admin(api_client: TestClient, user_token):
@@ -73,6 +75,7 @@ def test_update_user(api_client: TestClient, admin_user: TestUser):
# Change data
update_data["fullName"] = random_string()
update_data["email"] = random_email()
+ update_data["authMethod"] = AuthMethod.LDAP.value
response = api_client.put(
api_routes.admin_users_item_id(update_data["id"]), headers=admin_user.token, json=update_data
@@ -80,6 +83,11 @@ def test_update_user(api_client: TestClient, admin_user: TestUser):
assert response.status_code == 200
+ user_data = response.json()
+ assert user_data["fullName"] == update_data["fullName"]
+ assert user_data["email"] == update_data["email"]
+ assert user_data["authMethod"] == update_data["authMethod"]
+
def test_update_other_user_as_not_admin(api_client: TestClient, unique_user: TestUser, g2_user: TestUser):
settings = get_app_settings()
diff --git a/tests/integration_tests/user_tests/test_user_password_reset_service.py b/tests/integration_tests/user_tests/test_user_password_reset_service.py
index f54ce56b3dbd..34dd58ab47e3 100644
--- a/tests/integration_tests/user_tests/test_user_password_reset_service.py
+++ b/tests/integration_tests/user_tests/test_user_password_reset_service.py
@@ -4,6 +4,7 @@ import pytest
from fastapi.testclient import TestClient
from mealie.db.db_setup import session_context
+from mealie.schema.user.user import PrivateUser
from mealie.services.user_services.password_reset_service import PasswordResetService
from tests.utils import api_routes
from tests.utils.factories import random_string
@@ -56,3 +57,24 @@ def test_password_reset(api_client: TestClient, unique_user: TestUser, casing: s
# Test successful password reset
response = api_client.post(api_routes.users_reset_password, json=payload)
assert response.status_code == 400
+
+
+@pytest.mark.parametrize("casing", ["lower", "upper", "mixed"])
+def test_password_reset_ldap(ldap_user: PrivateUser, casing: str):
+ cased_email = ""
+ if casing == "lower":
+ cased_email = ldap_user.email.lower()
+ elif casing == "upper":
+ cased_email = ldap_user.email.upper()
+ else:
+ for i, letter in enumerate(ldap_user.email):
+ if i % 2 == 0:
+ cased_email += letter.upper()
+ else:
+ cased_email += letter.lower()
+ cased_email
+
+ with session_context() as session:
+ service = PasswordResetService(session)
+ token = service.generate_reset_token(cased_email)
+ assert token is None
diff --git a/tests/unit_tests/test_security.py b/tests/unit_tests/test_security.py
index c1bbcf451cb8..f9384f6eedb2 100644
--- a/tests/unit_tests/test_security.py
+++ b/tests/unit_tests/test_security.py
@@ -7,7 +7,9 @@ from mealie.core import security
from mealie.core.config import get_app_settings
from mealie.core.dependencies import validate_file_token
from mealie.db.db_setup import session_context
-from tests.utils.factories import random_string
+from mealie.db.models.users.users import AuthMethod
+from mealie.schema.user.user import PrivateUser
+from tests.utils import random_string
class LdapConnMock:
@@ -101,7 +103,7 @@ def test_create_file_token():
assert file_path == validate_file_token(file_token)
-def test_ldap_authentication_mocked(monkeypatch: MonkeyPatch):
+def test_ldap_user_creation(monkeypatch: MonkeyPatch):
user, mail, name, password, query_bind, query_password = setup_env(monkeypatch)
def ldap_initialize_mock(url):
@@ -122,7 +124,7 @@ def test_ldap_authentication_mocked(monkeypatch: MonkeyPatch):
assert result.admin is False
-def test_ldap_authentication_failed_mocked(monkeypatch: MonkeyPatch):
+def test_ldap_user_creation_fail(monkeypatch: MonkeyPatch):
user, mail, name, password, query_bind, query_password = setup_env(monkeypatch)
def ldap_initialize_mock(url):
@@ -139,7 +141,7 @@ def test_ldap_authentication_failed_mocked(monkeypatch: MonkeyPatch):
assert result is False
-def test_ldap_authentication_non_admin_mocked(monkeypatch: MonkeyPatch):
+def test_ldap_user_creation_non_admin(monkeypatch: MonkeyPatch):
user, mail, name, password, query_bind, query_password = setup_env(monkeypatch)
monkeypatch.setenv("LDAP_ADMIN_FILTER", "(memberOf=cn=admins,dc=example,dc=com)")
@@ -161,7 +163,7 @@ def test_ldap_authentication_non_admin_mocked(monkeypatch: MonkeyPatch):
assert result.admin is False
-def test_ldap_authentication_admin_mocked(monkeypatch: MonkeyPatch):
+def test_ldap_user_creation_admin(monkeypatch: MonkeyPatch):
user, mail, name, password, query_bind, query_password = setup_env(monkeypatch)
monkeypatch.setenv("LDAP_ADMIN_FILTER", "(memberOf=cn=admins,dc=example,dc=com)")
@@ -183,7 +185,7 @@ def test_ldap_authentication_admin_mocked(monkeypatch: MonkeyPatch):
assert result.admin
-def test_ldap_authentication_disabled_mocked(monkeypatch: MonkeyPatch):
+def test_ldap_disabled(monkeypatch: MonkeyPatch):
monkeypatch.setenv("LDAP_AUTH_ENABLED", "False")
user = random_string(10)
@@ -212,3 +214,29 @@ def test_ldap_authentication_disabled_mocked(monkeypatch: MonkeyPatch):
with session_context() as session:
security.authenticate_user(session, user, password)
+
+
+def test_user_login_ldap_auth_method(monkeypatch: MonkeyPatch, ldap_user: PrivateUser):
+ """
+ Test login from a user who was originally created in Mealie, but has since been converted
+ to LDAP auth method
+ """
+ _, _, name, ldap_password, query_bind, query_password = setup_env(monkeypatch)
+
+ def ldap_initialize_mock(url):
+ assert url == ""
+ return LdapConnMock(ldap_user.username, ldap_password, False, query_bind, query_password, ldap_user.email, name)
+
+ monkeypatch.setattr(ldap, "initialize", ldap_initialize_mock)
+
+ get_app_settings.cache_clear()
+
+ with session_context() as session:
+ result = security.authenticate_user(session, ldap_user.username, ldap_password)
+
+ assert result
+ assert result.username == ldap_user.username
+ assert result.email == ldap_user.email
+ assert result.full_name == ldap_user.full_name
+ assert result.admin == ldap_user.admin
+ assert result.auth_method == AuthMethod.LDAP
diff --git a/tests/utils/fixture_schemas.py b/tests/utils/fixture_schemas.py
index 6bb3abe6eca3..900a74e806d6 100644
--- a/tests/utils/fixture_schemas.py
+++ b/tests/utils/fixture_schemas.py
@@ -2,6 +2,8 @@ from dataclasses import dataclass
from typing import Any
from uuid import UUID
+from mealie.db.models.users.users import AuthMethod
+
@dataclass
class TestUser:
@@ -11,6 +13,7 @@ class TestUser:
password: str
_group_id: UUID
token: Any
+ auth_method = AuthMethod.MEALIE
@property
def group_id(self) -> str: