feat(auth): LDAP and SSO users keep their source; directory TLS is real (#5036)
#5036 "SSO and LDAP: users keep their auth source, no merging into local accounts, is_active checked, LDAP TLS real and filter escaped". - users gain auth_source (local/ldap/oidc) and external_id (migration 0027). A provider account is found by (source, subject) only: the LDAP entry DN or the OIDC sub. A first sign-in whose username or email another account holds is refused with a message naming why; a disabled user stays out. - Local passwords sign in local accounts only. - LDAP: port 636 is LDAPS, otherwise StartTLS before any bind; the server certificate is checked against the system CAs or a CA pasted in Settings -> Auth. The login name is escaped into the filter (RFC 4515), and an empty password is refused before the directory sees it. - OIDC: the ID token must carry this sign-in's nonce and the userinfo sub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
"""Users keep the auth source they came from
|
||||
|
||||
Revision ID: 0027_user_auth_source
|
||||
Revises: 0026_api_tokens
|
||||
Create Date: 2026-10-06
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "0027_user_auth_source"
|
||||
down_revision: Union[str, None] = "0026_api_tokens"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Existing rows are local: nothing recorded which provider made them, and
|
||||
# an SSO user who finds their name taken is told so at sign-in.
|
||||
op.add_column("users", sa.Column("auth_source", sa.String(16), nullable=False,
|
||||
server_default="local"))
|
||||
op.add_column("users", sa.Column("external_id", sa.String(512), nullable=True))
|
||||
op.create_unique_constraint("uq_users_auth_source_external_id", "users",
|
||||
["auth_source", "external_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint("uq_users_auth_source_external_id", "users", type_="unique")
|
||||
op.drop_column("users", "external_id")
|
||||
op.drop_column("users", "auth_source")
|
||||
Reference in New Issue
Block a user