Skip to content
This repository has been archived by the owner on Nov 30, 2022. It is now read-only.

Commit

Permalink
#391 track user privileges (#425)
Browse files Browse the repository at this point in the history
* Track user permissions across sessions

* Add tests for user permission updates

* Switch to autogenerated migration

* Update ClientDetail when user scope is updated

* Update UserPermissions validation

* Update permission URN
  • Loading branch information
TheAndrewJackson authored May 3, 2022
1 parent d7a5e6d commit 894aa1b
Show file tree
Hide file tree
Showing 14 changed files with 559 additions and 11 deletions.
2 changes: 2 additions & 0 deletions src/fidesops/api/v1/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
storage_endpoints,
saas_config_endpoints,
user_endpoints,
user_permission_endpoints,
)


Expand All @@ -31,3 +32,4 @@
api_router.include_router(storage_endpoints.router)
api_router.include_router(saas_config_endpoints.router)
api_router.include_router(user_endpoints.router)
api_router.include_router(user_permission_endpoints.router)
8 changes: 6 additions & 2 deletions src/fidesops/api/v1/endpoints/user_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from fidesops.api.v1.urn_registry import V1_URL_PREFIX
from fidesops.models.client import ADMIN_UI_ROOT, ClientDetail
from fidesops.models.fidesops_user import FidesopsUser
from fidesops.models.fidesops_user_permissions import FidesopsUserPermissions
from fidesops.schemas.oauth import AccessToken
from fidesops.schemas.user import (
UserCreate,
Expand All @@ -37,9 +38,9 @@

from fidesops.api.v1.scope_registry import (
USER_CREATE,
PRIVACY_REQUEST_READ,
USER_READ,
USER_DELETE,
SCOPE_REGISTRY,
)

logger = logging.getLogger(__name__)
Expand All @@ -54,7 +55,7 @@ def perform_login(db: Session, user: FidesopsUser) -> ClientDetail:
if not client:
logger.info("Creating client for login")
client, _ = ClientDetail.create_client_and_secret(
db, SCOPE_REGISTRY, user_id=user.id
db, user.permissions.scopes, user_id=user.id
)

user.last_login_at = datetime.utcnow()
Expand Down Expand Up @@ -82,6 +83,9 @@ def create_user(

user = FidesopsUser.create(db=db, data=user_data.dict())
logger.info(f"Created user with id: '{user.id}'.")
FidesopsUserPermissions.create(
db=db, data={"user_id": user.id, "scopes": [PRIVACY_REQUEST_READ]}
)
return user


Expand Down
93 changes: 93 additions & 0 deletions src/fidesops/api/v1/endpoints/user_permission_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import logging
from fastapi import Security, Depends, APIRouter, HTTPException
from starlette.status import HTTP_404_NOT_FOUND, HTTP_201_CREATED, HTTP_400_BAD_REQUEST

from fidesops.api import deps
from fidesops.api.v1 import urn_registry as urls
from fidesops.api.v1.urn_registry import V1_URL_PREFIX
from fidesops.models.fidesops_user import FidesopsUser
from fidesops.models.fidesops_user_permissions import FidesopsUserPermissions
from fidesops.schemas.oauth import AccessToken
from fidesops.util.oauth_util import verify_oauth_client
from sqlalchemy.orm import Session
from fidesops.api.v1.scope_registry import (
USER_PERMISSION_CREATE,
USER_PERMISSION_UPDATE,
USER_PERMISSION_READ,
)
from fidesops.schemas.user_permission import (
UserPermissionsResponse,
UserPermissionsCreate,
UserPermissionsEdit,
)

logger = logging.getLogger(__name__)
router = APIRouter(tags=["User Permissions"], prefix=V1_URL_PREFIX)


def validate_user_id(db: Session, user_id: str) -> FidesopsUser:
user = FidesopsUser.get_by(db, field="id", value=user_id)

if not user:
raise HTTPException(
status_code=HTTP_404_NOT_FOUND, detail=f"No user found with id {user_id}."
)
return user


@router.post(
urls.USER_PERMISSIONS,
dependencies=[Security(verify_oauth_client, scopes=[USER_PERMISSION_CREATE])],
status_code=HTTP_201_CREATED,
response_model=UserPermissionsResponse,
)
def create_user_permissions(
*,
db: Session = Depends(deps.get_db),
user_id: str,
permissions: UserPermissionsCreate,
) -> FidesopsUserPermissions:
user = validate_user_id(db, user_id)
if user.permissions is not None:
raise HTTPException(
status_code=HTTP_400_BAD_REQUEST,
detail=f"This user already has permissions set.",
)
logger.info("Created FidesopsUserPermission record")
return FidesopsUserPermissions.create(
db=db, data={"user_id": user_id, **permissions.dict()}
)


@router.put(
urls.USER_PERMISSIONS,
dependencies=[Security(verify_oauth_client, scopes=[USER_PERMISSION_UPDATE])],
response_model=UserPermissionsResponse,
)
def update_user_permissions(
*,
db: Session = Depends(deps.get_db),
user_id: str,
permissions: UserPermissionsEdit,
) -> FidesopsUserPermissions:
user = validate_user_id(db, user_id)
logger.info("Updated FidesopsUserPermission record")
if user.client:
user.client.update(db=db, data={"scopes": permissions.scopes})
return FidesopsUserPermissions.create_or_update(
db=db,
data={"id": user.permissions.id, "user_id": user_id, **permissions.dict()},
)


@router.get(
urls.USER_PERMISSIONS,
dependencies=[Security(verify_oauth_client, scopes=[USER_PERMISSION_READ])],
response_model=UserPermissionsResponse,
)
def get_user_permissions(
*, db: Session = Depends(deps.get_db), user_id: str
) -> FidesopsUserPermissions:
validate_user_id(db, user_id)
logger.info("Retrieved FidesopsUserPermission record")
return FidesopsUserPermissions.get_by(db, field="user_id", value=user_id)
7 changes: 7 additions & 0 deletions src/fidesops/api/v1/scope_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
USER_READ = "user:read"
USER_DELETE = "user:delete"

USER_PERMISSION_CREATE = "user-permission:create"
USER_PERMISSION_UPDATE = "user-permission:update"
USER_PERMISSION_READ = "user-permission:read"

SCOPE_REGISTRY = [
CLIENT_CREATE,
CLIENT_UPDATE,
Expand Down Expand Up @@ -84,4 +88,7 @@
USER_CREATE,
USER_READ,
USER_DELETE,
USER_PERMISSION_CREATE,
USER_PERMISSION_UPDATE,
USER_PERMISSION_READ,
]
3 changes: 3 additions & 0 deletions src/fidesops/api/v1/urn_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
USERS = "/user"
USER_DETAIL = "/user/{user_id}"

# User Permission URLs
USER_PERMISSIONS = "/user/{user_id}/permission"

# Login URLs
LOGIN = "/login"
LOGOUT = "/logout"
Expand Down
1 change: 1 addition & 0 deletions src/fidesops/db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
from fidesops.models.privacy_request import PrivacyRequest
from fidesops.models.storage import StorageConfig
from fidesops.models.fidesops_user import FidesopsUser
from fidesops.models.fidesops_user_permissions import FidesopsUserPermissions
20 changes: 20 additions & 0 deletions src/fidesops/models/fidesops_user_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from sqlalchemy import Column, String, ARRAY, ForeignKey
from sqlalchemy.orm import relationship, backref

from fidesops.db.base_class import Base
from fidesops.models.fidesops_user import FidesopsUser
from fidesops.api.v1.scope_registry import PRIVACY_REQUEST_READ


class FidesopsUserPermissions(Base):
"""The DB ORM model for FidesopsUserPermissions"""

user_id = Column(String, ForeignKey(FidesopsUser.id), nullable=False, unique=True)
# escaping curly braces requires doubling them. Not a "\". So {{{test123}}} renders as {test123}
scopes = Column(
ARRAY(String), nullable=False, default=f"{{{PRIVACY_REQUEST_READ}}}"
)
user = relationship(
FidesopsUser,
backref=backref("permissions", cascade="all,delete", uselist=False),
)
36 changes: 36 additions & 0 deletions src/fidesops/schemas/user_permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import List
from pydantic import validator
from fastapi import HTTPException
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
from fidesops.schemas.base_class import BaseSchema
from fidesops.api.v1.scope_registry import SCOPE_REGISTRY


class UserPermissionsCreate(BaseSchema):
"""Data required to create a FidesopsUserPermissions record"""

scopes: List[str]

@validator("scopes")
def validate_scopes(cls, scopes: List[str]) -> List[str]:
"""Validates that all incoming scopes are valid"""
diff = set(scopes).difference(set(SCOPE_REGISTRY))
if len(diff) > 0:
raise HTTPException(
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid Scope(s) {diff}. Scopes must be one of {SCOPE_REGISTRY}.",
)
return scopes


class UserPermissionsEdit(UserPermissionsCreate):
"""Data required to edit a FidesopsUserPermissions record"""

id: str


class UserPermissionsResponse(UserPermissionsCreate):
"""Response after creating, editing, or retrieving a FidesopsUserPermissions record"""

id: str
user_id: str
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""add fidesops user permissions
Revision ID: 90070db16d05
Revises: 530fb8533ca4
Create Date: 2022-04-27 17:24:31.548916
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "90070db16d05"
down_revision = "530fb8533ca4"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"fidesopsuserpermissions",
sa.Column("id", sa.String(length=255), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column("user_id", sa.String(), nullable=False),
sa.Column("scopes", sa.ARRAY(sa.String()), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["fidesopsuser.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id"),
)
op.create_index(
op.f("ix_fidesopsuserpermissions_id"),
"fidesopsuserpermissions",
["id"],
unique=False,
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f("ix_fidesopsuserpermissions_id"), table_name="fidesopsuserpermissions"
)
op.drop_table("fidesopsuserpermissions")
# ### end Alembic commands ###
Loading

0 comments on commit 894aa1b

Please sign in to comment.