Compare commits

...

16 Commits

Author SHA1 Message Date
6389258a60 Add more skills
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-08-16 22:43:20 +02:00
b8c2644086 implement generic search
All checks were successful
continuous-integration/drone/push Build is passing
closes #47
2021-08-16 22:41:03 +02:00
ec855a542d add level texts, closes #45
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-14 12:14:01 +02:00
9a1f3f842a extend language name
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-02 21:51:44 +02:00
2d49c70f0f fix column type
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-02 21:29:06 +02:00
39f014db86 add pymysql
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-02 19:57:19 +02:00
922b20c990 Merge pull request 'docker-compose lokal gebaute Images' (#43) from feature-docker-local into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #43
2021-08-02 18:02:26 +02:00
5a84ba0689 switch docker-compose to local build
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-07-28 22:28:34 +02:00
54f1c0f242 add integration docker-compose file
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-26 21:52:29 +02:00
178381b00c extend seed skills
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-26 20:17:38 +02:00
730878847b Merge pull request 'Anmeldung per LDAP' (#35) from feature-ldap into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #35
2021-07-26 19:44:19 +02:00
ea57f326b7 Merge pull request 'Profilsuche nach Nickname' (#41) from feature-nickname-search into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #41
2021-07-26 19:40:33 +02:00
76e95311f0 add search by nick
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
add sql special chars test
2021-07-26 19:31:29 +02:00
a1502fea9c fix tests
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-07-12 18:37:54 +02:00
5bd6c827d8 fix ldap3 version
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-07-11 12:19:41 +02:00
bbf8719fc9 add ldap auth
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-07-11 12:16:41 +02:00
18 changed files with 553 additions and 134 deletions

View File

@ -26,9 +26,6 @@ steps:
from_secret: "docker_username"
password:
from_secret: "docker_password"
when:
event:
- tag
image_pull_secrets:
- dockerconfig

View File

@ -16,6 +16,8 @@ sqlalchemy = "~=1.4.18"
waitress = "~=2.0.0"
pyyaml = "~=5.4.1"
flask-cors = "~=3.0.10"
ldap3 = "~=2.9"
pymysql = "~=1.0.2"
[dev-packages]
flake8 = "~=3.9.2"

162
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "128a3b90d2e3d5876c5942fb256dba2d5e3d0b3a545cae6d51aeda6925a0f593"
"sha256": "332b04c923ecb74bc066ee5348a664274ce87736981cc8e56e60070ff26e7d0e"
},
"pipfile-spec": 6,
"requires": {
@ -135,6 +135,17 @@
"markers": "python_version >= '3.6'",
"version": "==3.0.1"
},
"ldap3": {
"hashes": [
"sha256:2bc966556fc4d4fa9f445a1c31dc484ee81d44a51ab0e2d0fd05b62cac75daa6",
"sha256:5630d1383e09ba94839e253e013f1aa1a2cf7a547628ba1265cb7b9a844b5687",
"sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70",
"sha256:5ab7febc00689181375de40c396dcad4f2659cd260fc5e94c508b6d77c17e9d5",
"sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"
],
"index": "pypi",
"version": "==2.9.1"
},
"mako": {
"hashes": [
"sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab",
@ -183,13 +194,39 @@
"markers": "python_version >= '3.6'",
"version": "==2.0.1"
},
"pyasn1": {
"hashes": [
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
],
"version": "==0.4.8"
},
"pymysql": {
"hashes": [
"sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641",
"sha256:816927a350f38d56072aeca5dfb10221fe1dc653745853d30a216637f5d7ad36"
],
"index": "pypi",
"version": "==1.0.2"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.1"
"version": "==2.8.2"
},
"python-dotenv": {
"hashes": [
@ -254,39 +291,39 @@
},
"sqlalchemy": {
"hashes": [
"sha256:0f6d467b67a7e5048f1408e8ea60d6caa70be5b386d0eebbf1185ab49cb8c7e4",
"sha256:238d78b3110b7f7cffdb70bf9cda686e0d876a849bc78ba4d471aa7b1461f306",
"sha256:25c0e0f3a7e8c19350086b3c0fe93c4def045cec053d749ef15da710c4d54c81",
"sha256:2f60a2e599cf5cf5e5327ce60f2918b897e42ad9f405d10dd01e37869c0ce6fc",
"sha256:38ee3a266afef2978e82824650457f70c5d74ec0cadec1b10fe5ed6f038eb5d0",
"sha256:46361690f1e1c5385994a4caeb6e8126063ff593a5c635700bbc1245de793c1e",
"sha256:46b99eab618cdc1c871ea707b7c52edc23cfea6c750740cd242ba62b5c84de7f",
"sha256:4a67371752fd86d1d03a3b82d4e75404608f6f4d579b9676124079a22a40c79f",
"sha256:525dd3c2205b11a2bc6d770bf1ec63bde0253fd754b4c19c399d27ddc9dad0d3",
"sha256:6c8406c3d8c1c7d15da454de15d77f7bb48d14ede5db994f74226c348cf1050e",
"sha256:6da83225a23eaf7b3f48f3d5f53c91b2cf00fbfa48b24a7a758160112dd3e123",
"sha256:7150e5b543b466f45f668b352f7abda27998cc8035f051d1b7e9524ca9eb2f5f",
"sha256:76fbc24311a3d039d6cd147d396719f606d96d1413f3816c028a48e29367f646",
"sha256:854a7b15750e617e16f8d65dbc004f065a7963544b253b923f16109557648777",
"sha256:86c079732328f1add097b0b8079cd532b5d28e207fac93e9d6ea5f487506deef",
"sha256:8d860c62e3f51623ccd528d8fac44580501df557d4b467cc5581587fcf057719",
"sha256:9675d5bc7e4f96a7bb2b54d14e9b269a5fb6e5d36ecc7d01f0f65bb9af3185f9",
"sha256:9841762d114018c49483c089fa2d47f7e612e57666323f615913d7d7f46e9606",
"sha256:9eb25bcf9161e2fcbe9eebe8e829719b2334e849183f0e496bf4b83722bcccfa",
"sha256:aad3234a41340e9cf6184e621694e2a7233ba3f8aef9b1e6de8cba431b45ebd2",
"sha256:b502b5e2f08500cc4b8d29bfc4f51d805adcbc00f8d149e98fda8aae85ddb644",
"sha256:b86d83fefc8a8c394f3490c37e1953bc16c311a3d1d1cf91518793bfb9847fb4",
"sha256:c0eb2cd3ad4967fcbdd9e066e8cd91fe2c23c671dbae9952f0b4d3d42832cc5f",
"sha256:e0d48456e1aa4f0537f9c9af7be71e1f0659ff68bc1cd538ebc785f6b007bd0d",
"sha256:eaee5dd378f6f0d7c3ec49aeeb26564d55ac0ad73b9b4688bf29e66deabddf73",
"sha256:f14acb0fd16d404fda9370f93aace682f284340c89c3442ac747c5466ac7e2b5",
"sha256:f6fc526bd70898489d02bf52c8f0632ab377592ae954d0c0a5bb38d618dddaa9",
"sha256:fcd84e4d46a86291495d131a7824ba38d2e8278bda9425c50661a04633174319",
"sha256:ff38ecf89c69a531a7326c2dae71982edfe2f805f3c016cdc5bfd1a04ebf80cb",
"sha256:ff8bebc7a9d297dff2003460e01db2c20c63818b45fb19170f388b1a72fe5a14"
"sha256:09dbb4bc01a734ccddbf188deb2a69aede4b3c153a72b6d5c6900be7fb2945b1",
"sha256:12bac5fa1a6ea870bdccb96fe01610641dd44ebe001ed91ef7fcd980e9702db5",
"sha256:1fdae7d980a2fa617d119d0dc13ecb5c23cc63a8b04ffcb5298f2c59d86851e9",
"sha256:26daa429f039e29b1e523bf763bfab17490556b974c77b5ca7acb545b9230e9a",
"sha256:36a089dc604032d41343d86290ce85d4e6886012eea73faa88001260abf5ff81",
"sha256:39b5d36ab71f73c068cdcf70c38075511de73616e6c7fdd112d6268c2704d9f5",
"sha256:4014978de28163cd8027434916a92d0f5bb1a3a38dff5e8bf8bff4d9372a9117",
"sha256:44d23ea797a5e0be71bc5454b9ae99158ea0edc79e2393c6e9a2354de88329c0",
"sha256:488608953385d6c127d2dcbc4b11f8d7f2f30b89f6bd27c01b042253d985cc2f",
"sha256:5102b9face693e8b2db3b2539c7e1a5d9a5b4dc0d79967670626ffd2f710d6e6",
"sha256:5908ea6c652a050d768580d01219c98c071e71910ab8e7b42c02af4010608397",
"sha256:5d856cc50fd26fc8dd04892ed5a5a3d7eeb914fea2c2e484183e2d84c14926e0",
"sha256:68393d3fd31469845b6ba11f5b4209edbea0b58506be0e077aafbf9aa2e21e11",
"sha256:6a16c7c4452293da5143afa3056680db2d187b380b3ef4d470d4e29885720de3",
"sha256:756f5d2f5b92d27450167247fb574b09c4cd192a3f8c2e493b3e518a204ee543",
"sha256:891927a49b2363a4199763a9d436d97b0b42c65922a4ea09025600b81a00d17e",
"sha256:9bfe882d5a1bbde0245dca0bd48da0976bd6634cf2041d2fdf0417c5463e40e5",
"sha256:9fcbb4b4756b250ed19adc5e28c005b8ed56fdb5c21efa24c6822c0575b4964d",
"sha256:a00d9c6d3a8afe1d1681cd8a5266d2f0ed684b0b44bada2ca82403b9e8b25d39",
"sha256:a5e14cb0c0a4ac095395f24575a0e7ab5d1be27f5f9347f1762f21505e3ba9f1",
"sha256:b48148ceedfb55f764562e04c00539bb9ea72bf07820ca15a594a9a049ff6b0e",
"sha256:b7fb937c720847879c7402fe300cfdb2aeff22349fa4ea3651bca4e2d6555939",
"sha256:bc34a007e604091ca3a4a057525efc4cefd2b7fe970f44d20b9cfa109ab1bddb",
"sha256:c9373ef67a127799027091fa53449125351a8c943ddaa97bec4e99271dbb21f4",
"sha256:d09a760b0a045b4d799102ae7965b5491ccf102123f14b2a8cc6c01d1021a2d9",
"sha256:ec1be26cdccd60d180359a527d5980d959a26269a2c7b1b327a1eea0cab37ed8",
"sha256:eedd76f135461cf237534a6dc0d1e0f6bb88a1dc193678fab48a11d223462da5",
"sha256:f028ef6a1d828bc754852a022b2160e036202ac8658a6c7d34875aafd14a9a15",
"sha256:f814d80844969b0d22ea63663da4de5ca1c434cfbae226188901e5d368792c17",
"sha256:fd2102a8f8a659522719ed73865dff3d3cc76eb0833039dc473e0ad3041d04be"
],
"index": "pypi",
"version": "==1.4.20"
"version": "==1.4.22"
},
"waitress": {
"hashes": [
@ -306,12 +343,13 @@
}
},
"develop": {
"appdirs": {
"backports.entry-points-selectable": {
"hashes": [
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
"sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a",
"sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"
],
"version": "==1.4.4"
"markers": "python_version >= '2.7'",
"version": "==1.1.0"
},
"binaryornot": {
"hashes": [
@ -350,6 +388,14 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0"
},
"charset-normalizer": {
"hashes": [
"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
],
"markers": "python_version >= '3'",
"version": "==2.0.4"
},
"distlib": {
"hashes": [
"sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736",
@ -374,19 +420,19 @@
},
"identify": {
"hashes": [
"sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421",
"sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306"
"sha256:7abaecbb414e385752e8ce02d8c494f4fbc780c975074b46172598a28f1ab839",
"sha256:a0e700637abcbd1caae58e0463861250095dfe330a8371733a471af706a4a29a"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==2.2.10"
"version": "==2.2.11"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10"
"markers": "python_version >= '3'",
"version": "==3.2"
},
"jinja2": {
"hashes": [
@ -457,6 +503,14 @@
],
"version": "==1.6.0"
},
"platformdirs": {
"hashes": [
"sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c",
"sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"
],
"markers": "python_version >= '3.6'",
"version": "==2.2.0"
},
"pre-commit": {
"hashes": [
"sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378",
@ -526,11 +580,11 @@
},
"requests": {
"hashes": [
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.25.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==2.26.0"
},
"reuse": {
"hashes": [
@ -566,11 +620,11 @@
},
"virtualenv": {
"hashes": [
"sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467",
"sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"
"sha256:97066a978431ec096d163e72771df5357c5c898ffdd587048f45e0aecc228094",
"sha256:fdfdaaf0979ac03ae7f76d5224a05b58165f3c804f8aa633f3dd6f22fbd435d5"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.4.7"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==20.7.0"
},
"yapf": {
"hashes": [

View File

@ -68,9 +68,18 @@ flake8
### Testbenutzer
#### Lokal ohne LDAP
Für ein Login ohne LDAP werden die Benutzer aus der [`auth.yml`](./data/auth.yml) benutzt.
#### Lokal mit LDAP
Einen LDAP Server aufsetzen. Z.B. https://directory.apache.org/apacheds/
In der `.env` die LDAP Dinge ausfüllen (siehe [`env.dev`](./env.dev)).
### Beispiel-Requests
Beispiele brauchen curl und jq.
@ -113,6 +122,15 @@ curl -s \
http://localhost:5000/users/1/profile
```
Profilsuche nach Nickname:
```
curl -s \
-D "/dev/stderr" \
-H "Authorization: Bearer 22e6c5fc-8a5a-440e-b1f4-018deb9fd24e" \
http://localhost:5000/users/profiles
```
## Docker
@ -159,6 +177,26 @@ Für die Produktionsumgebung wird [waitress](https://docs.pylonsproject.org/proj
[`run_prod.py`](./run_prod.py) führt die DB Migrationen aus und startet den Server.
## Integrationsumgebung
Per [`docker-compose`](https://docs.docker.com/compose/) kann eine Integrationsumgebung gestartet werden.
Beide Projekte müssen nebeneinander ausgecheckt sein:
```
./
ki-backend
ki-frontend
```
Alles starten:
```
docker-compose up
```
Dann http://localhost:13337 aufrufen.
## Lizenzen
Dieses Projekt erfüllt die [REUSE](https://reuse.software/) Spezifikation.

19
app.py
View File

@ -8,25 +8,36 @@ import os
from dotenv import load_dotenv, find_dotenv
from flask import Flask
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask.logging import default_handler
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from ldap3.utils.log import logger as ldap3_logger
from ldap3.utils.log import set_library_log_detail_level, BASIC
load_dotenv(find_dotenv())
app = Flask(__name__)
loglevel = os.getenv("KI_LOGLEVEL", logging.WARNING)
loglevel = int(loglevel)
app.logger.setLevel(loglevel)
logging.basicConfig(level=loglevel)
logging.debug("Hello from KI")
app = Flask(__name__)
set_library_log_detail_level(BASIC)
ldap3_logger.addHandler(default_handler)
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("SQLALCHEMY_DATABASE_URI")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["KI_DATA_DIR"] = os.path.dirname(__file__) + "/data"
app.config["KI_AUTH"] = os.getenv("KI_AUTH")
app.config["CORS_ORIGINS"] = os.getenv("CORS_ORIGINS", "*")
app.config["KI_AUTH"] = os.getenv("KI_AUTH")
app.config["KI_LDAP_URL"] = os.getenv("KI_LDAP_URL")
app.config["KI_LDAP_ROOT_DN"] = os.getenv("KI_LDAP_ROOT_DN")
CORS(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
logging.debug("Hello from KI")
from ki import module # noqa

View File

@ -7,3 +7,5 @@ peter:
password: geheim
klaus:
password: jutta
dieter:
password: hunger

View File

@ -11,3 +11,116 @@ id,name
10,PostgreSQL
11,SQLite
12,Node.js
13,C++
14,C
15,VHDL
16,go
17,Perl
18,3D-Druck
19,ABAP
20,Android
21,Ansible
22,Arduino
23,Bash
24,batou
25,bind
26,Buchführung
27,C#
28,CAD
29,CAM
30,Cobol
31,CRM
32,CSS
33,D
34,Debian
35,Delphi
36,DevOPS
37,Discourse
38,Django
39,DNS
40,Dovecot
41,Elasticsearch
42,Emacs
43,Email
44,ERP
45,ESP
46,Excel
47,Fahrdienstleitung
48,Fedora
49,FLOSS
50,Geographie
51,Geologie
52,Gnome
53,GPS
54,Grafana
55,GrayLog
56,GSM
57,GTK
58,HTML
59,IBM Z
60,IMAP
61,Ionic
62,iOS
63,Java
64,Kryptographie
65,LDAP
66,LibreOffice
67,Linux
68,Literaturgeschichte
69,Lithographie
70,Lucene
71,Mailman
72,MariaDB
73,Markdown
74,Marketing
75,Microsoft Office
76,Monitoring
77,Nagios
78,nähen
79,NixOS
80,odoo
81,OpenOffice
82,OpenPGP
83,OpenStreetMap
84,openSUSE
85,Oracle
86,Percona
87,PGP
88,Plone
89,Postfix
90,PowerDNS
91,PowerPC
92,Projektmanagement
93,puppet
94,Qt
95,R/3
96,RedHat
97,RHEL
98,S/4
99,Salt
100,SAP
101,Scrum
102,ScummVM
103,Sensu
104,sh
105,Siemens S5
106,Siemens S7
107,Simatic
108,SLES
109,Spring
110,SQL
111,Stenographie
112,Steuerrecht
113,Teppich knüpfen
114,TeX
115,Verlagswesen
116,vi(m)
117,Windows 10
118,Windows 2000
119,Windows 2012R2
120,Windows 7
121,Windows NT
122,Windows XP
123,x86
124,Zope
125,zsh

1 id name
11 10 PostgreSQL
12 11 SQLite
13 12 Node.js
14 13 C++
15 14 C
16 15 VHDL
17 16 go
18 17 Perl
19 18 3D-Druck
20 19 ABAP
21 20 Android
22 21 Ansible
23 22 Arduino
24 23 Bash
25 24 batou
26 25 bind
27 26 Buchführung
28 27 C#
29 28 CAD
30 29 CAM
31 30 Cobol
32 31 CRM
33 32 CSS
34 33 D
35 34 Debian
36 35 Delphi
37 36 DevOPS
38 37 Discourse
39 38 Django
40 39 DNS
41 40 Dovecot
42 41 Elasticsearch
43 42 Emacs
44 43 Email
45 44 ERP
46 45 ESP
47 46 Excel
48 47 Fahrdienstleitung
49 48 Fedora
50 49 FLOSS
51 50 Geographie
52 51 Geologie
53 52 Gnome
54 53 GPS
55 54 Grafana
56 55 GrayLog
57 56 GSM
58 57 GTK
59 58 HTML
60 59 IBM Z
61 60 IMAP
62 61 Ionic
63 62 iOS
64 63 Java
65 64 Kryptographie
66 65 LDAP
67 66 LibreOffice
68 67 Linux
69 68 Literaturgeschichte
70 69 Lithographie
71 70 Lucene
72 71 Mailman
73 72 MariaDB
74 73 Markdown
75 74 Marketing
76 75 Microsoft Office
77 76 Monitoring
78 77 Nagios
79 78 nähen
80 79 NixOS
81 80 odoo
82 81 OpenOffice
83 82 OpenPGP
84 83 OpenStreetMap
85 84 openSUSE
86 85 Oracle
87 86 Percona
88 87 PGP
89 88 Plone
90 89 Postfix
91 90 PowerDNS
92 91 PowerPC
93 92 Projektmanagement
94 93 puppet
95 94 Qt
96 95 R/3
97 96 RedHat
98 97 RHEL
99 98 S/4
100 99 Salt
101 100 SAP
102 101 Scrum
103 102 ScummVM
104 103 Sensu
105 104 sh
106 105 Siemens S5
107 106 Siemens S7
108 107 Simatic
109 108 SLES
110 109 Spring
111 110 SQL
112 111 Stenographie
113 112 Steuerrecht
114 113 Teppich knüpfen
115 114 TeX
116 115 Verlagswesen
117 116 vi(m)
118 117 Windows 10
119 118 Windows 2000
120 119 Windows 2012R2
121 120 Windows 7
122 121 Windows NT
123 122 Windows XP
124 123 x86
125 124 Zope
126 125 zsh

22
docker-compose.yml Normal file
View File

@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2021 WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
version: "3"
services:
ki_backend:
build: .
restart: "no"
ports:
- "13338:5000"
volumes:
- "./env.dev:/app/.env"
ki_frontend:
build: ./../ki-frontend
restart: "no"
ports:
- "13337:80"
volumes:
- "../ki-frontend/public/config.js.int:/user/share/nginx/html/config.js"

View File

@ -9,7 +9,12 @@ CORS_ORIGINS=*
FLASK_APP=app.py
FLASK_ENV=development
# auth method: file or ldap
KI_AUTH=file
# ldap auth only
KI_LDAP_URL=ldap://localhost:10389
KI_LDAP_ROOT_DN=dc=example,dc=com
# 10 = debug
KI_LOGLEVEL=10

View File

@ -26,12 +26,37 @@ def seed_contacttypes():
db.session.add(ContactType(id=int(contacttype["id"]), name=contacttype["name"]))
def seed_user(nickname, visible=False, skills=[], languages=[], volunteerwork="", availability="", freetext=""):
app.logger.info(f"seeding {nickname} \\o/")
user = User(auth_id=nickname)
db.session.add(user)
profile = Profile(nickname=nickname,
pronouns="",
volunteerwork=volunteerwork,
availability=availability,
freetext=freetext,
visible=visible,
user=user)
for skill_data in skills:
skill = ProfileSkill(profile=profile, skill_id=skill_data[0], level=skill_data[1])
db.session.add(skill)
for language_data in languages:
language = ProfileLanguage(profile=profile, language_id=language_data[0], level=language_data[1])
db.session.add(language)
db.session.add(profile)
def seed(dev: bool):
seed_contacttypes()
skill_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/skills.csv"
logging.info("importing skills")
app.logger.info("importing skills")
with open(skill_seed_file_path) as skills_file:
skills_csv_reader = csv.DictReader(skills_file)
@ -43,7 +68,7 @@ def seed(dev: bool):
if db_skill is None:
db.session.add(Skill(id=int(skill["id"]), name=skill["name"]))
logging.info("importing languages")
app.logger.info("importing languages")
iso_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/iso_639_1.csv"
@ -58,7 +83,7 @@ def seed(dev: bool):
db.session.add(Language(id=iso["639-1"], name=iso["Sprache"]))
if dev:
logging.info("seeding peter :)")
app.logger.info("seeding peter :)")
peter = User(auth_id="peter")
db.session.add(peter)
@ -105,9 +130,21 @@ def seed(dev: bool):
peter_fr = ProfileLanguage(profile=peters_profile, language_id="fr", level=3)
db.session.add(peter_fr)
logging.info("seeding klaus :D")
seed_user("klaus")
klaus = User(auth_id="klaus")
db.session.add(klaus)
seed_user("dirtydieter",
visible=True,
volunteerwork="Müll sammeln",
availability="Nur nachts",
freetext="1001010010111!!!",
skills=[(Skill.skill_id_php, 5)])
seed_user("jutta",
visible=True,
languages=[("fr", 5)],
skills=[(Skill.skill_id_php, 3), (Skill.skill_id_mysql, 4)])
seed_user("giesela", visible=True, skills=[(Skill.skill_id_mysql, 3), (Skill.skill_id_postgresql, 5)])
seed_user("bertha", visible=False, skills=[(Skill.skill_id_sqlite, 3), (Skill.skill_id_postgresql, 5)])
seed_user("monique", visible=True, languages=[("fr", 4)])
db.session.commit()

View File

@ -5,11 +5,28 @@
import uuid
import yaml
from ldap3 import Server, Connection, ALL
from app import app, db
from ki.models import User, Token
def auth(username, password):
def create_user_token(username):
user = User.query.filter(User.auth_id.__eq__(username)).first()
if user is None:
user = User(auth_id=username)
db.session.add(user)
token = Token(token=str(uuid.uuid4()), user=user)
db.session.add(token)
db.session.commit()
return token
def file_auth(username, password):
app.logger.debug("performing file authentication")
auth_file_path = app.config["KI_DATA_DIR"] + "/auth.yml"
with open(auth_file_path, "r") as auth_file_stream:
@ -23,14 +40,32 @@ def auth(username, password):
if auth_user["password"] != password:
return None
user = User.query.filter(User.auth_id.__eq__(username)).first()
return create_user_token(username)
if user is None:
user = User(auth_id=username)
db.session.add(user)
token = Token(token=str(uuid.uuid4()), user=user)
db.session.add(token)
db.session.commit()
def ldap_auth(username, password):
app.logger.debug("performing LDAP authentication")
return token
server = Server(app.config['KI_LDAP_URL'], get_info=ALL)
root_dn = app.config['KI_LDAP_ROOT_DN']
ldap_user = f"cn={username},{root_dn}"
app.logger.debug(f"server: {server}")
connection = Connection(server, user=ldap_user, password=password)
if connection.bind():
connection.unbind()
return create_user_token(username)
connection.unbind()
return None
def auth(username, password):
if app.config['KI_AUTH'] == 'file':
return file_auth(username, password)
if app.config['KI_AUTH'] == 'ldap':
return ldap_auth(username, password)
raise RuntimeError('unknown auth method')

View File

@ -4,7 +4,7 @@
from flask import make_response, request
from ki.models import Profile
from ki.models import Profile, ProfileSkill, Skill, ProfileLanguage, Language
def find_profiles():
@ -18,11 +18,23 @@ def find_profiles():
if page_size > 100:
return make_response({"messages": {"page_size": "Die maximale Anzahl Einträge pro Seite beträgt 100"}}, 400)
query = Profile.query.filter(Profile.visible.is_(True)) \
.join(Profile.skills, isouter=True).join(ProfileSkill.skill, isouter=True) \
.join(Profile.languages, isouter=True).join(ProfileLanguage.language, isouter=True)
if "search" in request.args:
terms = request.args["search"].split(" ")
for term in terms:
query = query.filter(
Profile.nickname.like(f"%{term}%") | Skill.name.like(f"%{term}%") | Language.name.like(f"%{term}%"))
if "nickname" in request.args:
nickname = request.args.get("nickname")
query = query.filter(Profile.nickname.like(f"%{nickname}%"))
count = query.distinct(Profile.id).count()
offset = (page - 1) * page_size
query = Profile.query.filter(Profile.visible is True)
count = query.count()
db_profiles = query.limit(page_size).offset(offset).all()
api_profiles = []

View File

@ -53,7 +53,7 @@ class Profile(db.Model):
"availability": self.availability,
"freetext": self.freetext,
"visible": self.visible,
"address": self.address.to_dict(),
"address": self.address.to_dict() if self.address else None,
"contacts": list(map(lambda contact: contact.to_dict(), self.contacts)),
"skills": list(map(lambda skill: skill.to_dict(), self.skills)),
"searchtopics": list(map(lambda searchtopic: searchtopic.to_dict(), self.searchtopics)),
@ -133,6 +133,13 @@ class Address(db.Model):
class Skill(db.Model):
skill_id_php = 1
skill_id_python = 3
skill_id_sqlalchemy = 7
skill_id_mysql = 9
skill_id_postgresql = 10
skill_id_sqlite = 11
__tablename__ = "skill"
id = Column(Integer, primary_key=True)
@ -146,6 +153,19 @@ class Skill(db.Model):
class ProfileSkill(db.Model):
level1_text = "bis 6 Monate"
level2_text = "bis 1 Jahr"
level3_text = "bis 3 Jahre"
level4_text = "bis 5 Jahre"
level5_text = "mehr als 5 Jahre"
level_texts = {
1: level1_text,
2: level2_text,
3: level3_text,
4: level4_text,
5: level5_text,
}
__tablename__ = "profile_skill"
profile_id = Column(Integer, ForeignKey("profile.id"), primary_key=True)
@ -175,8 +195,8 @@ class ProfileSearchtopic(db.Model):
class Language(db.Model):
__tablename__ = "language"
id = Column(String(2), primary_key=True)
name = Column(String(25), nullable=False)
id = Column(String(4), primary_key=True)
name = Column(String(100), nullable=False)
profiles = relationship("ProfileLanguage", back_populates="language")
@ -185,10 +205,23 @@ class Language(db.Model):
class ProfileLanguage(db.Model):
level1_text = "keine Angabe"
level2_text = "Grundkenntnisse"
level3_text = "Gut"
level4_text = "Fließend"
level5_text = "Muttersprache"
level_texts = {
1: level1_text,
2: level2_text,
3: level3_text,
4: level4_text,
5: level5_text,
}
__tablename__ = "profile_language"
profile_id = Column(Integer, ForeignKey("profile.id"), primary_key=True)
language_id = Column(Integer, ForeignKey("language.id"), primary_key=True)
language_id = Column(String(4), ForeignKey("language.id"), primary_key=True)
level = Column(SmallInteger, nullable=False)
profile = relationship("Profile", back_populates="languages")

View File

@ -8,6 +8,7 @@ import unittest
from app import app, db, migrate
from ki.actions import seed
from ki.models import Skill
class ApiTest(unittest.TestCase):
@ -15,8 +16,10 @@ class ApiTest(unittest.TestCase):
def setUp(self):
app.debug = True
app.config["KI_AUTH"] = "file"
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
self.client = app.test_client()
with app.app_context():
@ -24,6 +27,8 @@ class ApiTest(unittest.TestCase):
command.upgrade(config, "head")
seed(True)
max_skill = Skill.query.order_by(Skill.id.desc()).first()
self.max_skill_id = max_skill.id
def tearDown(self):
db.drop_all()

View File

@ -7,36 +7,85 @@ import unittest
from ki.test.ApiTest import ApiTest
class TestSkillsEndpoint(ApiTest):
def test_skills_options(self):
response = self.client.options("/skills")
class TestFindProfilesEndpoint(ApiTest):
def test_find_profiles_options(self):
response = self.client.options("/users/profiles")
self.assertEqual(response.status_code, 200)
self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
def test_get_skills1(self):
def test_find_nobody(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/skills?search=p", headers={"Authorization": "Bearer " + token})
response = self.client.get("/users/profiles?nickname=horsthorsthorst",
headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertEqual(
{
"skills": [{
"id": 1,
"name": "PHP",
"icon_url": "/skills/1/icon"
}, {
"id": 10,
"name": "PostgreSQL",
"icon_url": "/skills/10/icon"
}, {
"id": 3,
"name": "Python",
"icon_url": "/skills/3/icon"
}]
}, response.json)
self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
self.assertEqual(response.json, {"total": 0, "profiles": []})
def test_find_sql_specialchars(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?nickname=%22%27%25", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {"total": 0, "profiles": []})
def test_find_all(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 4}, response.json)
self.assertDictContainsSubset({"nickname": "dirtydieter"}, response.json["profiles"][0])
def test_find_dieter(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?search=dieter%20php", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 1}, response.json)
self.assertDictContainsSubset({"nickname": "dirtydieter"}, response.json["profiles"][0])
def test_not_find_dieter(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?search=dieter%20sqlite",
headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 0}, response.json)
def test_find_sql(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?search=sql", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 2}, response.json)
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0])
self.assertDictContainsSubset({"nickname": "giesela"}, response.json["profiles"][1])
def test_find_postgres(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?search=post", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 1}, response.json)
self.assertDictContainsSubset({"nickname": "giesela"}, response.json["profiles"][0])
def test_find_php_franzosen(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?search=php%20franz", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 1}, response.json)
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0])
def test_find_franzosen(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?search=französisch", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 2}, response.json)
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0])
self.assertDictContainsSubset({"nickname": "monique"}, response.json["profiles"][1])
if __name__ == "main":

View File

@ -143,7 +143,7 @@ class TestProfileEndpoint(ApiTest):
self.assertEqual(first_skill.level, 4)
second_skill = skills[1]
self.assertEqual(second_skill.skill.id, 13)
self.assertEqual(second_skill.skill.id, self.max_skill_id + 1)
self.assertEqual(second_skill.skill.name, "Tschunkproduktion")
self.assertEqual(second_skill.level, 5)
@ -155,7 +155,7 @@ class TestProfileEndpoint(ApiTest):
self.assertEqual(first_searchtopic.skill.name, "Python")
second_searchtopic = searchtopics[1]
self.assertEqual(second_searchtopic.skill.id, 14)
self.assertEqual(second_searchtopic.skill.id, self.max_skill_id + 2)
self.assertEqual(second_searchtopic.skill.name, "Assembler")
languages = profile.languages
@ -174,7 +174,14 @@ class TestProfileEndpoint(ApiTest):
response = self.client.get("/users/1/profile")
self.assertEqual(response.status_code, 401)
def test_get_profile(self):
def test_get_visible_proifle(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/3/profile", headers={"Authorization": f"Bearer {token}"})
self.assertEqual(response.status_code, 200)
def test_get_own_profile(self):
login_data = {"username": "peter", "password": "geheim"}
login_response = self.client.post("/users/login", data=json.dumps(login_data), content_type="application/json")

View File

@ -14,27 +14,24 @@ class TestSkillsEndpoint(ApiTest):
self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
def test_get_skills1(self):
def test_find_skills_php(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/skills?search=php", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertGreaterEqual(len(response.json['skills']), 1)
self.assertIn({"id": 1, "name": "PHP", "icon_url": "/skills/1/icon"}, response.json['skills'])
self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
def test_find_skills_p(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/skills?search=p", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertEqual(
{
"skills": [{
"id": 1,
"name": "PHP",
"icon_url": "/skills/1/icon"
}, {
"id": 10,
"name": "PostgreSQL",
"icon_url": "/skills/10/icon"
}, {
"id": 3,
"name": "Python",
"icon_url": "/skills/3/icon"
}]
}, response.json)
self.assertGreaterEqual(len(response.json['skills']), 3)
self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")

View File

@ -1,8 +1,8 @@
"""Initial migration.
Revision ID: 44b45a772abd
Revision ID: 9183e2335b05
Revises:
Create Date: 2021-07-06 21:19:44.217722
Create Date: 2021-08-02 21:51:30.400680
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '44b45a772abd'
revision = '9183e2335b05'
down_revision = None
branch_labels = None
depends_on = None
@ -24,8 +24,8 @@ def upgrade():
sa.PrimaryKeyConstraint('id')
)
op.create_table('language',
sa.Column('id', sa.String(length=2), nullable=False),
sa.Column('name', sa.String(length=25), nullable=False),
sa.Column('id', sa.String(length=4), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('profile',
@ -71,7 +71,7 @@ def upgrade():
)
op.create_table('profile_language',
sa.Column('profile_id', sa.Integer(), nullable=False),
sa.Column('language_id', sa.Integer(), nullable=False),
sa.Column('language_id', sa.String(length=4), nullable=False),
sa.Column('level', sa.SmallInteger(), nullable=False),
sa.ForeignKeyConstraint(['language_id'], ['language.id'], ),
sa.ForeignKeyConstraint(['profile_id'], ['profile.id'], ),