diff --git a/Pipfile b/Pipfile index 5a526fb..9a0593e 100644 --- a/Pipfile +++ b/Pipfile @@ -16,6 +16,7 @@ sqlalchemy = "~=1.4.18" waitress = "~=2.0.0" pyyaml = "~=5.4.1" flask-cors = "~=3.0.10" +ldap3 = "~=2.9" [dev-packages] flake8 = "~=3.9.2" diff --git a/Pipfile.lock b/Pipfile.lock index 6ad64e8..c02a503 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "128a3b90d2e3d5876c5942fb256dba2d5e3d0b3a545cae6d51aeda6925a0f593" + "sha256": "9dfe9cdb37253c770013fcf80c413f9b0ffdfc1cc16210fb9d80cdac760c2b93" }, "pipfile-spec": 6, "requires": { @@ -29,7 +29,7 @@ "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==8.0.1" }, "flask": { @@ -124,7 +124,7 @@ "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.0.1" }, "jinja2": { @@ -132,9 +132,20 @@ "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==3.0.1" }, + "ldap3": { + "hashes": [ + "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91", + "sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59", + "sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c", + "sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056", + "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57" + ], + "index": "pypi", + "version": "==2.9" + }, "mako": { "hashes": [ "sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab", @@ -180,9 +191,27 @@ "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "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" + }, "python-dateutil": { "hashes": [ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", @@ -301,7 +330,7 @@ "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.0.1" } }, @@ -374,11 +403,11 @@ }, "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": [ @@ -393,7 +422,7 @@ "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==3.0.1" }, "license-expression": { @@ -440,7 +469,7 @@ "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.0.1" }, "mccabe": { diff --git a/README.md b/README.md index 68a9fb0..e146961 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app.py b/app.py index 4ddc17e..6f0ef7a 100644 --- a/app.py +++ b/app.py @@ -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 diff --git a/env.dev b/env.dev index e788b0e..d393498 100644 --- a/env.dev +++ b/env.dev @@ -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 diff --git a/ki/auth.py b/ki/auth.py index 49710f7..5dc7ae6 100644 --- a/ki/auth.py +++ b/ki/auth.py @@ -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') diff --git a/ki/test/ApiTest.py b/ki/test/ApiTest.py index 0248dca..493f837 100644 --- a/ki/test/ApiTest.py +++ b/ki/test/ApiTest.py @@ -15,8 +15,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():