Skip to content

Commit

Permalink
Re-reverse provider-user relationship orientation
Browse files Browse the repository at this point in the history
Effectively reverts ea6b455, 5a8c1a8 and 3f12735

As discussed in #23, providers are analogous to collections in terms of their grouping function - providers on the input side, collections on the output side. Unlike a role, which does nothing on its own but modifies users' capabilities when assigned to them, a provider object can exist independently of users. This is true in general, but notably in the case of an 'automated' provider with its own client (e.g. SAEON Obs DB). From this perspective it therefore makes sense to assign users to a provider rather than the converse.
  • Loading branch information
marksparkza committed Apr 18, 2024
1 parent 097ab3b commit 25a678f
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 153 deletions.
Binary file modified ERD.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""Archive integration
Revision ID: 7102c8734154
Revision ID: e9ac68f05e6b
Revises: df57d06e1ee5
Create Date: 2024-03-26 14:43:52.793498
Create Date: 2024-04-17 13:25:02.261646
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '7102c8734154'
revision = 'e9ac68f05e6b'
down_revision = 'df57d06e1ee5'
branch_labels = None
depends_on = None
Expand Down Expand Up @@ -38,6 +38,13 @@ def upgrade():
ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('provider_user',
sa.Column('provider_id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.ForeignKeyConstraint(['provider_id'], ['provider.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('provider_id', 'user_id')
)
op.create_table('resource',
sa.Column('id', sa.String(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
Expand All @@ -51,13 +58,6 @@ def upgrade():
sa.ForeignKeyConstraint(['provider_id'], ['provider.id'], ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user_provider',
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('provider_id', sa.String(), nullable=False),
sa.ForeignKeyConstraint(['provider_id'], ['provider.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id', 'provider_id')
)
op.create_table('archive_resource',
sa.Column('archive_id', sa.String(), nullable=False),
sa.Column('resource_id', sa.String(), nullable=False),
Expand Down Expand Up @@ -88,13 +88,13 @@ def upgrade():
op.add_column('client', sa.Column('provider_id', sa.String(), nullable=True))
op.create_foreign_key('client_provider_id_fkey', 'client', 'provider', ['provider_id'], ['id'], ondelete='SET NULL')
op.drop_column('client', 'collection_specific')
op.add_column('identity_audit', sa.Column('_providers', sa.ARRAY(sa.String()), nullable=True))
op.add_column('provider_audit', sa.Column('_users', sa.ARRAY(sa.String()), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - adjusted ###
op.drop_column('identity_audit', '_providers')
op.drop_column('provider_audit', '_users')
op.add_column('client', sa.Column('collection_specific', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False))
op.drop_constraint('client_provider_id_fkey', 'client', type_='foreignkey')
op.drop_column('client', 'provider_id')
Expand All @@ -109,8 +109,8 @@ def downgrade():
op.drop_table('record_package')
op.drop_table('package_resource')
op.drop_table('archive_resource')
op.drop_table('user_provider')
op.drop_table('resource')
op.drop_table('provider_user')
op.drop_table('package')
op.drop_table('archive')
# ### end Alembic commands ###
12 changes: 11 additions & 1 deletion odp/api/routers/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def output_audit_model(row) -> ProviderAuditModel:
provider_id=row.ProviderAudit._id,
provider_key=row.ProviderAudit._key,
provider_name=row.ProviderAudit._name,
provider_users=row.ProviderAudit._users or [],
)


Expand All @@ -70,6 +71,7 @@ def create_audit_record(
_id=provider.id,
_key=provider.key,
_name=provider.name,
_users=[user.id for user in provider.users],
).save()


Expand Down Expand Up @@ -142,6 +144,10 @@ async def create_provider(
provider = Provider(
key=provider_in.key,
name=provider_in.name,
users=[
Session.get(User, user_id)
for user_id in provider_in.user_ids
],
timestamp=(timestamp := datetime.now(timezone.utc)),
)
provider.save()
Expand Down Expand Up @@ -179,10 +185,14 @@ async def update_provider(

if (
provider.key != provider_in.key or
provider.name != provider_in.name
provider.name != provider_in.name # todo: or user_ids != ...
):
provider.key = provider_in.key
provider.name = provider_in.name
provider.users = [
Session.get(User, user_id)
for user_id in provider_in.user_ids
]
provider.timestamp = (timestamp := datetime.now(timezone.utc))
provider.save()
create_audit_record(auth, provider, timestamp, AuditCommand.update)
Expand Down
9 changes: 2 additions & 7 deletions odp/api/routers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from odp.const import ODPScope
from odp.const.db import IdentityCommand
from odp.db import Session
from odp.db.models import IdentityAudit, Provider, Role, User
from odp.db.models import IdentityAudit, Role, User

router = APIRouter()

Expand Down Expand Up @@ -49,7 +49,6 @@ def create_audit_record(
_email=user.email,
_active=user.active,
_roles=[role.id for role in user.roles],
_providers=[provider.id for provider in user.providers]
).save()


Expand All @@ -67,7 +66,6 @@ def output_audit_model(row) -> IdentityAuditModel:
user_email=row.IdentityAudit._email,
user_active=row.IdentityAudit._active,
user_roles=row.IdentityAudit._roles,
user_providers=row.IdentityAudit._providers,
)


Expand Down Expand Up @@ -109,15 +107,12 @@ async def update_user(
if not (user := Session.get(User, user_in.id)):
raise HTTPException(HTTP_404_NOT_FOUND)

# todo: if different...
user.active = user_in.active
user.roles = [
Session.get(Role, role_id)
for role_id in user_in.role_ids
]
user.providers = [
Session.get(Provider, provider_id)
for provider_id in user_in.provider_ids
]
user.save()
create_audit_record(auth, user, IdentityCommand.edit)

Expand Down
4 changes: 2 additions & 2 deletions odp/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from .client import Client, ClientScope
from .collection import Collection, CollectionAudit, CollectionTag, CollectionTagAudit
from .package import Package, PackageResource
from .provider import Provider, ProviderAudit
from .provider import Provider, ProviderAudit, ProviderUser
from .record import PublishedRecord, Record, RecordAudit, RecordPackage, RecordTag, RecordTagAudit
from .resource import Resource
from .role import Role, RoleCollection, RoleScope
from .schema import Schema
from .scope import Scope
from .tag import Tag
from .user import IdentityAudit, User, UserProvider, UserRole
from .user import IdentityAudit, User, UserRole
from .vocabulary import Vocabulary, VocabularyTerm, VocabularyTermAudit
27 changes: 22 additions & 5 deletions odp/db/models/provider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import uuid

from sqlalchemy import Column, Enum, Identity, Integer, String, TIMESTAMP
from sqlalchemy import ARRAY, Column, Enum, ForeignKey, Identity, Integer, String, TIMESTAMP
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship

Expand All @@ -21,19 +21,35 @@ class Provider(Base):
name = Column(String, nullable=False)
timestamp = Column(TIMESTAMP(timezone=True), nullable=False)

# view of associated users via many-to-many user_provider relation
provider_users = relationship('UserProvider', viewonly=True)
users = association_proxy('provider_users', 'user')

# view of associated collections (one-to-many)
collections = relationship('Collection', viewonly=True)

# view of associated clients (one-to-many)
clients = relationship('Client', viewonly=True)

# many-to-many provider_user entities are persisted by
# assigning/removing User instances to/from users
provider_users = relationship('ProviderUser', cascade='all, delete-orphan', passive_deletes=True)
users = association_proxy('provider_users', 'user', creator=lambda u: ProviderUser(user=u))

_repr_ = 'id', 'key', 'name'


class ProviderUser(Base):
"""Model of a many-to-many provider-user association, which enables
partitioning of package/resource access by (groups of) user."""

__tablename__ = 'provider_user'

provider_id = Column(String, ForeignKey('provider.id', ondelete='CASCADE'), primary_key=True)
user_id = Column(String, ForeignKey('user.id', ondelete='CASCADE'), primary_key=True)

provider = relationship('Provider', viewonly=True)
user = relationship('User')

_repr_ = 'provider_id', 'user_id'


class ProviderAudit(Base):
"""Provider audit log."""

Expand All @@ -48,3 +64,4 @@ class ProviderAudit(Base):
_id = Column(String, nullable=False)
_key = Column(String, nullable=False)
_name = Column(String, nullable=False)
_users = Column(ARRAY(String))
23 changes: 3 additions & 20 deletions odp/db/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ class User(Base):
user_roles = relationship('UserRole', cascade='all, delete-orphan', passive_deletes=True)
roles = association_proxy('user_roles', 'role', creator=lambda r: UserRole(role=r))

# many-to-many user_provider entities are persisted by
# assigning/removing Provider instances to/from providers
user_providers = relationship('UserProvider', cascade='all, delete-orphan', passive_deletes=True)
providers = association_proxy('user_providers', 'provider', creator=lambda p: UserProvider(provider=p))
# view of associated providers via many-to-many provider_user relation
user_providers = relationship('ProviderUser', viewonly=True)
providers = association_proxy('user_providers', 'provider')

_repr_ = 'id', 'email', 'name', 'active', 'verified'

Expand All @@ -49,21 +48,6 @@ class UserRole(Base):
_repr_ = 'user_id', 'role_id'


class UserProvider(Base):
"""A user-provider association, which enables partitioning of
package/resource access by (groups of) user."""

__tablename__ = 'user_provider'

user_id = Column(String, ForeignKey('user.id', ondelete='CASCADE'), primary_key=True)
provider_id = Column(String, ForeignKey('provider.id', ondelete='CASCADE'), primary_key=True)

user = relationship('User', viewonly=True)
provider = relationship('Provider')

_repr_ = 'user_id', 'provider_id'


class IdentityAudit(Base):
"""User identity audit log."""

Expand All @@ -81,4 +65,3 @@ class IdentityAudit(Base):
_email = Column(String)
_active = Column(Boolean)
_roles = Column(ARRAY(String))
_providers = Column(ARRAY(String))
Loading

0 comments on commit 25a678f

Please sign in to comment.