Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: [IMP] Access Control Enhancements #518

Draft
wants to merge 9 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ venv1/
.env*
.venv/
.vscode/
.devcontainer/
*.code-workspace
nohup.out
celerybeat-schedule.db
Expand Down
7 changes: 5 additions & 2 deletions source/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import json
import logging as logger
import os
import sys
import urllib.parse
from flask import Flask
from flask import session
Expand Down Expand Up @@ -109,7 +110,7 @@ def ac_current_user_has_manage_perms():
lm = LoginManager() # flask-loginmanager
lm.init_app(app) # init the login manager

ma = Marshmallow(app) # Init marshmallow
ma = Marshmallow(app) # Init marshmallow

dropzone = Dropzone(app)

Expand All @@ -134,4 +135,6 @@ def shutdown_session(exception=None):
db.session.remove()


from app import views
# Only import the remainder of the app if we are actually launching the app
if ".py" in sys.argv[0]:
from app import views
7 changes: 4 additions & 3 deletions source/app/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from app.configuration import SQLALCHEMY_BASE_ADMIN_URI, PG_DB_
import os
from alembic import context
from logging.config import fileConfig
from sqlalchemy import engine_from_config
Expand All @@ -11,13 +13,12 @@
# This line sets up loggers basically.
fileConfig(config.config_file_name)

import os
os.environ["ALEMBIC"] = "1"

from app.configuration import SQLALCHEMY_BASE_ADMIN_URI, PG_DB_

config.set_main_option('sqlalchemy.url', SQLALCHEMY_BASE_ADMIN_URI + PG_DB_)


# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
Expand Down Expand Up @@ -72,7 +73,7 @@ def run_migrations_online():
connection=connection, target_metadata=target_metadata
)

#with context.begin_transaction(): -- Fixes stuck transaction. Need more info on that
# with context.begin_transaction(): -- Fixes stuck transaction. Need more info on that
context.run_migrations()


Expand Down
69 changes: 69 additions & 0 deletions source/app/alembic/versions/c7a7606e930c_add_rbac_role_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Add RBAC Role tables

Revision ID: c7a7606e930c
Revises: 11aa5b725b8e
Create Date: 2024-07-04 12:26:01.299980

"""
from alembic import op
import sqlalchemy as sa
from app.alembic.alembic_utils import _has_table

# revision identifiers, used by Alembic.
revision = 'c7a7606e930c'
down_revision = '11aa5b725b8e'
branch_labels = None
depends_on = None


def upgrade():
"""Apply this upgrade by creating all the new RBAC tables"""

bind = op.get_bind()

# Create `role` table
if not _has_table("role"):
role_table = sa.Table(
'role', sa.MetaData(),
sa.Column('role_id', sa.BigInteger, primary_key=True),
sa.Column('name', sa.String(64)),
sa.Column('description', sa.Text),
sa.Column('entitlements', sa.JSON)
)
role_table.create(bind)

# Create `organisation_role` table
if not _has_table("organisation_role"):
organisation_role_table = sa.Table(
'organisation_role', sa.MetaData(),
sa.Column('role_id', sa.BigInteger, sa.ForeignKey(
'role.role_id'), primary_key=True),
sa.Column('org_id', sa.BigInteger, sa.ForeignKey(
'organisations.org_id'), primary_key=True),
sa.UniqueConstraint('role_id', 'org_id')
)
organisation_role_table.create(bind)

# Create `case_role` table
if not _has_table("case_role"):
case_role_table = sa.Table(
'case_role', sa.MetaData(),
sa.Column('role_id', sa.BigInteger, sa.ForeignKey(
'role.role_id'), primary_key=True),
sa.Column('case_id', sa.BigInteger, sa.ForeignKey(
'cases.case_id'), primary_key=True),
sa.UniqueConstraint('role_id', 'case_id')
)
case_role_table.create(bind)

bind.commit()


def downgrade():
"""
Downgrade this migration by dropping all the newly created tables
"""

op.drop_table('role')
op.drop_table('organisation_role')
op.drop_table('case_role')
53 changes: 53 additions & 0 deletions source/app/iris_engine/access_control/rbac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Literal


# ---- entitlements (where & what?) -------------------------------------------

ENTITLEMENT_GRANTS = Literal["read", "create", "update", "delete"]
"""grant types allowed on an entitlement"""


class Entitlement:
"""An Entitlement represents access to a group of resources/specific resource"""

grants: list[ENTITLEMENT_GRANTS] = []
"""the grant(s) (what kind of activity is this?)"""

resources: list[str] = []
"""the applicable resource by name

Values:
- "Case", allows 'grants' on 'Case' resources
- "Case:42", allows 'grants' on 'Case' #42 only
"""


# ---- roles (who & where?) ---------------------------------------------------

class Role:
"""A Role represents a group of entitlements"""

entitlements: list[Entitlement] = []
"""list of Entitlements granted to this role"""

# ---- logic ----

def __init__(self, entitlements: list[Entitlement]) -> None:
self.entitlements = entitlements

# ---- utils ----

@classmethod
def entitlement_check(self, *entitlement: list[Entitlement]) -> bool:
"""Check if the entitlement passed is granted by this role

Args:
entitlement (list[Entitlement]): The Entitlement to check

Returns:
bool: _description_
"""

# todo: implement this check logic

return False
58 changes: 52 additions & 6 deletions source/app/models/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,46 @@
from app import db


# ---- rbac ----

class Role(db.Model):
__tablename__ = 'role'

role_id = Column(BigInteger, primary_key=True)
name = Column(String(64))
description = Column(Text)
entitlements = Column(JSON)


class OrganisationRole(db.Model):
"""
Table handles assigning roles to the organisation-level
"""

__tablename__ = 'organisation_role'

role_id = Column(BigInteger, ForeignKey('role.role_id'), primary_key=True)
org_id = Column(BigInteger, ForeignKey(
'organisations.org_id'), primary_key=True)

UniqueConstraint('role_id', 'org_id')


class CaseRole(db.Model):
"""
Table handles assigning roles to the case-level
"""

__tablename__ = 'case_role'

role_id = Column(BigInteger, ForeignKey('role.role_id'), primary_key=True)
case_id = Column(BigInteger, ForeignKey('cases.case_id'), primary_key=True)

UniqueConstraint('role_id', 'case_id')


# ---- existing ----

class CaseAccessLevel(enum.Enum):
deny_all = 0x1
read_only = 0x2
Expand Down Expand Up @@ -70,7 +110,8 @@ class OrganisationCaseAccess(db.Model):
__tablename__ = "organisation_case_access"

id = Column(BigInteger, primary_key=True)
org_id = Column(BigInteger, ForeignKey('organisations.org_id'), nullable=False)
org_id = Column(BigInteger, ForeignKey(
'organisations.org_id'), nullable=False)
case_id = Column(BigInteger, ForeignKey('cases.case_id'), nullable=False)
access_level = Column(BigInteger, nullable=False)

Expand All @@ -90,7 +131,8 @@ class Group(db.Model):
group_description = Column(Text)
group_permissions = Column(BigInteger, nullable=False)
group_auto_follow = Column(Boolean, nullable=False, default=False)
group_auto_follow_access_level = Column(BigInteger, nullable=False, default=0)
group_auto_follow_access_level = Column(
BigInteger, nullable=False, default=0)

UniqueConstraint('group_name')

Expand All @@ -99,7 +141,8 @@ class GroupCaseAccess(db.Model):
__tablename__ = "group_case_access"

id = Column(BigInteger, primary_key=True)
group_id = Column(BigInteger, ForeignKey('groups.group_id'), nullable=False)
group_id = Column(BigInteger, ForeignKey(
'groups.group_id'), nullable=False)
case_id = Column(BigInteger, ForeignKey('cases.case_id'), nullable=False)
access_level = Column(BigInteger, nullable=False)

Expand Down Expand Up @@ -142,7 +185,8 @@ class UserOrganisation(db.Model):

id = Column(BigInteger, primary_key=True, nullable=False)
user_id = Column(BigInteger, ForeignKey('user.id'), nullable=False)
org_id = Column(BigInteger, ForeignKey('organisations.org_id'), nullable=False)
org_id = Column(BigInteger, ForeignKey(
'organisations.org_id'), nullable=False)
is_primary_org = Column(Boolean, nullable=False)

user = relationship('User')
Expand All @@ -156,7 +200,8 @@ class UserGroup(db.Model):

id = Column(BigInteger, primary_key=True, nullable=False)
user_id = Column(BigInteger, ForeignKey('user.id'), nullable=False)
group_id = Column(BigInteger, ForeignKey('groups.group_id'), nullable=False)
group_id = Column(BigInteger, ForeignKey(
'groups.group_id'), nullable=False)

user = relationship('User')
group = relationship('Group')
Expand All @@ -169,7 +214,8 @@ class UserClient(db.Model):

id = Column(BigInteger, primary_key=True, nullable=False)
user_id = Column(BigInteger, ForeignKey('user.id'), nullable=False)
client_id = Column(BigInteger, ForeignKey('client.client_id'), nullable=False)
client_id = Column(BigInteger, ForeignKey(
'client.client_id'), nullable=False)
access_level = Column(BigInteger, nullable=False)
allow_alerts = Column(Boolean, nullable=False)

Expand Down