diff --git a/Pipfile b/Pipfile index aaf06e6..698de38 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,7 @@ sqlalchemy = "*" waitress = "*" [dev-packages] +pyyaml = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 0f82e7a..bb7c51f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "88e5fb21e69421ebb5788f9c47069d778f9b87246dc340eae094275fb4873d1b" + "sha256": "1a3f310d3d4b4507d76b8074a0580f84ab15774f21edc1f178f2eb736d2c3467" }, "pipfile-spec": 6, "requires": { @@ -262,5 +262,41 @@ "version": "==2.0.1" } }, - "develop": {} + "develop": { + "pyyaml": { + "hashes": [ + "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", + "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", + "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", + "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", + "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", + "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", + "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", + "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", + "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", + "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", + "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", + "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", + "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", + "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", + "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", + "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", + "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", + "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", + "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", + "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", + "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", + "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", + "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" + ], + "index": "pypi", + "version": "==5.4.1" + } + } } diff --git a/README.md b/README.md index 1d85f6a..0c1088c 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,47 @@ ``` cp env.dev .env -pipenv install +pipenv install --dev pipenv shell export FLASK_APP=app.py flask db upgrade +flask seed flask run ``` http://localhost:5000/ + +### Testbenutzer + +Für ein Login ohne LDAP werden die Benutzer aus der [`auth.yml`](./data/auth.yml) benutzt. + + +### Test-Requests + +Beispiele brauchen curl und jq. + +``` +curl -s \ + -D "/dev/stderr" \ + http://localhost:5000/skills?search=ph | jq +``` + +``` +curl -s \ + -D "/dev/stderr" \ + http://localhost:5000/languages?search=fr | jq +``` + +``` +curl -s \ + -D "/dev/stderr" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"username": "peter", "password": "geheim"}' \ + http://localhost:5000/users/login | jq +``` + ### Produktionsumgebung Für die Produktionsumgebung wird [waitress](https://docs.pylonsproject.org/projects/waitress/en/latest/) benutzt. diff --git a/app.py b/app.py index fd7476b..6e5d7c0 100644 --- a/app.py +++ b/app.py @@ -11,6 +11,7 @@ app = Flask(__name__) 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") db = SQLAlchemy(app) migrate = Migrate(app, db) diff --git a/data/auth.yml b/data/auth.yml new file mode 100644 index 0000000..d833e3f --- /dev/null +++ b/data/auth.yml @@ -0,0 +1,3 @@ +--- +peter: + password: geheim diff --git a/env.dev b/env.dev index 6eef321..608ae12 100644 --- a/env.dev +++ b/env.dev @@ -1 +1,3 @@ SQLALCHEMY_DATABASE_URI = 'sqlite:///data/ki.sqlite' + +KI_AUTH=file diff --git a/ki/auth.py b/ki/auth.py new file mode 100644 index 0000000..6350a5a --- /dev/null +++ b/ki/auth.py @@ -0,0 +1,32 @@ +import uuid +import yaml + +from app import app, db +from ki.models import User, Token + + +def auth(username, password): + auth_file_path = app.config["KI_DATA_DIR"] + "/auth.yml" + + with open(auth_file_path, "r") as auth_file_stream: + users = yaml.safe_load(auth_file_stream) + + if username not in users: + return None + + auth_user = users[username] + + if auth_user["password"] != password: + return None + + user = User.query.filter(User.auth_id.__eq__(username)).first() + + if user is None: + user = User(nickname=username, auth_id=username) + db.session.add(user) + + token = Token(token=str(uuid.uuid4()), user=user) + db.session.add(token) + db.session.commit() + + return token diff --git a/ki/models.py b/ki/models.py index 9333426..1cac430 100644 --- a/ki/models.py +++ b/ki/models.py @@ -14,15 +14,15 @@ class User(db.Model): pronouns = Column(String(25), default="") volunteerwork = Column(String(4000), default="") freetext = Column(String(4000), default="") - created = Column(DateTime, nullable=False) - updated = Column(DateTime, onupdate=datetime.now, nullable=False) - auth_id = Column(String(50), nullable=False) + created = Column(DateTime, nullable=False, default=datetime.now) + updated = Column(DateTime, onupdate=datetime.now, nullable=False, default=datetime.now) + auth_id = Column(String(50), nullable=False, unique=True) contacts = relationship("Contact") address = relationship("Address", uselist=False, back_populates="user") tokens = relationship("Token", uselist=False, back_populates="user") - skills = relationship("UserSkill", back_populates="users") - languages = relationship("UserLanguage", "users") + skills = relationship("UserSkill", back_populates="user") + languages = relationship("UserLanguage", back_populates="user") class Token(db.Model): @@ -75,7 +75,7 @@ class Skill(db.Model): id = Column(Integer, primary_key=True) name = Column(String(25), unique=True, nullable=False) - users = relationship("User", back_populates="skills") + users = relationship("UserSkill", back_populates="skill") def to_dict(self): return {"id": self.id, "name": self.name} @@ -98,7 +98,7 @@ class Language(db.Model): id = Column(String(2), primary_key=True) name = Column(String(25), nullable=False) - users = relationship("UserLanguage", back_populates="languages") + users = relationship("UserLanguage", back_populates="language") def to_dict(self): return {"id": self.id, "name": self.name} diff --git a/ki/routes.py b/ki/routes.py index ac61ba6..f06b2c4 100644 --- a/ki/routes.py +++ b/ki/routes.py @@ -1,6 +1,7 @@ import os -from flask import make_response, request, send_file +from flask import jsonify, make_response, request, send_file +from ki.auth import auth from ki.models import Language, Skill from app import app @@ -64,6 +65,17 @@ def handle_icon_request(model, id, path): def hello_world(): return "KI" +@app.route("/users/login", methods=["POST"]) +def login(): + username = request.json.get("username", "") + password = request.json.get("password", "") + token = auth(username, password) + + if token is None: + return make_response({}, 403) + + return make_response({"token": token.token}) + @app.route("/skills") def get_skills(): diff --git a/migrations/versions/47ba0a9cf065_initial_migration.py b/migrations/versions/575a8924eb16_initial_migration.py similarity index 96% rename from migrations/versions/47ba0a9cf065_initial_migration.py rename to migrations/versions/575a8924eb16_initial_migration.py index 5f9d7c8..655f328 100644 --- a/migrations/versions/47ba0a9cf065_initial_migration.py +++ b/migrations/versions/575a8924eb16_initial_migration.py @@ -1,8 +1,8 @@ """Initial migration -Revision ID: 47ba0a9cf065 +Revision ID: 575a8924eb16 Revises: -Create Date: 2021-06-12 12:06:29.946450 +Create Date: 2021-06-12 13:18:24.903142 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '47ba0a9cf065' +revision = '575a8924eb16' down_revision = None branch_labels = None depends_on = None @@ -44,6 +44,7 @@ def upgrade(): sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('auth_id', sa.String(length=50), nullable=False), sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('auth_id'), sa.UniqueConstraint('nickname') ) op.create_table('address',