fix(proxy): normalize managed resource team owner field
This commit is contained in:
parent
799d79160a
commit
83971a8712
@ -104,7 +104,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
||||
model_mappings=model_mappings,
|
||||
flat_model_file_ids=list(model_mappings.values()),
|
||||
created_by=user_api_key_dict.user_id,
|
||||
created_by_team_id=user_api_key_dict.team_id,
|
||||
team_id=user_api_key_dict.team_id,
|
||||
updated_by=user_api_key_dict.user_id,
|
||||
)
|
||||
await self.internal_usage_cache.async_set_cache(
|
||||
@ -120,7 +120,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
||||
"model_mappings": json.dumps(model_mappings),
|
||||
"flat_model_file_ids": list(model_mappings.values()),
|
||||
"created_by": user_api_key_dict.user_id,
|
||||
"created_by_team_id": user_api_key_dict.team_id,
|
||||
"team_id": user_api_key_dict.team_id,
|
||||
"updated_by": user_api_key_dict.user_id,
|
||||
}
|
||||
|
||||
@ -178,7 +178,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
||||
"model_object_id": model_object_id,
|
||||
"file_purpose": file_purpose,
|
||||
"created_by": user_api_key_dict.user_id,
|
||||
"created_by_team_id": user_api_key_dict.team_id,
|
||||
"team_id": user_api_key_dict.team_id,
|
||||
"updated_by": user_api_key_dict.user_id,
|
||||
"status": file_object.status,
|
||||
},
|
||||
@ -245,7 +245,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
||||
return can_access_resource(
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
created_by=managed_file.created_by,
|
||||
created_by_team_id=managed_file.created_by_team_id,
|
||||
resource_team_id=managed_file.team_id,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
@ -265,7 +265,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
||||
return can_access_resource(
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
created_by=managed_object.created_by,
|
||||
created_by_team_id=managed_object.created_by_team_id,
|
||||
resource_team_id=managed_object.team_id,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
@ -349,7 +349,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
||||
"""
|
||||
Get all file ids the caller is allowed to see for a list of model
|
||||
object ids. Service-account keys (no user_id) are scoped to their
|
||||
team via ``created_by_team_id``; admins see all matches.
|
||||
team via ``team_id``; admins see all matches.
|
||||
|
||||
Returns:
|
||||
- List of OpenAIFileObject's
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
-- Adds `created_by_team_id` to managed-resource tables so service-account API
|
||||
-- Adds `team_id` to managed-resource tables so service-account API
|
||||
-- keys (no `user_id`) can be scoped by team instead of bypassing the
|
||||
-- `created_by` filter entirely. Existing rows keep `created_by_team_id = NULL`
|
||||
-- `created_by` filter entirely. Existing rows keep `team_id = NULL`
|
||||
-- and become invisible to team-only callers — that is the intended isolation
|
||||
-- outcome; backfill manually if legacy rows must remain visible.
|
||||
--
|
||||
@ -9,13 +9,12 @@
|
||||
-- request); a future operator with a large table can switch to
|
||||
-- CREATE INDEX CONCURRENTLY in a follow-up migration.
|
||||
|
||||
ALTER TABLE "LiteLLM_ManagedFileTable" ADD COLUMN IF NOT EXISTS "created_by_team_id" TEXT;
|
||||
ALTER TABLE "LiteLLM_ManagedObjectTable" ADD COLUMN IF NOT EXISTS "created_by_team_id" TEXT;
|
||||
ALTER TABLE "LiteLLM_ManagedVectorStoreTable" ADD COLUMN IF NOT EXISTS "created_by_team_id" TEXT;
|
||||
ALTER TABLE "LiteLLM_ManagedFileTable" ADD COLUMN IF NOT EXISTS "team_id" TEXT;
|
||||
ALTER TABLE "LiteLLM_ManagedObjectTable" ADD COLUMN IF NOT EXISTS "team_id" TEXT;
|
||||
ALTER TABLE "LiteLLM_ManagedVectorStoreTable" ADD COLUMN IF NOT EXISTS "team_id" TEXT;
|
||||
|
||||
-- Index names follow Prisma's auto-generated convention so `prisma migrate diff`
|
||||
-- against the schema is clean. Postgres caps identifier length at 63 chars,
|
||||
-- which truncates the vector-store name to `_created__idx`.
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedFileTable_created_by_team_id_created_at_idx" ON "LiteLLM_ManagedFileTable" ("created_by_team_id", "created_at" DESC);
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedObjectTable_created_by_team_id_created_at_idx" ON "LiteLLM_ManagedObjectTable" ("created_by_team_id", "created_at" DESC);
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedVectorStoreTable_created_by_team_id_created__idx" ON "LiteLLM_ManagedVectorStoreTable" ("created_by_team_id", "created_at" DESC);
|
||||
-- against the schema is clean.
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedFileTable_team_id_created_at_idx" ON "LiteLLM_ManagedFileTable" ("team_id", "created_at" DESC);
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedObjectTable_team_id_created_at_idx" ON "LiteLLM_ManagedObjectTable" ("team_id", "created_at" DESC);
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedVectorStoreTable_team_id_created_at_idx" ON "LiteLLM_ManagedVectorStoreTable" ("team_id", "created_at" DESC);
|
||||
|
||||
@ -885,12 +885,12 @@ model LiteLLM_ManagedFileTable {
|
||||
storage_url String? // The actual storage URL where the file is stored
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
created_by_team_id String? // Team that owns the resource; populated for service-account keys without a user_id so listings can isolate by team.
|
||||
team_id String? // Team that owns the resource; populated for service-account keys without a user_id so listings can isolate by team.
|
||||
updated_at DateTime @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@index([unified_file_id])
|
||||
@@index([created_by_team_id, created_at(sort: Desc)])
|
||||
@@index([team_id, created_at(sort: Desc)])
|
||||
}
|
||||
|
||||
model LiteLLM_ManagedObjectTable { // for batches or finetuning jobs which use the
|
||||
@ -903,13 +903,13 @@ model LiteLLM_ManagedObjectTable { // for batches or finetuning jobs which use t
|
||||
batch_processed Boolean @default(false) // set to true by CheckBatchCost after cost is computed
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
created_by_team_id String?
|
||||
team_id String?
|
||||
updated_at DateTime @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@index([unified_object_id])
|
||||
@@index([model_object_id])
|
||||
@@index([created_by_team_id, created_at(sort: Desc)])
|
||||
@@index([team_id, created_at(sort: Desc)])
|
||||
}
|
||||
|
||||
model LiteLLM_ManagedVectorStoreTable {
|
||||
@ -922,12 +922,12 @@ model LiteLLM_ManagedVectorStoreTable {
|
||||
storage_url String? // Storage URL (if applicable)
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
created_by_team_id String?
|
||||
team_id String?
|
||||
updated_at DateTime @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@index([unified_resource_id])
|
||||
@@index([created_by_team_id, created_at(sort: Desc)])
|
||||
@@index([team_id, created_at(sort: Desc)])
|
||||
}
|
||||
|
||||
model LiteLLM_ManagedVectorStoresTable {
|
||||
|
||||
@ -174,7 +174,7 @@ class BaseManagedResource(ABC, Generic[ResourceObjectType]):
|
||||
"model_mappings": model_mappings,
|
||||
"flat_model_resource_ids": list(model_mappings.values()),
|
||||
"created_by": user_api_key_dict.user_id,
|
||||
"created_by_team_id": user_api_key_dict.team_id,
|
||||
"team_id": user_api_key_dict.team_id,
|
||||
"updated_by": user_api_key_dict.user_id,
|
||||
}
|
||||
|
||||
@ -196,7 +196,7 @@ class BaseManagedResource(ABC, Generic[ResourceObjectType]):
|
||||
"model_mappings": json.dumps(model_mappings),
|
||||
"flat_model_resource_ids": list(model_mappings.values()),
|
||||
"created_by": user_api_key_dict.user_id,
|
||||
"created_by_team_id": user_api_key_dict.team_id,
|
||||
"team_id": user_api_key_dict.team_id,
|
||||
"updated_by": user_api_key_dict.user_id,
|
||||
}
|
||||
|
||||
@ -332,7 +332,7 @@ class BaseManagedResource(ABC, Generic[ResourceObjectType]):
|
||||
return can_access_resource(
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
created_by=resource.get("created_by"),
|
||||
created_by_team_id=resource.get("created_by_team_id"),
|
||||
resource_team_id=resource.get("team_id"),
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
@ -36,7 +36,7 @@ def build_owner_filter(
|
||||
|
||||
- ``{}`` means no scoping (proxy admins).
|
||||
- ``{"created_by": <user_id>}`` for user-keyed callers.
|
||||
- ``{"created_by_team_id": <team_id>}`` for service-account callers
|
||||
- ``{"team_id": <team_id>}`` for service-account callers
|
||||
that have a team but no user_id.
|
||||
- ``{"OR": [...]}`` when the caller has both — listing must include
|
||||
both their own resources and team-shared ones so it stays consistent
|
||||
@ -54,7 +54,7 @@ def build_owner_filter(
|
||||
return {
|
||||
"OR": [
|
||||
{"created_by": user_id},
|
||||
{"created_by_team_id": team_id},
|
||||
{"team_id": team_id},
|
||||
]
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ def build_owner_filter(
|
||||
return {"created_by": user_id}
|
||||
|
||||
if team_id is not None:
|
||||
return {"created_by_team_id": team_id}
|
||||
return {"team_id": team_id}
|
||||
|
||||
return None
|
||||
|
||||
@ -70,12 +70,12 @@ def build_owner_filter(
|
||||
def can_access_resource(
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
created_by: Optional[str],
|
||||
created_by_team_id: Optional[str],
|
||||
resource_team_id: Optional[str],
|
||||
) -> bool:
|
||||
"""Return True iff the caller may read/modify a managed resource.
|
||||
|
||||
Both ``created_by`` and ``created_by_team_id`` must be non-None to
|
||||
match the caller's identity — guarding against the ``None == None``
|
||||
The resource's ``created_by`` and ``team_id`` fields must be non-None
|
||||
to match the caller's identity — guarding against the ``None == None``
|
||||
bypass that previously let service-account keys read every keyless
|
||||
resource.
|
||||
"""
|
||||
@ -89,8 +89,8 @@ def can_access_resource(
|
||||
team_id = user_api_key_dict.team_id
|
||||
if (
|
||||
team_id is not None
|
||||
and created_by_team_id is not None
|
||||
and created_by_team_id == team_id
|
||||
and resource_team_id is not None
|
||||
and resource_team_id == team_id
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
@ -4539,7 +4539,7 @@ class LiteLLM_ManagedFileTable(LiteLLMPydanticObjectBase):
|
||||
model_mappings: Dict[str, str]
|
||||
flat_model_file_ids: List[str]
|
||||
created_by: Optional[str] = None
|
||||
created_by_team_id: Optional[str] = None
|
||||
team_id: Optional[str] = None
|
||||
updated_by: Optional[str] = None
|
||||
storage_backend: Optional[str] = None
|
||||
storage_url: Optional[str] = None
|
||||
@ -4551,7 +4551,7 @@ class LiteLLM_ManagedObjectTable(LiteLLMPydanticObjectBase):
|
||||
file_purpose: Literal["batch", "fine-tune", "response"]
|
||||
file_object: Union[LiteLLMBatch, LiteLLMFineTuningJob, ResponsesAPIResponse]
|
||||
created_by: Optional[str] = None
|
||||
created_by_team_id: Optional[str] = None
|
||||
team_id: Optional[str] = None
|
||||
|
||||
|
||||
class LiteLLM_ManagedVectorStoreTable(LiteLLMPydanticObjectBase):
|
||||
@ -4562,7 +4562,7 @@ class LiteLLM_ManagedVectorStoreTable(LiteLLMPydanticObjectBase):
|
||||
model_mappings: Dict[str, str]
|
||||
flat_model_resource_ids: List[str]
|
||||
created_by: Optional[str] = None
|
||||
created_by_team_id: Optional[str] = None
|
||||
team_id: Optional[str] = None
|
||||
updated_by: Optional[str] = None
|
||||
storage_backend: Optional[str] = None
|
||||
storage_url: Optional[str] = None
|
||||
|
||||
@ -885,12 +885,12 @@ model LiteLLM_ManagedFileTable {
|
||||
storage_url String? // The actual storage URL where the file is stored
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
created_by_team_id String? // Team that owns the resource; populated for service-account keys without a user_id so listings can isolate by team.
|
||||
team_id String? // Team that owns the resource; populated for service-account keys without a user_id so listings can isolate by team.
|
||||
updated_at DateTime @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@index([unified_file_id])
|
||||
@@index([created_by_team_id, created_at(sort: Desc)])
|
||||
@@index([team_id, created_at(sort: Desc)])
|
||||
}
|
||||
|
||||
model LiteLLM_ManagedObjectTable { // for batches or finetuning jobs which use the
|
||||
@ -903,13 +903,13 @@ model LiteLLM_ManagedObjectTable { // for batches or finetuning jobs which use t
|
||||
batch_processed Boolean @default(false) // set to true by CheckBatchCost after cost is computed
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
created_by_team_id String?
|
||||
team_id String?
|
||||
updated_at DateTime @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@index([unified_object_id])
|
||||
@@index([model_object_id])
|
||||
@@index([created_by_team_id, created_at(sort: Desc)])
|
||||
@@index([team_id, created_at(sort: Desc)])
|
||||
}
|
||||
|
||||
model LiteLLM_ManagedVectorStoreTable {
|
||||
@ -922,12 +922,12 @@ model LiteLLM_ManagedVectorStoreTable {
|
||||
storage_url String? // Storage URL (if applicable)
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
created_by_team_id String?
|
||||
team_id String?
|
||||
updated_at DateTime @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@index([unified_resource_id])
|
||||
@@index([created_by_team_id, created_at(sort: Desc)])
|
||||
@@index([team_id, created_at(sort: Desc)])
|
||||
}
|
||||
|
||||
model LiteLLM_ManagedVectorStoresTable {
|
||||
|
||||
@ -885,12 +885,12 @@ model LiteLLM_ManagedFileTable {
|
||||
storage_url String? // The actual storage URL where the file is stored
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
created_by_team_id String? // Team that owns the resource; populated for service-account keys without a user_id so listings can isolate by team.
|
||||
team_id String? // Team that owns the resource; populated for service-account keys without a user_id so listings can isolate by team.
|
||||
updated_at DateTime @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@index([unified_file_id])
|
||||
@@index([created_by_team_id, created_at(sort: Desc)])
|
||||
@@index([team_id, created_at(sort: Desc)])
|
||||
}
|
||||
|
||||
model LiteLLM_ManagedObjectTable { // for batches or finetuning jobs which use the
|
||||
@ -903,13 +903,13 @@ model LiteLLM_ManagedObjectTable { // for batches or finetuning jobs which use t
|
||||
batch_processed Boolean @default(false) // set to true by CheckBatchCost after cost is computed
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
created_by_team_id String?
|
||||
team_id String?
|
||||
updated_at DateTime @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@index([unified_object_id])
|
||||
@@index([model_object_id])
|
||||
@@index([created_by_team_id, created_at(sort: Desc)])
|
||||
@@index([team_id, created_at(sort: Desc)])
|
||||
}
|
||||
|
||||
model LiteLLM_ManagedVectorStoreTable {
|
||||
@ -922,12 +922,12 @@ model LiteLLM_ManagedVectorStoreTable {
|
||||
storage_url String? // Storage URL (if applicable)
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
created_by_team_id String?
|
||||
team_id String?
|
||||
updated_at DateTime @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@index([unified_resource_id])
|
||||
@@index([created_by_team_id, created_at(sort: Desc)])
|
||||
@@index([team_id, created_at(sort: Desc)])
|
||||
}
|
||||
|
||||
model LiteLLM_ManagedVectorStoresTable {
|
||||
|
||||
@ -34,7 +34,7 @@ def _make_unified_file_id() -> str:
|
||||
def _make_managed_files_instance(
|
||||
file_created_by: str,
|
||||
unified_file_id: str,
|
||||
file_created_by_team_id=None,
|
||||
file_team_id=None,
|
||||
):
|
||||
"""Create a _PROXY_LiteLLMManagedFiles with a mocked DB that returns a file owned by file_created_by."""
|
||||
from litellm_enterprise.proxy.hooks.managed_files import (
|
||||
@ -43,7 +43,7 @@ def _make_managed_files_instance(
|
||||
|
||||
mock_db_record = MagicMock()
|
||||
mock_db_record.created_by = file_created_by
|
||||
mock_db_record.created_by_team_id = file_created_by_team_id
|
||||
mock_db_record.team_id = file_team_id
|
||||
|
||||
mock_prisma = MagicMock()
|
||||
mock_prisma.db.litellm_managedfiletable.find_first = AsyncMock(
|
||||
@ -110,7 +110,7 @@ async def test_should_block_default_user_id_access():
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
|
||||
# --- Service-account isolation: created_by/created_by_team_id checks ---
|
||||
# --- Service-account isolation: created_by/team_id checks ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -120,7 +120,7 @@ async def test_keyless_caller_cannot_access_keyless_file():
|
||||
unified_file_id = _make_unified_file_id()
|
||||
managed_files = _make_managed_files_instance(
|
||||
file_created_by=None,
|
||||
file_created_by_team_id=None,
|
||||
file_team_id=None,
|
||||
unified_file_id=unified_file_id,
|
||||
)
|
||||
keyless = UserAPIKeyAuth(api_key="sk-test", parent_otel_span=None)
|
||||
@ -136,7 +136,7 @@ async def test_service_account_can_access_team_file():
|
||||
unified_file_id = _make_unified_file_id()
|
||||
managed_files = _make_managed_files_instance(
|
||||
file_created_by=None,
|
||||
file_created_by_team_id="team-eng",
|
||||
file_team_id="team-eng",
|
||||
unified_file_id=unified_file_id,
|
||||
)
|
||||
sa = UserAPIKeyAuth(api_key="sk-svc", team_id="team-eng", parent_otel_span=None)
|
||||
@ -150,7 +150,7 @@ async def test_service_account_blocked_from_other_team_file():
|
||||
unified_file_id = _make_unified_file_id()
|
||||
managed_files = _make_managed_files_instance(
|
||||
file_created_by=None,
|
||||
file_created_by_team_id="team-sales",
|
||||
file_team_id="team-sales",
|
||||
unified_file_id=unified_file_id,
|
||||
)
|
||||
sa = UserAPIKeyAuth(api_key="sk-svc", team_id="team-eng", parent_otel_span=None)
|
||||
|
||||
@ -58,7 +58,7 @@ async def test_list_admin_query_is_unscoped():
|
||||
table = resource.prisma_client.db.litellm_test_resource_table
|
||||
where = table.find_many.await_args.kwargs["where"]
|
||||
assert "created_by" not in where
|
||||
assert "created_by_team_id" not in where
|
||||
assert "team_id" not in where
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -72,7 +72,7 @@ async def test_list_user_filters_by_user_id():
|
||||
"where"
|
||||
]
|
||||
assert where["created_by"] == "alice"
|
||||
assert "created_by_team_id" not in where
|
||||
assert "team_id" not in where
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -85,7 +85,7 @@ async def test_list_service_account_filters_by_team_id():
|
||||
where = resource.prisma_client.db.litellm_test_resource_table.find_many.await_args.kwargs[
|
||||
"where"
|
||||
]
|
||||
assert where["created_by_team_id"] == "team-eng"
|
||||
assert where["team_id"] == "team-eng"
|
||||
assert "created_by" not in where
|
||||
|
||||
|
||||
@ -118,7 +118,7 @@ async def test_can_access_uses_team_id_for_service_account(caller_team_id, expec
|
||||
cache.async_get_cache = AsyncMock(
|
||||
return_value={
|
||||
"created_by": None,
|
||||
"created_by_team_id": "team-eng",
|
||||
"team_id": "team-eng",
|
||||
}
|
||||
)
|
||||
prisma = MagicMock()
|
||||
|
||||
@ -31,7 +31,7 @@ def test_owner_filter_user_scoped_to_user_id():
|
||||
|
||||
def test_owner_filter_service_account_scoped_to_team():
|
||||
service_account = UserAPIKeyAuth(team_id="team-eng")
|
||||
assert build_owner_filter(service_account) == {"created_by_team_id": "team-eng"}
|
||||
assert build_owner_filter(service_account) == {"team_id": "team-eng"}
|
||||
|
||||
|
||||
def test_owner_filter_user_with_team_returns_or_filter():
|
||||
@ -42,7 +42,7 @@ def test_owner_filter_user_with_team_returns_or_filter():
|
||||
assert build_owner_filter(user) == {
|
||||
"OR": [
|
||||
{"created_by": "alice"},
|
||||
{"created_by_team_id": "team-eng"},
|
||||
{"team_id": "team-eng"},
|
||||
]
|
||||
}
|
||||
|
||||
@ -64,14 +64,14 @@ def test_owner_filter_no_identity_returns_none():
|
||||
[LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"created_by,created_by_team_id",
|
||||
"created_by,resource_team_id",
|
||||
[("alice", "team-eng"), (None, None)],
|
||||
)
|
||||
def test_access_admin_can_read_any_resource(role, created_by, created_by_team_id):
|
||||
def test_access_admin_can_read_any_resource(role, created_by, resource_team_id):
|
||||
admin = UserAPIKeyAuth(user_role=role)
|
||||
assert (
|
||||
can_access_resource(
|
||||
admin, created_by=created_by, created_by_team_id=created_by_team_id
|
||||
admin, created_by=created_by, resource_team_id=resource_team_id
|
||||
)
|
||||
is True
|
||||
)
|
||||
@ -88,24 +88,26 @@ def test_access_admin_can_read_any_resource(role, created_by, created_by_team_id
|
||||
def test_access_user_id_match(user_id, created_by, expected):
|
||||
user = UserAPIKeyAuth(user_id=user_id)
|
||||
assert (
|
||||
can_access_resource(user, created_by=created_by, created_by_team_id=None)
|
||||
can_access_resource(user, created_by=created_by, resource_team_id=None)
|
||||
is expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"team_id,created_by_team_id,expected",
|
||||
"caller_team_id,resource_team_id,expected",
|
||||
[
|
||||
("team-eng", "team-eng", True),
|
||||
("team-eng", "team-sales", False),
|
||||
("team-eng", None, False),
|
||||
],
|
||||
)
|
||||
def test_access_service_account_team_id_match(team_id, created_by_team_id, expected):
|
||||
service_account = UserAPIKeyAuth(team_id=team_id)
|
||||
def test_access_service_account_team_id_match(
|
||||
caller_team_id, resource_team_id, expected
|
||||
):
|
||||
service_account = UserAPIKeyAuth(team_id=caller_team_id)
|
||||
assert (
|
||||
can_access_resource(
|
||||
service_account, created_by=None, created_by_team_id=created_by_team_id
|
||||
service_account, created_by=None, resource_team_id=resource_team_id
|
||||
)
|
||||
is expected
|
||||
)
|
||||
@ -117,9 +119,7 @@ def test_access_user_can_see_team_match_when_no_user_id_match():
|
||||
same team."""
|
||||
user = UserAPIKeyAuth(user_id="alice", team_id="team-eng")
|
||||
assert (
|
||||
can_access_resource(
|
||||
user, created_by="service-bot", created_by_team_id="team-eng"
|
||||
)
|
||||
can_access_resource(user, created_by="service-bot", resource_team_id="team-eng")
|
||||
is True
|
||||
)
|
||||
|
||||
@ -128,14 +128,14 @@ def test_access_service_account_denied_user_resource_in_different_team():
|
||||
service_account = UserAPIKeyAuth(team_id="team-eng")
|
||||
assert (
|
||||
can_access_resource(
|
||||
service_account, created_by="bob", created_by_team_id="team-sales"
|
||||
service_account, created_by="bob", resource_team_id="team-sales"
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"created_by,created_by_team_id",
|
||||
"created_by,resource_team_id",
|
||||
[
|
||||
(None, None),
|
||||
("anybody", None),
|
||||
@ -143,14 +143,14 @@ def test_access_service_account_denied_user_resource_in_different_team():
|
||||
("anybody", "any-team"),
|
||||
],
|
||||
)
|
||||
def test_access_identity_less_caller_always_denied(created_by, created_by_team_id):
|
||||
def test_access_identity_less_caller_always_denied(created_by, resource_team_id):
|
||||
"""The original `None == None` bypass — a caller with no admin role and
|
||||
no identifying ids is denied against every resource regardless of how
|
||||
the resource was tagged."""
|
||||
nobody = UserAPIKeyAuth()
|
||||
assert (
|
||||
can_access_resource(
|
||||
nobody, created_by=created_by, created_by_team_id=created_by_team_id
|
||||
nobody, created_by=created_by, resource_team_id=resource_team_id
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user