mirror of
https://gitlab.com/tildes/tildes.git
synced 2026-04-16 06:18:34 +02:00
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.
This commit is contained in:
@@ -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")
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Normal file
44
tildes/tildes/models/user/user_permissions.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user