Browse Source

Switch to a general "permissions" column on users

Previously there was a specific is_admin boolean column. This commit
changes to have a general permissions column which is stored in JSON,
and currently should either be a single string or list of strings. These
strings are used as the user's principals for the authorization system.
So now, setting a user as admin would involve adding the string "admin"
to their permissions column, instead of just setting is_admin to True.

As part of this change, I also moved the MutableDict associations onto
specific columns, instead of being attached to JSONB by default (since
this new column won't always be a dict).
merge-requests/29/head
Deimos 6 years ago
parent
commit
6a8290aa36
  1. 40
      tildes/alembic/versions/d33fb803a153_switch_to_general_permissions_column.py
  2. 7
      tildes/tildes/auth.py
  3. 5
      tildes/tildes/models/database_model.py
  4. 3
      tildes/tildes/models/log/log.py
  5. 3
      tildes/tildes/models/topic/topic.py
  6. 18
      tildes/tildes/models/user/user.py

40
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")

7
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: if user_id != request.user.user_id:
raise AssertionError("auth_callback called with different 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: def includeme(config: Configurator) -> None:

5
tildes/tildes/models/database_model.py

@ -4,9 +4,7 @@ from typing import Any, Optional, Type, TypeVar
from marshmallow import Schema from marshmallow import Schema
from sqlalchemy import event from sqlalchemy import event
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.schema import MetaData from sqlalchemy.schema import MetaData
from sqlalchemy.sql.schema import Table 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 # attach the listener for SQLAlchemy ORM attribute "set" events to all models
event.listen(DatabaseModel, "attribute_instrument", attach_set_listener) event.listen(DatabaseModel, "attribute_instrument", attach_set_listener)
# associate JSONB columns with MutableDict so value changes are detected
MutableDict.associate_with(JSONB)

3
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.dialects.postgresql import ENUM, INET, JSONB
from sqlalchemy.engine import Connection from sqlalchemy.engine import Connection
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql.expression import text from sqlalchemy.sql.expression import text
@ -51,7 +52,7 @@ class BaseLog:
@declared_attr @declared_attr
def info(self) -> Column: def info(self) -> Column:
"""Return the info column.""" """Return the info column."""
return Column(JSONB)
return Column(MutableDict.as_mutable(JSONB))
@declared_attr @declared_attr
def user(self) -> Any: def user(self) -> Any:

3
tildes/tildes/models/topic/topic.py

@ -16,6 +16,7 @@ from sqlalchemy import (
) )
from sqlalchemy.dialects.postgresql import ENUM, JSONB from sqlalchemy.dialects.postgresql import ENUM, JSONB
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import deferred, relationship from sqlalchemy.orm import deferred, relationship
from sqlalchemy.sql.expression import text from sqlalchemy.sql.expression import text
from sqlalchemy_utils import Ltree from sqlalchemy_utils import Ltree
@ -102,7 +103,7 @@ class Topic(DatabaseModel):
_markdown: Optional[str] = deferred(Column("markdown", Text)) _markdown: Optional[str] = deferred(Column("markdown", Text))
rendered_html: Optional[str] = Column(Text) rendered_html: Optional[str] = Column(Text)
link: 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_comments: int = Column(Integer, nullable=False, server_default="0", index=True)
num_votes: 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( _tags: List[Ltree] = Column(

18
tildes/tildes/models/user/user.py

@ -22,7 +22,7 @@ from sqlalchemy import (
Text, Text,
TIMESTAMP, 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.ext.hybrid import hybrid_property
from sqlalchemy.orm import deferred from sqlalchemy.orm import deferred
from sqlalchemy.sql.expression import text 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") open_new_tab_text: bool = Column(Boolean, nullable=False, server_default="false")
is_banned: 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_order: Optional[TopicSortOption] = Column(ENUM(TopicSortOption))
home_default_period: Optional[str] = Column(Text) home_default_period: Optional[str] = Column(Text)
_filtered_topic_tags: List[Ltree] = Column( _filtered_topic_tags: List[Ltree] = Column(
@ -204,3 +204,17 @@ class User(DatabaseModel):
def num_unread_total(self) -> int: def num_unread_total(self) -> int:
"""Return total number of unread items (notifications + messages).""" """Return total number of unread items (notifications + messages)."""
return self.num_unread_messages + self.num_unread_notifications 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")
Loading…
Cancel
Save