mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 21:35:45 +08:00
Merge branch 'pat' into deploy/enterprise
This commit is contained in:
12 files changed
+141
-42
No files matched your search
@@ -10,7 +10,6 @@ from typing import Final, Literal
|
||||
|
||||
from flask_restx import Resource
|
||||
|
||||
from constants.personal_access_token import PersonalAccessTokenPolicy
|
||||
from controllers.common.rbac import RBACPermission
|
||||
from controllers.openapi import openapi_ns
|
||||
from controllers.openapi._contract import Example, Kind, endpoint
|
||||
@@ -42,7 +41,6 @@ class WorkflowAppCreateApi(Resource):
|
||||
kind=Kind.OBJECT,
|
||||
summary="Create a blank workflow app; export its DSL to start building",
|
||||
examples=(Example(title="Create a workflow app", input={"name": "Release notes"}),),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=_CREATE_GUARDS,
|
||||
body=CreateAppPayload,
|
||||
returns=(HTTPStatus.CREATED, CreatedAppResponse, "Created"),
|
||||
|
||||
@@ -8,7 +8,6 @@ from flask_restx import Resource
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
from constants.oauth_bearer import Scope
|
||||
from constants.personal_access_token import PersonalAccessTokenPolicy
|
||||
from controllers.common.errors import ForbiddenError, NotFoundError
|
||||
from controllers.common.rbac import RBACPermission
|
||||
from controllers.openapi import openapi_ns
|
||||
@@ -114,7 +113,6 @@ class AppDslImportApi(Resource):
|
||||
@endpoint(
|
||||
context="workspace",
|
||||
op="import.console_app.dsl",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
effect=Effect.DESTRUCTIVE,
|
||||
summary="Import an app from DSL text or URL",
|
||||
@@ -215,7 +213,6 @@ class AppDslImportConfirmApi(Resource):
|
||||
@endpoint(
|
||||
context="workspace",
|
||||
op="confirm.console_app.dsl_import",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
effect=Effect.DESTRUCTIVE,
|
||||
summary="Confirm a pending DSL import",
|
||||
@@ -252,7 +249,6 @@ class AppDslExportApi(Resource):
|
||||
@endpoint(
|
||||
context="app",
|
||||
op="export.console_app.dsl",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Export app DSL as YAML text inside a JSON object",
|
||||
examples=(
|
||||
@@ -295,7 +291,6 @@ class AppDslCheckDependenciesApi(Resource):
|
||||
@endpoint(
|
||||
context="workspace",
|
||||
op="check.console_app.dependency",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Check plugin dependencies of an app",
|
||||
examples=(Example(title="Check which plugins an app needs", input={"app_id": "<app_id>"}),),
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Final
|
||||
from flask_restx import Resource
|
||||
|
||||
from constants.oauth_bearer import Scope
|
||||
from constants.personal_access_token import PersonalAccessTokenPolicy
|
||||
from controllers.common.rbac import AgentBehindApp, PlainApp, RBACCheck, RBACPermission
|
||||
from controllers.openapi import _app_settings as settings
|
||||
from controllers.openapi import openapi_ns
|
||||
@@ -49,7 +48,6 @@ class WorkflowAppInfoApi(Resource):
|
||||
kind=Kind.OBJECT,
|
||||
summary="Name, description, icon and run cap of a workflow app",
|
||||
examples=(_INFO_EXAMPLE,),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=account_settings_guards(_INFO_READ, scope=Scope.APPS_READ, modes=(AppMode.WORKFLOW,), roles=None),
|
||||
returns=(HTTPStatus.OK, AppSettingsInfo, "App info"),
|
||||
)
|
||||
@@ -58,7 +56,6 @@ class WorkflowAppInfoApi(Resource):
|
||||
|
||||
@endpoint(
|
||||
op="set.app_info.workflow",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Change a workflow app's info; only the fields passed change",
|
||||
examples=(_RENAME_EXAMPLE,),
|
||||
|
||||
@@ -4,7 +4,6 @@ from http import HTTPStatus
|
||||
|
||||
from flask_restx import Resource
|
||||
|
||||
from constants.personal_access_token import PersonalAccessTokenPolicy
|
||||
from controllers.openapi import openapi_ns
|
||||
from controllers.openapi._contract import Example, Kind, endpoint, op_of
|
||||
from controllers.openapi._models import (
|
||||
@@ -54,7 +53,6 @@ class ReleaseCheckApi(Resource):
|
||||
kind=Kind.OBJECT,
|
||||
summary="Is the draft ready to publish: draft valid, last test passed on this draft, and what changed",
|
||||
examples=(Example(title="Before publishing", input={"app_id": "<app_id>"}),),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=VERSION_READ_GUARDS,
|
||||
returns=(HTTPStatus.OK, ReleaseCheckResponse, "Release check"),
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ from werkzeug.exceptions import BadRequest
|
||||
|
||||
from constants import HIDDEN_VALUE
|
||||
from constants.oauth_bearer import Scope
|
||||
from constants.personal_access_token import PersonalAccessTokenPolicy
|
||||
from controllers.common.fields import SimpleResultResponse
|
||||
from controllers.common.rbac import RBACPermission
|
||||
from controllers.openapi import openapi_ns
|
||||
@@ -114,7 +113,6 @@ class AppRunListApi(Resource):
|
||||
input={"app_id": "<app_id>", "triggered_from": "debugging", "limit": 1},
|
||||
),
|
||||
),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=_RUN_READ_GUARDS,
|
||||
query=RunListQuery,
|
||||
returns=(HTTPStatus.OK, RunListResponse, "Run list"),
|
||||
@@ -148,7 +146,6 @@ class AppRunDescribeApi(Resource):
|
||||
kind=Kind.OBJECT,
|
||||
summary="One run: inputs, outputs, error, tokens and timing",
|
||||
examples=(Example(title="Read one run", input={"app_id": "<app_id>", "run_id": "<run_id>"}),),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=_RUN_READ_GUARDS,
|
||||
returns=(HTTPStatus.OK, WorkflowRunDetailResponse, "Run detail"),
|
||||
)
|
||||
@@ -164,7 +161,6 @@ class AppRunNodeListApi(Resource):
|
||||
kind=Kind.OBJECT,
|
||||
summary="Each node step of a run, to find the one that failed",
|
||||
examples=(Example(title="Node steps of one run", input={"app_id": "<app_id>", "run_id": "<run_id>"}),),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=_RUN_READ_GUARDS,
|
||||
returns=(HTTPStatus.OK, WorkflowRunNodeExecutionListResponse, "Node steps"),
|
||||
)
|
||||
@@ -226,7 +222,6 @@ class AppVersionListApi(Resource):
|
||||
Example(title="Latest versions", input={"app_id": "<app_id>"}),
|
||||
Example(title="Only named versions", input={"app_id": "<app_id>", "named_only": True}),
|
||||
),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=VERSION_READ_GUARDS,
|
||||
query=VersionListQuery,
|
||||
returns=(HTTPStatus.OK, VersionListResponse, "Version list"),
|
||||
@@ -330,7 +325,6 @@ class AppEnvListApi(Resource):
|
||||
summary="Environment variables of the draft. Each has an id, which set and delete take, "
|
||||
"and a name, which nodes use. Secrets with a value are masked; empty secrets read as empty",
|
||||
examples=(Example(title="List environment variables", input={"app_id": "<app_id>"}),),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=VERSION_READ_GUARDS,
|
||||
returns=(HTTPStatus.OK, EnvVariableListResponse, "Environment variables"),
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any
|
||||
|
||||
from flask_restx import Resource
|
||||
|
||||
from constants.personal_access_token import PersonalAccessTokenPolicy
|
||||
from controllers.openapi import openapi_ns
|
||||
from controllers.openapi._contract import Example, Kind, endpoint
|
||||
from controllers.openapi._models import (
|
||||
@@ -43,7 +42,6 @@ class KnowledgeBasesApi(Resource):
|
||||
kind=Kind.LIST,
|
||||
summary="Knowledge bases you can use, each with the knowledge-retrieval node data to paste",
|
||||
examples=(Example(title="Find product docs", input={"query": "product"}),),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=WORKSPACE_READ_GUARDS,
|
||||
query=KnowledgeBaseListQuery,
|
||||
returns=(HTTPStatus.OK, KnowledgeBaseListResponse, "Knowledge bases"),
|
||||
|
||||
@@ -200,6 +200,7 @@ def model_rows(models: Iterable[ModelWithProviderEntity], *, words: str, languag
|
||||
class ModelProviderApi(Resource):
|
||||
@endpoint(
|
||||
op="describe.model_provider",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="One model provider with its credential form and saved credentials (names only)",
|
||||
examples=(Example(title="OpenAI", input={"provider": _PROVIDER_EXAMPLE}),),
|
||||
@@ -264,6 +265,7 @@ class ModelProviderApi(Resource):
|
||||
class ModelProviderCredentialsApi(Resource):
|
||||
@endpoint(
|
||||
op="create.model_provider.credential",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Save a credential for a model provider; the provider checks it first",
|
||||
examples=(
|
||||
@@ -289,6 +291,7 @@ class ModelProviderCredentialsApi(Resource):
|
||||
class ModelProviderCredentialApi(Resource):
|
||||
@endpoint(
|
||||
op="set.model_provider.credential",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Replace a saved model provider credential",
|
||||
examples=(
|
||||
@@ -329,13 +332,13 @@ class ModelProviderCredentialApi(Resource):
|
||||
class ModelsApi(Resource):
|
||||
@endpoint(
|
||||
op="get.model",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.LIST,
|
||||
summary="Models in the workspace across providers, with whether each can be used now",
|
||||
examples=(
|
||||
Example(title="LLMs", input={"model_type": "llm"}),
|
||||
Example(title="OpenAI models", input={"provider": _PROVIDER_EXAMPLE}),
|
||||
),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=WORKSPACE_READ_GUARDS,
|
||||
query=ModelListQuery,
|
||||
returns=(HTTPStatus.OK, ModelListResponse, "Models"),
|
||||
@@ -362,6 +365,7 @@ class ModelsApi(Resource):
|
||||
class ModelCredentialsApi(Resource):
|
||||
@endpoint(
|
||||
op="create.model.credential",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Save a credential for one model you name, for providers such as OpenAI-compatible or Ollama; "
|
||||
"the first credential of a model not yet set up becomes its active credential",
|
||||
@@ -398,6 +402,7 @@ class ModelCredentialsApi(Resource):
|
||||
class ModelCredentialApi(Resource):
|
||||
@endpoint(
|
||||
op="set.model.credential",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Replace a saved credential of one model",
|
||||
examples=(
|
||||
|
||||
@@ -149,6 +149,7 @@ def _provider_hints(workspace_id: str, plugin_ids: Sequence[str]) -> list[Hint]:
|
||||
class MarketplacePluginsApi(Resource):
|
||||
@endpoint(
|
||||
op="get.marketplace.plugin",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.LIST,
|
||||
summary="Search the marketplace for plugins to install; each hit says if it is installed here",
|
||||
examples=(
|
||||
@@ -194,10 +195,10 @@ class MarketplacePluginsApi(Resource):
|
||||
class PluginsApi(Resource):
|
||||
@endpoint(
|
||||
op="get.plugin",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Plugins installed in the workspace, with the provider ids each one adds",
|
||||
examples=(Example(title="Installed model plugins", input={"category": "model"}),),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=WORKSPACE_READ_GUARDS,
|
||||
query=PluginListQuery,
|
||||
returns=(HTTPStatus.OK, PluginListResponse, "Installed plugins"),
|
||||
@@ -232,6 +233,7 @@ class PluginsApi(Resource):
|
||||
class PluginInstallApi(Resource):
|
||||
@endpoint(
|
||||
op="install.plugin",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Start installing marketplace plugins; finishes in the background, check describe.plugin.task",
|
||||
examples=(Example(title="Install one plugin", input={"identifiers": ["langgenius/openai:0.2.1@<sha256>"]}),),
|
||||
@@ -253,6 +255,7 @@ class PluginInstallApi(Resource):
|
||||
class PluginTaskApi(Resource):
|
||||
@endpoint(
|
||||
op="describe.plugin.task",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Progress of a plugin install or upgrade; check again while pending or running",
|
||||
examples=(Example(title="Check an install", input={"task_id": "<task_id>"}),),
|
||||
@@ -288,6 +291,7 @@ class PluginTaskApi(Resource):
|
||||
class PluginApi(Resource):
|
||||
@endpoint(
|
||||
op="delete.plugin",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Uninstall a plugin from the workspace",
|
||||
examples=(Example(title="Uninstall OpenAI", input={"plugin_id": "langgenius/openai"}),),
|
||||
|
||||
@@ -192,13 +192,13 @@ def tool_rows(
|
||||
class ToolsApi(Resource):
|
||||
@endpoint(
|
||||
op="get.tool",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.LIST,
|
||||
summary="Tools a workflow can call here, each with a ready tool node data",
|
||||
examples=(
|
||||
Example(title="Find GitHub tools", input={"query": "github"}),
|
||||
Example(title="Workflows published as tools", input={"provider_type": "workflow"}),
|
||||
),
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
requirements=WORKSPACE_READ_GUARDS,
|
||||
query=ToolListQuery,
|
||||
returns=(HTTPStatus.OK, ToolListResponse, "Tools"),
|
||||
@@ -224,6 +224,7 @@ class ToolsApi(Resource):
|
||||
class ToolProviderApi(Resource):
|
||||
@endpoint(
|
||||
op="describe.tool_provider",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="One tool provider with its API-key form and saved credentials (names only)",
|
||||
examples=(Example(title="Tavily", input={"provider": _PROVIDER_EXAMPLE}),),
|
||||
@@ -276,6 +277,7 @@ class ToolProviderApi(Resource):
|
||||
class ToolProviderCredentialsApi(Resource):
|
||||
@endpoint(
|
||||
op="create.tool_provider.credential",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Save an API-key credential for a tool provider; the provider checks it first",
|
||||
examples=(
|
||||
@@ -307,6 +309,7 @@ class ToolProviderCredentialsApi(Resource):
|
||||
class ToolProviderCredentialApi(Resource):
|
||||
@endpoint(
|
||||
op="set.tool_provider.credential",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Replace a saved tool provider credential",
|
||||
examples=(
|
||||
|
||||
@@ -101,7 +101,6 @@ class WorkspacesApi(Resource):
|
||||
class WorkspaceByIdApi(Resource):
|
||||
@endpoint(
|
||||
op="describe.workspace",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Workspace detail",
|
||||
examples=(
|
||||
@@ -130,7 +129,6 @@ class WorkspaceSwitchApi(Resource):
|
||||
|
||||
@endpoint(
|
||||
op="switch.workspace",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Server-side current workspace switch (shared with the web console)",
|
||||
examples=(Example(title="Make a workspace current on the server", input={"workspace_id": "<workspace_id>"}),),
|
||||
@@ -162,7 +160,6 @@ class WorkspaceMembersApi(Resource):
|
||||
|
||||
@endpoint(
|
||||
op="get.workspace.member",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.LIST,
|
||||
summary="List workspace members",
|
||||
examples=(Example(title="List members of the pinned workspace, first page", input={"page": 1, "limit": 20}),),
|
||||
@@ -187,7 +184,6 @@ class WorkspaceMembersApi(Resource):
|
||||
|
||||
@endpoint(
|
||||
op="invite.workspace.member",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Invite a member by email",
|
||||
examples=(
|
||||
@@ -239,7 +235,6 @@ class WorkspaceMemberApi(Resource):
|
||||
|
||||
@endpoint(
|
||||
op="delete.workspace.member",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Remove a member",
|
||||
examples=(Example(title="Remove a member by account id", input={"member_id": "<member_id>"}),),
|
||||
@@ -269,7 +264,6 @@ class WorkspaceMemberApi(Resource):
|
||||
|
||||
@endpoint(
|
||||
op="set.workspace.member_role",
|
||||
pat_policy=PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS,
|
||||
kind=Kind.OBJECT,
|
||||
summary="Change a member's role",
|
||||
examples=(
|
||||
|
||||
@@ -23,7 +23,7 @@ from extensions.ext_redis import RedisClientWrapper
|
||||
from libs import rate_limit
|
||||
from libs.passport import PassportService
|
||||
from libs.token import generate_csrf_token
|
||||
from models import App
|
||||
from models import App, AppMode
|
||||
from models.account import Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from tests.helpers.unit.config_override import apply_config_overrides
|
||||
from tests.helpers.unit.model_factories import make_account, make_app, make_tenant
|
||||
@@ -536,7 +536,98 @@ def test_app_scope_filters_discovery_and_workspace_scope_cannot_bypass_it(
|
||||
assert listed.get_json()["total"] == 1
|
||||
|
||||
|
||||
def test_selected_workspace_permits_member_reads_only_in_that_workspace(
|
||||
@pytest.mark.parametrize(
|
||||
("method", "path"),
|
||||
[
|
||||
("GET", "/workspaces/{id}"),
|
||||
("POST", "/workspaces/{id}:switch"),
|
||||
("GET", "/workspaces/{id}/members"),
|
||||
("POST", "/workspaces/{id}/members"),
|
||||
("PATCH", "/workspaces/{id}/members/{id}"),
|
||||
("DELETE", "/workspaces/{id}/members/{id}"),
|
||||
("POST", "/workspaces/{id}/apps/workflow"),
|
||||
("POST", "/workspaces/{id}/apps/imports"),
|
||||
("GET", "/workspaces/{id}/knowledge-bases"),
|
||||
("PATCH", "/apps/{id}/app-info/workflow"),
|
||||
("POST", "/apps/{id}:publish"),
|
||||
("GET", "/apps/{id}/dsl"),
|
||||
("GET", "/apps/{id}/versions"),
|
||||
],
|
||||
)
|
||||
def test_pat_rejects_management_outside_phase_one(
|
||||
client: FlaskClient, sqlite_session_factory: sessionmaker[Session], method: str, path: str
|
||||
) -> None:
|
||||
account = client.application.config["TEST_BROWSER_ACCOUNT"]
|
||||
target_id = str(uuid4())
|
||||
workspace = make_tenant(tenant_id=target_id)
|
||||
app = make_app(app_id=target_id, tenant_id=target_id, mode=AppMode.WORKFLOW)
|
||||
with sqlite_session_factory.begin() as session:
|
||||
session.add_all(
|
||||
[
|
||||
workspace,
|
||||
app,
|
||||
TenantAccountJoin(
|
||||
current=True, tenant_id=workspace.id, account_id=account.id, role=TenantAccountRole.OWNER
|
||||
),
|
||||
]
|
||||
)
|
||||
token = client.post(BASE, json={"name": "all scopes"}).get_json()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token['secret']}",
|
||||
CATALOG_HEADER: client.get("/openapi/v1/_catalog").headers[CATALOG_HEADER],
|
||||
}
|
||||
response = client.open("/openapi/v1" + path.format(id=target_id), method=method, json={}, headers=headers)
|
||||
assert response.status_code == 403, response.get_json()
|
||||
assert response.get_json()["message"] == "personal_access_token_not_supported"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rbac_enabled", [True], indirect=True)
|
||||
@pytest.mark.parametrize("allowed", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
("method", "suffix"),
|
||||
[
|
||||
("POST", "model-providers/openai/credentials"),
|
||||
("PATCH", "model-providers/openai/credentials/credential"),
|
||||
("POST", "model-providers/openai/models/credentials"),
|
||||
("PATCH", "model-providers/openai/models/credentials/credential"),
|
||||
("POST", "tool-providers/custom/credentials"),
|
||||
("PATCH", "tool-providers/custom/credentials/credential"),
|
||||
],
|
||||
)
|
||||
def test_pat_credential_management_still_requires_rbac(
|
||||
client: FlaskClient,
|
||||
sqlite_session_factory: sessionmaker[Session],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
method: str,
|
||||
suffix: str,
|
||||
allowed: bool,
|
||||
) -> None:
|
||||
from tests.helpers.unit.rbac_fakes import build_rbac_domain
|
||||
|
||||
domain = build_rbac_domain(sqlite_session_factory, monkeypatch)
|
||||
domain.transport.response = {"allowed": allowed}
|
||||
account = client.application.config["TEST_BROWSER_ACCOUNT"]
|
||||
workspace = make_tenant(tenant_id=str(uuid4()))
|
||||
with sqlite_session_factory.begin() as session:
|
||||
session.add_all(
|
||||
[
|
||||
workspace,
|
||||
TenantAccountJoin(
|
||||
current=True, tenant_id=workspace.id, account_id=account.id, role=TenantAccountRole.NORMAL
|
||||
),
|
||||
]
|
||||
)
|
||||
token = client.post(BASE, json={"name": "credentials", "resource_scope": {"mode": "none"}}).get_json()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token['secret']}",
|
||||
CATALOG_HEADER: client.get("/openapi/v1/_catalog").headers[CATALOG_HEADER],
|
||||
}
|
||||
response = client.open(f"/openapi/v1/workspaces/{workspace.id}/{suffix}", method=method, json={}, headers=headers)
|
||||
assert response.status_code == (422 if allowed else 403), response.get_json()
|
||||
assert [request.endpoint for request in domain.transport.requests] == ["/rbac/check-access"]
|
||||
|
||||
|
||||
def test_selected_workspace_permits_model_requests_only_in_that_workspace(
|
||||
client: FlaskClient,
|
||||
sqlite_session_factory: sessionmaker[Session],
|
||||
) -> None:
|
||||
@@ -571,16 +662,14 @@ def test_selected_workspace_permits_member_reads_only_in_that_workspace(
|
||||
"Authorization": f"Bearer {secret}",
|
||||
CATALOG_HEADER: client.get("/openapi/v1/_catalog").headers[CATALOG_HEADER],
|
||||
}
|
||||
members = client.get(f"/openapi/v1/workspaces/{first.id}/members", headers=headers)
|
||||
assert members.status_code == 200, members.get_json()
|
||||
assert client.get(f"/openapi/v1/workspaces/{second.id}/members", headers=headers).status_code == 403
|
||||
models = client.get(f"/openapi/v1/workspaces/{first.id}/models?limit=0", headers=headers)
|
||||
assert models.status_code == 422, models.get_json()
|
||||
assert client.get(f"/openapi/v1/workspaces/{second.id}/models?limit=0", headers=headers).status_code == 403
|
||||
listed = client.get("/openapi/v1/workspaces", headers=headers)
|
||||
assert [row["id"] for row in listed.get_json()["data"]] == [first.id]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"operation", ["workflow:run", "chat:run", "advanced-chat:run", "completion:run", "files", "dsl-import"]
|
||||
)
|
||||
@pytest.mark.parametrize("operation", ["workflow:run", "chat:run", "advanced-chat:run", "completion:run", "files"])
|
||||
def test_authorized_pat_reaches_existing_business_request_validation(
|
||||
client: FlaskClient,
|
||||
sqlite_session_factory: sessionmaker[Session],
|
||||
@@ -608,11 +697,7 @@ def test_authorized_pat_reaches_existing_business_request_validation(
|
||||
"Authorization": f"Bearer {secret}",
|
||||
CATALOG_HEADER: client.get("/openapi/v1/_catalog").headers[CATALOG_HEADER],
|
||||
}
|
||||
url = (
|
||||
f"/openapi/v1/workspaces/{workspace.id}/apps/imports"
|
||||
if operation == "dsl-import"
|
||||
else f"/openapi/v1/apps/{app.id}/{operation}"
|
||||
)
|
||||
url = f"/openapi/v1/apps/{app.id}/{operation}"
|
||||
# Invalid input must be rejected by the existing contract after PAT admission,
|
||||
# without executing workflows, invoking models or performing an upload/import.
|
||||
response = client.post(url, json={}, headers=headers)
|
||||
|
||||
@@ -2515,6 +2515,34 @@ class TestAuthMatrixCases:
|
||||
if expected.message is not None:
|
||||
assert body.get("message") == expected.message
|
||||
|
||||
@staticmethod
|
||||
def test_pat_workspace_management_is_limited_to_phase_one_routes(matrix_app: Flask) -> None:
|
||||
from constants.personal_access_token import PersonalAccessTokenPolicy
|
||||
|
||||
supported = {
|
||||
route.id
|
||||
for route in ROUTES
|
||||
if (spec := _spec_for(matrix_app, route)) is not None
|
||||
and spec.pat_policy is PersonalAccessTokenPolicy.WORKSPACE_OPERATIONS
|
||||
}
|
||||
assert supported == {
|
||||
"plugins.marketplace",
|
||||
"plugins.list",
|
||||
"plugins.install",
|
||||
"plugins.task",
|
||||
"plugins.delete",
|
||||
"model_providers.describe",
|
||||
"model_providers.credential.create",
|
||||
"model_providers.credential.set",
|
||||
"models.list",
|
||||
"models.credential.create",
|
||||
"models.credential.set",
|
||||
"tools.list",
|
||||
"tool_providers.describe",
|
||||
"tool_providers.credential.create",
|
||||
"tool_providers.credential.set",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def test_registered_openapi_routes_match_the_matrix(matrix_app: Flask) -> None:
|
||||
"""Every guarded /openapi/v1 route is in the table, and nothing else is.
|
||||
|
||||
Reference in new issue
Block a user