Skip to content
Snippets Groups Projects
Commit 89c740fa authored by Mart van Santen's avatar Mart van Santen
Browse files

Add first CLI interface to manage data in database

parent 93fbc6f6
No related branches found
No related tags found
1 merge request!50Implemente basic flask + database APIs
Pipeline #9576 passed with stage
in 2 minutes and 40 seconds
import logging
import os
import click
# Flask
from flask import abort, Flask, redirect, request, render_template
from os import urandom, environ
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask.cli import AppGroup
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
# Hydra admin
from hydra_client import HydraAdmin
import hydra_client
# Kratos ?
from kratos import User, KratosError
from forms import LoginForm
# Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import logging
import os
# Configure Hydra Admin port
HYDRA_ADMIN_URL = environ['HYDRA_ADMIN_URL']
HYDRA = HydraAdmin(HYDRA_ADMIN_URL)
# Initaliaze the FLASK app
app = Flask(__name__)
app.config.from_object(os.environ['APP_SETTINGS'])
app.config['SECRET_KEY'] = urandom(16)
app.config['HYDRA_ADMIN_URL'] = environ['HYDRA_ADMIN_URL'];
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Load config
app.config.from_object(os.environ['APP_SETTINGS'])
app.debug = True if "FLASK_ENV" in environ and environ["FLASK_ENV"] == "development" else False
# Move to config?
app.logger.setLevel(logging.INFO)
# Creat HYDRA admin interface
HYDRA_ADMIN_URL = os.environ['HYDRA_ADMIN_URL']
HYDRA = hydra_client.HydraAdmin(HYDRA_ADMIN_URL)
# Create DB interface
db = SQLAlchemy(app)
engine = create_engine(app.config["SQLALCHEMY_DATABASE_URI"])
# Migrate interface
migrate = Migrate(app, db)
# Import models
from models import User, App, AppRole
from models import User
# Import CLI routes
# Flask CLI
user_cli = AppGroup('user')
app_cli = AppGroup('app')
@app_cli.command('create')
@click.argument('slug')
@click.argument('name')
def create_app(slug, name):
app.logger.info("Creating app definition: {1} ({0})".format(slug, name))
obj = App()
obj.name = name
obj.slug = slug
db.session.add(obj)
db.session.commit()
@app_cli.command('list')
def list_app():
app.logger.info("Listing apps")
apps = App.query.all()
for obj in apps:
print("App name: %s \t Slug: %s" %(obj.name, obj.slug))
@app_cli.command('delete',)
@click.argument('slug')
def delete_app(slug):
app.logger.info("Trying to delete app: {0}".format(slug))
obj = App.query.filter_by(slug=slug).first()
if not obj:
app.logger.info("Not found")
return
db.session.delete(obj)
db.session.commit()
app.logger.info("Success")
return
app.cli.add_command(app_cli)
@user_cli.command('create')
@click.argument('email')
def create_user(email):
app.logger.info("Creating user: {0}".format(email))
app.cli.add_command(user_cli)
@app.route('/login', methods=['GET', 'POST'])
def login():
......@@ -63,19 +126,19 @@ def login():
# TODO: Empty/short passwors is a form Error
challenge = None
# Retrieve the challenge id from the request. Depending on the method it is saved in the
# form (POST) or in a GET variable.
# Retrieve the challenge id from the request. Depending on the method it is
# saved in the form (POST) or in a GET variable. If this variable is not set
# we can not continue.
if request.method == 'GET':
challenge = request.args.get("login_challenge")
if not challenge:
return abort(400)
elif login_form.validate_on_submit():
challenge = login_form.challenge.data
if request.method == 'POST':
challenge = request.args.post("login_challenge")
if not challenge:
app.logger.error("No challange given. Error in request")
abort(401)
abort(404)
# Now that we have the challenge id, we can request the challenge object from the hydra
# admin API
......@@ -88,6 +151,41 @@ def login():
app.logger.error("Conflict. Login request has been used already. challenge={0}".format(challenge))
abort(503)
# Skip, if true, let's us know that Hydra has already successfully authenticated
# the user. we don't need to check anything and we can accept the request right away.
if login_request.skip:
app.logger.info("{0} is already logged in. Skip authentication".format(login_request.subject))
return redirect(login_request.accept(login_request.subject))
#skip = request.args.get("skip")
#logout = request.args.get("logout")
#if skip:
# app.logger.info("{0} is already logged in. Skip authentication".format(login_request.subject))
# return redirect(login_request.accept(login_request.subject))
#elif logout:
# login_form.challenge.data = challenge
# HYDRA.invalidate_login_sessions(login_request.subject);
# return redirect(login_request.reject(
# "Login cancelled",
# error_description="Login was cancelled and user session was terminated"))
#else:
# return render_template('skip.html', challenge=challenge, logo=login_request.client.logo_uri, application_name=login_request.client.client_name, username=login_request.subject)
else:
app.logger.info("Not yet logged in. Requesting login credentials")
if "ory_kratos_session" not in request.cookies:
app.logger.info("No kratos cookie, get kratos cookie")
kratos = ory_kratos_client.ApiClient(app.config["KRATOS_PUBLIC_URL"])
login_flow = LoginFlow(app.config["KRATOS_PUBLIC_URL"])
return_to = f"{app.config['PUBLIC_URL']}/login?login_challenge={challenge}"
# We need to decide here whether we want to accept or decline the login request.
# if a login form was submitted, we need to confirm that the userdata, the agent
# send us via POST is valid
......@@ -119,24 +217,6 @@ def login():
app.logger.info("{0} failed to login".format(user.username))
return redirect(redirect_to)
# Skip, if true, let's us know that Hydra has already successfully authenticated
# the user. we don't need to check anything and we can accept the request right away.
elif login_request.skip:
skip = request.args.get("skip")
logout = request.args.get("logout")
if skip:
app.logger.info("{0} is already logged in. Skip authentication".format(login_request.subject))
return redirect(login_request.accept(login_request.subject))
elif logout:
login_form.challenge.data = challenge
HYDRA.invalidate_login_sessions(login_request.subject);
return redirect(login_request.reject(
"Login cancelled",
error_description="Login was cancelled and user session was terminated"))
else:
return render_template('skip.html', challenge=challenge, logo=login_request.client.logo_uri, application_name=login_request.client.client_name, username=login_request.subject)
# If Skip is not true and the user has not submitted any data via a form, we need
# to display a login form for the user to type in their username and password.
......
......@@ -8,8 +8,13 @@ class Config(object):
DEBUG = False
TESTING = False
CSRF_ENABLED = True
SECRET_KEY = 'this-really-needs-to-be-changed'
SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']
HYDRA_ADMIN_URL = os.environ['HYDRA_ADMIN_URL'];
SECRET_KEY = os.urandom(16)
SQLALCHEMY_TRACK_MODIFICATIONS = False
HYDRA_ADMIN_URL = os.environ['HYDRA_ADMIN_URL']
class ProductionConfig(Config):
......
"""Add roles and apps
"""empty message
Revision ID: 051274550b64
Revises: fc0307250700
Create Date: 2021-11-15 15:40:04.662936
Revision ID: 207081778bb2
Revises:
Create Date: 2021-11-16 08:18:27.697307
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '051274550b64'
down_revision = 'fc0307250700'
revision = '207081778bb2'
down_revision = None
branch_labels = None
depends_on = None
......@@ -20,11 +20,16 @@ def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('apps',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('app', sa.String(), nullable=True),
sa.Column('name', sa.String(), nullable=True),
sa.Column('slug', sa.String(), nullable=True),
sa.Column('result_all', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.Column('result_no_stop_words', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('slug')
)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.create_table('user_app_roles',
sa.Column('user_id', sa.Integer(), nullable=False),
......@@ -40,5 +45,6 @@ def upgrade():
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user_app_roles')
op.drop_table('users')
op.drop_table('apps')
# ### end Alembic commands ###
"""Initial migration.
Revision ID: fc0307250700
Revises:
Create Date: 2021-11-15 15:29:32.420084
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'fc0307250700'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=True),
sa.Column('result_all', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.Column('result_no_stop_words', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('users')
# ### end Alembic commands ###
from app import db
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, ForeignKey
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String())
result_all = db.Column(JSON)
result_no_stop_words = db.Column(JSON)
id = db.Column(Integer, primary_key=True)
email = db.Column(String, unique=True)
def __init__(self, email, result_all, result_no_stop_words):
self.email = email
self.result_all = result_all
self.result_no_stop_words = result_no_stop_words
app_roles = relationship('AppRole', back_populates="user")
def __repr__(self):
return '<id {}>'.format(self.id)
......@@ -24,17 +23,9 @@ class User(db.Model):
class App(db.Model):
__tablename__ = 'apps'
id = db.Column(db.Integer, primary_key=True)
app = db.Column(db.String())
slug = db.Column(db.String())
result_all = db.Column(JSON)
result_no_stop_words = db.Column(JSON)
def __init__(self, app, slug, result_all, result_no_stop_words):
self.app = app
self.slug = slug
self.result_all = result_all
self.result_no_stop_words = result_no_stop_words
id = db.Column(Integer, primary_key=True)
name = db.Column(String())
slug = db.Column(db.String(), unique=True)
def __repr__(self):
return '<id {}>'.format(self.id)
......@@ -43,13 +34,13 @@ class App(db.Model):
class AppRole(db.Model):
__tablename__ = 'user_app_roles'
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
user = db.relationship("User", back_populates="app_roles")
user_id = db.Column(Integer, ForeignKey('users.id'), primary_key=True)
user = relationship("User", back_populates="app_roles")
app_id = db.Column(db.Integer, db.ForeignKey('apps.id'),
app_id = db.Column(Integer, ForeignKey('apps.id'),
primary_key=True)
app = db.relationship("App")
app = relationship("App")
role = db.Column(db.String())
role = db.Column(String)
......@@ -6,5 +6,4 @@ Flask-SQLAlchemy
Flask-Migrate
Flask-Script
psycopg2
bcrypt
graphqlclient
ory-kratos-client
......@@ -4,5 +4,8 @@
export FLASK_RUN_HOST=0.0.0.0
export FLASK_RUN_PORT=5000
export HYDRA_ADMIN_URL=http://localhost:4445
export KRATOS_PUBLIC_URL=http://localhost:8080
export KRATOS_ADMIN_URL=http://localhost:8000
export DATABASE_URL="postgresql://stackspin:stackspin@localhost/stackspin"
export APP_SETTINGS="config.DevelopmentConfig"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment