Skip to content

Commit 5f92acf

Browse files
committed
Authorization backend changes
1 parent 717514d commit 5f92acf

19 files changed

+547
-128
lines changed

forms-flow-api/entrypoint

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
#!/bin/bash
2-
python manage.py db upgrade && gunicorn -b :5000 'formsflow_api:create_app()' --timeout 120 --worker-class=gthread --workers=5 --threads=10
1+
echo 'starting application'
2+
export FLASK_APP=manage.py
3+
flask db upgrade
4+
gunicorn -b :5000 'formsflow_api:create_app()' --timeout 120 --worker-class=gthread --workers=5 --threads=10

forms-flow-api/manage.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,19 @@
22

33
import logging
44

5-
from flask_migrate import Migrate, MigrateCommand
6-
from flask_script import Manager
5+
from flask_migrate import Migrate
76

87
# models included so that migrate can build the database migrations
98
from formsflow_api import models # noqa: F401 # pylint: disable=unused-import
109
from formsflow_api import create_app
1110
from formsflow_api.models import db
12-
11+
from flask.cli import FlaskGroup
1312

1413
APP = create_app()
15-
MIGRATE = Migrate(APP, db)
16-
MANAGER = Manager(APP)
14+
cli = FlaskGroup(APP)
1715

18-
MANAGER.add_command('db', MigrateCommand)
16+
MIGRATE = Migrate(APP, db)
1917

2018
if __name__ == '__main__':
2119
logging.log(logging.INFO, 'Running the Manager')
22-
MANAGER.run()
20+
cli()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""authorization
2+
3+
Revision ID: ddd2ec3a72f2
4+
Revises: 696557aef580
5+
Create Date: 2022-08-15 11:15:24.929985
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'ddd2ec3a72f2'
14+
down_revision = '696557aef580'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table('authorization',
22+
sa.Column('created', sa.DateTime(), nullable=False),
23+
sa.Column('modified', sa.DateTime(), nullable=True),
24+
sa.Column('created_by', sa.String(), nullable=False),
25+
sa.Column('modified_by', sa.String(), nullable=True),
26+
sa.Column('id', sa.Integer(), nullable=False, comment='Authorization ID'),
27+
sa.Column('tenant', sa.String(), nullable=True, comment='Tenant key'),
28+
sa.Column('auth_type', postgresql.ENUM('DASHBOARD', 'FORM', 'FILTER', name='authtype'), nullable=False, comment='Auth Type'),
29+
sa.Column('resource_id', sa.String(), nullable=False, comment='Resource identifier'),
30+
sa.Column('resource_details', sa.JSON(), nullable=True, comment='Resource details'),
31+
sa.Column('roles', postgresql.ARRAY(sa.String()), nullable=True, comment='Applicable roles'),
32+
sa.Column('user_name', sa.String(), nullable=True, comment='Applicable user'),
33+
sa.PrimaryKeyConstraint('id')
34+
)
35+
op.create_index(op.f('ix_authorization_auth_type'), 'authorization', ['auth_type'], unique=False)
36+
op.create_index(op.f('ix_authorization_resource_id'), 'authorization', ['resource_id'], unique=False)
37+
op.drop_column('application', 'form_url')
38+
# ### end Alembic commands ###
39+
40+
41+
def downgrade():
42+
# ### commands auto generated by Alembic - please adjust! ###
43+
op.add_column('application', sa.Column('form_url', sa.VARCHAR(length=500), autoincrement=False, nullable=True))
44+
op.drop_index(op.f('ix_authorization_resource_id'), table_name='authorization')
45+
op.drop_index(op.f('ix_authorization_auth_type'), table_name='authorization')
46+
op.drop_table('authorization')
47+
# ### end Alembic commands ###

forms-flow-api/requirements.txt

+31-29
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,43 @@
1-
Flask-Caching==1.10.1
2-
Flask-Migrate==2.5.2
1+
Flask-Caching==2.0.1
2+
Flask-Migrate==3.1.0
3+
Flask-Moment==1.0.4
34
Flask-SQLAlchemy==2.5.1
4-
Flask-Script==2.0.6
5-
Flask==1.1.4
6-
Jinja2==2.11.3
7-
Mako==1.1.6
8-
MarkupSafe==2.0.1
9-
SQLAlchemy-Utils==0.38.2
10-
SQLAlchemy==1.4.31
11-
Werkzeug==1.0.1
12-
alembic==1.7.6
5+
Flask==2.1.3
6+
Jinja2==3.1.2
7+
Mako==1.2.1
8+
MarkupSafe==2.1.1
9+
PyJWT==2.4.0
10+
SQLAlchemy-Utils==0.38.3
11+
SQLAlchemy==1.4.40
12+
Werkzeug==2.1.2
13+
alembic==1.8.1
1314
aniso8601==9.0.1
14-
attrs==21.4.0
15-
cachelib==0.6.0
16-
certifi==2021.10.8
17-
charset-normalizer==2.0.12
18-
click==7.1.2
19-
ecdsa==0.17.0
15+
attrs==22.1.0
16+
cachelib==0.9.0
17+
certifi==2022.6.15
18+
charset-normalizer==2.1.0
19+
click==8.1.3
20+
ecdsa==0.18.0
2021
flask-jwt-oidc==0.3.0
2122
flask-marshmallow==0.14.0
2223
flask-restx==0.5.1
2324
gunicorn==20.1.0
2425
idna==3.3
25-
itsdangerous==1.1.0
26-
jsonschema==4.4.0
27-
marshmallow-sqlalchemy==0.27.0
28-
marshmallow==3.14.1
26+
importlib-metadata==4.12.0
27+
itsdangerous==2.1.2
28+
jsonschema==4.9.1
29+
marshmallow-sqlalchemy==0.28.1
30+
marshmallow==3.17.0
31+
packaging==21.3
2932
psycopg2-binary==2.9.3
3033
pyasn1==0.4.8
34+
pyparsing==3.0.9
3135
pyrsistent==0.18.1
32-
python-dotenv==0.19.2
36+
python-dotenv==0.20.0
3337
python-jose==3.3.0
34-
pytz==2021.3
35-
requests==2.27.1
36-
rsa==4.8
38+
pytz==2022.2.1
39+
requests==2.28.1
40+
rsa==4.9
3741
six==1.16.0
38-
urllib3==1.26.8
39-
PyJWT==2.4.0
40-
selenium==3.141.0
41-
selenium-wire==3.0.0
42+
urllib3==1.26.11
43+
zipp==3.8.1

forms-flow-api/requirements/dev.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
-r prod.txt
22
autopep8
3-
black==21.12b0
3+
black
44
flake8
55
flake8-blind-except
66
flake8-debugger
77
flake8-docstrings
8-
flake8-isort==4.1.1
8+
flake8-isort
99
flake8-polyfill
1010
flake8-quotes
1111
lovely-pytest-docker

forms-flow-api/requirements/prod.txt

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
gunicorn
2-
Flask<2
2+
Flask
33
Flask-Caching
4-
Flask-Migrate==2.5.2
5-
Flask-Script<3
4+
Flask-Migrate
5+
Flask-Moment
66
Flask-SQLAlchemy
77
flask-restx
88
flask-marshmallow
@@ -11,7 +11,7 @@ python-dotenv
1111
psycopg2-binary
1212
marshmallow-sqlalchemy
1313
requests
14-
Werkzeug
14+
Werkzeug==2.1.2 # Watch out updates on https://github.com/python-restx/flask-restx/issues/460
1515
sqlalchemy_utils
16-
markupsafe==2.0.1
17-
PyJWT==2.4.0
16+
markupsafe
17+
PyJWT

forms-flow-api/src/formsflow_api/models/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .application import Application
44
from .application_history import ApplicationHistory
5+
from .authorization import Authorization, AuthType
56
from .base_model import BaseModel
67
from .db import db, ma
78
from .draft import Draft
@@ -15,4 +16,6 @@
1516
"BaseModel",
1617
"FormProcessMapper",
1718
"Draft",
19+
"AuthType",
20+
"Authorization",
1821
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""This manages Authorization Data."""
2+
3+
from __future__ import annotations
4+
5+
from enum import Enum, unique
6+
from typing import List, Optional
7+
8+
from sqlalchemy import JSON, or_
9+
from sqlalchemy.dialects.postgresql import ARRAY, ENUM
10+
11+
from .audit_mixin import AuditDateTimeMixin, AuditUserMixin
12+
from .base_model import BaseModel
13+
from .db import db
14+
15+
16+
@unique
17+
class AuthType(Enum):
18+
"""Admin type enum."""
19+
20+
DASHBOARD = "DASHBOARD"
21+
FORM = "FORM"
22+
FILTER = "FILTER"
23+
24+
def __str__(self):
25+
"""To string value."""
26+
return self.value
27+
28+
29+
class Authorization(AuditDateTimeMixin, AuditUserMixin, BaseModel, db.Model):
30+
"""This class manages authorization."""
31+
32+
id = db.Column(db.Integer, primary_key=True, comment="Authorization ID")
33+
tenant = db.Column(db.String, nullable=True, comment="Tenant key")
34+
auth_type = db.Column(
35+
ENUM(AuthType), nullable=False, index=True, comment="Auth Type"
36+
)
37+
resource_id = db.Column(
38+
db.String, nullable=False, index=True, comment="Resource identifier"
39+
)
40+
resource_details = db.Column(JSON, nullable=True, comment="Resource details")
41+
roles = db.Column(ARRAY(db.String), nullable=True, comment="Applicable roles")
42+
user_name = db.Column(db.String, nullable=True, comment="Applicable user")
43+
44+
@classmethod
45+
def find_user_authorizations(
46+
cls,
47+
auth_type: AuthType,
48+
roles: List[str] = None,
49+
user_name: str = None,
50+
tenant: str = None,
51+
) -> List[Authorization]:
52+
"""Find authorizations."""
53+
query = cls._auth_query(auth_type, roles, tenant, user_name)
54+
return query.all()
55+
56+
@classmethod
57+
def find_all_authorizations(
58+
cls, auth_type: AuthType, tenant: str = None
59+
) -> List[Authorization]:
60+
"""Find authorizations."""
61+
query = cls.query.filter(Authorization.auth_type == auth_type)
62+
63+
if tenant:
64+
query = query.filter(Authorization.tenant == tenant)
65+
return query.all()
66+
67+
@classmethod
68+
def _auth_query(cls, auth_type, roles, tenant, user_name):
69+
role_condition = [Authorization.roles.contains([role]) for role in roles]
70+
query = (
71+
cls.query.filter(Authorization.auth_type == auth_type)
72+
.filter(or_(*role_condition))
73+
.filter(
74+
or_(
75+
Authorization.user_name.is_(None),
76+
Authorization.user_name == user_name,
77+
)
78+
)
79+
)
80+
81+
if tenant:
82+
query = query.filter(Authorization.tenant == tenant)
83+
return query
84+
85+
@classmethod
86+
def find_resource_authorization( # pylint: disable=too-many-arguments
87+
cls,
88+
auth_type: AuthType,
89+
resource_id: str,
90+
roles: List[str] = None,
91+
user_name: str = None,
92+
tenant: str = None,
93+
) -> List[Authorization]:
94+
"""Find resource authorization."""
95+
query = cls._auth_query(auth_type, roles, tenant, user_name)
96+
query = query.filter(Authorization.resource_id == str(resource_id))
97+
return query.all()
98+
99+
@classmethod
100+
def find_resource_by_id(
101+
cls,
102+
auth_type: AuthType,
103+
resource_id: str,
104+
user_name: str = None,
105+
tenant: str = None,
106+
) -> Optional[Authorization]:
107+
"""Find resource authorization by id."""
108+
query = (
109+
cls.query.filter(Authorization.resource_id == str(resource_id))
110+
.filter(Authorization.auth_type == auth_type)
111+
.filter(
112+
or_(
113+
Authorization.user_name.is_(None),
114+
Authorization.user_name == user_name,
115+
)
116+
)
117+
)
118+
if tenant:
119+
query = query.filter(Authorization.tenant == tenant)
120+
return query.one_or_none()

forms-flow-api/src/formsflow_api/models/draft.py

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
)
1717
from formsflow_api.utils.enums import DraftStatus
1818
from formsflow_api.utils.user_context import UserContext, user_context
19+
1920
from .application import Application
2021
from .audit_mixin import AuditDateTimeMixin
2122
from .base_model import BaseModel

forms-flow-api/src/formsflow_api/resources/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from formsflow_api.resources.application_history import (
1313
API as APPLICATION_HISTORY_API,
1414
)
15+
from formsflow_api.resources.authorization import API as AUTHORIZATION_API
1516
from formsflow_api.resources.checkpoint import API as CHECKPOINT_API
1617
from formsflow_api.resources.dashboards import API as DASHBOARDS_API
1718
from formsflow_api.resources.draft import API as DRAFT_API
@@ -73,3 +74,4 @@ def handle_auth_error(error: AuthError):
7374
API.add_namespace(KEYCLOAK_USER_API, path="/user")
7475
API.add_namespace(DRAFT_API, path="/draft")
7576
API.add_namespace(FORMIO_API, path="/formio")
77+
API.add_namespace(AUTHORIZATION_API, path="/authorizations")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Resource to get Dashboard APIs from redash."""
2+
from http import HTTPStatus
3+
4+
from flask import request
5+
from flask_restx import Namespace, Resource
6+
7+
from formsflow_api.services import AuthorizationService
8+
from formsflow_api.utils import auth, cors_preflight, profiletime
9+
10+
API = Namespace("authorization", description="Authorization APIs")
11+
auth_service = AuthorizationService()
12+
13+
14+
@cors_preflight("GET, POST, OPTIONS")
15+
@API.route("/<string:auth_type>", methods=["GET", "POST", "OPTIONS"])
16+
class AuthorizationList(Resource):
17+
"""Resource to fetch Authorization List and cerate authorization."""
18+
19+
@staticmethod
20+
@API.doc("list_authorization")
21+
@auth.require
22+
@profiletime
23+
def get(auth_type: str):
24+
"""List all authorization."""
25+
return auth_service.get_authorizations(auth_type.upper()), HTTPStatus.OK
26+
27+
@staticmethod
28+
@API.doc("list_authorization")
29+
@auth.require
30+
@profiletime
31+
def post(auth_type: str):
32+
"""Create authorization."""
33+
return (
34+
auth_service.create_authorization(auth_type.upper(), request.get_json()),
35+
HTTPStatus.OK,
36+
)
37+
38+
39+
@cors_preflight("GET, POST, OPTIONS")
40+
@API.route("/users/<string:auth_type>", methods=["GET", "POST", "OPTIONS"])
41+
class UserAuthorizationList(Resource):
42+
"""Resource to fetch Authorization List for the current user."""
43+
44+
@staticmethod
45+
@API.doc("list_authorization")
46+
@auth.require
47+
@profiletime
48+
def get(auth_type: str):
49+
"""List all authorization for the current user."""
50+
return auth_service.get_user_authorizations(auth_type.upper()), HTTPStatus.OK

0 commit comments

Comments
 (0)