Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Application ID for groups #78

Merged
merged 4 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions alembic/versions/c3ded8ff2ea8_group_with_application_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Group with application ID

Revision ID: c3ded8ff2ea8
Revises: 746fcad41145
Create Date: 2024-08-05 18:39:23.470012

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = 'c3ded8ff2ea8'
down_revision = '746fcad41145'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('groups', sa.Column('application_id', postgresql.UUID(as_uuid=True), nullable=True))
op.create_foreign_key('fk_groups_applications_id', 'groups', 'applications', ['application_id'], ['id'], ondelete='CASCADE')
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('groups', 'application_id')
# ### end Alembic commands ###
50 changes: 38 additions & 12 deletions brood/actions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
User-related Brood operations
"""

import base64
import json
import logging
Expand Down Expand Up @@ -185,6 +186,12 @@ class NoInheritancePermission(Exception):
"""


class DifferentApplications(Exception):
"""
Raised when applications do not match.
"""


class TokenInvalidParameters(ValueError):
"""
Raised when operations are applied to a token but invalid parameters are provided.
Expand Down Expand Up @@ -464,9 +471,9 @@ def create_user(
payload = json.loads(payload_json)
verified = verify(
authorization_payload=payload,
application_to_check=str(application_id)
if application_id is not None
else "",
application_to_check=(
str(application_id) if application_id is not None else ""
),
)
if not verified:
logger.info("Web3 registration verification error")
Expand Down Expand Up @@ -657,6 +664,7 @@ def get_user_with_groups(
autogenerated=group.autogenerated,
group_name=group.name,
parent=group.parent,
application_id=group.application_id,
)
)

Expand Down Expand Up @@ -744,6 +752,7 @@ def get_current_user_with_groups_by_token(
autogenerated=object[3].autogenerated,
group_name=object[3].name,
parent=object[3].parent,
application_id=object[3].application_id,
)
)

Expand Down Expand Up @@ -1353,15 +1362,15 @@ def get_group(
session: Session,
group_id: Optional[uuid.UUID] = None,
group_name: Optional[str] = None,
application_id: Optional[str] = None,
user_id: Optional[uuid.UUID] = None,
) -> Group:
"""
Get a group by group_name, or group_id. If more than one of those fields is provided, will
Get a group by group_id, or group_name with application_id. If more than one of those fields is provided, will
look for a group having ALL the given parameters.

TODO: This should return a list of groups because the group_name is no longer unique. For now,
if a user tries to find a group by group_name and there are multiple groups with that name,

"""

if group_id is None and group_name is None:
Expand All @@ -1371,8 +1380,14 @@ def get_group(
query = session.query(Group)
if group_id is not None:
query = query.filter(Group.id == group_id)
if group_name is not None:
query = query.filter(Group.name == group_name)
elif group_name is not None:
query = query.filter(
Group.name == group_name, Group.application_id == application_id
)
else:
raise GroupInvalidParameters(
"In order to get group, at least one of group_id, or group_name must be specified"
)

if user_id is not None:
query = query.join(GroupUser, Group.id == GroupUser.group_id).filter(
Expand Down Expand Up @@ -1429,6 +1444,7 @@ def create_group(
name=group_name,
autogenerated=True if user.autogenerated is True else False,
parent=parent_id,
application_id=user.application_id,
)

try:
Expand Down Expand Up @@ -1473,6 +1489,7 @@ def get_groups_for_user(
GroupUser.user_id,
Group.autogenerated,
Group.parent,
Group.application_id,
)
.join(Group)
.filter(user_id == GroupUser.user_id)
Expand All @@ -1496,6 +1513,7 @@ def get_groups_for_user(
autogenerated=group.autogenerated,
group_name=group.name,
parent=group.parent,
application_id=group.application_id,
num_users=num_users,
num_seats=num_seats,
)
Expand Down Expand Up @@ -1571,6 +1589,9 @@ def set_user_in_group(

group = get_group(session, group_id=group_id)

if user.application_id != group.application_id:
raise DifferentApplications("User and group belong to different applications")

# Check what role proposed user already has
user_role = (
session.query(GroupUser)
Expand Down Expand Up @@ -1653,6 +1674,7 @@ def change_group_name(
for subscription_plan in group.subscriptions
],
parent=group.parent,
application_id=group.application_id,
created_at=group.created_at,
updated_at=group.updated_at,
)
Expand All @@ -1662,24 +1684,28 @@ def delete_user_from_group(
session: Session,
group_id: uuid.UUID,
current_user_autogenerated: bool,
user_id: Optional[uuid.UUID] = None,
username: Optional[str] = None,
email: Optional[str] = None,
application_id: Optional[uuid.UUID] = None,
) -> data.GroupUserResponse:
"""
Delete user from group.
"""
if username is None and email is None:
if user_id is None and username is None and email is None:
raise GroupInvalidParameters(
"In order to add user to group, at least one of username, or email must be specified"
"In order to add user to group, at least one of user_id, or username, or email must be specified"
)

# Convert to stored format
if username is not None:
if user_id is not None:
user = get_user(session, user_id=user_id, application_id=application_id)
elif username is not None:
username = cast(str, username)
username = username.lower()
user = get_user(session, username=username)
user = get_user(session, username=username, application_id=application_id)
else:
user = get_user(session, email=email)
user = get_user(session, email=email, application_id=application_id)

group = get_group(session, group_id=group_id)

Expand Down
40 changes: 36 additions & 4 deletions brood/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
The Brood HTTP API
"""

import logging
import uuid
from typing import Any, Dict, List, Optional, Tuple
Expand Down Expand Up @@ -796,7 +797,10 @@ async def find_group_handler(
raise HTTPException(status_code=404, detail="No groups matched your query")

return data.GroupFindResponse(
id=group_obj.id, name=group_obj.name, autogenerated=group_obj.autogenerated
id=group_obj.id,
name=group_obj.name,
autogenerated=group_obj.autogenerated,
application_id=group_obj.application_id,
)


Expand Down Expand Up @@ -849,6 +853,7 @@ async def get_group_handler(
],
children_url=children_url,
parent=group.parent,
application_id=group.application_id,
created_at=group.created_at,
updated_at=group.updated_at,
)
Expand Down Expand Up @@ -908,6 +913,7 @@ async def get_group_children_handler(
subscriptions=[],
children_url=None,
parent=child.parent,
application_id=child.application_id,
created_at=child.created_at,
updated_at=child.updated_at,
)
Expand Down Expand Up @@ -956,6 +962,7 @@ async def create_group_handler(
) -> data.GroupResponse:
"""
Creates group as a group owner.
The group inherits the application ID of its creator.

- **group_name** (string): Group name
- **parent** (uuid): Group parent if exists
Expand Down Expand Up @@ -999,6 +1006,7 @@ async def create_group_handler(
for subscription_plan in group.subscriptions
],
parent=group.parent,
application_id=group.application_id,
created_at=group.created_at,
updated_at=group.updated_at,
)
Expand Down Expand Up @@ -1028,7 +1036,7 @@ async def set_user_in_group_handler(
- **group_id** (uuid): Group ID
- **user_type** (string): Required user permission in group
- **username** (string): User name
- **application_id** (string): Application ID
- **application_id** (string): Application ID of user
- **email** (string): User email
"""
is_token_restricted, current_user = user_authorization
Expand Down Expand Up @@ -1078,6 +1086,15 @@ async def set_user_in_group_handler(
status_code=403,
detail="Please add user to parent group before he will be added to child",
)
except actions.DifferentApplications:
raise HTTPException(
status_code=403,
detail="User and group belong to different applications",
)
except Exception as e:
logger.error(f"Unhandled exception, err: {str(e)}")
raise HTTPException(status_code=500)

return group_user_response


Expand Down Expand Up @@ -1238,16 +1255,20 @@ async def set_group_name_handler(
async def delete_user_from_group_handler(
user_authorization: Tuple[bool, models.User] = Depends(request_user_authorization),
group_id: uuid.UUID = Path(...),
user_id: Optional[uuid.UUID] = Form(None),
username: Optional[str] = Form(None),
email: Optional[str] = Form(None),
application_id: Optional[uuid.UUID] = Form(None),
db_session=Depends(yield_db_session_from_env),
) -> data.GroupUserResponse:
"""
Removes user from group.

- **group_id** (uuid): Group ID
- **user_id** (uuid): User ID
- **username** (string): User username
- **email** (string): User email
- **application_id** (uuid) Application ID user belongs to
"""
is_token_restricted, current_user = user_authorization
if is_token_restricted:
Expand All @@ -1270,7 +1291,6 @@ async def delete_user_from_group_handler(
if (
group_user.user_type != models.Role.owner
and group_user.user_type != models.Role.admin
and username != current_user.username
):
raise HTTPException(
status_code=403,
Expand All @@ -1282,8 +1302,10 @@ async def delete_user_from_group_handler(
session=db_session,
group_id=group_id,
current_user_autogenerated=current_user.autogenerated,
user_id=user_id,
username=username,
email=email,
application_id=application_id,
)
except actions.LackOfUserSpace:
raise HTTPException(
Expand All @@ -1294,7 +1316,10 @@ async def delete_user_from_group_handler(
except actions.GroupNotFound:
raise HTTPException(status_code=404, detail="No group with that id")
except actions.UserNotFound:
raise HTTPException(status_code=404, detail="No user with that username/email")
raise HTTPException(
status_code=404,
detail="No user with that user_id/username/email and application_id combination",
)
except actions.RoleNotFound:
raise HTTPException(status_code=404, detail="User does not belong group")
except actions.NoPermissions:
Expand Down Expand Up @@ -1366,6 +1391,7 @@ async def delete_group_handler(
for subscription_plan in group.subscriptions
],
parent=group.parent,
application_id=group.application_id,
created_at=group.created_at,
updated_at=group.updated_at,
)
Expand Down Expand Up @@ -1948,6 +1974,12 @@ async def create_application_handler(
detail="Restricted tokens are not authorized to create applications.",
)

if current_user.application_id is not None:
raise HTTPException(
status_code=403,
detail="Application users are not authorized to create applications.",
)

try:
# Check user permissions
actions.check_user_type_in_group(
Expand Down
4 changes: 4 additions & 0 deletions brood/data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Pydantic schemas for the Brood HTTP API
"""

from datetime import datetime
from enum import Enum, unique
from typing import List, Optional
Expand Down Expand Up @@ -170,6 +171,7 @@ class GroupFindResponse(BaseModel):
id: uuid.UUID
name: str
autogenerated: bool
application_id: Optional[uuid.UUID] = None


class GroupResponse(BaseModel):
Expand All @@ -187,6 +189,7 @@ class GroupResponse(BaseModel):
subscriptions: List[uuid.UUID] = Field(default_factory=list)
parent: Optional[uuid.UUID] = None
children_url: Optional[str] = None
application_id: Optional[uuid.UUID] = None
created_at: datetime
updated_at: datetime

Expand Down Expand Up @@ -217,6 +220,7 @@ class GroupUserResponse(BaseModel):
autogenerated: Optional[bool] = None
group_name: Optional[str] = None
parent: Optional[uuid.UUID] = None
application_id: Optional[uuid.UUID] = None
num_users: Optional[int] = None
num_seats: Optional[int] = None

Expand Down
9 changes: 9 additions & 0 deletions brood/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Ref to changing an Enum in Python using SQLAlchemy:
https://markrailton.com/blog/creating-migrations-when-changing-an-enum-in-python-using-sql-alchemy
"""

import uuid
from enum import Enum, unique

Expand Down Expand Up @@ -283,6 +284,14 @@ class Group(Base): # type: ignore
)
autogenerated = Column(Boolean, default=False, nullable=False)

application_id = Column(
UUID(as_uuid=True),
ForeignKey(
"applications.id", name="fk_groups_applications_id", ondelete="CASCADE"
),
nullable=True,
)

created_at = Column(
DateTime(timezone=True), server_default=utcnow(), nullable=False
)
Expand Down
Loading