add wekan api
This commit is contained in:
parent
1577ab18bc
commit
5891616eb4
10 changed files with 380 additions and 5 deletions
2
Makefile
2
Makefile
|
@ -9,3 +9,5 @@ run:
|
||||||
|
|
||||||
test:
|
test:
|
||||||
./env/bin/pytest app
|
./env/bin/pytest app
|
||||||
|
|
||||||
|
.PHONY: init run test
|
|
@ -11,3 +11,8 @@ make init
|
||||||
make test
|
make test
|
||||||
make run
|
make run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://pydantic-docs.helpmanual.io/
|
||||||
|
https://jsontopydantic.com/
|
130
app/authentik.py
Normal file
130
app/authentik.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
from typing import Dict
|
||||||
|
import requests
|
||||||
|
from requests import Request
|
||||||
|
|
||||||
|
|
||||||
|
class Authentik:
|
||||||
|
|
||||||
|
def __init__(self, token, base="https://sso.lit.yksflip.de/"):
|
||||||
|
self.base = f"{base}api/v3/"
|
||||||
|
self.hook_endpoint = "https://webhook.site/0caf7e4d-c853-4e33-995e-dc12f82481f0"
|
||||||
|
self.token = token
|
||||||
|
self.headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
self.property_mapping = None
|
||||||
|
self.event_matcher_policy = None
|
||||||
|
self.event_transport = None
|
||||||
|
self.admin_group = None
|
||||||
|
self.event_rule = None
|
||||||
|
self.event_rule_link = None
|
||||||
|
|
||||||
|
def post(self, endpoint: str, data: Dict) -> Request:
|
||||||
|
return requests.post(url= f"{self.base}{endpoint}", json=data, headers=self.headers)
|
||||||
|
|
||||||
|
def get(self, endpoint: str, params: Dict) -> Request:
|
||||||
|
return requests.get(url=f"{self.base}{endpoint}", params=params, headers=self.headers)
|
||||||
|
|
||||||
|
def create_web_hook(self):
|
||||||
|
self.event_matcher_policy = self.create_event_matcher_policy()
|
||||||
|
self.property_mapping = self.create_property_mapping()
|
||||||
|
self.event_transport = self.create_event_transport(self.hook_endpoint, self.property_mapping["pk"])
|
||||||
|
self.admin_group = self.get_admin_group()
|
||||||
|
self.event_rule = self.create_event_rule(self.admin_group, self.event_transport["pk"])
|
||||||
|
self.event_rule_link = self.add_event_rule_link(self.event_matcher_policy["pk"], self.event_rule["pk"])
|
||||||
|
return self
|
||||||
|
|
||||||
|
def create_event_transport(self, hook_endpoint, property_mapping_pk):
|
||||||
|
url = "events/transports/"
|
||||||
|
data = {
|
||||||
|
"mode": "webhook",
|
||||||
|
"name": "my hook",
|
||||||
|
"send_once": False,
|
||||||
|
"webhook_mapping": property_mapping_pk,
|
||||||
|
"webhook_url": hook_endpoint
|
||||||
|
}
|
||||||
|
print(data)
|
||||||
|
# TODO: add check if model with same name already exists
|
||||||
|
r: Request = self.post(url, data)
|
||||||
|
if r.status_code == 201:
|
||||||
|
return r.json()
|
||||||
|
raise Exception(r.status_code, r.url, r.text)
|
||||||
|
|
||||||
|
def create_event_rule(self, admin_group, event_transport):
|
||||||
|
url = "events/rules/"
|
||||||
|
data = {
|
||||||
|
"group": admin_group["pk"],
|
||||||
|
"name": "event-rule",
|
||||||
|
"severity": "notice",
|
||||||
|
"transports": [
|
||||||
|
event_transport
|
||||||
|
]
|
||||||
|
}
|
||||||
|
# TODO: add check if model with same name already exists
|
||||||
|
r = self.post(url, data)
|
||||||
|
if r.status_code == 201:
|
||||||
|
return r.json()
|
||||||
|
raise Exception(r.status_code, r.url, r.text)
|
||||||
|
|
||||||
|
def add_event_rule_link(self, policy_pk, target_pk):
|
||||||
|
url = "policies/bindings/"
|
||||||
|
data = {
|
||||||
|
"enabled": True,
|
||||||
|
"group": "",
|
||||||
|
"negate": False,
|
||||||
|
"order": "0",
|
||||||
|
"policy": policy_pk,
|
||||||
|
"target": target_pk,
|
||||||
|
"timeout": "1",
|
||||||
|
"user": ""
|
||||||
|
}
|
||||||
|
r = self.post(url, data)
|
||||||
|
if r.status_code == 201:
|
||||||
|
return r.json()
|
||||||
|
raise Exception(r.status_code, r.url, r.text)
|
||||||
|
|
||||||
|
|
||||||
|
def get_admin_group(self):
|
||||||
|
url = "core/groups/"
|
||||||
|
args = {
|
||||||
|
"is_superuser": True,
|
||||||
|
"name": "authentik Admins"
|
||||||
|
}
|
||||||
|
r = self.get(url, args)
|
||||||
|
if r.status_code == 200:
|
||||||
|
groups = r.json()["results"]
|
||||||
|
if len(groups) == 1:
|
||||||
|
self.admin_group = groups[0]
|
||||||
|
return self.admin_group
|
||||||
|
raise Exception(r.status_code, r.url, r.text)
|
||||||
|
|
||||||
|
|
||||||
|
def create_event_matcher_policy(self):
|
||||||
|
url = "policies/event_matcher/"
|
||||||
|
data = {
|
||||||
|
"action": "model_created",
|
||||||
|
"app": "authentik.core",
|
||||||
|
"client_ip": "",
|
||||||
|
"execution_logging": True,
|
||||||
|
"name": "model created"
|
||||||
|
}
|
||||||
|
r = self.post(url, data)
|
||||||
|
if r.status_code == 201:
|
||||||
|
self.event_matcher_policy = r.json()
|
||||||
|
return r.json()
|
||||||
|
raise Exception(r.status_code, r.url, r.text)
|
||||||
|
|
||||||
|
|
||||||
|
def create_property_mapping(self):
|
||||||
|
url = "propertymappings/notification/"
|
||||||
|
data = {
|
||||||
|
"name": "new-mapper",
|
||||||
|
"expression": "fields = {}\nif notification:\n if notification.event:\n for k, v in notification.event.context.items():\n fields[k] = v\nreturn fields"
|
||||||
|
}
|
||||||
|
r = self.post(url, data)
|
||||||
|
if r.status_code == 201:
|
||||||
|
return r.json()
|
||||||
|
raise Exception(r.status_code, r.url, r.text)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user(self, user_pk):
|
||||||
|
pass
|
||||||
|
|
11
app/main.py
11
app/main.py
|
@ -1,8 +1,15 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
|
from .wekan.api import Wekan
|
||||||
|
import json
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return {'message': 'Hello World'}
|
return {'message': 'Hello World'}
|
||||||
|
|
||||||
|
@app.post("/hook")
|
||||||
|
async def hook(request: Request):
|
||||||
|
r = await request.json()
|
||||||
|
# model_created = json.loads(r['body'].split("model_created: ")[1])["model"]
|
||||||
|
# return wekan.create_user(model_created["pk"])
|
||||||
|
|
12
app/test_authentik.py
Normal file
12
app/test_authentik.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from .authentik import Authentik
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection():
|
||||||
|
a = Authentik(token="foobar123")
|
||||||
|
|
||||||
|
# res = a.create_property_mapping()
|
||||||
|
# res = a.create_event_matcher_policy()
|
||||||
|
# res = a.create_event_transport(hook_endpoint="http://localhost:8000")
|
||||||
|
# res = a.get_admin_group()
|
||||||
|
res = a.create_web_hook()
|
||||||
|
print(res)
|
|
@ -1,5 +1,7 @@
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.wekan.api import Wekan
|
||||||
|
|
||||||
from .main import app
|
from .main import app
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
@ -9,3 +11,35 @@ def test_read_main():
|
||||||
response = client.get("/")
|
response = client.get("/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {"message": "Hello World"}
|
assert response.json() == {"message": "Hello World"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_hook():
|
||||||
|
d = """{
|
||||||
|
"body": "Test Notification from transport hook",
|
||||||
|
"severity": "notice",
|
||||||
|
"user_email": "root@localhost",
|
||||||
|
"user_username": "akadmin"
|
||||||
|
}"""
|
||||||
|
response = client.post("/hook", data=d)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_hook_model_created(mocker):
|
||||||
|
mock = mocker.patch('app.wekan.api.Wekan.create_user',
|
||||||
|
return_value='fake user')
|
||||||
|
print(mock.mock_calls)
|
||||||
|
d = """
|
||||||
|
{
|
||||||
|
"body": "model_created: {'model': {'pk': 18, 'app': 'authentik_core', 'name': 'bernd', 'model_name': 'user'}, 'http_request': {'args': {}, 'path': '/api/v3/core/users/', 'method': 'POST'}}",
|
||||||
|
"severity": "alert",
|
||||||
|
"user_email": "flip@yksflip.de",
|
||||||
|
"user_username": "akadmin"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
response = client.post("/hook", data=d, )
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(mock.mock_calls) > 0
|
||||||
|
kall = mock.call_args
|
||||||
|
assert kall.args[0] == "18"
|
||||||
|
# assert str(response.text) == 'fake user'
|
||||||
|
|
||||||
|
|
0
app/wekan/__init__.py
Normal file
0
app/wekan/__init__.py
Normal file
78
app/wekan/api.py
Normal file
78
app/wekan/api.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
from optparse import Option
|
||||||
|
import string
|
||||||
|
import requests
|
||||||
|
from requests import Request
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from app.wekan.models import User, UserBase
|
||||||
|
|
||||||
|
|
||||||
|
class Wekan:
|
||||||
|
|
||||||
|
def __init__(self, base: str, api_user: str, password: str):
|
||||||
|
self.base = base
|
||||||
|
r = requests.post(f'{base}/users/login', data={
|
||||||
|
"username": api_user,
|
||||||
|
"password": password
|
||||||
|
}, )
|
||||||
|
if not r.status_code == 200:
|
||||||
|
raise Exception(r.url, r.text)
|
||||||
|
|
||||||
|
t = r.json()
|
||||||
|
self.token = {
|
||||||
|
"token": t["token"],
|
||||||
|
"id": t["id"],
|
||||||
|
"expires": t["tokenExpires"],
|
||||||
|
}
|
||||||
|
|
||||||
|
self.headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': f'Bearer { self.token["token"] }',
|
||||||
|
}
|
||||||
|
|
||||||
|
def get(self, endpoint) -> Request:
|
||||||
|
return requests.get(f'{self.base}/api/{endpoint}', headers=self.headers)
|
||||||
|
|
||||||
|
def post(self, endpoint: str, data: Dict) -> Request:
|
||||||
|
return requests.post(url=f"{self.base}/api/{endpoint}", json=data, headers=self.headers)
|
||||||
|
|
||||||
|
def delete(self, endpoint: str) -> Request:
|
||||||
|
return requests.delete(url=f"{self.base}/api/{endpoint}", headers=self.headers)
|
||||||
|
|
||||||
|
def get_user(self, _id: string) -> Optional[User]:
|
||||||
|
r = self.get(f"users/{_id}").json()
|
||||||
|
if "error" in r:
|
||||||
|
raise Exception(r)
|
||||||
|
if r == {}:
|
||||||
|
return None
|
||||||
|
print(r)
|
||||||
|
return User(**r)
|
||||||
|
|
||||||
|
def get_all_users(self) -> List[UserBase]:
|
||||||
|
r = self.get("users")
|
||||||
|
if r.status_code == 200:
|
||||||
|
return [UserBase(**u) for u in r.json()]
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
def create_user(self, username: str, email: str, password: str) -> Optional[User]:
|
||||||
|
data = {
|
||||||
|
"username": username,
|
||||||
|
"email": email,
|
||||||
|
"password": ""
|
||||||
|
}
|
||||||
|
r = self.post("users/", data).json()
|
||||||
|
if "error" in r:
|
||||||
|
if r["reason"] == "Username already exists.":
|
||||||
|
return self.get_user(username)
|
||||||
|
raise Exception(r)
|
||||||
|
if "_id" in r:
|
||||||
|
return self.get_user(r["_id"])
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
def delete_user(self, user: str):
|
||||||
|
r = self.delete(f"users/{user}").json()
|
||||||
|
print(r)
|
||||||
|
if "error" in r:
|
||||||
|
raise Exception(r)
|
79
app/wekan/models.py
Normal file
79
app/wekan/models.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class Org(BaseModel):
|
||||||
|
orgId: str
|
||||||
|
orgDisplayName: str
|
||||||
|
|
||||||
|
class Team(BaseModel):
|
||||||
|
teamId: str
|
||||||
|
teamDisplayName: str
|
||||||
|
|
||||||
|
class Email(BaseModel):
|
||||||
|
address: str
|
||||||
|
verified: bool
|
||||||
|
|
||||||
|
class Notification(BaseModel):
|
||||||
|
activity: str
|
||||||
|
read: str
|
||||||
|
|
||||||
|
class Profile(BaseModel):
|
||||||
|
avatarUrl: Optional[str]
|
||||||
|
emailBuffer: Optional[List[str]]
|
||||||
|
fullname: Optional[str]
|
||||||
|
showDesktopDragHandles: Optional[bool]
|
||||||
|
hideCheckedItems: Optional[bool]
|
||||||
|
cardMaximized: Optional[bool]
|
||||||
|
customFieldsGrid: Optional[bool]
|
||||||
|
hiddenSystemMessages: Optional[bool]
|
||||||
|
hiddenMinicardLabelText: Optional[bool]
|
||||||
|
initials: Optional[str]
|
||||||
|
invitedBoards: Optional[List[str]]
|
||||||
|
language: Optional[str]
|
||||||
|
moveAndCopyDialog: Optional[Dict[str, Any]]
|
||||||
|
moveChecklistDialog: Optional[Dict[str, Any]]
|
||||||
|
copyChecklistDialog: Optional[Dict[str, Any]]
|
||||||
|
notifications: Optional[List[Notification]]
|
||||||
|
showCardsCountAt: Optional[int]
|
||||||
|
startDayOfWeek: Optional[int]
|
||||||
|
starredBoards: Optional[List[str]]
|
||||||
|
icode: Optional[str]
|
||||||
|
boardView: str
|
||||||
|
listSortBy: str
|
||||||
|
templatesBoardId: str
|
||||||
|
cardTemplatesSwimlaneId: str
|
||||||
|
listTemplatesSwimlaneId: str
|
||||||
|
boardTemplatesSwimlaneId: str
|
||||||
|
|
||||||
|
class SessionData(BaseModel):
|
||||||
|
totalHits: Optional[int]
|
||||||
|
|
||||||
|
class Member(BaseModel):
|
||||||
|
id: str = Field(..., alias="userId")
|
||||||
|
isAdmin: bool = False
|
||||||
|
isNoComments: bool = False
|
||||||
|
isCommentOnly: bool = False
|
||||||
|
isWorker: Optional[bool] = False
|
||||||
|
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
id: str = Field(..., alias='_id')
|
||||||
|
username: Optional[str] = None
|
||||||
|
|
||||||
|
class User(UserBase):
|
||||||
|
username: str
|
||||||
|
orgs: Optional[List[Org]]
|
||||||
|
teams: Optional[List[Team]]
|
||||||
|
emails: List[Email]
|
||||||
|
createdAt: str
|
||||||
|
modifiedAt: str
|
||||||
|
profile: Optional[Profile]
|
||||||
|
services: Dict[str, Any]
|
||||||
|
heartbeat: Optional[str]
|
||||||
|
isAdmin: Optional[bool]
|
||||||
|
createdThroughApi: Optional[bool]
|
||||||
|
loginDisabled: Optional[bool]
|
||||||
|
authenticationMethod: str
|
||||||
|
sessionData: SessionData
|
||||||
|
importUsernames: Optional[List[Optional[str]]]
|
28
app/wekan/test_wekan.py
Normal file
28
app/wekan/test_wekan.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from app.wekan.models import User, UserBase
|
||||||
|
from .api import Wekan
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def api() -> Wekan:
|
||||||
|
return Wekan("https://board.lit.yksflip.de", "api", "foobar123")
|
||||||
|
|
||||||
|
def test_get_user(api: Wekan):
|
||||||
|
user = api.get_user("api")
|
||||||
|
assert user.username == "api"
|
||||||
|
assert type(user) == User
|
||||||
|
|
||||||
|
user = api.get_user("doesnotexist")
|
||||||
|
assert user == None
|
||||||
|
|
||||||
|
def test_get_users(api: Wekan):
|
||||||
|
assert True if "api" in [u.username for u in api.get_all_users()] else False
|
||||||
|
|
||||||
|
def test_create_user(api: Wekan):
|
||||||
|
user = api.create_user("foo", "foo@bar.com", "")
|
||||||
|
assert api.get_user("foo").username == "foo"
|
||||||
|
assert type(user) is User
|
||||||
|
|
||||||
|
def test_delete_user(api: Wekan):
|
||||||
|
api.create_user("foo", "foo@bar.com", "")
|
||||||
|
api.delete_user("foo") # TODO: doesn't work?
|
||||||
|
assert api.get_user("foo") == None
|
Loading…
Reference in a new issue