Skip to content

Commit

Permalink
Registered group implementation (#334)
Browse files Browse the repository at this point in the history
* Registered group implementation

* Group create/update changed

* Fixed migration

* Added comments to capabilities

* Apply suggestions from code review

Co-authored-by: msm-code <msm@tailcall.net>

* Update mwdb/resources/auth.py

* Update mwdb/core/capabilities.py

Co-authored-by: msm-code <msm@tailcall.net>

* Apply suggestions from code review

Co-authored-by: msm-code <msm@tailcall.net>

Co-authored-by: msm-code <msm@tailcall.net>
  • Loading branch information
psrok1 and msm-code committed Mar 31, 2021
1 parent d633104 commit 0b40b21
Show file tree
Hide file tree
Showing 19 changed files with 321 additions and 47 deletions.
22 changes: 19 additions & 3 deletions mwdb/cli/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,30 @@ def _initialize(admin_password):
"""
Creates initial objects in database
"""
public_group = Group(name=Group.PUBLIC_GROUP_NAME, capabilities=[])
public_group = Group(
name=Group.PUBLIC_GROUP_NAME, capabilities=[], workspace=False, default=True
)
db.session.add(public_group)

everything_group = Group(
name=Group.EVERYTHING_GROUP_NAME, capabilities=[Capabilities.access_all_objects]
name=Group.DEFAULT_EVERYTHING_GROUP_NAME,
capabilities=[Capabilities.access_all_objects],
workspace=False,
)
db.session.add(everything_group)

registered_group = Group(
name=Group.DEFAULT_REGISTERED_GROUP_NAME,
capabilities=[
Capabilities.adding_files,
Capabilities.manage_profile,
Capabilities.personalize,
],
workspace=False,
default=True,
)
db.session.add(registered_group)

admin_group = Group(
name=app_config.mwdb.admin_login, capabilities=Capabilities.all(), private=True
)
Expand All @@ -32,7 +48,7 @@ def _initialize(admin_password):
login=app_config.mwdb.admin_login,
email="admin@mwdb.local",
additional_info="MWDB built-in administrator account",
groups=[admin_group, everything_group, public_group],
groups=[admin_group, everything_group, public_group, registered_group],
)
admin_user.reset_sessions()
admin_user.set_password(admin_password)
Expand Down
23 changes: 22 additions & 1 deletion mwdb/core/capabilities.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
class Capabilities(object):
# Can create/update users and groups
manage_users = "manage_users"
# Queried objects by members are automatically shared with this group
share_queried_objects = "share_queried_objects"
# All new uploaded objects are automatically shared with this group
access_all_objects = "access_all_objects"
# Can share objects with all groups, have access to complete list of groups
sharing_objects = "sharing_objects"
# Can add tags
adding_tags = "adding_tags"
# Can remove tags
removing_tags = "removing_tags"
# Can add comments
adding_comments = "adding_comments"
# Can remove comments
removing_comments = "removing_comments"
# Can add parents
adding_parents = "adding_parents"
# Can read all attributes, regardless of ACLs
reading_all_attributes = "reading_all_attributes"
# Can set all attributes, regardless of ACLs
adding_all_attributes = "adding_all_attributes"
# Can add/edit/remove attribute keys
managing_attributes = "managing_attributes"
removing_attributes = "removing_attributes"
# Can upload files
adding_files = "adding_files"
# Can upload configs
adding_configs = "adding_configs"
# Can upload blobs
adding_blobs = "adding_blobs"
# Requests are not rate-limited for members of this group
unlimited_requests = "unlimited_requests"
# Can remove objects
removing_objects = "removing_objects"
# Can manage own profile (add API keys, change a password)
manage_profile = "manage_profile"
# Can personalize own profile (mark as favorite, manage quick queries)
personalize = "personalize"

@classmethod
def all(cls):
Expand Down
4 changes: 3 additions & 1 deletion mwdb/core/search/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,9 @@ def get_condition(self, expression: Expression, remainder: List[str]) -> Any:
db.session.query(User)
.join(User.memberships)
.join(Member.group)
.filter(and_(g.auth_user.is_member(Group.id), Group.name != "public"))
.filter(
and_(g.auth_user.is_member(Group.id), Group.workspace.is_(True))
)
.filter(or_(Group.name == value, User.login == value))
).all()
# Regular users can see only uploads to its own groups
Expand Down
22 changes: 21 additions & 1 deletion mwdb/model/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,24 @@ class Group(db.Model):
capabilities = db.Column(
"capabilities", ARRAY(db.Text), nullable=False, server_default="{}"
)
# Group is user's private group
private = db.Column(db.Boolean, nullable=False, default=False)
# New users are automatically added to this group
default = db.Column(db.Boolean, nullable=False, default=False)
# Workspace groups have two traits:
# - group members can list all the other group memebers
# - they are candidates for sharing when upload_as:*
workspace = db.Column(db.Boolean, nullable=False, default=True)

members = db.relationship(
"Member", back_populates="group", cascade="all, delete-orphan"
)
users = association_proxy("members", "user", creator=lambda user: Member(user=user))

PUBLIC_GROUP_NAME = "public"
EVERYTHING_GROUP_NAME = "everything"
# These groups are just pre-created for convenience by 'mwdb-core configure'
DEFAULT_EVERYTHING_GROUP_NAME = "everything"
DEFAULT_REGISTERED_GROUP_NAME = "registered"

@property
def pending_group(self):
Expand All @@ -37,6 +46,10 @@ def pending_group(self):

@property
def immutable(self):
"""
Immutable groups can't be renamed, joined and left.
The only thing that can be changed are capabilities.
"""
return self.private or self.name == self.PUBLIC_GROUP_NAME

@property
Expand Down Expand Up @@ -89,6 +102,13 @@ def all_access_groups():
.all()
)

@staticmethod
def all_default_groups():
"""
Return all default groups
"""
return db.session.query(Group).filter(Group.default.is_(True)).all()


class Member(db.Model):
__tablename__ = "member"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Add Group.default and Group.workspace fields
Revision ID: e304b81836b0
Revises: c8ba40a69421
Create Date: 2021-03-30 16:51:19.733285
"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "e304b81836b0"
down_revision = "c8ba40a69421"
branch_labels = None
depends_on = None


def upgrade():
op.add_column("group", sa.Column("default", sa.Boolean(), nullable=True))
op.execute('UPDATE "group" SET "default"=TRUE WHERE name=\'public\'')
op.execute('UPDATE "group" SET "default"=FALSE WHERE name<>\'public\'')
op.alter_column("group", "default", existing_type=sa.Boolean(), nullable=False)

op.add_column("group", sa.Column("workspace", sa.Boolean(), nullable=True))
op.execute('UPDATE "group" SET "workspace"=FALSE WHERE name=\'public\'')
op.execute('UPDATE "group" SET "workspace"=TRUE WHERE name<>\'public\'')
op.alter_column("group", "workspace", existing_type=sa.Boolean(), nullable=False)


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("group", "workspace")
op.drop_column("group", "default")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Create 'registered' group if database is empty
Revision ID: f4ccb4be2170
Revises: e304b81836b0
Create Date: 2021-03-30 16:52:07.740584
"""
import logging

import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects.postgresql.array import ARRAY

# revision identifiers, used by Alembic.
revision = "f4ccb4be2170"
down_revision = "e304b81836b0"
branch_labels = None
depends_on = None

logger = logging.getLogger("alembic")

group_helper = sa.Table(
"group",
sa.MetaData(),
sa.Column("id", sa.Integer()),
sa.Column("name", sa.String(32)),
sa.Column("capabilities", ARRAY(sa.Text())),
sa.Column("private", sa.Boolean()),
sa.Column("default", sa.Boolean()),
sa.Column("workspace", sa.Boolean()),
)

user_helper = sa.Table(
"user",
sa.MetaData(),
sa.Column("id", sa.Integer()),
)

member_helper = sa.Table(
"member",
sa.MetaData(),
sa.Column("user_id", sa.Integer()),
sa.Column("group_id", sa.Integer()),
sa.Column("group_admin", sa.Boolean()),
)


def upgrade():
# If 'public' doesn't exist: assume that there are no builtin objects at all
# They will be created by 'mwdb-core configure' initializer
connection = op.get_bind()
public_group = connection.execute(
group_helper.select().where(group_helper.c.name == "public")
).fetchone()
if not public_group:
logger.warning(
"'public' group doesn't exist: assuming there are no objects to migrate"
)
return

# Create 'registered' group with 'public' capabilities and some extra ones
logger.info("Creating 'registered' group")
registered_group_id = next(
connection.execute(
group_helper.insert().returning(group_helper.c.id),
name="registered",
capabilities=(
public_group.capabilities
+ ["adding_files", "manage_profile", "personalize"]
),
private=False,
default=True,
workspace=False,
)
).id

# 'public' capabilities will be moved to 'registered' group
logger.info("Wiping 'public' group capabilities")
connection.execute(
group_helper.update()
.where(group_helper.c.name == "public")
.values(capabilities=[])
)

# Add all users to 'registered' group
logger.info("Adding all existing users to 'registered' group")
for user in connection.execute(user_helper.select()):
connection.execute(
member_helper.insert(),
user_id=user.id,
group_id=registered_group_id,
group_admin=False,
)


def downgrade():
raise NotImplementedError("This migration is not downgradable")
24 changes: 14 additions & 10 deletions mwdb/model/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mwdb.core.config import app_config

from . import db
from .group import Member
from .group import Group, Member
from .object import ObjectPermission, favorites


Expand Down Expand Up @@ -112,7 +112,7 @@ def create(
feed_quality=feed_quality or "high",
pending=pending,
disabled=False,
groups=[user_group, Group.public_group()],
groups=[user_group] + Group.all_default_groups(),
)
user.reset_sessions()

Expand Down Expand Up @@ -202,18 +202,22 @@ def set_group_admin(self, group_id, set_admin):
member.group_admin = set_admin

def has_access_to_object(self, object_id):
"""
Query filter for objects visible by this user
"""
return object_id.in_(
db.session.query(ObjectPermission.object_id).filter(
self.is_member(ObjectPermission.group_id)
)
)

def has_uploaded_object(self, object_id):
return object_id.in_(
db.session.query(ObjectPermission.object_id).filter(
and_(
ObjectPermission.related_object == ObjectPermission.object_id,
ObjectPermission.related_user_id == self.id,
)
)
def workspaces(self):
"""
Query for workspace groups for this user
"""
return (
db.session.query(Group)
.join(Group.members)
.join(Member.user)
.filter(and_(Member.user_id == self.id, Group.workspace.is_(True)))
)
2 changes: 1 addition & 1 deletion mwdb/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def get_shares_for_upload(upload_as):
"""
if upload_as == "*":
# If '*' is provided: share with all user's groups except 'public'
share_with = [group for group in g.auth_user.groups if group.name != "public"]
share_with = [group for group in g.auth_user.groups if group.workspace]
elif upload_as == "private":
share_with = [Group.get_by_name(g.auth_user.login)]
else:
Expand Down

0 comments on commit 0b40b21

Please sign in to comment.