diff --git a/tildes/alembic/versions/d33fb803a153_switch_to_general_permissions_column.py b/tildes/alembic/versions/d33fb803a153_switch_to_general_permissions_column.py new file mode 100644 index 0000000..8f1fdf9 --- /dev/null +++ b/tildes/alembic/versions/d33fb803a153_switch_to_general_permissions_column.py @@ -0,0 +1,40 @@ +"""Switch to general permissions column + +Revision ID: d33fb803a153 +Revises: 67e332481a6e +Create Date: 2018-08-16 23:07:07.643208 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "d33fb803a153" +down_revision = "67e332481a6e" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "users", + sa.Column( + "permissions", postgresql.JSONB(astext_type=sa.Text()), nullable=True + ), + ) + op.drop_column("users", "is_admin") + + +def downgrade(): + op.add_column( + "users", + sa.Column( + "is_admin", + sa.BOOLEAN(), + server_default=sa.text("false"), + autoincrement=False, + nullable=False, + ), + ) + op.drop_column("users", "permissions") diff --git a/tildes/tildes/auth.py b/tildes/tildes/auth.py index 519ce67..8d3def3 100644 --- a/tildes/tildes/auth.py +++ b/tildes/tildes/auth.py @@ -56,12 +56,7 @@ def auth_callback(user_id: int, request: Request) -> Optional[Sequence[str]]: if user_id != request.user.user_id: raise AssertionError("auth_callback called with different user_id") - principals = [] - - if request.user.is_admin: - principals.append("admin") - - return principals + return request.user.auth_principals def includeme(config: Configurator) -> None: diff --git a/tildes/tildes/models/database_model.py b/tildes/tildes/models/database_model.py index 24a5297..0e166e3 100644 --- a/tildes/tildes/models/database_model.py +++ b/tildes/tildes/models/database_model.py @@ -4,9 +4,7 @@ from typing import Any, Optional, Type, TypeVar from marshmallow import Schema from sqlalchemy import event -from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.schema import MetaData from sqlalchemy.sql.schema import Table @@ -124,6 +122,3 @@ DatabaseModel = declarative_base( # pylint: disable=invalid-name # attach the listener for SQLAlchemy ORM attribute "set" events to all models event.listen(DatabaseModel, "attribute_instrument", attach_set_listener) - -# associate JSONB columns with MutableDict so value changes are detected -MutableDict.associate_with(JSONB) diff --git a/tildes/tildes/models/log/log.py b/tildes/tildes/models/log/log.py index ea1a4c5..c44c98d 100644 --- a/tildes/tildes/models/log/log.py +++ b/tildes/tildes/models/log/log.py @@ -7,6 +7,7 @@ from sqlalchemy import BigInteger, Column, event, ForeignKey, Integer, Table, TI from sqlalchemy.dialects.postgresql import ENUM, INET, JSONB from sqlalchemy.engine import Connection from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.orm import relationship from sqlalchemy.sql.expression import text @@ -51,7 +52,7 @@ class BaseLog: @declared_attr def info(self) -> Column: """Return the info column.""" - return Column(JSONB) + return Column(MutableDict.as_mutable(JSONB)) @declared_attr def user(self) -> Any: diff --git a/tildes/tildes/models/topic/topic.py b/tildes/tildes/models/topic/topic.py index 4b0ad94..c79d168 100644 --- a/tildes/tildes/models/topic/topic.py +++ b/tildes/tildes/models/topic/topic.py @@ -16,6 +16,7 @@ from sqlalchemy import ( ) from sqlalchemy.dialects.postgresql import ENUM, JSONB from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.orm import deferred, relationship from sqlalchemy.sql.expression import text from sqlalchemy_utils import Ltree @@ -102,7 +103,7 @@ class Topic(DatabaseModel): _markdown: Optional[str] = deferred(Column("markdown", Text)) rendered_html: Optional[str] = Column(Text) link: Optional[str] = Column(Text) - content_metadata: Dict[str, Any] = Column(JSONB) + content_metadata: Dict[str, Any] = Column(MutableDict.as_mutable(JSONB)) num_comments: int = Column(Integer, nullable=False, server_default="0", index=True) num_votes: int = Column(Integer, nullable=False, server_default="0", index=True) _tags: List[Ltree] = Column( diff --git a/tildes/tildes/models/user/user.py b/tildes/tildes/models/user/user.py index c808c0f..fe6f85c 100644 --- a/tildes/tildes/models/user/user.py +++ b/tildes/tildes/models/user/user.py @@ -22,7 +22,7 @@ from sqlalchemy import ( Text, TIMESTAMP, ) -from sqlalchemy.dialects.postgresql import ARRAY, ENUM +from sqlalchemy.dialects.postgresql import ARRAY, ENUM, JSONB from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import deferred from sqlalchemy.sql.expression import text @@ -88,7 +88,7 @@ class User(DatabaseModel): ) open_new_tab_text: bool = Column(Boolean, nullable=False, server_default="false") is_banned: bool = Column(Boolean, nullable=False, server_default="false") - is_admin: bool = Column(Boolean, nullable=False, server_default="false") + permissions: Any = Column(JSONB) home_default_order: Optional[TopicSortOption] = Column(ENUM(TopicSortOption)) home_default_period: Optional[str] = Column(Text) _filtered_topic_tags: List[Ltree] = Column( @@ -204,3 +204,17 @@ class User(DatabaseModel): def num_unread_total(self) -> int: """Return total number of unread items (notifications + messages).""" return self.num_unread_messages + self.num_unread_notifications + + @property + def auth_principals(self) -> List[str]: + """Return the user's authorization principals (used for permissions).""" + if not self.permissions: + return [] + + if isinstance(self.permissions, str): + return [self.permissions] + + if isinstance(self.permissions, list): + return self.permissions + + raise ValueError("Unknown permissions format")