Skip to content

Commit

Permalink
Review system and change history (#44)
Browse files Browse the repository at this point in the history
* Checked out code related to traceability from crazyscientist/devel
* Require authentication/authorization for reviewing requests
* Require appropriate permissions for creating and reviewing requests
* Removed obsolete model `BoundFK`
* Replaced `bound_fk` and `bind_to_schema` with `bound_schema_id`
* Format dates in entity lists according to locale

This pull request is based on work by @commanderprice. Thank you for your contribution.
  • Loading branch information
crazyscientist authored Feb 15, 2022
1 parent b6f0e42 commit 412d3a1
Show file tree
Hide file tree
Showing 55 changed files with 4,942 additions and 2,406 deletions.
19 changes: 15 additions & 4 deletions backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

from .config import settings
from .database import SessionLocal
from .enum import FilterEnum
from .dynamic_routes import create_dynamic_router
from .models import Schema, AttributeDefinition
from .models import Schema, AttributeDefinition, AttrType
from .general_routes import router


Expand All @@ -17,20 +18,30 @@ def load_schemas(db: Session) -> List[models.Schema]:
select(Schema)
.options(
subqueryload(Schema.attr_defs)
.subqueryload(AttributeDefinition.attribute)
)
).scalars().all()
return schemas


def generate_api_description() -> str:
description = '# Filters\n\n**Filters list**:'
for filter in FilterEnum:
description += '\n* `{}` - {}'.format(filter.value.name, filter.value.description)

description += '\n\n**Available filters for each type**:'
for atype in AttrType:
description += '\n* `{}`: {}'.format(atype.name, ', '.join([f'`{i.value.name}`' for i in atype.value.filters]))
return description


def load_dynamic_routes(db: Session, app: FastAPI):
schemas = load_schemas(db)
for schema in schemas:
create_dynamic_router(schema=schema, app=app, get_db=lambda: db)
create_dynamic_router(schema=schema, app=app)


def create_app(session: Optional[Session] = None) -> FastAPI:
app = FastAPI()
app = FastAPI(description=generate_api_description())
origins = ['*']
app.add_middleware(CORSMiddleware,
allow_origins=origins,
Expand Down
136 changes: 136 additions & 0 deletions backend/alembic/versions/5e7b010c57b8_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""review
Revision ID: 5e7b010c57b8
Revises: 5c72f33e6b82
Create Date: 2022-01-26 13:49:15.993571
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '5e7b010c57b8'
down_revision = '5c72f33e6b82'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('change_values_bool',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('old_value', sa.Boolean(), nullable=True),
sa.Column('new_value', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_change_values_bool_id'), 'change_values_bool', ['id'], unique=False)
op.create_table('change_values_date',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('old_value', sa.Date(), nullable=True),
sa.Column('new_value', sa.Date(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_change_values_date_id'), 'change_values_date', ['id'], unique=False)
op.create_table('change_values_datetime',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('old_value', sa.DateTime(timezone=True), nullable=True),
sa.Column('new_value', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_change_values_datetime_id'), 'change_values_datetime', ['id'], unique=False)
op.create_table('change_values_fk',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('old_value', sa.Integer(), nullable=True),
sa.Column('new_value', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_change_values_fk_id'), 'change_values_fk', ['id'], unique=False)
op.create_table('change_values_float',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('old_value', sa.Float(), nullable=True),
sa.Column('new_value', sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_change_values_float_id'), 'change_values_float', ['id'], unique=False)
op.create_table('change_values_int',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('old_value', sa.Integer(), nullable=True),
sa.Column('new_value', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_change_values_int_id'), 'change_values_int', ['id'], unique=False)
op.create_table('change_values_str',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('old_value', sa.String(), nullable=True),
sa.Column('new_value', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_change_values_str_id'), 'change_values_str', ['id'], unique=False)
op.create_table('change_requests',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_by_user_id', sa.Integer(), nullable=False),
sa.Column('reviewed_by_user_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('reviewed_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('status', sa.Enum('PENDING', 'DECLINED', 'APPROVED', name='changestatus'), nullable=False),
sa.Column('comment', sa.String(length=1024), nullable=True),
sa.Column('object_type', sa.Enum('SCHEMA', 'ENTITY', name='editableobjecttype'), nullable=False),
sa.Column('object_id', sa.Integer(), nullable=True),
sa.Column('change_type', sa.Enum('CREATE', 'UPDATE', 'DELETE', name='changetype'), nullable=False),
sa.CheckConstraint("object_id IS NOT NULL OR (change_type = 'CREATE' AND status <> 'APPROVED')"),
sa.ForeignKeyConstraint(['created_by_user_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['reviewed_by_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('changes',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('change_request_id', sa.Integer(), nullable=False),
sa.Column('attribute_id', sa.Integer(), nullable=True),
sa.Column('object_id', sa.Integer(), nullable=True),
sa.Column('value_id', sa.Integer(), nullable=False),
sa.Column('content_type', sa.Enum('ATTRIBUTE', 'ATTRIBUTE_DEFINITION', 'ENTITY', 'SCHEMA', name='contenttype'), nullable=False),
sa.Column('change_type', sa.Enum('CREATE', 'UPDATE', 'DELETE', name='changetype'), nullable=False),
sa.Column('field_name', sa.String(), nullable=True),
sa.Column('data_type', sa.Enum('STR', 'BOOL', 'INT', 'FLOAT', 'FK', 'DT', 'DATE', name='changeattrtype'), nullable=False),
sa.CheckConstraint('NOT(attribute_id IS NULL AND field_name IS NULL) AND NOT (attribute_id IS NOT NULL AND field_name IS NOT NULL)'),
sa.ForeignKeyConstraint(['attribute_id'], ['attributes.id'], ),
sa.ForeignKeyConstraint(['change_request_id'], ['change_requests.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('values_date',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('value', sa.Date(), nullable=True),
sa.Column('entity_id', sa.Integer(), nullable=True),
sa.Column('attribute_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['attribute_id'], ['attributes.id'], ),
sa.ForeignKeyConstraint(['entity_id'], ['entities.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_values_date_id'), 'values_date', ['id'], unique=False)
op.add_column('schemas', sa.Column('reviewable', sa.Boolean(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('schemas', 'reviewable')
op.drop_index(op.f('ix_values_date_id'), table_name='values_date')
op.drop_table('values_date')
op.drop_table('changes')
op.drop_table('change_requests')
op.drop_index(op.f('ix_change_values_str_id'), table_name='change_values_str')
op.drop_table('change_values_str')
op.drop_index(op.f('ix_change_values_int_id'), table_name='change_values_int')
op.drop_table('change_values_int')
op.drop_index(op.f('ix_change_values_float_id'), table_name='change_values_float')
op.drop_table('change_values_float')
op.drop_index(op.f('ix_change_values_fk_id'), table_name='change_values_fk')
op.drop_table('change_values_fk')
op.drop_index(op.f('ix_change_values_datetime_id'), table_name='change_values_datetime')
op.drop_table('change_values_datetime')
op.drop_index(op.f('ix_change_values_date_id'), table_name='change_values_date')
op.drop_table('change_values_date')
op.drop_index(op.f('ix_change_values_bool_id'), table_name='change_values_bool')
op.drop_table('change_values_bool')
# ### end Alembic commands ###
41 changes: 41 additions & 0 deletions backend/alembic/versions/ef9a7b3217c3_bound_fk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Bound FK
Revision ID: ef9a7b3217c3
Revises: 5e7b010c57b8
Create Date: 2022-02-04 08:55:06.006728
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'ef9a7b3217c3'
down_revision = '5e7b010c57b8'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_bound_foreign_keys_id', table_name='bound_foreign_keys')
op.drop_table('bound_foreign_keys')
op.add_column('attr_definitions', sa.Column('bound_schema_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'attr_definitions', 'schemas', ['bound_schema_id'], ['id'])
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'attr_definitions', type_='foreignkey')
op.drop_column('attr_definitions', 'bound_schema_id')
op.create_table('bound_foreign_keys',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('attr_def_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('schema_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['attr_def_id'], ['attr_definitions.id'], name='bound_foreign_keys_attr_def_id_fkey'),
sa.ForeignKeyConstraint(['schema_id'], ['schemas.id'], name='bound_foreign_keys_schema_id_fkey'),
sa.PrimaryKeyConstraint('id', name='bound_foreign_keys_pkey')
)
op.create_index('ix_bound_foreign_keys_id', 'bound_foreign_keys', ['id'], unique=False)
# ### end Alembic commands ###
4 changes: 3 additions & 1 deletion backend/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
from ..config import settings as s
from .context import pwd_context
from ..database import get_db
from ..models import User, Group, Schema, Entity
from ..models import Schema, Entity
from ..schemas.auth import RequirePermission
from .backends import get
from .crud import get_user, has_permission
from .enum import PermissionType
from .models import User, Group


_enabled_backends = get()
Expand Down
2 changes: 1 addition & 1 deletion backend/auth/backends/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm.session import Session

from ...models import User
from ..models import User


class AbstractBackend(ABC):
Expand Down
2 changes: 1 addition & 1 deletion backend/auth/backends/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from ldap3 import Server, Connection, LEVEL, ALL_ATTRIBUTES, SYNC, Entry

from ...config import settings
from ...models import User
from ...schemas.auth import UserCreateSchema
from ..crud import get_or_create_user
from ..models import User
from .abstract import AbstractBackend


Expand Down
2 changes: 1 addition & 1 deletion backend/auth/backends/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .abstract import AbstractBackend
from ..context import pwd_context
from ..crud import get_user
from ...models import User
from ..models import User


class Backend(AbstractBackend):
Expand Down
3 changes: 2 additions & 1 deletion backend/auth/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

from ..database import STATEMENTS
from .. import exceptions
from ..models import User, Group, Permission, UserGroup, Schema, Entity
from ..models import Schema, Entity
from .models import User, Group, Permission, UserGroup
from ..schemas.auth import BaseGroupSchema, PermissionSchema, GroupSchema, \
UserCreateSchema, RequirePermission, PermissionWithIdSchema
from .enum import PermissionType, PermissionTargetType, RecipientType
Expand Down
87 changes: 87 additions & 0 deletions backend/auth/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, Enum
from sqlalchemy.orm import backref, relationship
from sqlalchemy.sql.schema import UniqueConstraint

from ..base_models import Base
from .enum import RecipientType, PermissionTargetType, PermissionType

class Group(Base):
__tablename__ = 'groups'

id = Column(Integer, primary_key=True)
name = Column(String(128), unique=True, nullable=False)
parent_id = Column(Integer, ForeignKey('groups.id'))
parent = relationship('Group', remote_side=[id], backref=backref('subgroups'))

def __str__(self):
return self.name


class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(128), unique=True, nullable=False)
email = Column(String(128), unique=True, nullable=False)
password = Column(String, nullable=True)
firstname = Column(String(128), nullable=True)
lastname = Column(String(128), nullable=True)
is_active = Column(Boolean, default=True)

def __str__(self):
return self.username


class Permission(Base):
__tablename__ = 'permissions'
id = Column(Integer, primary_key=True)
recipient_type = Column(Enum(RecipientType), nullable=False)
recipient_id = Column(Integer, nullable=False)
obj_type = Column(Enum(PermissionTargetType), nullable=True)
obj_id = Column(Integer, nullable=True)
permission = Column(Enum(PermissionType), nullable=False)

user = relationship('User',
primaryjoin="and_(User.id == foreign(Permission.recipient_id), "
"Permission.recipient_type == 'USER')",
overlaps="group")
group = relationship('Group',
primaryjoin="and_(Group.id == foreign(Permission.recipient_id), "
"Permission.recipient_type == 'GROUP')",
overlaps="user")
schema = relationship('Schema',
primaryjoin="and_(Schema.id == foreign(Permission.obj_id), "
"Permission.obj_type == 'SCHEMA')",
overlaps="entity, managed_group"
)
entity = relationship('Entity',
primaryjoin="and_(Entity.id == foreign(Permission.obj_id), "
"Permission.obj_type == 'ENTITY')",
overlaps="schema, managed_group"
)
managed_group = relationship('Group',
primaryjoin="and_(Group.id == foreign(Permission.obj_id), "
"Permission.obj_type == 'GROUP')",
overlaps="schema, entity"
)

__table_args__ = (
UniqueConstraint('recipient_type', 'recipient_id', 'obj_type', 'obj_id', 'permission'),
)

@property
def recipient_name(self):
return self.user.username if self.recipient_type == RecipientType.USER else self.group.name


class UserGroup(Base):
__tablename__ = 'user_groups'
id = Column(Integer, primary_key=True)
group_id = Column(Integer, ForeignKey('groups.id'), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)

group = relationship('Group')
user = relationship('User')

__table_args__ = (
UniqueConstraint('user_id', 'group_id'),
)
33 changes: 33 additions & 0 deletions backend/base_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import typing
from typing import NamedTuple

from .database import Base

from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relationship


class Value(Base):
__abstract__ = True

id = Column(Integer, primary_key=True, index=True)

@declared_attr
def entity_id(cls):
return Column(Integer, ForeignKey('entities.id'))

@declared_attr
def attribute_id(cls):
return Column(Integer, ForeignKey('attributes.id'))

@declared_attr
def entity(cls):
return relationship('Entity')


class Mapping(NamedTuple):
model: Value
converter: type
filters: list = []

Loading

0 comments on commit 412d3a1

Please sign in to comment.