make apps abstract
This commit is contained in:
parent
b0112e05b5
commit
104a576908
26 changed files with 342 additions and 99 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
||||||
env
|
env
|
||||||
__pycache__
|
.env
|
||||||
|
__pycache__
|
||||||
|
.vscode/
|
||||||
|
|
15
Makefile
15
Makefile
|
@ -5,6 +5,7 @@ init:
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
|
|
||||||
up:
|
up:
|
||||||
|
docker context use default
|
||||||
docker-compose -f compose.authentik.yml up -d
|
docker-compose -f compose.authentik.yml up -d
|
||||||
docker-compose -f compose.wekan.yml up -d
|
docker-compose -f compose.wekan.yml up -d
|
||||||
|
|
||||||
|
@ -15,13 +16,15 @@ down:
|
||||||
run:
|
run:
|
||||||
./env/bin/uvicorn app.main:app --reload --host 0.0.0.0
|
./env/bin/uvicorn app.main:app --reload --host 0.0.0.0
|
||||||
|
|
||||||
run integration:
|
|
||||||
./env/bin/uvicorn app.main:app --reload --host 0.0.0.0
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
./env/bin/pytest app
|
./env/bin/pytest app
|
||||||
|
|
||||||
|
integration:
|
||||||
|
./env/bin/uvicorn app.main:app --reload --host 0.0.0.0
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
./env/bin/pip freeze -l > requirements.txt
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: init run test
|
||||||
|
|
||||||
.PHONY: init run test
|
|
||||||
|
|
20
README.md
20
README.md
|
@ -16,6 +16,26 @@ make down
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# notes
|
||||||
|
|
||||||
|
**Provider:** a leading system with userdata and a notification channel integrate can connect with (e.g. Authentik)
|
||||||
|
**Apps:** Have API that integrate can interact with and e.g. can create user (e.g. Nextcloud, Wekan)
|
||||||
|
|
||||||
|
|
||||||
|
## challenges
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* How to handle errors in connected Apps
|
||||||
|
|
||||||
|
* Apps can have different unique constrains than the Provider
|
||||||
|
e.g. Wekan requires a unique Mail addr and authentik doesn't
|
||||||
|
the specifig module for the apps api needs to know how to handle these errors
|
||||||
|
|
||||||
|
* User in App could already exist
|
||||||
|
*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
https://pydantic-docs.helpmanual.io/
|
https://pydantic-docs.helpmanual.io/
|
||||||
https://jsontopydantic.com/
|
https://jsontopydantic.com/
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .wekan import main
|
||||||
|
from .nextcloud import main
|
|
@ -3,16 +3,19 @@ from typing import Dict, Optional
|
||||||
import requests
|
import requests
|
||||||
from requests import Request
|
from requests import Request
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
|
from app.authentik.settings import AuthentikSettings
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
logging = structlog.get_logger()
|
logging = structlog.get_logger()
|
||||||
|
|
||||||
|
|
||||||
class Authentik:
|
class Authentik:
|
||||||
|
|
||||||
def __init__(self, token, base):
|
def __init__(self, settings: AuthentikSettings):
|
||||||
self.base = f"{base}api/v3/"
|
self.base = f"{settings.baseurl}api/v3/"
|
||||||
self.token = token
|
self.token = settings.token
|
||||||
self.headers = {"Authorization": f"Bearer {token}"}
|
self.headers = {"Authorization": f"Bearer {self.token}"}
|
||||||
self.hook_endpoint = None
|
self.hook_endpoint = None
|
||||||
self.property_mapping = None
|
self.property_mapping = None
|
||||||
self.event_matcher_policy = None
|
self.event_matcher_policy = None
|
||||||
|
@ -22,9 +25,9 @@ class Authentik:
|
||||||
self.event_rule_link = None
|
self.event_rule_link = None
|
||||||
|
|
||||||
# check connection
|
# check connection
|
||||||
r= requests.get(url=f"{self.base}/admin/version", headers=self.headers)
|
r = requests.get(
|
||||||
|
url=f"{self.base}/admin/version", headers=self.headers)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def get(self, endpoint: str, params: Dict = {}) -> Request:
|
def get(self, endpoint: str, params: Dict = {}) -> Request:
|
||||||
return requests.get(url=f"{self.base}{endpoint}", params=params, headers=self.headers)
|
return requests.get(url=f"{self.base}{endpoint}", params=params, headers=self.headers)
|
||||||
|
@ -57,8 +60,11 @@ class Authentik:
|
||||||
"webhook_mapping": property_mapping_pk,
|
"webhook_mapping": property_mapping_pk,
|
||||||
"webhook_url": hook_endpoint
|
"webhook_url": hook_endpoint
|
||||||
}
|
}
|
||||||
print(data)
|
|
||||||
# TODO: add check if model with same name already exists
|
for result in self.get(url).json()["results"]:
|
||||||
|
if result["name"] == "my hook":
|
||||||
|
raise Exception("Event Rule already exists")
|
||||||
|
|
||||||
r: Request = self.post(url, data)
|
r: Request = self.post(url, data)
|
||||||
if r.status_code == 201:
|
if r.status_code == 201:
|
||||||
return r.json()
|
return r.json()
|
||||||
|
@ -74,7 +80,11 @@ class Authentik:
|
||||||
event_transport
|
event_transport
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
# TODO: add check if model with same name already exists
|
|
||||||
|
for result in self.get(url).json()["results"]:
|
||||||
|
if result["name"] == "event-rule":
|
||||||
|
raise Exception("Event Rule already exists")
|
||||||
|
|
||||||
r = self.post(url, data)
|
r = self.post(url, data)
|
||||||
if r.status_code == 201:
|
if r.status_code == 201:
|
||||||
return r.json()
|
return r.json()
|
||||||
|
@ -143,8 +153,6 @@ class Authentik:
|
||||||
return User(**r)
|
return User(**r)
|
||||||
raise Exception(r)
|
raise Exception(r)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_user(self, user: User) -> Optional[User]:
|
def get_user(self, user: User) -> Optional[User]:
|
||||||
if user.pk:
|
if user.pk:
|
||||||
r = self.get(f"core/users/{user.pk}").json()
|
r = self.get(f"core/users/{user.pk}").json()
|
||||||
|
|
|
@ -1,15 +1,5 @@
|
||||||
from pydantic import BaseSettings, Field
|
from pydantic import BaseSettings, Field
|
||||||
|
|
||||||
|
|
||||||
class WekanSettings(BaseSettings):
|
|
||||||
baseurl: str = ""
|
|
||||||
user: str = ""
|
|
||||||
password: str = ""
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_file = '.env'
|
|
||||||
env_prefix = 'WEKAN_'
|
|
||||||
|
|
||||||
class AuthentikSettings(BaseSettings):
|
class AuthentikSettings(BaseSettings):
|
||||||
baseurl: str = ""
|
baseurl: str = ""
|
||||||
token: str = ""
|
token: str = ""
|
|
@ -13,6 +13,24 @@ def api() -> Authentik:
|
||||||
pytest.skip("API not reachable? Did you start docker-compose?")
|
pytest.skip("API not reachable? Did you start docker-compose?")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_event_transport_already_exists(api: Authentik):
|
||||||
|
exception = None
|
||||||
|
try:
|
||||||
|
api.create_event_transport("/","asd")
|
||||||
|
except Exception as e:
|
||||||
|
exception = e
|
||||||
|
assert not exception == None # TODO create exception types
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_event_rule_already_exists(api: Authentik):
|
||||||
|
exception = None
|
||||||
|
try:
|
||||||
|
api.create_event_rule({"pk" : "asd"},"webhook")
|
||||||
|
except Exception as e:
|
||||||
|
exception = e
|
||||||
|
assert not exception == None # TODO create exception types
|
||||||
|
|
||||||
def test_get_user_by_username(api: Authentik):
|
def test_get_user_by_username(api: Authentik):
|
||||||
u = User(username="akadmin",
|
u = User(username="akadmin",
|
||||||
name="",
|
name="",
|
||||||
|
@ -42,3 +60,5 @@ def test_create_user(api: Authentik):
|
||||||
c = api.create_user(u)
|
c = api.create_user(u)
|
||||||
assert u.username == c.username
|
assert u.username == c.username
|
||||||
assert not c.pk == None
|
assert not c.pk == None
|
||||||
|
|
||||||
|
assert api.delete_user(c) == True
|
0
app/dependencies.py
Normal file
0
app/dependencies.py
Normal file
|
@ -1,10 +1,15 @@
|
||||||
|
import structlog
|
||||||
|
from typing import List
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Depends
|
||||||
from app.authentik.api import Authentik
|
from app.authentik.api import Authentik
|
||||||
from app.authentik.models import User
|
from app.authentik.models import User
|
||||||
from app.wekan.api import Wekan
|
from app.sink import Sink
|
||||||
from app.settings import AuthentikSettings, WekanSettings
|
from app.wekan.api import WekanApi
|
||||||
|
from app.authentik.settings import AuthentikSettings
|
||||||
|
from app.wekan.main import WekanSink
|
||||||
|
|
||||||
|
logging = structlog.get_logger()
|
||||||
|
|
||||||
class Authentik_Hook_Model(BaseModel):
|
class Authentik_Hook_Model(BaseModel):
|
||||||
pk: str
|
pk: str
|
||||||
|
@ -20,29 +25,30 @@ class Http_request(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
authentikSettings = AuthentikSettings()
|
authentikSettings = AuthentikSettings()
|
||||||
wekanSettings = WekanSettings()
|
|
||||||
|
|
||||||
|
|
||||||
class EventController:
|
class EventController:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, authentik_api: Authentik):
|
||||||
try:
|
# try:
|
||||||
self.authentik = Authentik(
|
self._authentik = authentik_api
|
||||||
authentikSettings.token, authentikSettings.baseurl)
|
self._sinks = []
|
||||||
self.wekan = Wekan(wekanSettings.baseurl,
|
for sc in Sink.__subclasses__():
|
||||||
wekanSettings.user, wekanSettings.password)
|
obj = sc()
|
||||||
except Exception as e:
|
self._sinks.append(obj)
|
||||||
raise Exception("Failed to init Api", e)
|
|
||||||
|
|
||||||
|
# except Exception as e:
|
||||||
|
# raise Exception("Failed to init Api", e)
|
||||||
self.jobs = []
|
self.jobs = []
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def register_api(self, authentik: Authentik, wekan: Wekan):
|
def register_api(self, authentik: Authentik, sinks: List[Sink]):
|
||||||
self.authentik = authentik
|
self._authentik = authentik
|
||||||
self.wekan = wekan
|
self._sinks = sinks
|
||||||
|
|
||||||
def handle_model_created_event(self, model: Authentik_Hook_Model):
|
def handle_model_created_event(self, model: Authentik_Hook_Model):
|
||||||
user: User = self.authentik.get_user_by_pk(model.pk)
|
user: User = self._authentik.get_user_by_pk(model.pk)
|
||||||
if not self.wekan.get_user(user.name):
|
for sink in self._sinks:
|
||||||
self.wekan.create_user(
|
logging.info(f"Creating User {user.username} in {sink.__class__}")
|
||||||
username=user.username, email=user.email, password="")
|
sink.create_user(user)
|
||||||
return True
|
return True
|
||||||
|
|
19
app/main.py
19
app/main.py
|
@ -1,34 +1,29 @@
|
||||||
import logging
|
|
||||||
import structlog
|
import structlog
|
||||||
from unicodedata import name
|
from unicodedata import name
|
||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
from fastapi import Depends, FastAPI, Request, BackgroundTasks
|
from fastapi import Depends, FastAPI, Request, BackgroundTasks
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from app.sink import BaseUser, Sink
|
||||||
from app.authentik.api import Authentik
|
from app.authentik.api import Authentik
|
||||||
from app.authentik.models import User
|
from app.authentik.models import User
|
||||||
from app.event_controller import Authentik_Hook_Model, EventController, Http_request
|
from app.event_controller import Authentik_Hook_Model, EventController, Http_request
|
||||||
from app.settings import AuthentikSettings
|
from app.authentik.settings import AuthentikSettings
|
||||||
from .wekan.api import Wekan
|
from .wekan.api import WekanApi
|
||||||
|
from .routers import identityProvider
|
||||||
import json
|
import json
|
||||||
|
|
||||||
logging = structlog.get_logger()
|
logging = structlog.get_logger()
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.include_router(identityProvider.router)
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return {'message': 'Hello World'}
|
return {'message': 'Hello World'}
|
||||||
|
|
||||||
@app.post("/authentik/hook/")
|
|
||||||
async def hook(model: Authentik_Hook_Model,
|
|
||||||
http_request: Http_request,
|
|
||||||
ec: EventController = Depends()):
|
|
||||||
logging.info(model)
|
|
||||||
logging.info(http_request)
|
|
||||||
if http_request.path == "/api/v3/core/users/":
|
|
||||||
ec.handle_model_created_event(model)
|
|
||||||
return 200
|
|
||||||
|
|
||||||
|
### for testing purposes
|
||||||
@app.get("/authentik/create_hook/")
|
@app.get("/authentik/create_hook/")
|
||||||
async def hook(request: Request):
|
async def hook(request: Request):
|
||||||
a = Authentik(base="http://localhost:9000/", token="foobar123")
|
a = Authentik(base="http://localhost:9000/", token="foobar123")
|
||||||
|
|
0
app/nextcloud/__init__.py
Normal file
0
app/nextcloud/__init__.py
Normal file
26
app/nextcloud/main.py
Normal file
26
app/nextcloud/main.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from app.sink import BaseGroup, BaseUser, Sink
|
||||||
|
|
||||||
|
class Api:
|
||||||
|
def create_user(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NextcloudSink(Sink):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._api: Api = Api()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api(self):
|
||||||
|
return self._api
|
||||||
|
|
||||||
|
@api.setter
|
||||||
|
def api(self, api):
|
||||||
|
self._api = api
|
||||||
|
|
||||||
|
def create_user(self, user: BaseUser): # Wekan):
|
||||||
|
return self.api.create_user()
|
||||||
|
|
||||||
|
def create_group(self, group: BaseGroup):
|
||||||
|
print("Create Nextcloud Group: ", group)
|
||||||
|
pass
|
0
app/routers/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
20
app/routers/identityProvider.py
Normal file
20
app/routers/identityProvider.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import logging
|
||||||
|
from app import dependencies
|
||||||
|
from app.authentik.api import Authentik
|
||||||
|
from app.event_controller import Authentik_Hook_Model, EventController, Http_request
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app.authentik.settings import AuthentikSettings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/authentik/hook/")
|
||||||
|
async def hook(model: Authentik_Hook_Model,
|
||||||
|
http_request: Http_request,
|
||||||
|
):
|
||||||
|
logging.info(model)
|
||||||
|
logging.info(http_request)
|
||||||
|
ec = EventController(Authentik(AuthentikSettings()))
|
||||||
|
if http_request.path == "/api/v3/core/users/":
|
||||||
|
ec.handle_model_created_event(model)
|
||||||
|
return 200
|
27
app/sink.py
Normal file
27
app/sink.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from abc import ABC, abstractclassmethod, abstractproperty
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class BaseUser(BaseModel):
|
||||||
|
email: str
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
username: str
|
||||||
|
|
||||||
|
class BaseGroup(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class Sink(ABC):
|
||||||
|
|
||||||
|
@abstractproperty
|
||||||
|
def api(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractclassmethod
|
||||||
|
def create_user(user: BaseUser) -> BaseUser:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractclassmethod
|
||||||
|
def create_group(group: BaseGroup) -> BaseGroup:
|
||||||
|
pass
|
|
@ -11,12 +11,11 @@ def test_handle_model_created_event(mocker: MockerFixture):
|
||||||
authentik_mock = mocker.MagicMock()
|
authentik_mock = mocker.MagicMock()
|
||||||
authentik_mock.get_user_by_pk.return_value = mock_user
|
authentik_mock.get_user_by_pk.return_value = mock_user
|
||||||
model = Authentik_Hook_Model(pk=mock_user.pk, app="authentik_core", name=mock_user.name, model_name="user")
|
model = Authentik_Hook_Model(pk=mock_user.pk, app="authentik_core", name=mock_user.name, model_name="user")
|
||||||
ec = EventController()
|
ec = EventController(authentik_mock)
|
||||||
ec.register_api(authentik_mock, wekan_mock)
|
ec.register_api(authentik_mock, [wekan_mock])
|
||||||
ec.handle_model_created_event(model)
|
ec.handle_model_created_event(model)
|
||||||
ec.authentik.get_user_by_pk.assert_called()
|
ec._authentik.get_user_by_pk.assert_called()
|
||||||
ec.authentik.get_user_by_pk.assert_called_with("5")
|
ec._authentik.get_user_by_pk.assert_called_with("5")
|
||||||
ec.wekan.get_user.assert_called()
|
|
||||||
ec.wekan.get_user.assert_called_with("asd")
|
wekan_mock.create_user.assert_called()
|
||||||
ec.wekan.create_user.assert_called()
|
wekan_mock.create_user.assert_called_with(mock_user)
|
||||||
ec.wekan.create_user.assert_called_with(username=mock_user.username, email=mock_user.email, password="")
|
|
||||||
|
|
58
app/test_integration.py
Normal file
58
app/test_integration.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
from cmath import log
|
||||||
|
from email.mime import base
|
||||||
|
import logging
|
||||||
|
from re import A
|
||||||
|
from time import sleep
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from app import event_controller
|
||||||
|
|
||||||
|
from app.authentik.api import Authentik
|
||||||
|
from app.authentik.models import User
|
||||||
|
from app.authentik.settings import AuthentikSettings
|
||||||
|
from app.wekan.models import User as WekanUser
|
||||||
|
from app.wekan.api import WekanApi
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def wekan_api():
|
||||||
|
w = None
|
||||||
|
try:
|
||||||
|
r = requests.post("http://localhost:3000/users/register", json={"username": "api", "password": "foobar123", "email": "foo@example.org"})
|
||||||
|
w = WekanApi("http://localhost:3000","api", "foobar123")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
return w
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def authentik_api():
|
||||||
|
|
||||||
|
settings = AuthentikSettings(baseurl="http://localhost:9000/", token="foobar123")
|
||||||
|
a = Authentik(settings)
|
||||||
|
try:
|
||||||
|
r = a.create_web_hook(hook_endpoint="http://172.17.0.1:8000/authentik/hook/") # docker localhost
|
||||||
|
except Exception as e:
|
||||||
|
logging.info(e)
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_user(wekan_api: WekanApi, authentik_api: Authentik):
|
||||||
|
user = authentik_api.create_user(User(username="banane43", email="banane@example.org", name="Herr Banane"))
|
||||||
|
print(user)
|
||||||
|
sleep(5)
|
||||||
|
authentik_api.delete_user(user)
|
||||||
|
# authentik username == wekan username
|
||||||
|
# user must be created from authentik user and noch api to trigger the notiftication rule?
|
||||||
|
logging.error("authentik notifcation rule doesn't work with api?? , plz create user in authentik")
|
||||||
|
assert False
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_create_user_with_same_email(wekan_api, authentik_api):
|
||||||
|
logging.error("authentik notifcation rule doesn't work with api?? , create two user with identical email in authentik")
|
||||||
|
assert False
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_user_already_exists_excepts():
|
||||||
|
assert False
|
|
@ -1,7 +1,7 @@
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.wekan.api import Wekan
|
from app.wekan.api import WekanApi
|
||||||
|
|
||||||
from .main import Authentik_Hook_Model, app
|
from .main import Authentik_Hook_Model, app
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ def test_hook_fails_for_wrong_input():
|
||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
|
|
||||||
def test_hook_model_created(mocker):
|
def test_hook_model_created(mocker):
|
||||||
mock = mocker.patch("app.event_controller.Event_Controller.handle_model_created_event")
|
mock = mocker.patch("app.event_controller.EventController.handle_model_created_event")
|
||||||
d = """
|
d = """
|
||||||
{"model": {"pk": 5, "app": "authentik_core", "name": "asd", "model_name": "user"}, "http_request": {"args": {}, "path": "/api/v3/core/users/", "method": "POST"}}
|
{"model": {"pk": 5, "app": "authentik_core", "name": "asd", "model_name": "user"}, "http_request": {"args": {}, "path": "/api/v3/core/users/", "method": "POST"}}
|
||||||
"""
|
"""
|
||||||
|
|
28
app/test_sink.py
Normal file
28
app/test_sink.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from .nextcloud.main import BaseUser, Sink
|
||||||
|
from .wekan import main
|
||||||
|
from .nextcloud import main
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# TODO how to import all sink from smth like a "addons" folder?
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def testUser():
|
||||||
|
return BaseUser(email="foo", id="asd", name="sd", username="sdfsfd")
|
||||||
|
|
||||||
|
def test_get_subclasses():
|
||||||
|
l = []
|
||||||
|
for sc in Sink.__subclasses__():
|
||||||
|
l.append(sc.__name__)
|
||||||
|
assert "NextcloudSink" in l
|
||||||
|
assert "WekanSink" in l
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_create_user(mocker: MockerFixture, testUser: BaseUser):
|
||||||
|
u = []
|
||||||
|
for sc in Sink.__subclasses__():
|
||||||
|
print(sc.__name__)
|
||||||
|
# app = sc()
|
||||||
|
# app.api = mocker.MagicMock()
|
||||||
|
# app.create_user(testUser)
|
||||||
|
# app.api.create_user.assert_called()
|
|
@ -1,3 +1,4 @@
|
||||||
|
import logging
|
||||||
from optparse import Option
|
from optparse import Option
|
||||||
import string
|
import string
|
||||||
import requests
|
import requests
|
||||||
|
@ -5,19 +6,20 @@ from requests import Request
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from app.wekan.models import User, UserBase
|
from .models import User, UserBase
|
||||||
|
from .settings import WekanSettings
|
||||||
|
|
||||||
|
|
||||||
class Wekan:
|
class WekanApi:
|
||||||
|
|
||||||
def __init__(self, base: str, api_user: str, password: str):
|
def __init__(self, settings: WekanSettings):
|
||||||
self.base = base
|
self.base = settings.baseurl
|
||||||
r = requests.post(f'{base}/users/login', data={
|
r = requests.post(f'{self.base}/users/login', data={
|
||||||
"username": api_user,
|
"username": settings.user,
|
||||||
"password": password
|
"password": settings.password
|
||||||
}, )
|
}, )
|
||||||
if not r.status_code == 200:
|
if not r.status_code == 200:
|
||||||
raise Exception(r.url, r.text)
|
raise Exception("[WekanAPI] Failed to init")
|
||||||
|
|
||||||
t = r.json()
|
t = r.json()
|
||||||
self.token = {
|
self.token = {
|
||||||
|
@ -60,7 +62,7 @@ class Wekan:
|
||||||
data = {
|
data = {
|
||||||
"username": username,
|
"username": username,
|
||||||
"email": email,
|
"email": email,
|
||||||
"password": ""
|
"password": password
|
||||||
}
|
}
|
||||||
r = self.post("users/", data).json()
|
r = self.post("users/", data).json()
|
||||||
if "error" in r:
|
if "error" in r:
|
||||||
|
|
30
app/wekan/main.py
Normal file
30
app/wekan/main.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from .settings import WekanSettings
|
||||||
|
from app.sink import BaseGroup, BaseUser, Sink
|
||||||
|
from .api import WekanApi
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class WekanSink(Sink):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._settings = WekanSettings()
|
||||||
|
self._api = WekanApi(self._settings)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api(self):
|
||||||
|
return self._api
|
||||||
|
|
||||||
|
@api.setter
|
||||||
|
def api(self, api):
|
||||||
|
self._api = api
|
||||||
|
|
||||||
|
def create_user(self, user: BaseUser):
|
||||||
|
# TODO if not self.wekan.get_user(user.name):
|
||||||
|
return self._api.create_user(username=user.username, email=user.email, password="")
|
||||||
|
|
||||||
|
def create_group(self, group: BaseGroup):
|
||||||
|
print("Create Wekan Group: ", group)
|
||||||
|
pass
|
||||||
|
|
10
app/wekan/settings.py
Normal file
10
app/wekan/settings.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
|
class WekanSettings(BaseSettings):
|
||||||
|
baseurl: str = ""
|
||||||
|
user: str = ""
|
||||||
|
password: str = ""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = '.env'
|
||||||
|
env_prefix = 'WEKAN_'
|
0
app/wekan/sources.py
Normal file
0
app/wekan/sources.py
Normal file
|
@ -1,18 +1,18 @@
|
||||||
import requests
|
import requests
|
||||||
from app.wekan.models import User, UserBase
|
from app.wekan.models import User, UserBase
|
||||||
from .api import Wekan
|
from .api import WekanApi
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def api() -> Wekan:
|
def api() -> WekanApi:
|
||||||
try:
|
try:
|
||||||
r = requests.post("http://localhost:3000/users/register", json={"username": "api", "password": "foobar123", "email": "foo@example.org"})
|
r = requests.post("http://localhost:3000/users/register", json={"username": "api", "password": "foobar123", "email": "foo@example.org"})
|
||||||
return Wekan("http://localhost:3000", "api", "foobar123")
|
return WekanApi("http://localhost:3000", "api", "foobar123")
|
||||||
except:
|
except:
|
||||||
pytest.skip("API not reachable? Did you start docker-compose?")
|
pytest.skip("API not reachable? Did you start docker-compose?")
|
||||||
|
|
||||||
|
|
||||||
def test_get_user(api: Wekan):
|
def test_get_user(api: WekanApi):
|
||||||
user = api.get_user("api")
|
user = api.get_user("api")
|
||||||
assert user.username == "api"
|
assert user.username == "api"
|
||||||
assert type(user) == User
|
assert type(user) == User
|
||||||
|
@ -20,15 +20,15 @@ def test_get_user(api: Wekan):
|
||||||
user = api.get_user("doesnotexist")
|
user = api.get_user("doesnotexist")
|
||||||
assert user == None
|
assert user == None
|
||||||
|
|
||||||
def test_get_users(api: Wekan):
|
def test_get_users(api: WekanApi):
|
||||||
assert True if "api" in [u.username for u in api.get_all_users()] else False
|
assert True if "api" in [u.username for u in api.get_all_users()] else False
|
||||||
|
|
||||||
def test_create_user(api: Wekan):
|
def test_create_user(api: WekanApi):
|
||||||
user = api.create_user("foo", "foo@bar.com", "")
|
user = api.create_user("foo", "foo@bar.com", "")
|
||||||
assert api.get_user("foo").username == "foo"
|
assert api.get_user("foo").username == "foo"
|
||||||
assert type(user) is User
|
assert type(user) is User
|
||||||
|
|
||||||
def test_delete_user(api: Wekan):
|
def test_delete_user(api: WekanApi):
|
||||||
api.create_user("foo", "foo@bar.com", "")
|
api.create_user("foo", "foo@bar.com", "")
|
||||||
api.delete_user("foo") # TODO: doesn't work?
|
api.delete_user("foo") # TODO: doesn't work?
|
||||||
pytest.skip("smth wrong with wekan api")
|
pytest.skip("smth wrong with wekan api")
|
||||||
|
|
|
@ -15,7 +15,7 @@ services:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.2.1}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.4.1}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
|
@ -38,7 +38,7 @@ services:
|
||||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
- "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
||||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
- "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.2.1}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.4.1}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -1,29 +1,26 @@
|
||||||
anyio==3.5.0
|
anyio==3.5.0
|
||||||
asgiref==3.5.0
|
asgiref==3.5.0
|
||||||
attrs==21.4.0
|
attrs==21.4.0
|
||||||
autopep8==1.6.0
|
|
||||||
certifi==2021.10.8
|
certifi==2021.10.8
|
||||||
charset-normalizer==2.0.12
|
charset-normalizer==2.0.12
|
||||||
click==8.0.4
|
click==8.1.2
|
||||||
fastapi==0.74.1
|
fastapi==0.75.2
|
||||||
h11==0.13.0
|
h11==0.13.0
|
||||||
idna==3.3
|
idna==3.3
|
||||||
iniconfig==1.1.1
|
iniconfig==1.1.1
|
||||||
packaging==21.3
|
packaging==21.3
|
||||||
pluggy==1.0.0
|
pluggy==1.0.0
|
||||||
py==1.11.0
|
py==1.11.0
|
||||||
pycodestyle==2.8.0
|
|
||||||
pydantic==1.9.0
|
pydantic==1.9.0
|
||||||
pyparsing==3.0.7
|
pyparsing==3.0.8
|
||||||
pytest==7.0.1
|
pytest==7.1.2
|
||||||
pytest-mock==3.7.0
|
pytest-mock==3.7.0
|
||||||
python-dotenv==0.19.2
|
python-dotenv==0.20.0
|
||||||
requests==2.27.1
|
requests==2.27.1
|
||||||
sniffio==1.2.0
|
sniffio==1.2.0
|
||||||
starlette==0.17.1
|
starlette==0.17.1
|
||||||
structlog==21.5.0
|
structlog==21.5.0
|
||||||
toml==0.10.2
|
|
||||||
tomli==2.0.1
|
tomli==2.0.1
|
||||||
typing_extensions==4.1.1
|
typing_extensions==4.2.0
|
||||||
urllib3==1.26.8
|
urllib3==1.26.9
|
||||||
uvicorn==0.17.5
|
uvicorn==0.17.6
|
||||||
|
|
Loading…
Reference in a new issue