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:
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:

5
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)

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.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:

3
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(

18
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")
Loading…
Cancel
Save