Skip to content

Commit

Permalink
refactor: impl vfolder RBAC APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
fregataa committed May 12, 2024
1 parent 7ac278f commit 35cce07
Show file tree
Hide file tree
Showing 6 changed files with 749 additions and 4 deletions.
63 changes: 63 additions & 0 deletions src/ai/backend/manager/models/acl.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from __future__ import annotations

import uuid
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, List, Mapping, Sequence

import graphene

from ai.backend.common.types import VFolderHostPermission

from .user import UserRole

if TYPE_CHECKING:
from .gql import GraphQueryContext

Expand All @@ -16,6 +20,65 @@
)


@dataclass(frozen=True)
class RequesterContext:
domain_name: str
user_id: uuid.UUID
user_role: UserRole

# Key: vfolder id / Value: a set of actions the user can perform on. This will be replaced with new `Permission`
# overriden_perm_ctx is not nullable since it is always needed
# overriden_perm_ctx: Mapping[uuid.UUID, frozenset[VFolderACLPermission]]
# project_ctx: Mapping[uuid.UUID, UserRoleInProject] | None

# vfolder host permission. 이건 phase 2로 가자...
# vfolder는 user/project 소유이며, storage host에 속할 수 있다.
# domain, project, user 별로 storage host에 대해 permission 부여 가능.
# 이 기능이 많이 쓰이지는 않으므로 storage host - {domain, project, user} 중계 테이블을 두고
# 여기에 action block list를 넣어두는 방식은 어떨지.
# 중계 테이블을 둬도 쿼리만 잘 짜면 pagination도 어렵지 않을 것으로 보이는데..이건 해봐야 알듯.
# vfolders.storage_host 컬럼이 storage_host 테이블 id가 되는데... 옵션
# 1번. 세 중계 테이블에 모두 left outer join 걸어서 조건 걸기. vfolders pagination이 가능하지만 join이 불필요하게 많아진다는 단점
# 2번. 세 중계 테이블에 먼저 쿼리하고 action blocklist 확인 후 vfolders는 따로 쿼리하기. 애초에 중계 테이블이 blocklist 방식이라 쿼리결과도 그리 크진 않을 것
# Storage host permisions ---
# 이 호스트에서 vfolder에 대해 (혹은 호스트에 속한 vfolder에 대해서) 어떤 액션을 할 수 있는지를 정의. 어떤 권한이 더 있을지는 생각해보자..
# CREATE_VFOLDERS, READ_ATTR, READ_CONTENT, MOUNT ...
# storage_host: str

# async def init_project_ctx(self, db_session: SASession) -> None:
# stmt = sa.select(AssocGroupUserRow).where(AssocGroupUserRow.user_id == self.user_id)
# rows = cast(list[AssocGroupUserRow], await db_session.scalars(stmt))

# def from_row(row: AssocGroupUserRow) -> UserRoleInProject:
# # TODO: get user_role_in_project from the row
# # For now, assume it is a project admin if admin is associated with a group
# if self.user_role in (UserRole.SUPERADMIN, UserRole.ADMIN):
# return UserRoleInProject.ADMIN
# else:
# return UserRoleInProject.USER

# self.project_ctx = {row.group_id: from_row(row) for row in rows}


class RequestedScope:
pass


@dataclass(frozen=True)
class RequestedDomainScope(RequestedScope):
domain_name: str


@dataclass(frozen=True)
class RequestedProjectScope(RequestedScope):
project_id: uuid.UUID


@dataclass(frozen=True)
class RequestedUserScope(RequestedScope):
user_id: uuid.UUID


def get_all_vfolder_host_permissions() -> List[str]:
return [perm.value for perm in VFolderHostPermission]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""add_id_to_vfolder_permissions
Revision ID: 2be0ce116c35
Revises: 37410c773b8c
Create Date: 2024-05-11 00:53:28.387238
"""

import sqlalchemy as sa
from alembic import op

from ai.backend.manager.models.base import GUID

# revision identifiers, used by Alembic.
revision = "2be0ce116c35"
down_revision = "37410c773b8c"
branch_labels = None
depends_on = None


def upgrade():
op.add_column(
"vfolder_permissions",
sa.Column("id", GUID(), server_default=sa.text("uuid_generate_v4()"), nullable=False),
)
op.create_primary_key("pk_vfolder_permissions", "vfolder_permissions", ["id"])


def downgrade():
op.drop_constraint("pk_vfolder_permissions", "vfolder_permissions", type_="primary")
op.drop_column("vfolder_permissions", "id")
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""add_vfolders_domain_name
Revision ID: 37410c773b8c
Revises: dddf9be580f5
Create Date: 2024-05-06 20:51:54.658829
"""

import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql import text

# revision identifiers, used by Alembic.
revision = "37410c773b8c"
down_revision = "dddf9be580f5"
branch_labels = None
depends_on = None


def upgrade():
conn = op.get_bind()
op.add_column("vfolders", sa.Column("domain_name", sa.String(length=64), nullable=True))

conn.execute(
text(
"""\
UPDATE vfolders
SET domain_name = COALESCE(
(SELECT domain_name FROM users WHERE vfolders.user = users.uuid),
(SELECT domain_name FROM groups WHERE vfolders.group = groups.id)
)
WHERE domain_name IS NULL;
"""
)
)

op.alter_column("vfolders", column_name="domain_name", nullable=False)
op.create_index(op.f("ix_vfolders_domain_name"), "vfolders", ["domain_name"], unique=False)


def downgrade():
op.drop_index(op.f("ix_vfolders_domain_name"), table_name="vfolders")
op.drop_column("vfolders", "domain_name")
9 changes: 9 additions & 0 deletions src/ai/backend/manager/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@
MAXIMUM_DOTFILE_SIZE = 64 * 1024 # 61 KiB
_rx_slug = re.compile(r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$")


class UserRoleInProject(enum.StrEnum):
ADMIN = "admin" # User is associated as admin. TODO: impl project admin
USER = "user" # User is associated as user
UNKNOWN = "unknown"
NONE = "none" # User is not associated


association_groups_users = sa.Table(
"association_groups_users",
mapper_registry.metadata,
Expand Down Expand Up @@ -197,6 +205,7 @@ class GroupRow(Base):
users = relationship("AssocGroupUserRow", back_populates="group")
resource_policy_row = relationship("ProjectResourcePolicyRow", back_populates="projects")
kernels = relationship("KernelRow", back_populates="group_row")
vfolders = relationship("VFolderRow", back_populates="project_row")


def _build_group_query(cond: sa.sql.BinaryExpression, domain_name: str) -> sa.sql.Select:
Expand Down
2 changes: 2 additions & 0 deletions src/ai/backend/manager/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ class UserRow(Base):

main_keypair = relationship("KeyPairRow", foreign_keys=users.c.main_access_key)

vfolders = relationship("VFolderRow", back_populates="user_row")


class UserGroup(graphene.ObjectType):
id = graphene.UUID()
Expand Down
Loading

0 comments on commit 35cce07

Please sign in to comment.