Skip to content

Commit

Permalink
Merge pull request #135 from mekanix/feature/ldap-register
Browse files Browse the repository at this point in the history
Implement LDAP for auth DB
  • Loading branch information
mekanix authored Apr 15, 2024
2 parents 6db20a9 + b891ed5 commit 21bab2e
Show file tree
Hide file tree
Showing 10 changed files with 477 additions and 134 deletions.
2 changes: 1 addition & 1 deletion freenit/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.3.1"
__version__ = "0.3.2"
19 changes: 16 additions & 3 deletions freenit/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,8 @@ async def login(credentials: LoginInput, response: Response):
}


@api.post("/auth/register", tags=["auth"])
async def register(credentials: LoginInput, host=Header(default="")):
async def register_ormar(credentials: LoginInput) -> User:
import ormar.exceptions

try:
user = await User.objects.get(email=credentials.email)
raise HTTPException(status_code=409, detail="User already registered")
Expand All @@ -73,6 +71,21 @@ async def register(credentials: LoginInput, host=Header(default="")):
active=False,
)
await user.save()
return user


async def register_bonsai(credentials: LoginInput) -> User:
user = await User.register(credentials)
await user.save()
return user


@api.post("/auth/register", tags=["auth"])
async def register(credentials: LoginInput, host=Header(default="")):
if User.dbtype() == "ormar":
user = await register_ormar(credentials)
else:
user = await register_bonsai(credentials)
token = encode(user)
print(token)
mail = config.mail
Expand Down
196 changes: 138 additions & 58 deletions freenit/api/role.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import List

import ormar
import ormar.exceptions
from fastapi import Depends, Header, HTTPException
Expand All @@ -23,85 +21,167 @@ async def get(
perpage: int = Header(default=10),
_: User = Depends(role_perms),
) -> Page[RoleSafe]:
roles = Role.objects
return await paginate(roles, page, perpage)
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 = Depends(role_perms)) -> RoleSafe:
await role.save()
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: int, _: 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
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 = Role.get(id)
return role
raise HTTPException(status_code=409, detail="Unknown role type")

@staticmethod
async def patch(
id: int, role_data: RoleOptional, _: User = Depends(role_perms)
id, role_data: RoleOptional, _: 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.patch(role_data)
return role
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: int, _: 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
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: int, user_id: int, _: User = Depends(role_perms)
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
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: int, user_id: int, _: User = Depends(role_perms)
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
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")
94 changes: 70 additions & 24 deletions freenit/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ async def get(
elif User.dbtype() == "bonsai":
import bonsai

client = bonsai.LDAPClient(f"ldap://{config.ldap.host}", config.ldap.tls)
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(
Expand Down Expand Up @@ -64,35 +66,79 @@ async def get(
@route("/users/{id}", tags=tags)
class UserDetailAPI:
@staticmethod
async def get(id: int, _: 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
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: int, data: UserOptional, _: User = Depends(user_perms)
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
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: int, _: 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
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"])
Expand Down
Loading

0 comments on commit 21bab2e

Please sign in to comment.