Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/ldap #121

Merged
merged 4 commits into from
Aug 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions bin/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,9 @@ setup() {
fi
. ${HOME}/.virtualenvs/${VIRTUALENV}/bin/activate

INSTALL_TARGET=".[${DB_TYPE}"
if [ "${FREENIT_ENV}" != "prod" ]; then
INSTALL_TARGET="${INSTALL_TARGET},${FREENIT_ENV}"
fi
INSTALL_TARGET="${INSTALL_TARGET}]"
if [ "${1}" != "no" -a "${OFFLINE}" != "yes" ]; then
${PIP_INSTALL} pip wheel
${PIP_INSTALL} -e "${INSTALL_TARGET}"
${PIP_INSTALL} -e ".[${DB_TYPE},${FREENIT_ENV}]"
fi
fi

Expand Down
2 changes: 1 addition & 1 deletion bin/devel.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/sh

BIN_DIR=`dirname $0`
export FREENIT_ENV="dev"
export FREENIT_ENV="dev,all"
export OFFLINE=${OFFLINE:="no"}


Expand Down
12 changes: 6 additions & 6 deletions bin/freenit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export SED_CMD="sed -i"
backend() {
PROJECT_ROOT=`python${PY_VERSION} -c 'import os; import freenit; print(os.path.dirname(os.path.abspath(freenit.__file__)))'`

mkdir backend
cd backend
mkdir "${NAME}"
cd "${NAME}"
cp -r ${PROJECT_ROOT}/project/* .
case `uname` in
*BSD)
Expand Down Expand Up @@ -215,6 +215,7 @@ echo "========"
cd "\${PROJECT_ROOT}"
rm -rf build
yarn run build
touch build/.keep
EOF
chmod +x collect.sh

Expand Down Expand Up @@ -395,8 +396,7 @@ EOF

svelte() {
yarn create svelte "${NAME}"
mv "${NAME}" frontend
cd frontend
cd "${NAME}"
yarn install
frontend_common
yarn add --dev @zerodevx/svelte-toast @freenit-framework/svelte-base
Expand Down Expand Up @@ -677,14 +677,13 @@ services/
vars.mk
EOF

echo "DEVEL_MODE = YES" >vars.mk

echo "Creating services"
mkdir services
cd services

echo "Creating backend"
backend
mv "${NAME}" backend

echo "Creating frontend"
FRONTEND_TYPE=${FRONTEND_TYPE:=svelte}
Expand All @@ -696,6 +695,7 @@ EOF
help >&2
exit 1
fi
mv "${NAME}" frontend
cd ..
}

Expand Down
55 changes: 24 additions & 31 deletions freenit/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from email.mime.text import MIMEText

import ormar
import ormar.exceptions
import pydantic
from fastapi import Header, HTTPException, Request, Response

Expand Down Expand Up @@ -36,39 +34,34 @@ class Verification(pydantic.BaseModel):

@api.post("/auth/login", response_model=LoginResponse, tags=["auth"])
async def login(credentials: LoginInput, response: Response):
try:
user = await User.objects.get(email=credentials.email, active=True)
valid = user.check(credentials.password)
if valid:
access = encode(user)
refresh = encode(user)
response.set_cookie(
"access",
access,
httponly=True,
secure=config.auth.secure,
)
response.set_cookie(
"refresh",
refresh,
httponly=True,
secure=config.auth.secure,
)
return {
"user": user.dict(exclude={"password"}),
"expire": {
"access": config.auth.expire,
"refresh": config.auth.refresh_expire,
},
}
except ormar.exceptions.NoMatch:
pass
raise HTTPException(status_code=403, detail="Failed to login")
user = await User.login(credentials)
access = encode(user)
refresh = encode(user)
response.set_cookie(
"access",
access,
httponly=True,
secure=config.auth.secure,
)
response.set_cookie(
"refresh",
refresh,
httponly=True,
secure=config.auth.secure,
)
return {
"user": user,
"expire": {
"access": config.auth.expire,
"refresh": config.auth.refresh_expire,
},
}


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

try:
user = await User.objects.get(email=credentials.email)
raise HTTPException(status_code=409, detail="User already registered")
Expand Down
38 changes: 37 additions & 1 deletion freenit/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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
Expand All @@ -12,6 +13,8 @@

tags = ["user"]

config = getConfig()


@route("/users", tags=tags)
class UserListAPI:
Expand All @@ -22,7 +25,40 @@ async def get(
perpage: int = Header(default=10),
_: User = Depends(user_perms),
) -> Page[UserSafe]:
return await paginate(User.objects, page, perpage)
if User.Meta.type == "ormar":
return await paginate(User.objects, page, perpage)
elif User.Meta.type == "bonsai":
import bonsai

client = bonsai.LDAPClient(f"ldap://{config.ldap.host}", config.ldap.tls)
try:
async with client.connect(is_async=True) as conn:
res = await conn.search(
f"dc=account,dc=ldap",
bonsai.LDAPSearchScope.SUB,
"objectClass=person",
)
except bonsai.errors.AuthenticationError:
raise HTTPException(status_code=403, detail="Failed to login")

data = []
for udata in res:
email = udata.get("mail", None)
if email is None:
continue
user = User(
email=email[0],
sn=udata["sn"][0],
cn=udata["cn"][0],
dn=str(udata["dn"]),
uid=udata["uid"][0],
)
data.append(user)

total = len(res)
page = Page(total=total, page=1, pages=1, perpage=total, data=data)
return page
raise HTTPException(status_code=409, detail="Unknown user type")


@route("/users/{id}", tags=tags)
Expand Down
80 changes: 55 additions & 25 deletions freenit/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import jwt
import ormar
import ormar.exceptions
from fastapi import HTTPException, Request
from passlib.hash import pbkdf2_sha256

Expand All @@ -17,16 +15,44 @@ async def decode(token):
pk = data.get("pk", None)
if pk is None:
raise HTTPException(status_code=403, detail="Unauthorized")
try:
user = await User.objects.get(pk=pk)
if User.Meta.type == "ormar":
import ormar
import ormar.exceptions

try:
user = await User.objects.get(pk=pk)
return user
except ormar.exceptions.NoMatch:
raise HTTPException(status_code=403, detail="Unauthorized")
elif User.Meta.type == "bonsai":
import bonsai

client = bonsai.LDAPClient(f"ldap://{config.ldap.host}", config.ldap.tls)
async with client.connect(is_async=True) as conn:
res = await conn.search(
pk,
bonsai.LDAPSearchScope.BASE,
"objectClass=person",
)
data = res[0]
user = User(
email=data["mail"][0],
sn=data["sn"][0],
cn=data["cn"][0],
dn=str(data["dn"]),
uid=data["uid"][0],
)
return user
except ormar.exceptions.NoMatch:
raise HTTPException(status_code=403, detail="Unauthorized")
raise HTTPException(status_code=409, detail="Unknown user type")


def encode(user):
config = getConfig()
payload = {"pk": user.pk}
payload = {}
if user.Meta.type == "ormar":
payload = {"pk": user.pk, "type": user.Meta.type}
elif user.Meta.type == "bonsai":
payload = {"pk": user.dn, "type": user.Meta.type}
return jwt.encode(payload, config.secret, algorithm="HS256")


Expand All @@ -35,27 +61,31 @@ async def authorize(request: Request, roles=[], allof=[], cookie="access"):
if not token:
raise HTTPException(status_code=403, detail="Unauthorized")
user = await decode(token)
await user.load_all()
if not user.active:
raise HTTPException(status_code=403, detail="Permission denied")
if user.admin:
return user
if len(user.roles) == 0:
if len(roles) > 0 or len(allof) > 0:
if user.Meta.type == "ormar":
await user.load_all()
if not user.active:
raise HTTPException(status_code=403, detail="Permission denied")
else:
if len(roles) > 0:
found = False
for role in user.roles:
if role.name in roles:
found = True
break
if not found:
if user.admin:
return user
if len(user.roles) == 0:
if len(roles) > 0 or len(allof) > 0:
raise HTTPException(status_code=403, detail="Permission denied")
if len(allof) > 0:
for role in user.roles:
if role.name not in allof:
else:
if len(roles) > 0:
found = False
for role in user.roles:
if role.name in roles:
found = True
break
if not found:
raise HTTPException(status_code=403, detail="Permission denied")
if len(allof) > 0:
for role in user.roles:
if role.name not in allof:
raise HTTPException(status_code=403, detail="Permission denied")
return user
# elif user.Meta.type == "bonsai":
# pass
return user


Expand Down
28 changes: 20 additions & 8 deletions freenit/base_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
hour = 60 * minute
day = 24 * hour
year = 365 * day
register_message = """Hello,

Please confirm user registration by following this link

{}

Regards,
Freenit
"""


class Auth:
Expand All @@ -28,14 +37,7 @@ def __init__(
tls=True,
from_addr="[email protected]",
register_subject="[Freenit] User Registration",
register_message="""Hello,

Please confirm user registration by following this link

{}

Regards,
Freenit""",
register_message=register_message,
) -> None:
self.server = server
self.user = user
Expand All @@ -47,6 +49,15 @@ def __init__(
self.register_message = register_message


class LDAP:
def __init__(
self, host="ldap.example.com", tls=True, base="uid={},ou={},dc=account,dc=ldap"
):
self.host = host
self.tls = tls
self.base = base


class BaseConfig:
name = "Freenit"
version = "0.0.1"
Expand All @@ -66,6 +77,7 @@ class BaseConfig:
meta = None
auth = Auth()
mail = Mail()
ldap = LDAP()

def __init__(self):
self.database = databases.Database(self.dburl)
Expand Down
Empty file added freenit/models/ldap/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions freenit/models/ldap/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Generic, TypeVar

from pydantic import EmailStr, Field, generics

T = TypeVar("T")


class LDAPBaseModel(generics.GenericModel, Generic[T]):
class Meta:
type = "bonsai"

dn: str = Field("", description=("Distinguished name"))


class LDAPUserMixin:
uid: str = Field("", description=("User ID"))
email: EmailStr = Field("", description=("Email"))
cn: str = Field("", description=("Common name"))
sn: str = Field("", description=("Surname"))
Loading
Loading