Skip to content

Commit d205324

Browse files
authored
Feature/ldap (#121)
LDAP implementation
1 parent c47c0ea commit d205324

18 files changed

+296
-161
lines changed

bin/common.sh

+1-6
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,9 @@ setup() {
2121
fi
2222
. ${HOME}/.virtualenvs/${VIRTUALENV}/bin/activate
2323

24-
INSTALL_TARGET=".[${DB_TYPE}"
25-
if [ "${FREENIT_ENV}" != "prod" ]; then
26-
INSTALL_TARGET="${INSTALL_TARGET},${FREENIT_ENV}"
27-
fi
28-
INSTALL_TARGET="${INSTALL_TARGET}]"
2924
if [ "${1}" != "no" -a "${OFFLINE}" != "yes" ]; then
3025
${PIP_INSTALL} pip wheel
31-
${PIP_INSTALL} -e "${INSTALL_TARGET}"
26+
${PIP_INSTALL} -e ".[${DB_TYPE},${FREENIT_ENV}]"
3227
fi
3328
fi
3429

bin/devel.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/sh
22

33
BIN_DIR=`dirname $0`
4-
export FREENIT_ENV="dev"
4+
export FREENIT_ENV="dev,all"
55
export OFFLINE=${OFFLINE:="no"}
66

77

bin/freenit.sh

+6-6
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ export SED_CMD="sed -i"
4545
backend() {
4646
PROJECT_ROOT=`python${PY_VERSION} -c 'import os; import freenit; print(os.path.dirname(os.path.abspath(freenit.__file__)))'`
4747

48-
mkdir backend
49-
cd backend
48+
mkdir "${NAME}"
49+
cd "${NAME}"
5050
cp -r ${PROJECT_ROOT}/project/* .
5151
case `uname` in
5252
*BSD)
@@ -215,6 +215,7 @@ echo "========"
215215
cd "\${PROJECT_ROOT}"
216216
rm -rf build
217217
yarn run build
218+
touch build/.keep
218219
EOF
219220
chmod +x collect.sh
220221

@@ -395,8 +396,7 @@ EOF
395396

396397
svelte() {
397398
yarn create svelte "${NAME}"
398-
mv "${NAME}" frontend
399-
cd frontend
399+
cd "${NAME}"
400400
yarn install
401401
frontend_common
402402
yarn add --dev @zerodevx/svelte-toast @freenit-framework/svelte-base
@@ -677,14 +677,13 @@ services/
677677
vars.mk
678678
EOF
679679

680-
echo "DEVEL_MODE = YES" >vars.mk
681-
682680
echo "Creating services"
683681
mkdir services
684682
cd services
685683

686684
echo "Creating backend"
687685
backend
686+
mv "${NAME}" backend
688687

689688
echo "Creating frontend"
690689
FRONTEND_TYPE=${FRONTEND_TYPE:=svelte}
@@ -696,6 +695,7 @@ EOF
696695
help >&2
697696
exit 1
698697
fi
698+
mv "${NAME}" frontend
699699
cd ..
700700
}
701701

freenit/api/auth.py

+24-31
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from email.mime.text import MIMEText
22

3-
import ormar
4-
import ormar.exceptions
53
import pydantic
64
from fastapi import Header, HTTPException, Request, Response
75

@@ -36,39 +34,34 @@ class Verification(pydantic.BaseModel):
3634

3735
@api.post("/auth/login", response_model=LoginResponse, tags=["auth"])
3836
async def login(credentials: LoginInput, response: Response):
39-
try:
40-
user = await User.objects.get(email=credentials.email, active=True)
41-
valid = user.check(credentials.password)
42-
if valid:
43-
access = encode(user)
44-
refresh = encode(user)
45-
response.set_cookie(
46-
"access",
47-
access,
48-
httponly=True,
49-
secure=config.auth.secure,
50-
)
51-
response.set_cookie(
52-
"refresh",
53-
refresh,
54-
httponly=True,
55-
secure=config.auth.secure,
56-
)
57-
return {
58-
"user": user.dict(exclude={"password"}),
59-
"expire": {
60-
"access": config.auth.expire,
61-
"refresh": config.auth.refresh_expire,
62-
},
63-
}
64-
except ormar.exceptions.NoMatch:
65-
pass
66-
raise HTTPException(status_code=403, detail="Failed to login")
37+
user = await User.login(credentials)
38+
access = encode(user)
39+
refresh = encode(user)
40+
response.set_cookie(
41+
"access",
42+
access,
43+
httponly=True,
44+
secure=config.auth.secure,
45+
)
46+
response.set_cookie(
47+
"refresh",
48+
refresh,
49+
httponly=True,
50+
secure=config.auth.secure,
51+
)
52+
return {
53+
"user": user,
54+
"expire": {
55+
"access": config.auth.expire,
56+
"refresh": config.auth.refresh_expire,
57+
},
58+
}
6759

6860

6961
@api.post("/auth/register", tags=["auth"])
7062
async def register(credentials: LoginInput, host=Header(default="")):
71-
print("host", host)
63+
import ormar.exceptions
64+
7265
try:
7366
user = await User.objects.get(email=credentials.email)
7467
raise HTTPException(status_code=409, detail="User already registered")

freenit/api/user.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from freenit.api.router import route
66
from freenit.auth import encrypt
7+
from freenit.config import getConfig
78
from freenit.decorators import description
89
from freenit.models.pagination import Page, paginate
910
from freenit.models.safe import UserSafe
@@ -12,6 +13,8 @@
1213

1314
tags = ["user"]
1415

16+
config = getConfig()
17+
1518

1619
@route("/users", tags=tags)
1720
class UserListAPI:
@@ -22,7 +25,40 @@ async def get(
2225
perpage: int = Header(default=10),
2326
_: User = Depends(user_perms),
2427
) -> Page[UserSafe]:
25-
return await paginate(User.objects, page, perpage)
28+
if User.Meta.type == "ormar":
29+
return await paginate(User.objects, page, perpage)
30+
elif User.Meta.type == "bonsai":
31+
import bonsai
32+
33+
client = bonsai.LDAPClient(f"ldap://{config.ldap.host}", config.ldap.tls)
34+
try:
35+
async with client.connect(is_async=True) as conn:
36+
res = await conn.search(
37+
f"dc=account,dc=ldap",
38+
bonsai.LDAPSearchScope.SUB,
39+
"objectClass=person",
40+
)
41+
except bonsai.errors.AuthenticationError:
42+
raise HTTPException(status_code=403, detail="Failed to login")
43+
44+
data = []
45+
for udata in res:
46+
email = udata.get("mail", None)
47+
if email is None:
48+
continue
49+
user = User(
50+
email=email[0],
51+
sn=udata["sn"][0],
52+
cn=udata["cn"][0],
53+
dn=str(udata["dn"]),
54+
uid=udata["uid"][0],
55+
)
56+
data.append(user)
57+
58+
total = len(res)
59+
page = Page(total=total, page=1, pages=1, perpage=total, data=data)
60+
return page
61+
raise HTTPException(status_code=409, detail="Unknown user type")
2662

2763

2864
@route("/users/{id}", tags=tags)

freenit/auth.py

+55-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import jwt
2-
import ormar
3-
import ormar.exceptions
42
from fastapi import HTTPException, Request
53
from passlib.hash import pbkdf2_sha256
64

@@ -17,16 +15,44 @@ async def decode(token):
1715
pk = data.get("pk", None)
1816
if pk is None:
1917
raise HTTPException(status_code=403, detail="Unauthorized")
20-
try:
21-
user = await User.objects.get(pk=pk)
18+
if User.Meta.type == "ormar":
19+
import ormar
20+
import ormar.exceptions
21+
22+
try:
23+
user = await User.objects.get(pk=pk)
24+
return user
25+
except ormar.exceptions.NoMatch:
26+
raise HTTPException(status_code=403, detail="Unauthorized")
27+
elif User.Meta.type == "bonsai":
28+
import bonsai
29+
30+
client = bonsai.LDAPClient(f"ldap://{config.ldap.host}", config.ldap.tls)
31+
async with client.connect(is_async=True) as conn:
32+
res = await conn.search(
33+
pk,
34+
bonsai.LDAPSearchScope.BASE,
35+
"objectClass=person",
36+
)
37+
data = res[0]
38+
user = User(
39+
email=data["mail"][0],
40+
sn=data["sn"][0],
41+
cn=data["cn"][0],
42+
dn=str(data["dn"]),
43+
uid=data["uid"][0],
44+
)
2245
return user
23-
except ormar.exceptions.NoMatch:
24-
raise HTTPException(status_code=403, detail="Unauthorized")
46+
raise HTTPException(status_code=409, detail="Unknown user type")
2547

2648

2749
def encode(user):
2850
config = getConfig()
29-
payload = {"pk": user.pk}
51+
payload = {}
52+
if user.Meta.type == "ormar":
53+
payload = {"pk": user.pk, "type": user.Meta.type}
54+
elif user.Meta.type == "bonsai":
55+
payload = {"pk": user.dn, "type": user.Meta.type}
3056
return jwt.encode(payload, config.secret, algorithm="HS256")
3157

3258

@@ -35,27 +61,31 @@ async def authorize(request: Request, roles=[], allof=[], cookie="access"):
3561
if not token:
3662
raise HTTPException(status_code=403, detail="Unauthorized")
3763
user = await decode(token)
38-
await user.load_all()
39-
if not user.active:
40-
raise HTTPException(status_code=403, detail="Permission denied")
41-
if user.admin:
42-
return user
43-
if len(user.roles) == 0:
44-
if len(roles) > 0 or len(allof) > 0:
64+
if user.Meta.type == "ormar":
65+
await user.load_all()
66+
if not user.active:
4567
raise HTTPException(status_code=403, detail="Permission denied")
46-
else:
47-
if len(roles) > 0:
48-
found = False
49-
for role in user.roles:
50-
if role.name in roles:
51-
found = True
52-
break
53-
if not found:
68+
if user.admin:
69+
return user
70+
if len(user.roles) == 0:
71+
if len(roles) > 0 or len(allof) > 0:
5472
raise HTTPException(status_code=403, detail="Permission denied")
55-
if len(allof) > 0:
56-
for role in user.roles:
57-
if role.name not in allof:
73+
else:
74+
if len(roles) > 0:
75+
found = False
76+
for role in user.roles:
77+
if role.name in roles:
78+
found = True
79+
break
80+
if not found:
5881
raise HTTPException(status_code=403, detail="Permission denied")
82+
if len(allof) > 0:
83+
for role in user.roles:
84+
if role.name not in allof:
85+
raise HTTPException(status_code=403, detail="Permission denied")
86+
return user
87+
# elif user.Meta.type == "bonsai":
88+
# pass
5989
return user
6090

6191

freenit/base_config.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
hour = 60 * minute
1010
day = 24 * hour
1111
year = 365 * day
12+
register_message = """Hello,
13+
14+
Please confirm user registration by following this link
15+
16+
{}
17+
18+
Regards,
19+
Freenit
20+
"""
1221

1322

1423
class Auth:
@@ -28,14 +37,7 @@ def __init__(
2837
tls=True,
2938
from_addr="[email protected]",
3039
register_subject="[Freenit] User Registration",
31-
register_message="""Hello,
32-
33-
Please confirm user registration by following this link
34-
35-
{}
36-
37-
Regards,
38-
Freenit""",
40+
register_message=register_message,
3941
) -> None:
4042
self.server = server
4143
self.user = user
@@ -47,6 +49,15 @@ def __init__(
4749
self.register_message = register_message
4850

4951

52+
class LDAP:
53+
def __init__(
54+
self, host="ldap.example.com", tls=True, base="uid={},ou={},dc=account,dc=ldap"
55+
):
56+
self.host = host
57+
self.tls = tls
58+
self.base = base
59+
60+
5061
class BaseConfig:
5162
name = "Freenit"
5263
version = "0.0.1"
@@ -66,6 +77,7 @@ class BaseConfig:
6677
meta = None
6778
auth = Auth()
6879
mail = Mail()
80+
ldap = LDAP()
6981

7082
def __init__(self):
7183
self.database = databases.Database(self.dburl)

freenit/models/ldap/__init__.py

Whitespace-only changes.

freenit/models/ldap/base.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import Generic, TypeVar
2+
3+
from pydantic import EmailStr, Field, generics
4+
5+
T = TypeVar("T")
6+
7+
8+
class LDAPBaseModel(generics.GenericModel, Generic[T]):
9+
class Meta:
10+
type = "bonsai"
11+
12+
dn: str = Field("", description=("Distinguished name"))
13+
14+
15+
class LDAPUserMixin:
16+
uid: str = Field("", description=("User ID"))
17+
email: EmailStr = Field("", description=("Email"))
18+
cn: str = Field("", description=("Common name"))
19+
sn: str = Field("", description=("Surname"))

0 commit comments

Comments
 (0)