Skip to content

Add global functions user permissions 🎨 #7868

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4518f10
Add global abilities for user wrt functions (read,write,execute)
wvangeit Jun 10, 2025
569871b
Add function user abilities
wvangeit Jun 10, 2025
1441336
Merge branch 'master' into add_global_functions_user_abilities
wvangeit Jun 10, 2025
ff3c380
Merge branch 'master' into add_global_functions_user_abilities
wvangeit Jun 11, 2025
72b7fd1
All tests working
wvangeit Jun 13, 2025
d71a213
Merge branch 'add_global_functions_user_abilities' of github.com:wvan…
wvangeit Jun 13, 2025
ea0755a
Address some comments in the PR
wvangeit Jun 13, 2025
b4c8472
Merge branch 'master' into add_global_functions_user_abilities
wvangeit Jun 13, 2025
62e9301
Merge branch 'master' into add_global_functions_user_abilities
wvangeit Jun 13, 2025
21f045e
Renamed abilities to api_access_rights
wvangeit Jun 13, 2025
0c1aefd
Rm migration script
wvangeit Jun 13, 2025
db50854
Add migration script
wvangeit Jun 13, 2025
de88627
Add more tests
wvangeit Jun 13, 2025
74a54d4
Merge branch 'add_global_functions_user_abilities' of github.com:wvan…
wvangeit Jun 13, 2025
183702c
Merge branch 'master' into add_global_functions_user_abilities
wvangeit Jun 13, 2025
b706b59
Merge branch 'master' into add_global_functions_user_abilities
wvangeit Jun 13, 2025
9a6549b
Some more abilities renaming
wvangeit Jun 13, 2025
fcdf6a9
Merge branch 'add_global_functions_user_abilities' of github.com:wvan…
wvangeit Jun 13, 2025
876e4f7
Merge branch 'master' into add_global_functions_user_abilities
wvangeit Jun 13, 2025
a3892a9
Merge branch 'master' into add_global_functions_user_abilities
wvangeit Jun 13, 2025
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
32 changes: 32 additions & 0 deletions packages/models-library/src/models_library/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from models_library.products import ProductName
from models_library.services_types import ServiceKey, ServiceVersion
from models_library.users import UserID
from models_library.utils.enums import StrAutoEnum
from pydantic import BaseModel, ConfigDict, Field

from .projects import ProjectID
Expand Down Expand Up @@ -301,6 +302,25 @@ class FunctionAccessRightsDB(BaseModel):
)


class FunctionUserApiAccessRights(BaseModel):
user_id: UserID
read_functions: bool = False
write_functions: bool = False
execute_functions: bool = False
read_function_jobs: bool = False
write_function_jobs: bool = False
execute_function_jobs: bool = False
read_function_job_collections: bool = False
write_function_job_collections: bool = False
execute_function_job_collections: bool = False

model_config = ConfigDict(
alias_generator=snake_to_camel,
populate_by_name=True,
extra="forbid",
)


FunctionJobAccessRights: TypeAlias = FunctionAccessRights
FunctionJobAccessRightsDB: TypeAlias = FunctionAccessRightsDB
FunctionJobUserAccessRights: TypeAlias = FunctionUserAccessRights
Expand All @@ -310,3 +330,15 @@ class FunctionAccessRightsDB(BaseModel):
FunctionJobCollectionAccessRightsDB: TypeAlias = FunctionAccessRightsDB
FunctionJobCollectionUserAccessRights: TypeAlias = FunctionUserAccessRights
FunctionJobCollectionGroupAccessRights: TypeAlias = FunctionGroupAccessRights


class FunctionsApiAccessRights(StrAutoEnum):
READ_FUNCTIONS = "read_functions"
WRITE_FUNCTIONS = "write_functions"
EXECUTE_FUNCTIONS = "execute_functions"
READ_FUNCTION_JOBS = "read_function_jobs"
WRITE_FUNCTION_JOBS = "write_function_jobs"
EXECUTE_FUNCTION_JOBS = "execute_function_jobs"
READ_FUNCTION_JOB_COLLECTIONS = "read_function_job_collections"
WRITE_FUNCTION_JOB_COLLECTIONS = "write_function_job_collections"
EXECUTE_FUNCTION_JOB_COLLECTIONS = "execute_function_job_collections"
59 changes: 59 additions & 0 deletions packages/models-library/src/models_library/functions_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,62 @@ class FunctionJobCollectionExecuteAccessDeniedError(FunctionBaseError):
"Function job collection {function_job_collection_id} execute access denied for user {user_id}"
)
status_code: int = 403 # Forbidden


class FunctionsReadApiAccessDeniedError(FunctionBaseError):
msg_template: str = "User {user_id} does not have the permission to read functions"
status_code: int = 403 # Forbidden


class FunctionsWriteApiAccessDeniedError(FunctionBaseError):
msg_template: str = "User {user_id} does not have the permission to write functions"
status_code: int = 403 # Forbidden


class FunctionsExecuteApiAccessDeniedError(FunctionBaseError):
msg_template: str = (
"User {user_id} does not have the permission to execute functions"
)
status_code: int = 403 # Forbidden


class FunctionJobsReadApiAccessDeniedError(FunctionBaseError):
msg_template: str = (
"User {user_id} does not have the permission to read function jobs"
)
status_code: int = 403 # Forbidden


class FunctionJobsWriteApiAccessDeniedError(FunctionBaseError):
msg_template: str = (
"User {user_id} does not have the permission to write function jobs"
)
status_code: int = 403 # Forbidden


class FunctionJobsExecuteApiAccessDeniedError(FunctionBaseError):
msg_template: str = (
"User {user_id} does not have the permission to execute function jobs"
)
status_code: int = 403 # Forbidden


class FunctionJobCollectionsReadApiAccessDeniedError(FunctionBaseError):
msg_template: str = (
"User {user_id} does not have the permission to read function job collections"
)
status_code: int = 403 # Forbidden


class FunctionJobCollectionsWriteApiAccessDeniedError(FunctionBaseError):
msg_template: str = (
"User {user_id} does not have the permission to write function job collections"
)
status_code: int = 403 # Forbidden


class FunctionJobCollectionsExecuteApiAccessDeniedError(FunctionBaseError):
msg_template: str = (
"User {user_id} does not have the permission to execute function job collections"
)
status_code: int = 403 # Forbidden
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Add functions api access rights

Revision ID: 4f6fd2586491
Revises: afb1ba08f3c2
Create Date: 2025-06-13 12:14:59.317685+00:00

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "4f6fd2586491"
down_revision = "afb1ba08f3c2"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"funcapi_group_api_access_rights",
sa.Column("group_id", sa.BigInteger(), nullable=False),
sa.Column("product_name", sa.String(), nullable=False),
sa.Column("read_functions", sa.Boolean(), nullable=True),
sa.Column("write_functions", sa.Boolean(), nullable=True),
sa.Column("execute_functions", sa.Boolean(), nullable=True),
sa.Column("read_function_jobs", sa.Boolean(), nullable=True),
sa.Column("write_function_jobs", sa.Boolean(), nullable=True),
sa.Column("execute_function_jobs", sa.Boolean(), nullable=True),
sa.Column("read_function_job_collections", sa.Boolean(), nullable=True),
sa.Column("write_function_job_collections", sa.Boolean(), nullable=True),
sa.Column("execute_function_job_collections", sa.Boolean(), nullable=True),
sa.Column(
"created",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"modified",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["group_id"],
["groups.gid"],
name="fk_func_access_to_groups_group_id",
onupdate="CASCADE",
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["product_name"],
["products.name"],
name="fk_func_access_to_products_product_name",
onupdate="CASCADE",
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint(
"group_id",
"product_name",
name="pk_func_group_product_name_to_api_access_rights",
),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("funcapi_group_api_access_rights")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Function api access rights of groups (read, write, execute)"""

import sqlalchemy as sa
from simcore_postgres_database.models._common import (
RefActions,
column_created_datetime,
column_modified_datetime,
)

from .base import metadata

funcapi_api_access_rights_table = sa.Table(
"funcapi_group_api_access_rights",
metadata,
sa.Column(
"group_id",
sa.ForeignKey(
"groups.gid",
name="fk_func_access_to_groups_group_id",
onupdate=RefActions.CASCADE,
ondelete=RefActions.CASCADE,
),
nullable=False,
),
sa.Column(
"product_name",
sa.ForeignKey(
"products.name",
name="fk_func_access_to_products_product_name",
onupdate=RefActions.CASCADE,
ondelete=RefActions.CASCADE,
),
nullable=False,
),
sa.Column(
"read_functions",
sa.Boolean,
default=False,
),
sa.Column(
"write_functions",
sa.Boolean,
default=False,
),
sa.Column(
"execute_functions",
sa.Boolean,
default=False,
),
sa.Column(
"read_function_jobs",
sa.Boolean,
default=False,
),
sa.Column(
"write_function_jobs",
sa.Boolean,
default=False,
),
sa.Column(
"execute_function_jobs",
sa.Boolean,
default=False,
),
sa.Column(
"read_function_job_collections",
sa.Boolean,
default=False,
),
sa.Column(
"write_function_job_collections",
sa.Boolean,
default=False,
),
sa.Column(
"execute_function_job_collections",
sa.Boolean,
default=False,
),
column_created_datetime(),
column_modified_datetime(),
sa.PrimaryKeyConstraint(
"group_id",
"product_name",
name="pk_func_group_product_name_to_api_access_rights",
),
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
ondelete=RefActions.CASCADE,
),
nullable=False,
doc="Unique identifier of the function",
),
sa.Column(
"group_id",
Expand All @@ -33,7 +32,6 @@
ondelete=RefActions.CASCADE,
),
nullable=False,
doc="Group id",
),
sa.Column(
"product_name",
Expand All @@ -44,25 +42,21 @@
ondelete=RefActions.CASCADE,
),
nullable=False,
doc="Name of the product",
),
sa.Column(
"read",
sa.Boolean,
default=False,
doc="Read access right for the function",
),
sa.Column(
"write",
sa.Boolean,
default=False,
doc="Write access right for the function",
),
sa.Column(
"execute",
sa.Boolean,
default=False,
doc="Execute access right for the function",
),
column_created_datetime(),
column_modified_datetime(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
RegisteredFunctionJob,
RegisteredFunctionJobCollection,
)
from models_library.functions import FunctionUserAccessRights
from models_library.functions import (
FunctionUserAccessRights,
FunctionUserApiAccessRights,
)
from models_library.products import ProductName
from models_library.rabbitmq_basic_types import RPCMethodName
from models_library.rest_pagination import PageMetaInfoLimitOffset
Expand Down Expand Up @@ -407,3 +410,21 @@ async def get_function_user_permissions(
product_name=product_name,
)
return TypeAdapter(FunctionUserAccessRights).validate_python(result)


@log_decorator(_logger, level=logging.DEBUG)
async def get_functions_user_api_access_rights(
rabbitmq_rpc_client: RabbitMQRPCClient,
*,
user_id: UserID,
product_name: ProductName,
) -> FunctionUserApiAccessRights:
result = await rabbitmq_rpc_client.request(
WEBSERVER_RPC_NAMESPACE,
TypeAdapter(RPCMethodName).validate_python(
"get_functions_user_api_access_rights"
),
user_id=user_id,
product_name=product_name,
)
return TypeAdapter(FunctionUserApiAccessRights).validate_python(result)
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from models_library.functions_errors import (
FunctionExecuteAccessDeniedError,
FunctionInputsValidationError,
FunctionReadAccessDeniedError,
FunctionsExecuteApiAccessDeniedError,
UnsupportedFunctionClassError,
)
from models_library.products import ProductName
Expand Down Expand Up @@ -374,16 +374,23 @@ async def run_function( # noqa: PLR0913
job_service: Annotated[JobService, Depends(get_job_service)],
) -> RegisteredFunctionJob:

# Make sure the user is allowed to execute any function
# (read/write right is checked in the other endpoint called in this method)
user_api_access_rights = await wb_api_rpc.get_functions_user_api_access_rights(
user_id=user_id, product_name=product_name
)
if not user_api_access_rights.execute_functions:
raise FunctionsExecuteApiAccessDeniedError(
user_id=user_id,
function_id=function_id,
)
# Make sure the user is allowed to execute this particular function
# (read/write right is checked in the other endpoint called in this method)
user_permissions: FunctionUserAccessRights = (
await wb_api_rpc.get_function_user_permissions(
function_id=function_id, user_id=user_id, product_name=product_name
)
)
if not user_permissions.read:
raise FunctionReadAccessDeniedError(
user_id=user_id,
function_id=function_id,
)
if not user_permissions.execute:
raise FunctionExecuteAccessDeniedError(
user_id=user_id,
Expand Down
Loading
Loading