mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 04:13:36 +08:00
docs-gated-dependencies-for-python-314
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 91cf5573b6 | chore: Update AGENTS.md authorization guards | |||
| 28c8fe3deb |
feat(authz): finalize authz_share — variable share-aware fetch, dedup, tests (#13444)
* feat(authz): finalize authz_share — variable share-aware fetch, dedup, tests Phase 3 (authz_share) was mostly shipped, but `variable` — though advertised as a shareable resource (ShareResourceType, VariableAction, ensure_variable_permission, owner-lookup) — was the one resource whose routes never became share-aware. They hard-scoped to the caller and passed variable_user_id=current_user.id, so the owner-override always tripped: a permitted non-owner PATCH/DELETE 404'd before enforcement and variable shares were silently dead. - variable.py: PATCH/DELETE now use authorized_or_owner_scoped and pass the resolved owner to ensure_variable_permission, wrapping plugin-deny as 404 via deny_to_404. Mutations run against the resolved owner so the owner-scoped service queries match. OSS behavior is unchanged (owner-scoped). A missing variable on DELETE is now 404 (was 500), consistent with PATCH. - authz_shares.py: extract a single _share_visible() predicate shared by get_share and list_shares, removing duplicated owner/PUBLIC/USER/TEAM logic. - knowledge_bases.py: wire filter_visible_resources into the KB list to match the flows/deployments/projects convention (no-op in OSS; gives plugins the documented per-row hook). - AGENTS.md: list variable PATCH/DELETE among the share-aware fetch helpers. - tests: variable cross-user (OSS 404, plugin allow/deny), TEAM-scope share visibility via get_share/list_shares, the _share_visible predicate, and the cache-invalidation contract. * test(authz): cover cross-user DELETE plugin paths for variables Mirror the PATCH plugin tests for DELETE: a plugin-allowed non-owner delete removes the owner's row (exercises the owner-scoped delete under the resolved owner), and a plugin-denied delete returns 404 via deny_to_404 with the owner's row intact. * fix(authz): gate KB disk fallback on raw DB rows, not authz-filtered rows list_knowledge_bases reassigned 'rows' to the filter_visible_resources result, so if an authorization plugin filtered out all of a user's KB rows, the empty result fell through to the disk fallback (kb_path.exists()) and re-surfaced KBs the plugin had just hidden. Keep the raw DB result as original_rows, filter into a separate 'rows' for the returned list, and gate the disk fallback on 'not original_rows' so it only fires when the user genuinely has no DB rows. Adds a regression test. * revert(authz): drop inert KB list filter_visible_resources The KB list only fetches the caller's own rows, and filter_visible_resources lets owner rows bypass batch_enforce, so the call could never narrow anything — pure forward-compat scaffolding that also introduced the disk-fallback edge case. Revert list_knowledge_bases to its owner-scoped form (single-KB fetch stays share-aware). Removes the mock-only regression test that could only cover the now-removed gate. KB share-awareness can be revisited if/when the list grows non-owned candidates. * test(authz): mark new authz test modules no_blockbuster Add module-level pytestmark = pytest.mark.no_blockbuster to the variable and share-route test modules, per the project review guideline. No-op today (the blockbuster fixture is autouse=False) but keeps the modules correct if blockbuster is re-enabled repo-wide. * fix(authz): deny_to_404 surfaces non-403 errors unchanged The non-403 branch preserved the original status code but relabeled the detail as "<resource> not found", so any 4xx/5xx raised by a guard would surface with a misleading "not found" message. Return the original exception unchanged; only a 403 maps to 404 for UUID privacy. Latent today (the authz guards raise only 403). Strengthen test_fetch.py to assert the non-403 detail survives. |
|||
| fefa4262ae | docs: bump python version reqs to 3.14 and add release note (#13289) | |||
| aafce557aa |
feat(authz): admin APIs, share-aware build_flow, and base service additions (#13371)
* feat(authz): add roles, role-assignments, teams, and me/permissions APIs
Completes the OSS admin surface for RBAC: full CRUD on authz_role,
authz_role_assignment, authz_team, and authz_team_member tables, plus a
per-user effective-permissions endpoint backing the frontend permission gate.
Plugin (enterprise Casbin) is invalidated on every write so the next enforce
sees the change.
New endpoints:
* GET/POST/PATCH/DELETE /api/v1/authz/roles -- custom-role CRUD with parent
cycle detection, system-role protection, and FK-aware delete (409 when
assignments still reference the role).
* GET/POST/DELETE /api/v1/authz/role-assignments -- list filterable by
user/role/domain; superuser-only for write, self-read allowed.
* GET/POST/PATCH/DELETE /api/v1/authz/teams + GET/POST/DELETE
/api/v1/authz/teams/{id}/members -- backs the share-with-team flow and
the team admin UI.
* POST /api/v1/authz/me/permissions -- returns per-resource allowed actions
for the current user (capped at 500 ids), backs the FE permission gate.
Base service additions (lfx.services.authorization.base):
* list_visible_resource_ids -- plugin SQL prefilter for list endpoints; OSS
returns None ("no prefilter, fall through to filter_visible_resources").
* get_effective_permissions -- default impl uses batch_enforce so plugins
inherit it for free; Casbin plugin can override for a tighter query.
* fix(authz): make build_flow share-aware so shared flows can be run by non-owners
The build_flow handler was loading flows with an owner-OR-public SQL filter,
short-circuiting the ensure_flow_permission(EXECUTE) check on private flows
shared with the current user. The existing in-code comment flagged this as a
Phase 3 prerequisite for cross-user build support.
Switch to the share-aware load helper (_read_flow / authorized_or_owner_scoped):
* When the registered authorization plugin signals SUPPORTS_CROSS_USER_FETCH
(enterprise Casbin), the row loads by id alone and the plugin decides via
enforce(). OSS pass-through keeps the historical owner-only behavior.
* PUBLIC flows are still buildable by any authenticated user in both modes
(preserved via the explicit access_type=PUBLIC fallback).
* Plugin deny is translated to 404 (not 403) via deny_to_404 so the response
is indistinguishable from "flow does not exist" and the caller cannot
enumerate UUIDs by probing for 403 vs 404.
* test(authz): cover new admin routes and share-aware build_flow
Pins the security floor and validation behavior added in the prior commits so
they don't silently regress. Uses the existing fake async session + stub authz
service pattern from test_authz_share_routes.py.
test_authz_admin_routes.py (17 tests):
* /authz/roles -- superuser gate on create, persist + invalidate_all, 409 on
name conflict, system-role protection on update + delete, self-parent and
parent-cycle rejection, 409 when deleting a role with active assignments.
* /authz/role-assignments -- non-superuser blocked from listing other users'
assignments; self-list allowed; 404 when user_id is unknown; create
invokes invalidate_user(target).
* /authz/teams -- superuser gate on create, add_member invokes
invalidate_user(target), duplicate add returns 409.
* /authz/me/permissions -- correct per-resource shape, 400 when over 500
ids, empty request returns empty.
test_chat_build_flow_authz.py (5 tests):
* Owner happy path preserved.
* Plugin 403 deny converted to 404 (UUID privacy).
* Unknown flow + no PUBLIC fallback -> 404.
* Share-aware load miss + PUBLIC fallback hits -> success.
* Non-403 errors (e.g. 500) pass through with status preserved.
Total: 22 new tests, all passing locally.
* fix(authz): normalize, dedupe, and cap the actions list on /authz/me/permissions
The resource_ids cap (500) bounded one side of the cartesian product, but the
actions list was unbounded. A client could submit actions=["read"] * 100000
and trigger a 50M-entry batch_enforce, plus the same action in different
casings ("READ", "read") would have been treated as distinct entries.
Add a field_validator on EffectivePermissionsRequest.actions that:
* strips whitespace and lowercases each entry
* de-duplicates while preserving order
* caps the result at 10 unique actions (headroom over the 6 known actions:
read, write, execute, delete, create, manage)
* returns None when the normalized list is empty so the handler still falls
back to _DEFAULT_ACTIONS
Over-cap requests raise ValidationError (HTTP 422 at the request boundary),
which matches Pydantic conventions for input shape problems and differs from
the resource_ids cap (HTTP 400 in the handler) by design -- bound-violation
on list size is structurally a validation error, not application logic.
6 new tests: normalization+dedupe, empty-collapses-to-None, over-cap rejection,
50-entry dedupe stays under cap, None stays None, handler sees normalized list.
* fix(authz): default /authz/role-assignments list to caller's own assignments
The gate `if user_id is None or user_id != current_user.id` required superuser
whenever user_id was omitted, so any non-superuser calling
GET /authz/role-assignments with no query params got 403 even though the
docstring promised "Users may query their own assignments".
Resolve user_id to current_user.id when omitted, then only gate superuser when
the resolved id differs from the caller. The query is now always filtered by
the resolved user_id, so admins doing cross-user lookups make one call per
user instead of relying on an unscoped see-all.
Adds test_list_assignments_no_user_id_defaults_to_self pinning the new
behavior. The other two assignment-list tests (self with explicit user_id,
non-superuser blocked from another user) remain unchanged and still pass.
* fix(authz): use model_fields_set so PATCH /authz/roles can clear nullable fields
The "if payload.<field> is not None" guards made it impossible to clear
description or parent_role_id via PATCH -- an explicit "description": null
in the body was indistinguishable from omitting the field, leaving the old
value in place.
Switch to presence checks via payload.model_fields_set:
* description: nullable on the DB side, so an explicit null clears the field.
* parent_role_id: nullable + SET NULL on cascade; null removes the parent.
* name: NOT NULL + unique on the DB side. The presence check applies, but
an explicit null is rejected at the boundary with HTTP 400 so the caller
sees a clear error instead of an opaque 409 "Name conflict" from the
IntegrityError catch block below.
* permissions: nullable=False (default_factory=list) on the DB side. An
empty list is the natural "clear"; null is rejected with HTTP 400.
6 new tests: clear description via null, clear parent via null, omitted
fields untouched, reject null name, clear permissions via empty list,
reject null permissions.
Adds _make_role_row helper so SimpleNamespace fakes carry every field
that RoleRead.model_validate expects (workspace_id, created_at, created_by).
* fix(authz): use model_fields_set so PATCH /authz/teams can clear description
The "if payload.description is not None" guard made the description field
unclearable via PATCH -- an explicit "description": null was indistinguishable
from omitting the field, leaving the old value in place.
Switch to a presence check via payload.model_fields_set so null clears the
field, while omission still leaves the row alone. The DB column is nullable,
so None is a legitimate value here.
Scope intentionally narrow: team_name and adom_name are NOT NULL on the DB
side (null would crash on commit) and is_active is bool (null doesn't model
any state), so they keep the existing is-not-None guards.
Adds 2 tests: clear via explicit null, omission leaves value untouched.
Adds _make_team_row helper for parity with _make_role_row.
* fix(authz): validate batch_enforce result length in get_effective_permissions
The default get_effective_permissions impl built `requests` as a cartesian
product of resource_ids x actions, called batch_enforce, then sliced the
flat result back into per-resource action lists. If a plugin's batch_enforce
returned the wrong number of results -- contract violation -- the slicing
silently produced incorrect mappings:
* too-short flat: later resources got `[]` (out-of-bounds slice returns empty
in Python) instead of their real permissions
* too-long flat: tail entries silently dropped
The inner `zip(..., strict=True)` only caught the per-slice mismatch when a
slice happened to be too short, not the global length mismatch.
Validate `len(flat) == len(requests)` immediately after the call and raise
ValueError with expected/actual counts so plugin authors find the contract
break at the first wrong call, not via mysteriously-empty permission lists.
Adds two lfx tests:
* default impl returns full action list per resource (no enforcement)
* mocked batch_enforce returning a wrong-sized list raises ValueError
* fix: remove EE references
* fix: harden authz admin APIs based on PR review
- Pin permission slug format to canonical <resource>:<action> and validate
RoleCreate/RoleUpdate at the API boundary so bad slugs cannot land in
authz_role.permissions and silently fail policy sync.
- Tighten RoleAssignmentCreate: domain_type is a Literal and a model
validator enforces domain_id consistency (required for org/workspace/
project, forbidden for global).
- Restore _resolve_casbin_domain re-export for plugin backward compat.
- Audit-log policy-changing admin writes (role CRUD, role-assignment
create/delete, team CRUD, team membership add/remove) so privilege
grants are visible alongside enforcement decisions.
- Invalidate authorization cache when a team's adom_name or is_active
changes; pure display edits still skip the flush.
* review feedback
* [autofix.ci] apply automated fixes
* fix(authz): harden build_flow and expose authz_enabled on config
Block non-owners from overriding flow graph data on shared builds,
revert unrelated component_index.json churn, and surface
LANGFLOW_AUTHZ_ENABLED on public and authenticated config responses
for EE UI gating.
* [autofix.ci] apply automated fixes
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: himavarshagoutham <himavarshajan17@gmail.com>
Co-authored-by: Himavarsha <40851462+HimavarshaVS@users.noreply.github.com>
|
|||
| fb3d6ec90b |
feat: OSS authorization foundations for enterprise RBAC (#13153)
* feat: add OSS authorization foundations for enterprise RBAC
Introduce pluggable AuthorizationService, authz plugin tables, and flow
API enforcement hooks. Disabled by default (LANGFLOW_AUTHZ_ENABLED=false);
enterprise Casbin plugin can override via lfx.services entry point.
* fix(authz): allow non-superusers when OSS authorization service is active
OSS LangflowAuthorizationService is a pass-through until enterprise Casbin
replaces it; the previous fail-closed superuser-only check caused 403 on flow APIs.
* fix(authz): harden OSS authorization foundations (bug + tests + docs) (#13216)
* fix: harden OSS authorization foundations (PR (a) — bug + lint + tests)
Hardening pass for #13153 to clear pre-merge checks and raise unit-test
coverage on the new authorization surface before broader feature work.
- Fix read_public_flow: query the flow directly and return when
access_type == PUBLIC; remove user-impersonation via
get_user_by_flow_id_or_endpoint_name and the read_flow delegation
(public means public — no authz call).
- Fix _token_alias_candidates ordering so the _access_token branch is
reachable (previously shadowed by the _token check).
- Register BaseAuthorizationService in import_all_services_into_a_dict so
AuthorizationServiceFactory can resolve its create() return annotation
at startup (mirrors the BaseAuthService pattern).
- Rename test_non_wxo_access_token_does_not_create_bearer_alias to
test_generic_access_token_exposes_bearer_alias; body asserts the alias
IS created.
- Uppercase env-var literals in test_minimal_services to clear
Ruff SIM112/S105; per-line noqa only for the lowercased global header
name x-langflow-global-var-access-token.
- Add unit coverage for batch_enforce (enabled/disabled/empty),
get_allowed_actions, ensure_permission, ensure_flow_permission,
AuthorizationServiceFactory.create, and the LFX no-op default
AuthorizationService.
- Persist+read tests for the seven previously-untested authz models
(AuthzRoleAssignment, AuthzTeam, AuthzTeamMember, AuthzShare,
AuthzEditLock, AuthzAuditLog).
- One-line docstrings on undocumented public symbols across the new
authorization modules and authz database models.
* fix(authz): align authz_role.permissions model nullability with migration
Migration f7a8b9c0d1e2 declares ``permissions`` as ``nullable=False`` but
the model used the default ``Column(sa.JSON)`` (nullable=True), which made
``test_no_phantom_migrations`` detect a phantom modify_nullable diff after
upgrading to head. Explicitly set ``nullable=False`` so the model matches
the migration.
* feat(authz): finish Phase 1 OSS — FlowAction enum, audit, owner override, project domain (#13220)
PR (b) of the OSS authz rollout. Lands every piece the enterprise Casbin
plugin needs to enforce RBAC on flows end-to-end:
- New FlowAction(str, Enum) — canonical action vocabulary (read/write/
create/delete/execute/deploy). Re-used by enterprise PolicySync when
compiling roles + shares into casbin_rule.
- ensure_flow_permission now accepts FlowAction|str, computes domain
from folder_id ('project:{folder_id}' or '*'), and applies an owner
override: when current_user.id == flow.user_id, the call short-circuits
before contacting the plugin (so solo installs keep working with
LANGFLOW_AUTHZ_ENABLED=true). Owner-override is audited too, with
result='owner_override'.
- Guards wired on all 9 flow CRUD routes (was 4): create, read, patch,
delete, put-upsert (write|create branches), batch-create, upload,
bulk-delete, bulk-download. /public_flow/{id} stays unguarded by
design (public means public), and GET /flows/ list-filtering is
deferred to a follow-up PR.
- audit_decision() schedules an AuthzAuditLog row via asyncio.create_task
on every allow/deny/owner_override decision. Failures inside the task
are logged and swallowed so audit never blocks a request. Gated on a
new AUTHZ_AUDIT_ENABLED setting (defaults True; ignored when AUTHZ is
off).
- filter_visible_resources() helper — generic batch_enforce-based filter
for list endpoints. No-op when AUTHZ disabled. Plumbing for PR (c);
GET /flows/ rewrite is intentionally NOT in this PR.
- BaseAuthorizationService gains invalidate_user / invalidate_role /
invalidate_all hooks (no-op default bodies). Enterprise plugin
overrides these for L1/L2 cache invalidation when roles or shares
change.
OSS LangflowAuthorizationService remains a pass-through; enforcement is
entirely the enterprise plugin's job. Tests use a stub fail-closed
service via monkeypatch to verify guards forward the right tuple, and
source-level checks on flows.py ensure no route silently regresses to
a bare-string action or loses its guard.
47 backend + 12 lfx new/extended tests. Migration regression suite
still green.
* feat(authz): add deployment API guards aligned with Phase 1 flow authz
Wire ensure_deployment_permission on deployment CRUD and run routes using
DeploymentAction, project/workspace domain resolution, owner override, and
audit logging consistent with Eric's flow authorization helpers.
* feat(authz): add `langflow authz dry-run` CLI for policy simulation (#13224)
Adds a self-contained subcommand that simulates every flow-CRUD guard
site under a configurable stub policy and prints what *would* happen —
the Casbin tuple (user, domain, obj, act), the audit row that would be
written, and the resulting HTTP outcome. No network, no DB writes.
Useful for:
- validating an upcoming Casbin policy before shipping it
- showing operators what the audit log will look like
- smoke-testing the guard wiring after a refactor
Built-in policies:
- allow-all (matches the OSS default)
- deny-non-owner (owners + superusers allowed)
- deny-writes (read/execute allowed; write/create/delete denied)
- owner-only (only the flow owner; no superuser bypass)
Usage:
langflow authz dry-run
langflow authz dry-run --policy deny-writes
langflow authz dry-run --policy deny-non-owner --json --output report.json
Implementation: monkey-patches the three helpers in
langflow.services.authorization.utils (get_settings_service,
get_authorization_service, audit_decision) for the duration of the run,
walks _FLOW_GUARDS x three actors (owner / non-owner / superuser), and
renders a Rich table or JSON document with per-row decision + summary
footer.
Tests: 18 cases covering each stub policy, the runner output shape, JSON
serialisation, and CLI integration via typer.testing.CliRunner.
* fix(authz): keep user-derived is_superuser authoritative in ensure_permission
Caller-supplied context was merged after `_auth_context(user)`, which let a
caller forge `context={"is_superuser": True}` and bypass intent of the guard.
Flip the merge order so user-derived auth fields always win, and add a
regression test that asserts a malicious caller cannot override the flag.
* fix(authz): per-flow CREATE checks in batch and upload paths
The batch (POST /flows/batch/) and upload (POST /flows/upload/) handlers were
calling ensure_flow_permission once at the route boundary without per-flow
scope. Because the FlowCreate payload carries caller-supplied workspace_id and
folder_id, a single coarse check let payloads target workspaces/folders the
caller has no create permission on — invisible under OSS pass-through but a
real bypass once an enterprise plugin enforces workspace-scoped roles.
Both handlers now iterate flow_list.flows and call ensure_flow_permission with
the flow's own workspace_id and the effective folder_id (the query parameter
wins on upload, matching _upsert_flow_list's actual write behavior).
Adds two AST-level regression tests that assert each route calls
ensure_flow_permission inside a `for ... in flow_list.flows` loop with the
flow's destination kwargs.
* fix(authz): authorize destination scope when PATCH/PUT moves a flow
update_flow (PATCH) and upsert_flow's update branch (PUT) only checked WRITE
permission against the existing flow's workspace_id/folder_id. Because
_patch_flow and _update_existing_flow apply payload workspace_id/folder_id via
model_dump(exclude_unset=True, exclude_none=True), a caller authorized to
write a flow in scope A could move it into scope B without ever being checked
against B — invisible under OSS pass-through but a real bypass once an
enterprise plugin enforces workspace/share-scoped roles.
Both handlers now compute the effective destination (payload wins when
non-None, else existing) and, when it differs from the existing scope, run a
second ensure_flow_permission(WRITE, ...) call against the destination. No
extra DB hits — the audit row already captures move attempts.
Adds two AST-level regression tests asserting each route makes the
destination check using target_workspace_id/target_folder_id kwargs.
* fix(authz): partial unique indexes on authz_role_assignment for NULL domain_id
The original UNIQUE(user_id, role_id, domain_type, domain_id) constraint on
authz_role_assignment allowed duplicate global role assignments because SQL
treats NULL domain_id values as never-equal. Two ("global", NULL) rows for the
same (user, role) pair both passed.
Replaces the broken constraint with two partial unique indexes:
- uq_authz_role_assignment_scoped: unique on (user_id, role_id, domain_type,
domain_id) WHERE domain_id IS NOT NULL — covers workspace/org assignments.
- uq_authz_role_assignment_global: unique on (user_id, role_id, domain_type)
WHERE domain_type='global' AND domain_id IS NULL — covers global assignments
without relying on NULL equality.
Postgres and SQLite both support partial unique indexes via WHERE clauses.
New Alembic migration (Phase: EXPAND) drops the broken constraint and creates
the two indexes, using batch_alter_table so SQLite recreates the table cleanly.
Three new DB-level regression tests in test_authz_models.py:
- test_authz_role_assignment_blocks_duplicate_global
- test_authz_role_assignment_blocks_duplicate_scoped
- test_authz_role_assignment_allows_distinct_workspaces
* test(lfx): remove redundant @pytest.mark.asyncio decorators
src/lfx/pyproject.toml has asyncio_mode = "auto", so pytest-asyncio
auto-detects async test functions. The decorators on the seven async tests in
test_default_authorization_service.py are no-ops. Removed for clarity; tests
continue to pass via auto-detect (mode=Mode.AUTO confirmed in pytest output).
import pytest stays for the @pytest.fixture decorator on `service`.
* test(authz): complete DeploymentAction enum coverage in test_actions
test_deployment_action_values_match_casbin_strings only asserted READ, CREATE,
EXECUTE — WRITE and DELETE were missing despite being members of the enum.
Mirrored the FlowAction test structure: full value coverage plus a new
test_deployment_action_is_iterable_and_complete that enumerates members.
DeploymentAction has no DEPLOY (deploy is a flow-level action that mints a
deployment), so the expected set is five values.
* fix(authz): partial unique indexes on authz_share for NULL target_id
The original UNIQUE(resource_type, resource_id, scope, target_id) constraint
on authz_share did not prevent duplicate PRIVATE/PUBLIC shares because
target_id is NULL for those scopes and SQL treats NULL values as never-equal.
Replaces the constraint with two partial unique indexes:
- uq_authz_share_targeted: unique on (resource_type, resource_id, scope,
target_id) WHERE target_id IS NOT NULL — covers TEAM/USER scopes.
- uq_authz_share_untargeted: unique on (resource_type, resource_id, scope)
WHERE target_id IS NULL — covers PRIVATE/PUBLIC scopes without relying on
NULL equality.
Chose partial indexes over CodeRabbit's sentinel-UUID suggestion to keep
target_id genuinely nullable (sentinel values leak into every read path that
filters by target_id, and any future FK on target_id would break).
Consistent with the d9e8f7a6b5c4 fix for authz_role_assignment.
New Alembic migration (Phase: EXPAND, validator: ✅) drops the broken
constraint via batch_alter_table and creates both partial indexes.
Three new DB-level regression tests in test_authz_models.py:
- test_authz_share_blocks_duplicate_targeted (USER scope dup → IntegrityError)
- test_authz_share_blocks_duplicate_untargeted (PUBLIC dup → IntegrityError)
- test_authz_share_allows_distinct_targets (sanity: different users OK)
* fix(authz): prefer project over workspace in _resolve_flow_domain
Casbin g2 inheritance is directional: a child resource inherits from its
parent, not the other way round. Declaring `g2, project:xyz, workspace:abc`
lets a workspace-scoped grant flow down to checks made against project:xyz,
but a project-scoped grant does NOT flow up to checks made against
workspace:abc.
The original precedence picked workspace whenever it was set, which made
project-scoped grants invisible to the enforcer the moment a workspace was
also known. Flip the precedence so the more specific domain (project) wins —
project-scoped grants now match directly, and workspace-scoped grants still
match via g2 inheritance. Both ids stay in the enforce context for ABAC.
- _resolve_flow_domain returns project first, then workspace, then "*".
- Docstrings on both _resolve_flow_domain and its callers updated.
- test_ensure_flow_permission_workspace_beats_project renamed to
test_ensure_flow_permission_project_beats_workspace with inverted assertion
and an explanation of g2 directionality.
- test_resolve_flow_domain_precedence updated accordingly.
Deployment helper uses the same function but only one deployment test passes
both ids; deployment behavior is unchanged in tests that pass project_id only.
* feat(authz): Phase 2 — list filtering, execute guards, project routes (#13255)
* feat(authz): Phase 2 — list filtering, execute guards, project routes
Extends the Phase 1 authorization foundations (#13153) with the route coverage
the IBM RBAC design note flags as "Not hooked yet" under §4.1 and the Phase 2
deliverables under §5.3 (workspace/project domain, list/filter APIs).
List filtering
- GET /flows/ now calls filter_visible_resources(FlowAction.READ) on the loaded
list. OSS pass-through returns the input unchanged; the enterprise plugin
uses batch_enforce to honor role + share grants.
- GET /projects/ applies the same filter against ProjectAction.READ.
Execute guards (FlowAction.EXECUTE)
- POST /build/{flow_id}/flow (chat.py): authorizes after loading the flow row.
- POST /run/{flow_id_or_name} (endpoints.py, api-key auth).
- POST /run/session/{flow_id_or_name} (endpoints.py, session auth).
- POST /webhook/{flow_id_or_name} (endpoints.py, webhook auth).
Project (folder) authorization
- New ProjectAction enum (READ/WRITE/CREATE/DELETE).
- New ensure_project_permission helper with workspace domain + owner override,
mirroring the flow/deployment helpers.
- Guards wired into create_project, read_project, update_project,
delete_project, download_file, upload_file.
Tests
- 7 new ensure_project_permission tests in test_utils.py (enum coercion,
workspace domain, owner override, wildcard create object).
- 4 new test_execute_surfaces_guard_with_flow_execute parametrized cases.
- 6 new test_project_routes_guarded parametrized cases.
- test_read_flows_list_unchanged_in_this_pr renamed to
test_read_flows_list_uses_filter_visible_resources; it now asserts the
filter is called exactly once.
Docs
- Added an "Authorization (RBAC)" section to AGENTS.md describing the plugin
model, route helpers, Casbin request shape, and the dry-run CLI.
Out of scope for Phase 2 (per design note §5.3):
- Knowledge bases and global variables (Phase 4: deployments, components,
secrets, audit UI).
- Deployment list filtering (paginated + shaped items; needs SQL-level
prefiltering once authz_share lands in Phase 3).
- authz_share CRUD APIs and UI (Phase 3 — required for full RBAC).
* docs(authz): correct domain precedence note in AGENTS.md
Follows the upstream _resolve_flow_domain fix on the parent branch — project
wins over workspace so project-scoped grants match directly and workspace
grants still flow down via Casbin g2.
* fix(authz): align dry-run CLI with project-over-workspace domain precedence
The _resolve_flow_domain flip (project wins over workspace,
|
|||
| bc927abef2 |
chore: improve AI-agent context (CLAUDE.md auto-load + AGENTS.md tweaks) (#13028)
* chore: import AGENTS.md from CLAUDE.md so Claude Code loads project instructions Claude Code does not yet natively support AGENTS.md. The previous CLAUDE.md was a human-readable redirect, but the agent never followed it. Using the @AGENTS.md import directive (which Claude Code does support) makes the project instructions auto-load. * chore: clarify dev-deps sync and pre-commit workflow in AGENTS.md - Note that running tests inside a sub-package needs uv sync --group dev --package <name>; the default sync skips sub-package dev deps and leaves things like fakeredis uninstalled. - Tighten the pre-commit workflow section: pre-commit hooks already run ruff/biome on commit, so the previous 5-step manual sequence overstated what's needed. |
|||
| 922f86411b |
chore: add AGENTS.md for AI coding agents (#11411)
* feat: add AGENTS.md for AI coding agents This adds a general AGENTS.md file (see https://agents.md) for use by AI coding agents. This also adds an explicit CLAUDE.md file that simply references AGENTS.md, since Claude Code has not yet adopted the emerging standard of AGENTS.md (see https://github.com/anthropics/claude-code/issues/6235). * Update AGENTS.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> |