Skip to content

Commit 4939445

Browse files
authored
Merge pull request #178 from mekanix/admin
Put "admin" and "active" classes in their fields
2 parents 874a62c + aa388bf commit 4939445

File tree

7 files changed

+156
-52
lines changed

7 files changed

+156
-52
lines changed

alembic.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ prepend_sys_path = .
4141

4242
#sqlalchemy.url = sqlite:///db.sqlite
4343

44+
path_separator = os
45+
4446

4547
[post_write_hooks]
4648
# post_write_hooks defines scripts or Python functions that are run

freenit/__init__.py

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

freenit/api/role/ldap_group.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,16 @@ async def delete(domain, name) -> Group:
6262
class GroupUserAPI:
6363
@staticmethod
6464
@description("Assign user to group")
65-
async def post(domain, name, id, _: User = Depends(group_perms)) -> Group:
66-
user = await User.get_by_uid(id)
65+
async def post(domain, name, uid, _: User = Depends(group_perms)) -> Group:
66+
user = await User.get_by_uid(uid)
6767
group = await Group.get(name, domain)
6868
await group.add(user)
6969
return group
7070

7171
@staticmethod
7272
@description("Remove user from group")
73-
async def delete(domain, name, id, _: User = Depends(group_perms)) -> Group:
74-
user = await User.get_by_uid(id)
73+
async def delete(domain, name, uid, _: User = Depends(group_perms)) -> Group:
74+
user = await User.get_by_uid(uid)
7575
group = await Group.get(name, domain)
7676
await group.remove(user)
7777
return group

freenit/api/user/ldap.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ async def get(id, _: User = Depends(user_perms)) -> UserSafe:
3838

3939
@staticmethod
4040
async def patch(id, data: UserOptional, _: User = Depends(user_perms)) -> UserSafe:
41-
user = await User.get(id)
41+
user = await User.get_by_uid(id)
4242
update = {
4343
field: getattr(data, field)
4444
for field in data.__fields__
4545
if getattr(data, field) != ""
4646
}
47-
await user.update(active=user.userClass, **update)
47+
await user.update(**update)
4848
return user
4949

5050
@staticmethod
@@ -74,5 +74,5 @@ async def patch(
7474
for field in data.__fields__
7575
if getattr(data, field) != ""
7676
}
77-
await user.update(active=user.userClass, **update)
77+
await user.update(**update)
7878
return user

freenit/auth.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,24 @@ async def authorize(request: Request, roles=[], allof=[], cookie="access"):
6868
raise HTTPException(status_code=403, detail="Permission denied")
6969
return user
7070
elif user.dbtype() == "ldap":
71-
pass
71+
from freenit.models.ldap.base import get_client, class2filter
72+
from bonsai import LDAPSearchScope, errors
73+
74+
config = getConfig()
75+
_, domain = user.email.split("@")
76+
classes = class2filter(config.ldap.groupClasses)
77+
dn = f"{config.ldap.domainDN.format(domain)},{config.ldap.roleBase}"
78+
client = get_client()
79+
async with client.connect(is_async=True) as conn:
80+
try:
81+
res = await conn.search(
82+
dn,
83+
LDAPSearchScope.SUB,
84+
f"(&{classes}(memberUid={user.uidNumber}))",
85+
)
86+
except errors.AuthenticationError:
87+
raise HTTPException(status_code=403, detail="Failed to login")
88+
user.groups = [g["gidNumber"][0] for g in res]
7289
return user
7390

7491

freenit/models/ldap/group.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def from_entry(cls, entry):
1717
group = cls(
1818
cn=entry["cn"][0],
1919
dn=str(entry["dn"]),
20-
users=entry["memberUid"],
20+
users=entry.get("memberUid", []),
2121
)
2222
return group
2323

@@ -85,6 +85,8 @@ async def add(self, user):
8585
raise HTTPException(status_code=409, detail="Multiple groups found")
8686
data = res[0]
8787
try:
88+
if "memberUid" not in data:
89+
data["memberUid"] = []
8890
data["memberUid"].append(user.uidNumber)
8991
except ValueError:
9092
raise HTTPException(

freenit/models/ldap/user.py

Lines changed: 125 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ class UserSafe(LDAPBaseModel):
2222
email: EmailStr = Field("", description=("Email"))
2323
cn: str = Field("", description=("Common name"))
2424
sn: str = Field("", description=("Surname"))
25-
userClass: str = Field("", description=("User class"))
25+
userClass: list[str] = Field([], description=("User class"))
2626
roles: list = Field([], description=("Roles the user is a member of"))
27-
uidNumber: int = Field(0, description=("UID"))
28-
gidNumber: int = Field(0, description=("GID"))
27+
groups: list = Field([], description=("Groups the user is a member of"))
28+
uidNumber: int = Field(0, description=("User ID number"))
29+
gidNumber: int = Field(0, description=("Group ID number"))
30+
active: bool = Field(False, description=("Active user"))
31+
admin: bool = Field(False, description=("Admin user"))
2932

3033
@classmethod
3134
async def _login(cls, credentials) -> dict:
@@ -36,17 +39,37 @@ async def _login(cls, credentials) -> dict:
3639
dn = config.ldap.userDN.format(username, domain)
3740
res = await conn.search(dn, LDAPSearchScope.BASE, "objectClass=person")
3841
except errors.ConnectionError:
39-
raise HTTPException(status_code=409, detail="Can not connect to LDAP server")
42+
raise HTTPException(
43+
status_code=409, detail="Can not connect to LDAP server"
44+
)
4045
except errors.AuthenticationError:
4146
raise HTTPException(status_code=403, detail="Failed to login")
4247
data = res[0]
4348
return data
4449

50+
async def _fill_groups(self):
51+
_, domain = self.email.split('@')
52+
classes = class2filter(config.ldap.groupClasses)
53+
dn = f"{config.ldap.domainDN.format(domain)},{config.ldap.roleBase}"
54+
client = get_client()
55+
async with client.connect(is_async=True) as conn:
56+
try:
57+
res = await conn.search(
58+
dn,
59+
LDAPSearchScope.SUB,
60+
f"(&{classes}(memberUid={self.uidNumber}))",
61+
)
62+
except errors.AuthenticationError:
63+
raise HTTPException(status_code=403, detail="Failed to login")
64+
self.groups = [g["gidNumber"][0] for g in res]
65+
4566
@classmethod
4667
async def login(cls, credentials):
4768
data = await cls._login(credentials)
4869
user = cls.from_entry(data)
49-
return user
70+
if user.active:
71+
return user
72+
raise HTTPException(status_code=403, detail="Failed to login")
5073

5174
@classmethod
5275
async def register(cls, credentials):
@@ -61,32 +84,49 @@ async def register(cls, credentials):
6184
except errors.UnwillingToPerform:
6285
raise HTTPException(status_code=409, detail="Can not bind to LDAP")
6386
except errors.AuthenticationError:
64-
raise HTTPException(status_code=409, detail="Can not bind to LDAP")
87+
raise HTTPException(
88+
status_code=409, detail="Can not login as service to LDAP"
89+
)
6590
user = cls(
6691
dn=dn,
6792
cn="Common Name",
6893
sn="Surname",
6994
uid=username,
7095
email=credentials.email,
71-
userClass="disabled",
96+
userClass=[],
7297
uidNumber=65535,
7398
gidNumber=65535,
7499
roles=[],
100+
groups=[],
101+
active=False,
102+
admin=False,
75103
)
76104
return user
77105

78106
@classmethod
79-
def from_entry(cls, entry) -> UserSafe:
107+
def from_entry(cls, entry, groups=[]) -> UserSafe:
108+
active = False
109+
admin = False
110+
userClass = entry.get("userClass", [])
111+
if "active" in userClass:
112+
userClass.remove("active")
113+
active = True
114+
if "admin" in userClass:
115+
userClass.remove("admin")
116+
admin = True
80117
user = cls(
81118
email=entry["mail"][0],
82119
sn=entry["sn"][0],
83120
cn=entry["cn"][0],
84121
dn=str(entry["dn"]),
85122
uid=entry["uid"][0],
86-
userClass=entry["userClass"][0],
123+
userClass=userClass,
87124
roles=entry.get("memberOf", []),
125+
groups=groups,
88126
uidNumber=entry["uidNumber"][0],
89127
gidNumber=entry["gidNumber"][0],
128+
active=active,
129+
admin=admin,
90130
)
91131
return user
92132

@@ -107,6 +147,7 @@ async def get_all(cls):
107147
data = []
108148
for udata in res:
109149
user = cls.from_entry(udata)
150+
await user._fill_groups()
110151
data.append(user)
111152
return data
112153

@@ -131,26 +172,28 @@ async def get(cls, dn):
131172
raise HTTPException(status_code=409, detail="Multiple users found")
132173
data = res[0]
133174
user = cls.from_entry(data)
175+
await user._fill_groups()
134176
return user
135177

136178
@classmethod
137179
async def get_by_uid(cls, uid: int):
138180
client = get_client()
139-
try:
140-
async with client.connect(is_async=True) as conn:
181+
async with client.connect(is_async=True) as conn:
182+
try:
141183
res = await conn.search(
142184
config.ldap.userBase,
143185
LDAPSearchScope.SUB,
144186
f"(&(objectClass=person)(uidNumber={uid}))",
145187
["*", config.ldap.userMemberAttr],
146188
)
147-
except errors.AuthenticationError:
148-
raise HTTPException(status_code=403, detail="Failed to login")
149-
if len(res) < 1:
150-
raise HTTPException(status_code=404, detail="No such user")
151-
if len(res) > 1:
152-
raise HTTPException(status_code=409, detail="Multiple users found")
189+
except errors.AuthenticationError:
190+
raise HTTPException(status_code=403, detail="Failed to login")
191+
if len(res) < 1:
192+
raise HTTPException(status_code=404, detail="No such user")
193+
if len(res) > 1:
194+
raise HTTPException(status_code=409, detail="Multiple users found")
153195
user = cls.from_entry(res[0])
196+
await user._fill_groups()
154197
return user
155198

156199
@classmethod
@@ -173,6 +216,7 @@ async def get_by_email(cls, email):
173216
if len(res) > 1:
174217
raise HTTPException(status_code=409, detail="Multiple users found")
175218
user = cls.from_entry(res[0])
219+
await user._fill_groups()
176220
return user
177221

178222

@@ -191,6 +235,11 @@ async def save(self):
191235
data["memberUid"] = uidNext
192236
await save_data(data)
193237

238+
userClass = self.userClass.copy()
239+
if self.active:
240+
userClass.append("active")
241+
if self.admin:
242+
userClass.append("admin")
194243
data = LDAPEntry(self.dn)
195244
data["objectClass"] = config.ldap.userClasses
196245
data["uid"] = self.uid
@@ -212,44 +261,78 @@ async def save(self):
212261
async with client.connect(is_async=True) as conn:
213262
await conn.modify_password(self.dn, self.password)
214263

215-
async def update(self, active=False, **kwargs):
264+
async def update(
265+
self,
266+
active=None,
267+
admin=None,
268+
password=None,
269+
roles=None,
270+
userClass=None,
271+
**kwargs,
272+
):
216273
client = get_client()
217-
userClass = "disabled"
218-
if active:
219-
userClass = "enabled"
220-
special = {"password", "roles"}
221-
filtered = {x: kwargs[x] for x in kwargs if x not in special}
222274
async with client.connect(is_async=True) as conn:
223275
res = await conn.search(self.dn, LDAPSearchScope.BASE)
224276
data = res[0]
225-
if len(filtered.keys()) > 0:
226-
for field in filtered:
227-
data[field] = filtered[field]
228-
password = kwargs.get("password", None)
277+
for field in kwargs:
278+
if field == 'uidNumber' or field == 'gidNumber':
279+
if kwargs[field] == 0:
280+
continue
281+
data[field] = kwargs[field]
282+
uclass = self.userClass.copy()
283+
if self.active:
284+
uclass.append('active')
285+
if self.admin:
286+
uclass.append('admin')
287+
if active is not None:
288+
if active:
289+
if 'active' not in uclass:
290+
uclass.append('active')
291+
self.active = True
292+
else:
293+
if 'active' in uclass:
294+
uclass.remove('active')
295+
self.active = False
296+
if admin is not None:
297+
if admin:
298+
if 'admin' not in uclass:
299+
uclass.append('admin')
300+
self.admin = True
301+
else:
302+
if 'admin' in uclass:
303+
uclass.remove('admin')
304+
self.admin = False
305+
data["userClass"] = uclass
306+
await data.modify()
229307
if password is not None:
230308
await conn.modify_password(self.dn, password)
231-
data["userClass"] = userClass
232-
data.change_attribute("userClass", LDAPModOp.REPLACE, userClass)
233-
self.userClass = userClass
234-
await data.modify()
235-
for field in filtered:
236-
setattr(self, field, filtered[field])
309+
for field in kwargs:
310+
if field == 'uidNumber' or field == 'gidNumber':
311+
if kwargs[field] == 0:
312+
continue
313+
setattr(self, field, kwargs[field])
237314

238315
async def destroy(self):
239316
client = get_client()
240317
async with client.connect(is_async=True) as conn:
318+
classes = class2filter(config.ldap.roleClasses)
319+
filter_exp = f"(&(uniqueMember={self.dn}){classes})"
320+
res = await conn.search(
321+
config.ldap.roleBase, LDAPSearchScope.SUB, filter_exp
322+
)
323+
for role in res:
324+
if len(role["uniqueMember"]) == 1:
325+
raise ValueError(
326+
f"Can not destroy user as it is only member of role {role.cn}!"
327+
)
241328
classes = class2filter(config.ldap.groupClasses)
242-
filter_exp=f"(&(memberUid={self.uidNumber}){classes})"
243-
res = await conn.search(config.ldap.roleBase, LDAPSearchScope.SUB, filter_exp)
329+
filter_exp = f"(&(memberUid={self.uidNumber}){classes})"
330+
res = await conn.search(
331+
config.ldap.roleBase, LDAPSearchScope.SUB, filter_exp
332+
)
244333
for group in res:
245-
if len(group['memberUid']):
334+
if len(group["memberUid"]) == 1:
246335
await group.delete()
247-
classes = class2filter(config.ldap.roleClasses)
248-
filter_exp=f"(&(uniqueMember={self.dn}){classes})"
249-
res = await conn.search(config.ldap.roleBase, LDAPSearchScope.SUB, filter_exp)
250-
for role in res:
251-
if len(role['uniqueMember']):
252-
raise ValueError(f"Can not destroy user as it is only member of role {role.cn}!")
253336
res = await conn.search(self.dn, LDAPSearchScope.BASE)
254337
data = res[0]
255338
await data.delete()

0 commit comments

Comments
 (0)