From be3f47f8b7d52ab1c180ce5072fde630e68c5188 Mon Sep 17 00:00:00 2001 From: Carter Mintey Date: Fri, 17 Feb 2023 16:45:01 +0000 Subject: [PATCH 1/8] adds authentication method for users --- ...1a87a3b7_adds_auth_method_to_user_table.py | 32 +++++++++++++++++++ frontend/components/global/AutoForm.vue | 2 ++ frontend/composables/use-users/user-form.ts | 7 ++++ frontend/lang/messages/en-US.json | 1 + frontend/lib/api/types/user.ts | 7 ++++ frontend/pages/admin/manage/users/index.vue | 1 + frontend/types/auto-forms.ts | 6 ++++ mealie/core/security/security.py | 4 ++- mealie/db/models/users/users.py | 9 +++++- mealie/lang/messages/en-US.json | 5 +-- mealie/routes/users/crud.py | 5 +++ mealie/schema/user/user.py | 2 ++ .../user_services/password_reset_service.py | 4 +++ 13 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py diff --git a/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py b/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py new file mode 100644 index 00000000000..07687899950 --- /dev/null +++ b/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py @@ -0,0 +1,32 @@ +"""adds auth_method to user table + +Revision ID: 59da1a87a3b7 +Revises: 16160bf731a0 +Create Date: 2023-02-16 21:26:00.693018 + +""" +import sqlalchemy as sa + +import mealie.db.migration_types +from alembic import op + +# revision identifiers, used by Alembic. +revision = "59da1a87a3b7" +down_revision = "16160bf731a0" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "users", + sa.Column("auth_method", sa.Enum("MEALIE", "LDAP", name="authMethod"), nullable=False, server_default="MEALIE"), + ) + op.execute("UPDATE users SET auth_method = 'LDAP' WHERE password = 'LDAP'") + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.drop_column("auth_method") + # ### end Alembic commands ### diff --git a/frontend/components/global/AutoForm.vue b/frontend/components/global/AutoForm.vue index fa62387830b..36ce8a0da0c 100644 --- a/frontend/components/global/AutoForm.vue +++ b/frontend/components/global/AutoForm.vue @@ -75,6 +75,8 @@ :name="inputField.varName" :items="inputField.options" :return-object="false" + :hint="inputField.hint" + persistent-hint lazy-validation @blur="emitBlur" > diff --git a/frontend/composables/use-users/user-form.ts b/frontend/composables/use-users/user-form.ts index 1c0f4bc5d80..5d4a048f3a3 100644 --- a/frontend/composables/use-users/user-form.ts +++ b/frontend/composables/use-users/user-form.ts @@ -29,6 +29,13 @@ 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'", + 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 61dfc0453ce..ab2367c014a 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 b3987474883..5818b049b7f 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/index.vue b/frontend/pages/admin/manage/users/index.vue index ec0901ad958..4a26607de25 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 a82ccb8fb01..adf2518acdc 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,7 @@ export interface FormField { type: FormFieldType; rules?: string[]; disableUpdate?: boolean; + options?: FormSelectOption[]; } export type AutoFormItems = FormField[]; diff --git a/mealie/core/security/security.py b/mealie/core/security/security.py index f5219970a46..40276075e96 100644 --- a/mealie/core/security/security.py +++ b/mealie/core/security/security.py @@ -6,6 +6,7 @@ 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 069530720e4..5252785c72c 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 @@ def __init__(self, name, token, user_id, **_) -> None: 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 ac67c30864e..a4990159d5d 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 bc28f44c065..a795276b9bd 100644 --- a/mealie/routes/users/crud.py +++ b/mealie/routes/users/crud.py @@ -2,6 +2,7 @@ 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 @@ def get_logged_in_user(self): @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 76829bb45c5..15dbb639538 100644 --- a/mealie/schema/user/user.py +++ b/mealie/schema/user/user.py @@ -9,6 +9,7 @@ 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 1fe69653f26..95fbac7c002 100644 --- a/mealie/services/user_services/password_reset_service.py +++ b/mealie/services/user_services/password_reset_service.py @@ -2,6 +2,7 @@ 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 @@ def generate_reset_token(self, email: str) -> SavePasswordResetToken | None: 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() From e41024d3a0e172bdb94d66fa7db6cfeddda18a82 Mon Sep 17 00:00:00 2001 From: Carter Mintey Date: Fri, 17 Feb 2023 19:59:00 +0000 Subject: [PATCH 2/8] fix db migration with postgres --- ...da1a87a3b7_adds_auth_method_to_user_table.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py b/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py index 07687899950..668e85c8ca1 100644 --- a/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py +++ b/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py @@ -17,16 +17,27 @@ 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", sa.Enum("MEALIE", "LDAP", name="authMethod"), nullable=False, server_default="MEALIE"), + sa.Column("auth_method", authMethod, nullable=False, server_default="MEALIE"), ) op.execute("UPDATE users SET auth_method = 'LDAP' WHERE password = 'LDAP'") def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("users", schema=None) as batch_op: batch_op.drop_column("auth_method") - # ### end Alembic commands ### + + if is_postgres(): + authMethod.drop(op.get_bind()) From 93a00ec8b498dce7d7526b36c9b0cfac3f0178a9 Mon Sep 17 00:00:00 2001 From: Carter Mintey Date: Fri, 17 Feb 2023 20:44:46 +0000 Subject: [PATCH 3/8] tests for auth method --- tests/fixtures/fixture_users.py | 25 ++++++++++++ .../admin_tests/test_admin_user_actions.py | 8 ++++ .../test_user_password_reset_service.py | 22 ++++++++++ tests/unit_tests/test_security.py | 40 ++++++++++++++++--- tests/utils/fixture_schemas.py | 3 ++ 5 files changed, 92 insertions(+), 6 deletions(-) diff --git a/tests/fixtures/fixture_users.py b/tests/fixtures/fixture_users.py index d9f22905e6e..6d938b58348 100644 --- a/tests/fixtures/fixture_users.py +++ b/tests/fixtures/fixture_users.py @@ -3,6 +3,9 @@ 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 0365ed42683..257f8987054 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 f54ce56b3db..34dd58ab47e 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 @@ 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 c1bbcf451cb..f9384f6eedb 100644 --- a/tests/unit_tests/test_security.py +++ b/tests/unit_tests/test_security.py @@ -7,7 +7,9 @@ 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 ldap_initialize_mock(url): 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 ldap_initialize_mock(url): 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 ldap_initialize_mock(url): 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 ldap_initialize_mock(url): 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 ldap_initialize_mock(url): 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 6bb3abe6eca..900a74e806d 100644 --- a/tests/utils/fixture_schemas.py +++ b/tests/utils/fixture_schemas.py @@ -2,6 +2,8 @@ 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: From 33de6d795f7188f4b5341fc9a2dd3c3492d768ef Mon Sep 17 00:00:00 2001 From: Carter Mintey Date: Mon, 20 Feb 2023 16:21:20 +0000 Subject: [PATCH 4/8] update migration ids --- ...16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py b/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py index 668e85c8ca1..7c914e54b13 100644 --- a/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py +++ b/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py @@ -1,7 +1,7 @@ """adds auth_method to user table Revision ID: 59da1a87a3b7 -Revises: 16160bf731a0 +Revises: 5ab195a474eb Create Date: 2023-02-16 21:26:00.693018 """ @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. revision = "59da1a87a3b7" -down_revision = "16160bf731a0" +down_revision = "5ab195a474eb" branch_labels = None depends_on = None From 4f30d3d8a2c4aeee0762d983630fe42c4d8edd2e Mon Sep 17 00:00:00 2001 From: Carter Mintey Date: Mon, 20 Feb 2023 16:21:55 +0000 Subject: [PATCH 5/8] hide auth method on user creation form --- frontend/composables/use-users/user-form.ts | 7 +++++-- frontend/pages/admin/manage/users/_id.vue | 2 +- frontend/pages/admin/manage/users/create.vue | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/composables/use-users/user-form.ts b/frontend/composables/use-users/user-form.ts index 5d4a048f3a3..c883cb83db4 100644 --- a/frontend/composables/use-users/user-form.ts +++ b/frontend/composables/use-users/user-form.ts @@ -1,7 +1,7 @@ import { fieldTypes } from "../forms"; import { AutoFormItems } from "~/types/auto-forms"; -export const useUserForm = () => { +export const useUserForm = (updateMode = false) => { const userForm: AutoFormItems = [ { section: "User Details", @@ -69,7 +69,10 @@ export const useUserForm = () => { }, ]; + // fields that are hidden on creation, but shown on update + const hiddenOnCreate = ["authMethod"]; + return { - userForm, + userForm: userForm.filter((field) => updateMode || !hiddenOnCreate.includes(field.varName)), }; }; diff --git a/frontend/pages/admin/manage/users/_id.vue b/frontend/pages/admin/manage/users/_id.vue index 331065ba637..0b7edf39ac1 100644 --- a/frontend/pages/admin/manage/users/_id.vue +++ b/frontend/pages/admin/manage/users/_id.vue @@ -50,7 +50,7 @@ import { UserOut } from "~/lib/api/types/user"; export default defineComponent({ layout: "admin", setup() { - const { userForm } = useUserForm(); + const { userForm } = useUserForm(true); const { groups } = useGroups(); const route = useRoute(); diff --git a/frontend/pages/admin/manage/users/create.vue b/frontend/pages/admin/manage/users/create.vue index 2f5953a3bb7..8033a43106e 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({ }); - + From 4697ddee465050bc237a03829d60a8faf97e011e Mon Sep 17 00:00:00 2001 From: Carter Mintey Date: Mon, 20 Feb 2023 17:19:37 +0000 Subject: [PATCH 6/8] (docs): Added documentation for the new authentication method --- .../getting-started/installation/backend-config.md | 7 +++---- docs/docs/documentation/getting-started/usage/ldap.md | 8 ++++++++ docs/docs/overrides/api.html | 2 +- docs/mkdocs.yml | 1 + template.env | 6 +++++- 5 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 docs/docs/documentation/getting-started/usage/ldap.md diff --git a/docs/docs/documentation/getting-started/installation/backend-config.md b/docs/docs/documentation/getting-started/installation/backend-config.md index 36738700e17..f4ba501036f 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 00000000000..365c82a5b9a --- /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 2d7e85f3663..85f0cd975a5 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 eb89182c1cc..22f95c089d3 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/template.env b/template.env index 921efebf0f4..2c346b7786d 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 From 0c6ca8b1ee21b9b63dee3c490b73ff7df323d32f Mon Sep 17 00:00:00 2001 From: Carter Mintey Date: Wed, 22 Feb 2023 21:49:42 +0000 Subject: [PATCH 7/8] update migration --- ...52_38514b39a824_add_auth_method_to_user_table.py} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename alembic/versions/{2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py => 2023-02-22-21.45.52_38514b39a824_add_auth_method_to_user_table.py} (81%) diff --git a/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py b/alembic/versions/2023-02-22-21.45.52_38514b39a824_add_auth_method_to_user_table.py similarity index 81% rename from alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py rename to alembic/versions/2023-02-22-21.45.52_38514b39a824_add_auth_method_to_user_table.py index 7c914e54b13..95d92086539 100644 --- a/alembic/versions/2023-02-16-21.26.00_59da1a87a3b7_adds_auth_method_to_user_table.py +++ b/alembic/versions/2023-02-22-21.45.52_38514b39a824_add_auth_method_to_user_table.py @@ -1,8 +1,8 @@ -"""adds auth_method to user table +"""add auth_method to user table -Revision ID: 59da1a87a3b7 -Revises: 5ab195a474eb -Create Date: 2023-02-16 21:26:00.693018 +Revision ID: 38514b39a824 +Revises: b04a08da2108 +Create Date: 2023-02-22 21:45:52.900964 """ import sqlalchemy as sa @@ -11,8 +11,8 @@ from alembic import op # revision identifiers, used by Alembic. -revision = "59da1a87a3b7" -down_revision = "5ab195a474eb" +revision = "38514b39a824" +down_revision = "b04a08da2108" branch_labels = None depends_on = None From 6b29fd65b6120ab7b861564feeae4986da3af3d5 Mon Sep 17 00:00:00 2001 From: Carter Mintey Date: Fri, 24 Feb 2023 15:01:04 +0000 Subject: [PATCH 8/8] add to auto-form instead of having hidden fields --- frontend/components/global/AutoForm.vue | 13 +++++++------ frontend/composables/use-users/user-form.ts | 8 +++----- frontend/pages/admin/manage/users/_id.vue | 2 +- frontend/types/auto-forms.ts | 5 +++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/components/global/AutoForm.vue b/frontend/components/global/AutoForm.vue index 36ce8a0da0c..8ad0cbcd9e4 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 @@ { +export const useUserForm = () => { const userForm: AutoFormItems = [ { section: "User Details", @@ -34,6 +34,7 @@ export const useUserForm = (updateMode = false) => { 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" }], }, { @@ -69,10 +70,7 @@ export const useUserForm = (updateMode = false) => { }, ]; - // fields that are hidden on creation, but shown on update - const hiddenOnCreate = ["authMethod"]; - return { - userForm: userForm.filter((field) => updateMode || !hiddenOnCreate.includes(field.varName)), + userForm, }; }; diff --git a/frontend/pages/admin/manage/users/_id.vue b/frontend/pages/admin/manage/users/_id.vue index 0b7edf39ac1..331065ba637 100644 --- a/frontend/pages/admin/manage/users/_id.vue +++ b/frontend/pages/admin/manage/users/_id.vue @@ -50,7 +50,7 @@ import { UserOut } from "~/lib/api/types/user"; export default defineComponent({ layout: "admin", setup() { - const { userForm } = useUserForm(true); + const { userForm } = useUserForm(); const { groups } = useGroups(); const route = useRoute(); diff --git a/frontend/types/auto-forms.ts b/frontend/types/auto-forms.ts index adf2518acdc..eb08aa837df 100644 --- a/frontend/types/auto-forms.ts +++ b/frontend/types/auto-forms.ts @@ -1,8 +1,8 @@ type FormFieldType = "text" | "textarea" | "list" | "select" | "object" | "boolean" | "color" | "password"; export interface FormSelectOption { - text: string; - description?: string; + text: string; + description?: string; } export interface FormField { @@ -14,6 +14,7 @@ export interface FormField { type: FormFieldType; rules?: string[]; disableUpdate?: boolean; + disableCreate?: boolean; options?: FormSelectOption[]; }