From a26e1d1608c2151381c2cee7dd6492705ab8950b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Goran=20Meki=C4=87?= Date: Mon, 15 Apr 2024 22:14:02 +0200 Subject: [PATCH] Reorganize LDAP and SQL implementation --- freenit/api/{auth.py => auth/__init__.py} | 10 +- freenit/api/role.py | 187 ---------------------- freenit/api/role/__init__.py | 6 + freenit/api/role/ldap.py | 86 ++++++++++ freenit/api/role/sql.py | 106 ++++++++++++ freenit/api/theme.py | 2 - freenit/api/user.py | 142 ---------------- freenit/api/user/__init__.py | 6 + freenit/api/user/ldap.py | 96 +++++++++++ freenit/api/user/sql.py | 80 +++++++++ freenit/auth.py | 12 +- freenit/base_config.py | 6 +- freenit/models/ldap/base.py | 2 +- freenit/models/ldap/role.py | 32 +++- freenit/models/ldap/user.py | 4 - freenit/models/safe.py | 4 +- freenit/models/{ormar => sql}/__init__.py | 0 freenit/models/{ormar => sql}/base.py | 3 +- freenit/models/{ormar => sql}/role.py | 0 freenit/models/{ormar => sql}/theme.py | 0 freenit/models/{ormar => sql}/user.py | 2 +- 21 files changed, 426 insertions(+), 360 deletions(-) rename freenit/api/{auth.py => auth/__init__.py} (92%) delete mode 100644 freenit/api/role.py create mode 100644 freenit/api/role/__init__.py create mode 100644 freenit/api/role/ldap.py create mode 100644 freenit/api/role/sql.py delete mode 100644 freenit/api/user.py create mode 100644 freenit/api/user/__init__.py create mode 100644 freenit/api/user/ldap.py create mode 100644 freenit/api/user/sql.py rename freenit/models/{ormar => sql}/__init__.py (100%) rename freenit/models/{ormar => sql}/base.py (97%) rename freenit/models/{ormar => sql}/role.py (100%) rename freenit/models/{ormar => sql}/theme.py (100%) rename freenit/models/{ormar => sql}/user.py (96%) diff --git a/freenit/api/auth.py b/freenit/api/auth/__init__.py similarity index 92% rename from freenit/api/auth.py rename to freenit/api/auth/__init__.py index c2433a6..ef87e2b 100644 --- a/freenit/api/auth.py +++ b/freenit/api/auth/__init__.py @@ -58,7 +58,7 @@ async def login(credentials: LoginInput, response: Response): } -async def register_ormar(credentials: LoginInput) -> User: +async def register_sql(credentials: LoginInput) -> User: import ormar.exceptions try: user = await User.objects.get(email=credentials.email) @@ -74,7 +74,7 @@ async def register_ormar(credentials: LoginInput) -> User: return user -async def register_bonsai(credentials: LoginInput) -> User: +async def register_ldap(credentials: LoginInput) -> User: user = await User.register(credentials) await user.save() return user @@ -82,10 +82,10 @@ async def register_bonsai(credentials: LoginInput) -> User: @api.post("/auth/register", tags=["auth"]) async def register(credentials: LoginInput, host=Header(default="")): - if User.dbtype() == "ormar": - user = await register_ormar(credentials) + if User.dbtype() == "sql": + user = await register_sql(credentials) else: - user = await register_bonsai(credentials) + user = await register_ldap(credentials) token = encode(user) print(token) mail = config.mail diff --git a/freenit/api/role.py b/freenit/api/role.py deleted file mode 100644 index 698e52c..0000000 --- a/freenit/api/role.py +++ /dev/null @@ -1,187 +0,0 @@ -import ormar -import ormar.exceptions -from fastapi import Depends, Header, HTTPException -from freenit.api.router import route -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 - -tags = ["role"] - - -@route("/roles", tags=tags) -class RoleListAPI: - @staticmethod - @description("Get roles") - async def get( - page: int = Header(default=1), - perpage: int = Header(default=10), - _: User = Depends(role_perms), - ) -> Page[RoleSafe]: - if Role.dbtype() == "ormar": - return await paginate(Role.objects, page, perpage) - elif Role.dbtype() == "bonsai": - import bonsai - - from freenit.models.ldap.base import get_client - - client = get_client() - try: - async with client.connect(is_async=True) as conn: - res = await conn.search( - f"dc=group,dc=ldap", - bonsai.LDAPSearchScope.SUB, - "objectClass=groupOfUniqueNames", - ) - except bonsai.errors.AuthenticationError: - raise HTTPException(status_code=403, detail="Failed to login") - data = [] - for gdata in res: - role = Role( - cn=gdata["cn"][0], - dn=str(gdata["dn"]), - uniqueMembers=gdata["uniqueMember"], - ) - data.append(role) - - total = len(res) - page = Page(total=total, page=1, pages=1, perpage=total, data=data) - return page - raise HTTPException(status_code=409, detail="Unknown group type") - - @staticmethod - async def post(role: Role, user: User = Depends(role_perms)) -> RoleSafe: - if Role.dbtype() == "ormar": - await role.save() - elif Role.dbtype() == "bonsai": - import bonsai - try: - await role.create(user) - except bonsai.errors.AlreadyExists: - raise HTTPException(status_code=409, detail="Role already exists") - return role - - -@route("/roles/{id}", tags=tags) -class RoleDetailAPI: - @staticmethod - async def get(id, _: User = Depends(role_perms)) -> RoleSafe: - if Role.dbtype() == "ormar": - try: - role = await Role.objects.get(pk=id) - except ormar.exceptions.NoMatch: - raise HTTPException(status_code=404, detail="No such role") - await role.load_all(follow=True) - return role - elif Role.dbtype() == "bonsai": - role = await Role.get(id) - return role - raise HTTPException(status_code=409, detail="Unknown role type") - - async def patch( - id, role_data: RoleOptional, _: User = Depends(role_perms) - ) -> RoleSafe: - if Role.dbtype() == "ormar": - try: - role = await Role.objects.get(pk=id) - except ormar.exceptions.NoMatch: - raise HTTPException(status_code=404, detail="No such role") - await role.patch(role_data) - return role - raise HTTPException(status_code=409, detail=f"Role type {Role.dbtype()} doesn't support PATCH method") - - @staticmethod - async def delete(id, _: User = Depends(role_perms)) -> RoleSafe: - if Role.dbtype() == "ormar": - try: - role = await Role.objects.get(pk=id) - except ormar.exceptions.NoMatch: - raise HTTPException(status_code=404, detail="No such role") - await role.delete() - return role - elif Role.dbtype() == "bonsai": - import bonsai - - from freenit.models.ldap.base import get_client - - client = get_client() - try: - async with client.connect(is_async=True) as conn: - res = await conn.search( - id, bonsai.LDAPSearchScope.SUB, "objectClass=groupOfUniqueNames" - ) - if len(res) < 1: - raise HTTPException(status_code=404, detail="No such role") - if len(res) > 1: - raise HTTPException(status_code=409, detail="Multiple role found") - existing = res[0] - role = Role( - cn=existing["cn"][0], - dn=str(existing["dn"]), - uniqueMembers=existing["uniqueMember"], - ) - await existing.delete() - return role - except bonsai.errors.AuthenticationError: - raise HTTPException(status_code=403, detail="Failed to login") - raise HTTPException(status_code=409, detail="Unknown role type") - - -@route("/roles/{role_id}/{user_id}", tags=tags) -class RoleUserAPI: - @staticmethod - @description("Assign user to role") - async def post( - role_id, user_id, _: User = Depends(role_perms) - ) -> UserSafe: - if Role.dbtype() == "ormar": - try: - user = await User.objects.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") - try: - role = await Role.objects.get(pk=role_id) - except ormar.exceptions.NoMatch: - raise HTTPException(status_code=404, detail="No such role") - await user.roles.add(role) - return user - elif Role.dbtype() == "bonsai": - user = await User.get(user_id) - role = await Role.get(role_id) - await role.add(user) - return user - raise HTTPException(status_code=409, detail="Unknown role type") - - @staticmethod - @description("Deassign user to role") - async def delete( - role_id, user_id, _: User = Depends(role_perms) - ) -> UserSafe: - if Role.dbtype() == "ormar": - try: - user = await User.objects.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: - raise HTTPException(status_code=404, detail="User is not part of role") - return user - elif Role.dbtype() == "bonsai": - user = await User.get(user_id) - role = await Role.get(role_id) - await role.remove(user) - return user - raise HTTPException(status_code=409, detail="Unknown role type") diff --git a/freenit/api/role/__init__.py b/freenit/api/role/__init__.py new file mode 100644 index 0000000..3b7b46e --- /dev/null +++ b/freenit/api/role/__init__.py @@ -0,0 +1,6 @@ +from freenit.models.role import Role + +if Role.dbtype() == "sql": + from .sql import RoleListAPI, RoleDetailAPI, RoleUserAPI +elif Role.dbtype() == "ldap": + from .ldap import RoleListAPI, RoleDetailAPI, RoleUserAPI diff --git a/freenit/api/role/ldap.py b/freenit/api/role/ldap.py new file mode 100644 index 0000000..4465ac3 --- /dev/null +++ b/freenit/api/role/ldap.py @@ -0,0 +1,86 @@ +import bonsai +from fastapi import Depends, Header, HTTPException + +from freenit.api.router import route +from freenit.decorators import description +from freenit.models.ldap.base import get_client +from freenit.models.pagination import Page +from freenit.models.role import Role +from freenit.models.safe import RoleSafe, UserSafe +from freenit.models.user import User +from freenit.permissions import role_perms + +tags = ["role"] + + +@route("/roles", tags=tags) +class RoleListAPI: + @staticmethod + @description("Get roles") + async def get( + page: int = Header(default=1), + _: int = Header(default=10), + user: User = Depends(role_perms), + ) -> Page[RoleSafe]: + data = await Role.get_all() + total = len(data) + page = Page(total=total, page=1, pages=1, perpage=total, data=data) + return page + + @staticmethod + async def post(role: Role, user: User = Depends(role_perms)) -> RoleSafe: + try: + await role.create(user) + except bonsai.errors.AlreadyExists: + raise HTTPException(status_code=409, detail="Role already exists") + return role + + +@route("/roles/{id}", tags=tags) +class RoleDetailAPI: + @staticmethod + async def get(id, _: User = Depends(role_perms)) -> RoleSafe: + role = await Role.get(id) + return role + + @staticmethod + async def delete(id, _: User = Depends(role_perms)) -> RoleSafe: + client = get_client() + try: + async with client.connect(is_async=True) as conn: + res = await conn.search( + id, bonsai.LDAPSearchScope.SUB, "objectClass=groupOfUniqueNames" + ) + if len(res) < 1: + raise HTTPException(status_code=404, detail="No such role") + if len(res) > 1: + raise HTTPException(status_code=409, detail="Multiple role found") + existing = res[0] + role = Role( + cn=existing["cn"][0], + dn=str(existing["dn"]), + users=existing["uniqueMember"], + ) + await existing.delete() + return role + except bonsai.errors.AuthenticationError: + raise HTTPException(status_code=403, detail="Failed to login") + + +@route("/roles/{role_id}/{user_id}", tags=tags) +class RoleUserAPI: + @staticmethod + @description("Assign user to role") + async def post(role_id, user_id, _: User = Depends(role_perms)) -> UserSafe: + user = await User.get(user_id) + role = await Role.get(role_id) + await role.add(user) + return user + + @staticmethod + @description("Deassign user to role") + async def delete(role_id, user_id, _: User = Depends(role_perms)) -> UserSafe: + user = await User.get(user_id) + role = await Role.get(role_id) + await role.remove(user) + return user diff --git a/freenit/api/role/sql.py b/freenit/api/role/sql.py new file mode 100644 index 0000000..e9e9913 --- /dev/null +++ b/freenit/api/role/sql.py @@ -0,0 +1,106 @@ +import ormar +import ormar.exceptions +from fastapi import Depends, Header, HTTPException + +from freenit.api.router import route +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 + +tags = ["role"] + + +@route("/roles", tags=tags) +class RoleListAPI: + @staticmethod + @description("Get roles") + 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) + + @staticmethod + async def post(role: Role, _: User = Depends(role_perms)) -> RoleSafe: + await role.save() + return role + + +@route("/roles/{id}", tags=tags) +class RoleDetailAPI: + @staticmethod + async def get(id, _: User = Depends(role_perms)) -> RoleSafe: + try: + role = await Role.objects.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: + if Role.dbtype() == "sql": + try: + role = await Role.objects.get(pk=id) + except ormar.exceptions.NoMatch: + raise HTTPException(status_code=404, detail="No such role") + await role.patch(role_data) + return role + raise HTTPException( + status_code=409, + detail=f"Role type {Role.dbtype()} doesn't support PATCH method", + ) + + @staticmethod + async def delete(id, _: User = Depends(role_perms)) -> RoleSafe: + try: + role = await Role.objects.get(pk=id) + except ormar.exceptions.NoMatch: + raise HTTPException(status_code=404, detail="No such role") + await role.delete() + return role + + +@route("/roles/{role_id}/{user_id}", tags=tags) +class RoleUserAPI: + @staticmethod + @description("Assign user to role") + async def post(role_id, user_id, _: User = Depends(role_perms)) -> UserSafe: + try: + user = await User.objects.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") + try: + role = await Role.objects.get(pk=role_id) + except ormar.exceptions.NoMatch: + raise HTTPException(status_code=404, detail="No such role") + await user.roles.add(role) + return user + + @staticmethod + @description("Deassign user to role") + async def delete(role_id, user_id, _: User = Depends(role_perms)) -> UserSafe: + try: + user = await User.objects.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: + raise HTTPException(status_code=404, detail="User is not part of role") + return user diff --git a/freenit/api/theme.py b/freenit/api/theme.py index 5b111a1..8df6bda 100644 --- a/freenit/api/theme.py +++ b/freenit/api/theme.py @@ -1,5 +1,3 @@ -from typing import List - import ormar import ormar.exceptions from fastapi import Depends, Header, HTTPException diff --git a/freenit/api/user.py b/freenit/api/user.py deleted file mode 100644 index 7ec3b9e..0000000 --- a/freenit/api/user.py +++ /dev/null @@ -1,142 +0,0 @@ -import ormar -import ormar.exceptions -from fastapi import Depends, Header, HTTPException - -from freenit.api.router import route -from freenit.auth import encrypt -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 - -tags = ["user"] - -config = getConfig() - - -@route("/users", tags=tags) -class UserListAPI: - @staticmethod - @description("Get users") - async def get( - page: int = Header(default=1), - perpage: int = Header(default=10), - _: User = Depends(user_perms), - ) -> Page[UserSafe]: - if User.dbtype() == "ormar": - return await paginate(User.objects, page, perpage) - elif User.dbtype() == "bonsai": - users = await User.get_all() - total = len(users) - page = Page(total=total, page=1, pages=1, perpage=total, data=users) - return page - raise HTTPException(status_code=409, detail="Unknown user type") - - -@route("/users/{id}", tags=tags) -class UserDetailAPI: - @staticmethod - async def get(id, _: User = Depends(user_perms)) -> UserSafe: - if User.dbtype() == "ormar": - try: - user = await User.objects.get(pk=id) - except ormar.exceptions.NoMatch: - raise HTTPException(status_code=404, detail="No such user") - await user.load_all(follow=True) - return user - elif User.dbtype() == "bonsai": - user = await User.get(id) - return user - raise HTTPException(status_code=409, detail="Unknown user type") - - @staticmethod - async def patch( - id, data: UserOptional, _: User = Depends(user_perms) - ) -> UserSafe: - if User.dbtype() == "ormar": - if data.password: - data.password = encrypt(data.password) - try: - user = await User.objects.get(pk=id) - except ormar.exceptions.NoMatch: - raise HTTPException(status_code=404, detail="No such user") - await user.patch(data) - return user - elif User.dbtype() == "bonsai": - user = await User.get(id) - update = { - field: getattr(data, field) for field in data.__fields__ if getattr(data, field) != '' - } - await user.update(active=user.userClass, **update) - return user - raise HTTPException(status_code=409, detail="Unknown user type") - - @staticmethod - async def delete(id, _: User = Depends(user_perms)) -> UserSafe: - if User.dbtype() == "ormar": - try: - user = await User.objects.get(pk=id) - except ormar.exceptions.NoMatch: - raise HTTPException(status_code=404, detail="No such user") - await user.delete() - return user - elif User.dbtype() == "bonsai": - import bonsai - - from freenit.models.ldap.base import get_client - - client = get_client() - try: - async with client.connect(is_async=True) as conn: - res = await conn.search( - id, bonsai.LDAPSearchScope.SUB, "objectClass=person" - ) - if len(res) < 1: - raise HTTPException(status_code=404, detail="No such user") - if len(res) > 1: - raise HTTPException(status_code=409, detail="Multiple users found") - existing = res[0] - user = User( - email=existing["mail"][0], - sn=existing["sn"][0], - cn=existing["cn"][0], - dn=str(existing["dn"]), - uid=existing["uid"][0], - userClass=existing["userClass"][0], - ) - await existing.delete() - return user - except bonsai.errors.AuthenticationError: - raise HTTPException(status_code=403, detail="Failed to login") - raise HTTPException(status_code=409, detail="Unknown user type") - - -@route("/profile", tags=["profile"]) -class ProfileDetailAPI: - @staticmethod - @description("Get my profile") - async def get(user: User = Depends(profile_perms)) -> UserSafe: - if User.dbtype() == "ormar": - await user.load_all() - return user - - @staticmethod - @description("Edit my profile") - async def patch( - data: UserOptional, user: User = Depends(profile_perms) - ) -> UserSafe: - if User.dbtype() == "ormar": - if data.password: - data.password = encrypt(data.password) - await user.patch(data) - await user.load_all() - return user - elif User.dbtype() == "bonsai": - update = { - field: getattr(data, field) for field in data.__fields__ if getattr(data, field) != '' - } - await user.update(active=user.userClass, **update) - return user - raise HTTPException(status_code=409, detail="Unknown user type") diff --git a/freenit/api/user/__init__.py b/freenit/api/user/__init__.py new file mode 100644 index 0000000..7e75148 --- /dev/null +++ b/freenit/api/user/__init__.py @@ -0,0 +1,6 @@ +from freenit.models.user import User + +if User.dbtype() == "sql": + from .sql import UserListAPI, UserDetailAPI, ProfileDetailAPI +elif User.dbtype() == "ldap": + from .ldap import UserListAPI, UserDetailAPI, ProfileDetailAPI diff --git a/freenit/api/user/ldap.py b/freenit/api/user/ldap.py new file mode 100644 index 0000000..ac81766 --- /dev/null +++ b/freenit/api/user/ldap.py @@ -0,0 +1,96 @@ +import bonsai +from fastapi import Depends, Header, HTTPException + +from freenit.api.router import route +from freenit.config import getConfig +from freenit.decorators import description +from freenit.models.ldap.base import get_client +from freenit.models.pagination import Page +from freenit.models.safe import UserSafe +from freenit.models.user import User, UserOptional +from freenit.permissions import profile_perms, user_perms + +tags = ["user"] + +config = getConfig() + + +@route("/users", tags=tags) +class UserListAPI: + @staticmethod + @description("Get users") + async def get( + page: int = Header(default=1), + perpage: int = Header(default=10), + _: User = Depends(user_perms), + ) -> Page[UserSafe]: + users = await User.get_all() + total = len(users) + page = Page(total=total, page=1, pages=1, perpage=total, data=users) + return page + + +@route("/users/{id}", tags=tags) +class UserDetailAPI: + @staticmethod + async def get(id, _: User = Depends(user_perms)) -> UserSafe: + user = await User.get(id) + return user + + @staticmethod + async def patch(id, data: UserOptional, _: User = Depends(user_perms)) -> UserSafe: + user = await User.get(id) + update = { + field: getattr(data, field) + for field in data.__fields__ + if getattr(data, field) != "" + } + await user.update(active=user.userClass, **update) + return user + + @staticmethod + async def delete(id, _: User = Depends(user_perms)) -> UserSafe: + client = get_client() + try: + async with client.connect(is_async=True) as conn: + res = await conn.search( + id, bonsai.LDAPSearchScope.SUB, "objectClass=person" + ) + if len(res) < 1: + raise HTTPException(status_code=404, detail="No such user") + if len(res) > 1: + raise HTTPException(status_code=409, detail="Multiple users found") + existing = res[0] + user = User( + email=existing["mail"][0], + sn=existing["sn"][0], + cn=existing["cn"][0], + dn=str(existing["dn"]), + uid=existing["uid"][0], + userClass=existing["userClass"][0], + ) + await existing.delete() + return user + except bonsai.errors.AuthenticationError: + raise HTTPException(status_code=403, detail="Failed to login") + + +@route("/profile", tags=["profile"]) +class ProfileDetailAPI: + @staticmethod + @description("Get my profile") + async def get(user: User = Depends(profile_perms)) -> UserSafe: + return user + + @staticmethod + @description("Edit my profile") + async def patch( + data: UserOptional, user: User = Depends(profile_perms) + ) -> UserSafe: + update = { + field: getattr(data, field) + for field in data.__fields__ + if getattr(data, field) != "" + } + await user.update(active=user.userClass, **update) + return user diff --git a/freenit/api/user/sql.py b/freenit/api/user/sql.py new file mode 100644 index 0000000..8234010 --- /dev/null +++ b/freenit/api/user/sql.py @@ -0,0 +1,80 @@ +import ormar +import ormar.exceptions +from fastapi import Depends, Header, HTTPException + +from freenit.api.router import route +from freenit.auth import encrypt +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 + +tags = ["user"] + +config = getConfig() + + +@route("/users", tags=tags) +class UserListAPI: + @staticmethod + @description("Get users") + 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) + + +@route("/users/{id}", tags=tags) +class UserDetailAPI: + @staticmethod + async def get(id, _: User = Depends(user_perms)) -> UserSafe: + try: + user = await User.objects.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: + if data.password: + data.password = encrypt(data.password) + try: + user = await User.objects.get(pk=id) + except ormar.exceptions.NoMatch: + raise HTTPException(status_code=404, detail="No such user") + await user.patch(data) + return user + + @staticmethod + async def delete(id, _: User = Depends(user_perms)) -> UserSafe: + try: + user = await User.objects.get(pk=id) + except ormar.exceptions.NoMatch: + raise HTTPException(status_code=404, detail="No such user") + await user.delete() + return user + + +@route("/profile", tags=["profile"]) +class ProfileDetailAPI: + @staticmethod + @description("Get my profile") + async def get(user: User = Depends(profile_perms)) -> UserSafe: + await user.load_all() + return user + + @staticmethod + @description("Edit my profile") + async def patch( + data: UserOptional, user: User = Depends(profile_perms) + ) -> UserSafe: + if data.password: + data.password = encrypt(data.password) + await user.patch(data) + await user.load_all() + return user diff --git a/freenit/auth.py b/freenit/auth.py index 1ee9f18..a7c52a0 100644 --- a/freenit/auth.py +++ b/freenit/auth.py @@ -15,7 +15,7 @@ async def decode(token): pk = data.get("pk", None) if pk is None: raise HTTPException(status_code=403, detail="Unauthorized") - if User.dbtype() == "ormar": + if User.dbtype() == "sql": import ormar import ormar.exceptions @@ -24,7 +24,7 @@ async def decode(token): return user except ormar.exceptions.NoMatch: raise HTTPException(status_code=403, detail="Unauthorized") - elif User.dbtype() == "bonsai": + elif User.dbtype() == "ldap": user = await User.get(pk) return user raise HTTPException(status_code=409, detail="Unknown user type") @@ -33,9 +33,9 @@ async def decode(token): def encode(user): config = getConfig() payload = {} - if user.dbtype() == "ormar": + if user.dbtype() == "sql": payload = {"pk": user.pk, "type": "ormar"} - elif user.dbtype() == "bonsai": + elif user.dbtype() == "ldap": payload = {"pk": user.dn, "type": "bonsai"} return jwt.encode(payload, config.secret, algorithm="HS256") @@ -45,7 +45,7 @@ async def authorize(request: Request, roles=[], allof=[], cookie="access"): if not token: raise HTTPException(status_code=403, detail="Unauthorized") user = await decode(token) - if user.dbtype() == "ormar": + if user.dbtype() == "sql": await user.load_all() if not user.active: raise HTTPException(status_code=403, detail="Permission denied") @@ -68,7 +68,7 @@ async def authorize(request: Request, roles=[], allof=[], cookie="access"): if role.name not in allof: raise HTTPException(status_code=403, detail="Permission denied") return user - elif user.dbtype() == "bonsai": + elif user.dbtype() == "ldap": pass return user diff --git a/freenit/base_config.py b/freenit/base_config.py index 8d4808d..be8fb8b 100644 --- a/freenit/base_config.py +++ b/freenit/base_config.py @@ -81,9 +81,9 @@ class BaseConfig: database = None engine = None secret = "SECRET" # nosec - user = "freenit.models.ormar.user" - role = "freenit.models.ormar.role" - theme = "freenit.models.ormar.theme" + user = "freenit.models.sql.user" + role = "freenit.models.sql.role" + theme = "freenit.models.sql.theme" theme_name = "Freenit" meta = None auth = Auth() diff --git a/freenit/models/ldap/base.py b/freenit/models/ldap/base.py index c736644..7a1039a 100644 --- a/freenit/models/ldap/base.py +++ b/freenit/models/ldap/base.py @@ -37,7 +37,7 @@ async def save_data(data): class LDAPBaseModel(BaseModel, Generic[T]): @classmethod def dbtype(cls): - return "bonsai" + return "ldap" dn: str = Field("", description=("Distinguished name")) diff --git a/freenit/models/ldap/role.py b/freenit/models/ldap/role.py index 2fe0b82..a165707 100644 --- a/freenit/models/ldap/role.py +++ b/freenit/models/ldap/role.py @@ -10,7 +10,7 @@ class Role(LDAPBaseModel): cn: str = Field("", description=("Common name")) - uniqueMembers: list = Field([], description=("Role members")) + users: list = Field([], description=("Role members")) @classmethod async def get(cls, dn): @@ -32,10 +32,32 @@ async def get(cls, dn): role = cls( cn=data["cn"][0], dn=str(data["dn"]), - uniqueMembers=data["uniqueMember"], + users=data["uniqueMember"], ) return role + @classmethod + async def get_all(cls): + client = get_client() + try: + async with client.connect(is_async=True) as conn: + res = await conn.search( + f"dc=group,dc=ldap", + LDAPSearchScope.SUB, + "objectClass=groupOfUniqueNames", + ) + data = [] + for gdata in res: + role = Role( + cn=gdata["cn"][0], + dn=str(gdata["dn"]), + users=gdata["uniqueMember"], + ) + data.append(role) + except errors.AuthenticationError: + raise HTTPException(status_code=403, detail="Failed to login") + return data + async def create(self, user): data = LDAPEntry(self.dn) @@ -43,7 +65,7 @@ async def create(self, user): data["cn"] = self.cn data["uniqueMember"] = user.dn await save_data(data) - self.uniqueMembers = data["uniqueMember"] + self.users = data["uniqueMember"] async def add(self, user): client = get_client() @@ -64,7 +86,7 @@ async def add(self, user): await data.modify() except errors.AuthenticationError: raise HTTPException(status_code=403, detail="Failed to login") - self.uniqueMembers.append(user) + self.users.append(user) async def remove(self, user): client = get_client() @@ -85,7 +107,7 @@ async def remove(self, user): await data.modify() except errors.AuthenticationError: raise HTTPException(status_code=403, detail="Failed to login") - self.uniqueMembers.append(user) + self.users.remove(user) RoleOptional = Role diff --git a/freenit/models/ldap/user.py b/freenit/models/ldap/user.py index 38463ee..1162f82 100644 --- a/freenit/models/ldap/user.py +++ b/freenit/models/ldap/user.py @@ -106,11 +106,7 @@ async def get_all(cls): dn=str(udata["dn"]), uid=udata["uid"][0], userClass=udata["userClass"][0], -<<<<<<< HEAD - roles=data["memberOf"], -======= roles=udata.get("memberOf", []), ->>>>>>> 1243228 (Make front/back integration better) ) data.append(user) return data diff --git a/freenit/models/safe.py b/freenit/models/safe.py index 78f5957..61cc643 100644 --- a/freenit/models/safe.py +++ b/freenit/models/safe.py @@ -1,10 +1,10 @@ from freenit.models.role import Role from freenit.models.user import User -if User.dbtype() == "ormar": +if User.dbtype() == "sql": RoleSafe = Role.get_pydantic(exclude={"users__password"}) UserSafe = User.get_pydantic(exclude={"password"}) -elif User.dbtype() == "bonsai": +elif User.dbtype() == "ldap": from freenit.config import getConfig config = getConfig() auth = config.get_model("user") diff --git a/freenit/models/ormar/__init__.py b/freenit/models/sql/__init__.py similarity index 100% rename from freenit/models/ormar/__init__.py rename to freenit/models/sql/__init__.py diff --git a/freenit/models/ormar/base.py b/freenit/models/sql/base.py similarity index 97% rename from freenit/models/ormar/base.py rename to freenit/models/sql/base.py index a49f13b..fbd5b3f 100644 --- a/freenit/models/ormar/base.py +++ b/freenit/models/sql/base.py @@ -3,14 +3,13 @@ from freenit.config import getConfig - config = getConfig() class OrmarBaseModel(ormar.Model): @classmethod def dbtype(cls): - return 'ormar' + return "sql" async def patch(self, fields): result = {} diff --git a/freenit/models/ormar/role.py b/freenit/models/sql/role.py similarity index 100% rename from freenit/models/ormar/role.py rename to freenit/models/sql/role.py diff --git a/freenit/models/ormar/theme.py b/freenit/models/sql/theme.py similarity index 100% rename from freenit/models/ormar/theme.py rename to freenit/models/sql/theme.py diff --git a/freenit/models/ormar/user.py b/freenit/models/sql/user.py similarity index 96% rename from freenit/models/ormar/user.py rename to freenit/models/sql/user.py index 58c5590..ccbc159 100644 --- a/freenit/models/ormar/user.py +++ b/freenit/models/sql/user.py @@ -5,7 +5,7 @@ from fastapi import HTTPException from freenit.auth import verify -from freenit.models.ormar.base import ( +from freenit.models.sql.base import ( OrmarBaseModel, OrmarUserMixin, generate_optional,