Skip to content

Commit 874a62c

Browse files
authored
Merge pull request #177 from mekanix/ldap
Implement domain management
2 parents f1fce82 + fdaabf5 commit 874a62c

File tree

8 files changed

+172
-8
lines changed

8 files changed

+172
-8
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# Freenit Backend
22
![freenit badge](https://github.com/freenit-framework/backend/actions/workflows/pythonapp.yml/badge.svg)
33

4-
[Documentation](https://freenit.org/backend/quickstart)
4+
[Documentation](https://freenit.org/)
55

66
[Source](https://github.com/freenit-framework/backend)
77

88
Freenit is based on
99

1010
* [FastAPI](https://fastapi.tiangolo.com/)
1111
* [Ormar](https://github.com/collerek/ormar)
12+
* [Bonsai](https://github.com/noirello/bonsai)
1213
* [Svelte](https://svelte.dev)

freenit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.16"
1+
__version__ = "0.3.17"

freenit/api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import freenit.api.auth
2+
import freenit.api.domain
23
import freenit.api.role
34
import freenit.api.theme
45
import freenit.api.user

freenit/api/domain/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from freenit.models.user import User
2+
3+
if User.dbtype() == "ldap":
4+
from .ldap import DomainListAPI, DomainDetailAPI
5+

freenit/api/domain/ldap.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import bonsai
2+
from fastapi import Depends, Header, HTTPException
3+
4+
from freenit.api.router import route
5+
from freenit.config import getConfig
6+
from freenit.decorators import description
7+
from freenit.models.ldap.domain import Domain, DomainCreate
8+
from freenit.models.pagination import Page
9+
from freenit.models.user import User
10+
from freenit.permissions import domain_perms
11+
12+
tags = ["domain"]
13+
config = getConfig()
14+
15+
16+
@route("/domains", tags=tags)
17+
class DomainListAPI:
18+
@staticmethod
19+
@description("Get domains")
20+
async def get(
21+
page: int = Header(default=1),
22+
perpage: int = Header(default=10),
23+
_: User = Depends(domain_perms),
24+
) -> Page[Domain]:
25+
data = await Domain.get_all()
26+
perpage = len(data)
27+
data = Page(total=perpage, page=page, pages=1, perpage=perpage, data=data)
28+
return data
29+
30+
@staticmethod
31+
async def post(data: DomainCreate, _: User = Depends(domain_perms)) -> Domain:
32+
if data.name == "":
33+
raise HTTPException(status_code=409, detail="Name is mandatory")
34+
rdomain, udomain = Domain.create(data.name)
35+
try:
36+
await rdomain.save()
37+
await udomain.save()
38+
except bonsai.errors.AlreadyExists:
39+
raise HTTPException(status_code=409, detail="Domain already exists")
40+
return udomain
41+
42+
43+
@route("/domains/{name}", tags=tags)
44+
class DomainDetailAPI:
45+
@staticmethod
46+
async def get(name, _: User = Depends(domain_perms)) -> Domain:
47+
domain = await Domain.get(name)
48+
return domain
49+
50+
@staticmethod
51+
async def delete(name, _: User = Depends(domain_perms)) -> Domain:
52+
try:
53+
rdomain = await Domain.get_rdomain(name)
54+
await rdomain.destroy()
55+
domain = await Domain.get(name)
56+
await domain.destroy()
57+
return domain
58+
except bonsai.errors.AuthenticationError:
59+
raise HTTPException(status_code=403, detail="Failed to login")

freenit/base_config.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,10 @@ def __init__(
6464
roleBase="dc=group,dc=ldap",
6565
roleClasses=["groupOfUniqueNames"],
6666
roleMemberAttr="uniqueMember",
67-
groupBase="ou={},dc=group,dc=ldap",
6867
groupDN="cn={}",
6968
groupClasses=["posixGroup"],
7069
userBase="dc=account,dc=ldap",
71-
userDN="uid={},ou={}",
70+
userDN="uid={}",
7271
userClasses=["pilotPerson", "posixAccount"],
7372
userMemberAttr="memberOf",
7473
uidNextClass="uidNext",
@@ -77,6 +76,8 @@ def __init__(
7776
gidNextClass="gidNext",
7877
gidNextDN="cn=gidnext,dc=ldap",
7978
gidNextField="gidNumber",
79+
domainDN="ou={}",
80+
domainClasses=["organizationalUnit", "pmiDelegationPath"],
8081
):
8182
self.host = host
8283
self.tls = tls
@@ -87,9 +88,9 @@ def __init__(
8788
self.roleDN = f"{roleDN},{roleBase}"
8889
self.roleMemberAttr = roleMemberAttr
8990
self.groupClasses = groupClasses
90-
self.groupDN = f"{groupDN},{groupBase}"
91+
self.groupDN = f"{groupDN},{domainDN},{roleBase}"
9192
self.userBase = userBase
92-
self.userDN = f"{userDN},{userBase}"
93+
self.userDN = f"{userDN},{domainDN},{userBase}"
9394
self.userClasses = userClasses
9495
self.userMemberAttr = userMemberAttr
9596
self.uidNextClass = uidNextClass
@@ -98,6 +99,8 @@ def __init__(
9899
self.gidNextClass = gidNextClass
99100
self.gidNextDN = gidNextDN
100101
self.gidNextField = gidNextField
102+
self.domainDN = domainDN
103+
self.domainClasses = domainClasses
101104

102105

103106
class BaseConfig:

freenit/models/ldap/domain.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from bonsai import LDAPEntry, LDAPSearchScope, errors
2+
from fastapi import HTTPException
3+
from pydantic import BaseModel, Field
4+
5+
from freenit.config import getConfig
6+
from freenit.models.ldap.base import LDAPBaseModel, get_client, save_data, class2filter
7+
8+
config = getConfig()
9+
10+
11+
class Domain(LDAPBaseModel):
12+
ou: str = Field("", description=("Domain name"))
13+
14+
@classmethod
15+
def from_entry(cls, entry):
16+
domain = cls(dn=str(entry["dn"]), ou=entry["ou"][0])
17+
return domain
18+
19+
@classmethod
20+
def create(cls, fqdn):
21+
dn = f"{config.ldap.domainDN},{config.ldap.roleBase}"
22+
rdomain = Domain(dn=dn.format(fqdn), ou=fqdn)
23+
dn = f"{config.ldap.domainDN},{config.ldap.userBase}"
24+
udomain = Domain(dn=dn.format(fqdn), ou=fqdn)
25+
return (rdomain, udomain)
26+
27+
@classmethod
28+
async def get(cls, fqdn):
29+
classes = class2filter(config.ldap.domainClasses)
30+
client = get_client()
31+
try:
32+
async with client.connect(is_async=True) as conn:
33+
dn = f"{config.ldap.domainDN},{config.ldap.userBase}"
34+
res = await conn.search(dn.format(fqdn), LDAPSearchScope.SUB, f"(|{classes})")
35+
except errors.AuthenticationError:
36+
raise HTTPException(status_code=403, detail="Failed to login")
37+
if len(res) < 1:
38+
raise HTTPException(status_code=404, detail="No such domain")
39+
if len(res) > 1:
40+
raise HTTPException(status_code=409, detail="Multiple domains found")
41+
data = res[0]
42+
domain = cls.from_entry(data)
43+
return domain
44+
45+
@classmethod
46+
async def get_rdomain(cls, fqdn):
47+
classes = class2filter(config.ldap.domainClasses)
48+
client = get_client()
49+
try:
50+
async with client.connect(is_async=True) as conn:
51+
dn = f"{config.ldap.domainDN},{config.ldap.roleBase}"
52+
res = await conn.search(dn.format(fqdn), LDAPSearchScope.SUB, f"(|{classes})")
53+
except errors.AuthenticationError:
54+
raise HTTPException(status_code=403, detail="Failed to login")
55+
if len(res) < 1:
56+
raise HTTPException(status_code=404, detail="No such domain")
57+
if len(res) > 1:
58+
raise HTTPException(status_code=409, detail="Multiple domains found")
59+
data = res[0]
60+
domain = cls.from_entry(data)
61+
return domain
62+
63+
@classmethod
64+
async def get_all(cls):
65+
classes = class2filter(config.ldap.domainClasses)
66+
client = get_client()
67+
try:
68+
async with client.connect(is_async=True) as conn:
69+
dn = config.ldap.userBase
70+
res = await conn.search(dn, LDAPSearchScope.SUB, f"(|{classes})")
71+
data = []
72+
for gdata in res:
73+
data.append(cls.from_entry(gdata))
74+
except errors.AuthenticationError:
75+
raise HTTPException(status_code=403, detail="Failed to login")
76+
return data
77+
78+
async def save(self):
79+
data = LDAPEntry(self.dn)
80+
data["objectClass"] = config.ldap.domainClasses
81+
data["ou"] = self.ou
82+
await save_data(data)
83+
84+
async def destroy(self):
85+
client = get_client()
86+
try:
87+
async with client.connect(is_async=True) as conn:
88+
await conn.delete(self.dn)
89+
except errors.AuthenticationError:
90+
raise HTTPException(status_code=403, detail="Failed to login")
91+
92+
93+
class DomainCreate(BaseModel):
94+
name: str = Field(description=("Common name"))

freenit/permissions.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from freenit.auth import permissions
22

3-
role_perms = permissions()
3+
domain_perms = permissions()
44
group_perms = permissions()
55
profile_perms = permissions()
6-
user_perms = permissions()
6+
role_perms = permissions()
77
theme_perms = permissions()
8+
user_perms = permissions()

0 commit comments

Comments
 (0)