Browse Source

Move user permissions into their own table

This is a bit of an odd commit: it adds a user_permissions table that
has capabilities that are not yet usable. Specifically, the table allows
setting DENY permissions as well as restricting permissions to an
individual group, but neither of those work yet. I want to make sure
that the existing, limited permission system seems to transfer over
properly before adding the additional complexity for those.

The Alembic data migrations for this commit is fairly ugly, but seem to
work okay.
merge-requests/102/head
Deimos 5 years ago
parent
commit
62b0adc983
  1. 113
      tildes/alembic/versions/054aaef690cd_move_user_permissions_to_their_own_table.py
  2. 7
      tildes/tildes/auth.py
  3. 7
      tildes/tildes/enums.py
  4. 1
      tildes/tildes/models/user/__init__.py
  5. 15
      tildes/tildes/models/user/user.py
  6. 44
      tildes/tildes/models/user/user_permissions.py

113
tildes/alembic/versions/054aaef690cd_move_user_permissions_to_their_own_table.py

@ -0,0 +1,113 @@
"""Move user permissions to their own table
Revision ID: 054aaef690cd
Revises: 51a1012f4f63
Create Date: 2020-02-28 00:13:17.634015
"""
from collections import defaultdict
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from tildes.models.user import User
# revision identifiers, used by Alembic.
revision = "054aaef690cd"
down_revision = "51a1012f4f63"
branch_labels = None
depends_on = None
# minimal definition for users table to query/update
users_table = sa.sql.table(
"users",
sa.sql.column("user_id", sa.Integer),
sa.sql.column("permissions", sa.dialects.postgresql.JSONB(none_as_null=True)),
)
def upgrade():
permissions_table = op.create_table(
"user_permissions",
sa.Column("permission_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("group_id", sa.Integer(), nullable=True),
sa.Column("permission", sa.Text(), nullable=False),
sa.Column(
"permission_type",
postgresql.ENUM("ALLOW", "DENY", name="userpermissiontype"),
server_default="ALLOW",
nullable=False,
),
sa.ForeignKeyConstraint(
["group_id"],
["groups.group_id"],
name=op.f("fk_user_permissions_group_id_groups"),
),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
name=op.f("fk_user_permissions_user_id_users"),
),
sa.PrimaryKeyConstraint("permission_id", name=op.f("pk_user_permissions")),
)
# convert existing permissions to rows in the new table
session = sa.orm.Session(bind=op.get_bind())
users = session.query(users_table).filter("permissions" != None).all()
permission_rows = []
for user in users:
if isinstance(user.permissions, str):
permission_rows.append(
{"user_id": user.user_id, "permission": user.permissions}
)
elif isinstance(user.permissions, list):
for permission in user.permissions:
permission_rows.append(
{"user_id": user.user_id, "permission": permission}
)
if permission_rows:
op.bulk_insert(permissions_table, permission_rows)
op.drop_column("users", "permissions")
def downgrade():
op.add_column(
"users",
sa.Column(
"permissions",
postgresql.JSONB(astext_type=sa.Text()),
autoincrement=False,
nullable=True,
),
)
# convert user_permissions rows back to JSONB columns in the users table
session = sa.orm.Session(bind=op.get_bind())
permissions_table = sa.sql.table(
"user_permissions",
sa.sql.column("user_id", sa.Integer),
sa.sql.column("permission", sa.Text),
)
permissions_rows = session.query(permissions_table).all()
permissions_updates = defaultdict(list)
for permission in permissions_rows:
permissions_updates[permission.user_id].append(permission.permission)
for user_id, permissions in permissions_updates.items():
session.query(users_table).filter_by(user_id=user_id).update(
{"permissions": permissions}, synchronize_session=False
)
session.commit()
op.drop_table("user_permissions")
op.execute("drop type userpermissiontype")

7
tildes/tildes/auth.py

@ -11,6 +11,7 @@ from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPFound
from pyramid.request import Request
from pyramid.security import Allow, Everyone
from sqlalchemy.orm import joinedload
from tildes.models.user import User
@ -37,7 +38,11 @@ def get_authenticated_user(request: Request) -> Optional[User]:
if not user_id:
return None
query = request.query(User).filter_by(user_id=user_id)
query = (
request.query(User)
.options(joinedload("permissions"))
.filter_by(user_id=user_id)
)
return query.one_or_none()

7
tildes/tildes/enums.py

@ -281,3 +281,10 @@ class HTMLSanitizationContext(enum.Enum):
"""Enum for the possible contexts for HTML sanitization."""
USER_BIO = enum.auto()
class UserPermissionType(enum.Enum):
"""Enum for the types of user permissions."""
ALLOW = enum.auto()
DENY = enum.auto()

1
tildes/tildes/models/user/__init__.py

@ -3,4 +3,5 @@
from .user import User
from .user_group_settings import UserGroupSettings
from .user_invite_code import UserInviteCode
from .user_permissions import UserPermissions
from .user_rate_limit import UserRateLimit

15
tildes/tildes/models/user/user.py

@ -25,7 +25,7 @@ from sqlalchemy import (
Text,
TIMESTAMP,
)
from sqlalchemy.dialects.postgresql import ARRAY, ENUM, JSONB
from sqlalchemy.dialects.postgresql import ARRAY, ENUM
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import deferred
from sqlalchemy.sql.expression import text
@ -126,7 +126,6 @@ class User(DatabaseModel):
deleted_time: Optional[datetime] = Column(TIMESTAMP(timezone=True))
is_banned: bool = Column(Boolean, nullable=False, server_default="false")
banned_time: Optional[datetime] = Column(TIMESTAMP(timezone=True))
permissions: Any = Column(JSONB(none_as_null=True))
home_default_order: Optional[TopicSortOption] = Column(ENUM(TopicSortOption))
home_default_period: Optional[str] = Column(Text)
filtered_topic_tags: List[str] = Column(
@ -307,17 +306,7 @@ class User(DatabaseModel):
@property
def auth_principals(self) -> List[str]:
"""Return the user's authorization principals (used for permissions)."""
principals = []
# start with any principals manually defined in the permissions column
if not self.permissions:
pass
elif isinstance(self.permissions, str):
principals = [self.permissions]
elif isinstance(self.permissions, list):
principals = self.permissions
else:
raise ValueError("Unknown permissions format")
principals = [permission.auth_principal for permission in self.permissions]
# give the user the "comment.label" permission if they're over a week old
if self.age > timedelta(days=7):

44
tildes/tildes/models/user/user_permissions.py

@ -0,0 +1,44 @@
# Copyright (c) 2020 Tildes contributors <code@tildes.net>
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Contains the UserPermissions class."""
from sqlalchemy import Column, ForeignKey, Integer, Text
from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship
from tildes.enums import UserPermissionType
from tildes.models import DatabaseModel
from tildes.models.group import Group
from tildes.models.user import User
class UserPermissions(DatabaseModel):
"""Model for a user's permissions in a group (or all groups)."""
__tablename__ = "user_permissions"
permission_id: int = Column(Integer, primary_key=True)
user_id: int = Column(Integer, ForeignKey("users.user_id"), nullable=False)
group_id: int = Column(Integer, ForeignKey("groups.group_id"), nullable=True)
permission: str = Column(Text, nullable=False)
permission_type: UserPermissionType = Column(
ENUM(UserPermissionType), nullable=False, server_default="ALLOW"
)
user: User = relationship("User", innerjoin=True, backref="permissions")
group: Group = relationship("Group", innerjoin=True)
@property
def auth_principal(self) -> str:
"""Return the permission as a string usable as an auth principal.
WARNING: This isn't currently complete, and only handles ALLOW for all groups.
"""
if self.permission_type != UserPermissionType.ALLOW:
raise ValueError("Not an ALLOW permission.")
if self.group_id:
raise ValueError("Not an all-groups permission.")
return self.permission
Loading…
Cancel
Save