Remote access used to collapse every portal session onto the account that performed the enrolment. That was the only thing it could do while nothing here knew who was at the other end, and it is why letting a second person in meant handing them the first one's account. `user.portal_sub` is where a portal identity meets a local one: set for the enrolling superuser at enrolment, and for each person a superuser admits afterwards through Settings -> Remote access -> Add remote user. The code they type comes from the newcomer's own portal account, and it is redeemed against the hub with this installation's tunnel credential rather than with a portal session, so being let in is not itself the power to let others in. The account created is never a superuser, which closes the same door from this side. A proxy token now resolves through that mapping and nowhere else. An identity nobody mapped resolves to no user rather than falling back on the enroller, so deleting the local row under Admin -> Users is the whole of the revocation: it bites on a credential already in flight, and it does not wait on the portal being reachable to be told. Telling the portal is best effort for exactly that reason. The cost is stated where it lands, in DEPLOY.md: an installation enrolled before this has no mapping, so its owner reconnects once with a fresh code. Panels and the health summary still act as the enrolling account - neither of them is a person, and neither gained a way to name one.
238 lines
7.0 KiB
Python
238 lines
7.0 KiB
Python
import uuid
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlmodel import col, func, select
|
|
|
|
from app import crud
|
|
from app.api.deps import (
|
|
CurrentUser,
|
|
SessionDep,
|
|
get_current_active_superuser,
|
|
)
|
|
from app.api.routes.cloud import forget_remote_user
|
|
from app.core.config import settings
|
|
from app.core.security import get_password_hash, verify_password
|
|
from app.models import (
|
|
Message,
|
|
UpdatePassword,
|
|
User,
|
|
UserCreate,
|
|
UserPublic,
|
|
UserRegister,
|
|
UsersPublic,
|
|
UserUpdate,
|
|
UserUpdateMe,
|
|
)
|
|
from app.utils import generate_new_account_email, send_email
|
|
|
|
router = APIRouter(prefix="/users", tags=["users"])
|
|
|
|
|
|
@router.get(
|
|
"/",
|
|
dependencies=[Depends(get_current_active_superuser)],
|
|
response_model=UsersPublic,
|
|
)
|
|
def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
|
|
"""
|
|
Retrieve users.
|
|
"""
|
|
|
|
count_statement = select(func.count()).select_from(User)
|
|
count = session.exec(count_statement).one()
|
|
|
|
statement = (
|
|
select(User).order_by(col(User.created_at).desc()).offset(skip).limit(limit)
|
|
)
|
|
users = session.exec(statement).all()
|
|
|
|
return UsersPublic(data=users, count=count)
|
|
|
|
|
|
@router.post(
|
|
"/", dependencies=[Depends(get_current_active_superuser)], response_model=UserPublic
|
|
)
|
|
def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
|
|
"""
|
|
Create new user.
|
|
"""
|
|
user = crud.get_user_by_email(session=session, email=user_in.email)
|
|
if user:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="The user with this email already exists in the system.",
|
|
)
|
|
|
|
user = crud.create_user(session=session, user_create=user_in)
|
|
if settings.emails_enabled and user_in.email:
|
|
email_data = generate_new_account_email(
|
|
email_to=user_in.email, username=user_in.email, password=user_in.password
|
|
)
|
|
send_email(
|
|
email_to=user_in.email,
|
|
subject=email_data.subject,
|
|
html_content=email_data.html_content,
|
|
)
|
|
return user
|
|
|
|
|
|
@router.patch("/me", response_model=UserPublic)
|
|
def update_user_me(
|
|
*, session: SessionDep, user_in: UserUpdateMe, current_user: CurrentUser
|
|
) -> Any:
|
|
"""
|
|
Update own user.
|
|
"""
|
|
|
|
if user_in.email:
|
|
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
|
|
if existing_user and existing_user.id != current_user.id:
|
|
raise HTTPException(
|
|
status_code=409, detail="User with this email already exists"
|
|
)
|
|
user_data = user_in.model_dump(exclude_unset=True)
|
|
current_user.sqlmodel_update(user_data)
|
|
session.add(current_user)
|
|
session.commit()
|
|
session.refresh(current_user)
|
|
return current_user
|
|
|
|
|
|
@router.patch("/me/password", response_model=Message)
|
|
def update_password_me(
|
|
*, session: SessionDep, body: UpdatePassword, current_user: CurrentUser
|
|
) -> Any:
|
|
"""
|
|
Update own password.
|
|
"""
|
|
verified, _ = verify_password(body.current_password, current_user.hashed_password)
|
|
if not verified:
|
|
raise HTTPException(status_code=400, detail="Incorrect password")
|
|
if body.current_password == body.new_password:
|
|
raise HTTPException(
|
|
status_code=400, detail="New password cannot be the same as the current one"
|
|
)
|
|
hashed_password = get_password_hash(body.new_password)
|
|
current_user.hashed_password = hashed_password
|
|
session.add(current_user)
|
|
session.commit()
|
|
return Message(message="Password updated successfully")
|
|
|
|
|
|
@router.get("/me", response_model=UserPublic)
|
|
def read_user_me(current_user: CurrentUser) -> Any:
|
|
"""
|
|
Get current user.
|
|
"""
|
|
return current_user
|
|
|
|
|
|
@router.delete("/me", response_model=Message)
|
|
def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any:
|
|
"""
|
|
Delete own user.
|
|
"""
|
|
if current_user.is_superuser:
|
|
raise HTTPException(
|
|
status_code=403, detail="Super users are not allowed to delete themselves"
|
|
)
|
|
portal_sub = current_user.portal_sub
|
|
session.delete(current_user)
|
|
session.commit()
|
|
if portal_sub:
|
|
forget_remote_user(portal_sub)
|
|
return Message(message="User deleted successfully")
|
|
|
|
|
|
@router.post("/signup", response_model=UserPublic)
|
|
def register_user(session: SessionDep, user_in: UserRegister) -> Any:
|
|
"""
|
|
Create new user without the need to be logged in.
|
|
"""
|
|
user = crud.get_user_by_email(session=session, email=user_in.email)
|
|
if user:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="The user with this email already exists in the system",
|
|
)
|
|
user_create = UserCreate.model_validate(user_in)
|
|
user = crud.create_user(session=session, user_create=user_create)
|
|
return user
|
|
|
|
|
|
@router.get("/{user_id}", response_model=UserPublic)
|
|
def read_user_by_id(
|
|
user_id: uuid.UUID, session: SessionDep, current_user: CurrentUser
|
|
) -> Any:
|
|
"""
|
|
Get a specific user by id.
|
|
"""
|
|
user = session.get(User, user_id)
|
|
if user == current_user:
|
|
return user
|
|
if not current_user.is_superuser:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="The user doesn't have enough privileges",
|
|
)
|
|
if user is None:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return user
|
|
|
|
|
|
@router.patch(
|
|
"/{user_id}",
|
|
dependencies=[Depends(get_current_active_superuser)],
|
|
response_model=UserPublic,
|
|
)
|
|
def update_user(
|
|
*,
|
|
session: SessionDep,
|
|
user_id: uuid.UUID,
|
|
user_in: UserUpdate,
|
|
) -> Any:
|
|
"""
|
|
Update a user.
|
|
"""
|
|
|
|
db_user = session.get(User, user_id)
|
|
if not db_user:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="The user with this id does not exist in the system",
|
|
)
|
|
if user_in.email:
|
|
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
|
|
if existing_user and existing_user.id != user_id:
|
|
raise HTTPException(
|
|
status_code=409, detail="User with this email already exists"
|
|
)
|
|
|
|
db_user = crud.update_user(session=session, db_user=db_user, user_in=user_in)
|
|
return db_user
|
|
|
|
|
|
@router.delete("/{user_id}", dependencies=[Depends(get_current_active_superuser)])
|
|
def delete_user(
|
|
session: SessionDep, current_user: CurrentUser, user_id: uuid.UUID
|
|
) -> Message:
|
|
"""
|
|
Delete a user.
|
|
"""
|
|
user = session.get(User, user_id)
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
if user == current_user:
|
|
raise HTTPException(
|
|
status_code=403, detail="Super users are not allowed to delete themselves"
|
|
)
|
|
portal_sub = user.portal_sub
|
|
session.delete(user)
|
|
session.commit()
|
|
# The local row is the access; dropping the portal's is tidying up, and it
|
|
# happens after the thing that actually revokes has already happened.
|
|
if portal_sub:
|
|
forget_remote_user(portal_sub)
|
|
return Message(message="User deleted successfully")
|