diff --git a/alembic/versions/ecd6a3f19a8f_initial.py b/alembic/versions/a754f05fd8e5_initial.py similarity index 95% rename from alembic/versions/ecd6a3f19a8f_initial.py rename to alembic/versions/a754f05fd8e5_initial.py index d5bd043..adb4200 100644 --- a/alembic/versions/ecd6a3f19a8f_initial.py +++ b/alembic/versions/a754f05fd8e5_initial.py @@ -1,8 +1,8 @@ """initial -Revision ID: ecd6a3f19a8f +Revision ID: a754f05fd8e5 Revises: -Create Date: 2025-01-18 00:10:08.290307 +Create Date: 2025-01-25 19:46:28.006950 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. -revision = "ecd6a3f19a8f" +revision = "a754f05fd8e5" down_revision = None branch_labels = None depends_on = None @@ -51,7 +51,7 @@ def upgrade(): "users", sa.Column("id", sa.Integer(), nullable=False), sa.Column("email", sa.Text(), nullable=False), - sa.Column("password", sa.Text(), nullable=False), + sa.Column("password", sa.Text(), nullable=True), sa.Column("active", sa.Boolean(), nullable=True), sa.Column("admin", sa.Boolean(), nullable=True), sa.PrimaryKeyConstraint("id"), diff --git a/freenit/api/auth/__init__.py b/freenit/api/auth/__init__.py index 68788b3..694d3dd 100644 --- a/freenit/api/auth/__init__.py +++ b/freenit/api/auth/__init__.py @@ -7,7 +7,6 @@ from freenit.auth import authorize, decode, encode, encrypt from freenit.config import getConfig from freenit.mail import sendmail -from freenit.models.safe import UserSafe from freenit.models.user import User config = getConfig() @@ -24,7 +23,7 @@ class TokenExpire(pydantic.BaseModel): class LoginResponse(pydantic.BaseModel): - user: UserSafe + user: User expire: TokenExpire @@ -49,6 +48,7 @@ async def login(credentials: LoginInput, response: Response): httponly=True, secure=config.auth.secure, ) + user.password = None return { "user": user, "expire": { @@ -78,6 +78,7 @@ async def register_sql(credentials: LoginInput) -> User: active=False, ) await user.save() + user.password = None return user @@ -105,7 +106,7 @@ async def register(credentials: LoginInput, host=Header(default="")): return {"status": True} -@api.post("/auth/verify", response_model=UserSafe, tags=["auth"]) +@api.post("/auth/verify", response_model=User, tags=["auth"]) async def verify(verification: Verification): user = await decode(verification.verification) await user.update(active=True) @@ -117,6 +118,7 @@ async def refresh(request: Request, response: Response): user = await authorize(request, cookie="refresh") access = encode(user) response.set_cookie("access", access, httponly=True, secure=config.auth.secure) + user.password = None return { "user": user, "expire": { diff --git a/freenit/api/role/sql.py b/freenit/api/role/sql.py index e9e9913..5ae73bf 100644 --- a/freenit/api/role/sql.py +++ b/freenit/api/role/sql.py @@ -6,7 +6,6 @@ from freenit.decorators import description from freenit.models.pagination import Page, paginate from freenit.models.role import Role, RoleOptional -from freenit.models.safe import RoleSafe, UserSafe from freenit.models.user import User from freenit.permissions import role_perms @@ -21,11 +20,15 @@ async def get( page: int = Header(default=1), perpage: int = Header(default=10), _: User = Depends(role_perms), - ) -> Page[RoleSafe]: - return await paginate(Role.objects, page, perpage) + ) -> Page[Role]: + return await paginate( + Role.objects.select_related("users").exclude_fields("users__password"), + page, + perpage, + ) @staticmethod - async def post(role: Role, _: User = Depends(role_perms)) -> RoleSafe: + async def post(role: Role, _: User = Depends(role_perms)) -> Role: await role.save() return role @@ -33,18 +36,19 @@ async def post(role: Role, _: User = Depends(role_perms)) -> RoleSafe: @route("/roles/{id}", tags=tags) class RoleDetailAPI: @staticmethod - async def get(id, _: User = Depends(role_perms)) -> RoleSafe: + async def get(id, _: User = Depends(role_perms)) -> Role: try: - role = await Role.objects.get(pk=id) + role = ( + await Role.objects.select_related("users") + .exclude_fields("users__password") + .get(pk=id) + ) except ormar.exceptions.NoMatch: raise HTTPException(status_code=404, detail="No such role") - await role.load_all(follow=True) return role @staticmethod - async def patch( - id, role_data: RoleOptional, _: User = Depends(role_perms) - ) -> RoleSafe: + async def patch(id, role_data: RoleOptional, _: User = Depends(role_perms)) -> Role: if Role.dbtype() == "sql": try: role = await Role.objects.get(pk=id) @@ -58,7 +62,7 @@ async def patch( ) @staticmethod - async def delete(id, _: User = Depends(role_perms)) -> RoleSafe: + async def delete(id, _: User = Depends(role_perms)) -> Role: try: role = await Role.objects.get(pk=id) except ormar.exceptions.NoMatch: @@ -71,12 +75,15 @@ async def delete(id, _: User = Depends(role_perms)) -> RoleSafe: class RoleUserAPI: @staticmethod @description("Assign user to role") - async def post(role_id, user_id, _: User = Depends(role_perms)) -> UserSafe: + async def post(role_id, user_id, _: User = Depends(role_perms)) -> User: try: - user = await User.objects.get(pk=user_id) + user = ( + await User.objects.select_related("roles") + .exclude_fields("password") + .get(pk=user_id) + ) except ormar.exceptions.NoMatch: raise HTTPException(status_code=404, detail="No such user") - await user.load_all() for role in user.roles: if role.id == role_id: raise HTTPException(status_code=409, detail="User already assigned") @@ -89,16 +96,19 @@ async def post(role_id, user_id, _: User = Depends(role_perms)) -> UserSafe: @staticmethod @description("Deassign user to role") - async def delete(role_id, user_id, _: User = Depends(role_perms)) -> UserSafe: + async def delete(role_id, user_id, _: User = Depends(role_perms)) -> User: try: - user = await User.objects.get(pk=user_id) + user = ( + await User.objects.select_related("roles") + .exclude_fields("password") + .get(pk=user_id) + ) except ormar.exceptions.NoMatch: raise HTTPException(status_code=404, detail="No such user") try: role = await Role.objects.get(pk=role_id) except ormar.exceptions.NoMatch: raise HTTPException(status_code=404, detail="No such role") - await user.load_all() try: await user.roles.remove(role) except ormar.exceptions.NoMatch: diff --git a/freenit/api/user/sql.py b/freenit/api/user/sql.py index 8234010..af321a0 100644 --- a/freenit/api/user/sql.py +++ b/freenit/api/user/sql.py @@ -7,7 +7,6 @@ from freenit.config import getConfig from freenit.decorators import description from freenit.models.pagination import Page, paginate -from freenit.models.safe import UserSafe from freenit.models.user import User, UserOptional from freenit.permissions import profile_perms, user_perms @@ -24,23 +23,30 @@ async def get( page: int = Header(default=1), perpage: int = Header(default=10), _: User = Depends(user_perms), - ) -> Page[UserSafe]: - return await paginate(User.objects, page, perpage) + ) -> Page[User]: + return await paginate( + User.objects.select_related("roles").exclude_fields("password"), + page, + perpage, + ) @route("/users/{id}", tags=tags) class UserDetailAPI: @staticmethod - async def get(id, _: User = Depends(user_perms)) -> UserSafe: + async def get(id, _: User = Depends(user_perms)) -> User: try: - user = await User.objects.get(pk=id) + user = ( + await User.objects.select_related("roles") + .exclude_fields("password") + .get(pk=id) + ) except ormar.exceptions.NoMatch: raise HTTPException(status_code=404, detail="No such user") - await user.load_all(follow=True) return user @staticmethod - async def patch(id, data: UserOptional, _: User = Depends(user_perms)) -> UserSafe: + async def patch(id, data: UserOptional, _: User = Depends(user_perms)) -> User: if data.password: data.password = encrypt(data.password) try: @@ -51,7 +57,7 @@ async def patch(id, data: UserOptional, _: User = Depends(user_perms)) -> UserSa return user @staticmethod - async def delete(id, _: User = Depends(user_perms)) -> UserSafe: + async def delete(id, _: User = Depends(user_perms)) -> User: try: user = await User.objects.get(pk=id) except ormar.exceptions.NoMatch: @@ -64,7 +70,7 @@ async def delete(id, _: User = Depends(user_perms)) -> UserSafe: class ProfileDetailAPI: @staticmethod @description("Get my profile") - async def get(user: User = Depends(profile_perms)) -> UserSafe: + async def get(user: User = Depends(profile_perms)) -> User: await user.load_all() return user @@ -72,7 +78,7 @@ async def get(user: User = Depends(profile_perms)) -> UserSafe: @description("Edit my profile") async def patch( data: UserOptional, user: User = Depends(profile_perms) - ) -> UserSafe: + ) -> User: if data.password: data.password = encrypt(data.password) await user.patch(data) diff --git a/freenit/auth.py b/freenit/auth.py index a7c52a0..392f17c 100644 --- a/freenit/auth.py +++ b/freenit/auth.py @@ -20,7 +20,7 @@ async def decode(token): import ormar.exceptions try: - user = await User.objects.get(pk=pk) + user = await User.objects.select_related("roles").get(pk=pk) return user except ormar.exceptions.NoMatch: raise HTTPException(status_code=403, detail="Unauthorized") @@ -46,7 +46,6 @@ async def authorize(request: Request, roles=[], allof=[], cookie="access"): raise HTTPException(status_code=403, detail="Unauthorized") user = await decode(token) if user.dbtype() == "sql": - await user.load_all() if not user.active: raise HTTPException(status_code=403, detail="Permission denied") if user.admin: diff --git a/freenit/models/safe.py b/freenit/models/safe.py deleted file mode 100644 index 61cc643..0000000 --- a/freenit/models/safe.py +++ /dev/null @@ -1,12 +0,0 @@ -from freenit.models.role import Role -from freenit.models.user import User - -if User.dbtype() == "sql": - RoleSafe = Role.get_pydantic(exclude={"users__password"}) - UserSafe = User.get_pydantic(exclude={"password"}) -elif User.dbtype() == "ldap": - from freenit.config import getConfig - config = getConfig() - auth = config.get_model("user") - UserSafe = auth.UserSafe - RoleSafe = Role diff --git a/freenit/models/sql/base.py b/freenit/models/sql/base.py index 1c62332..b5e6799 100644 --- a/freenit/models/sql/base.py +++ b/freenit/models/sql/base.py @@ -23,7 +23,7 @@ async def patch(self, fields): class OrmarUserMixin: id: int = ormar.Integer(primary_key=True) email: pydantic.EmailStr = ormar.Text(unique=True) - password: str = ormar.Text() + password: str = ormar.Text(nullable=True) active: bool = ormar.Boolean(default=False) admin: bool = ormar.Boolean(default=False) diff --git a/freenit/models/sql/user.py b/freenit/models/sql/user.py index dd398e6..df869fe 100644 --- a/freenit/models/sql/user.py +++ b/freenit/models/sql/user.py @@ -16,12 +16,16 @@ class BaseUser(OrmarBaseModel, OrmarUserMixin): def check(self, password: str) -> bool: + if self.password is None: + return False return verify(password, self.password) @classmethod async def login(cls, credentials) -> BaseUser: try: - user = await cls.objects.get(email=credentials.email, active=True) + user = await cls.objects.select_related("roles").get( + email=credentials.email, active=True + ) except ormar.exceptions.NoMatch: raise HTTPException(status_code=403, detail="Failed to login") if user.check(credentials.password):