mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 02:32:13 +08:00
* 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, ac3e8bcf0f) made
test_domain_is_workspace_prefixed unsatisfiable — every dry-run scenario passes
both workspace_id and folder_id, so every audited row now resolves to
project:{folder_id}.
- Renamed the test to test_domain_is_project_prefixed and updated the assertion.
- Updated the matching fallback literal in authz_dry_run.py so it reflects
what the resolver would actually emit (the fallback fires only when no audit
row is written, which is rare since AUTHZ_ENABLED is forced True in dry-run).
* fix(authz): block-merge fixes — note_translations leak, schema hardening
Eight fixes from the post-merge review of #13153:
C1 security — GET /flows/{flow_id}/note_translations leaked note text across
users. The handler authenticated the caller but never scoped the fetch or
called ensure_flow_permission, so any authenticated user could read note
content from any flow by guessing the UUID. Switch to _read_flow (owner-scoped)
and add ensure_flow_permission(READ); return {} on miss so missing-flow vs
not-owned vs not-authorized are indistinguishable.
C2 database — Casbin's loader filters by ptype on every load_policy() and
AddPolicy(). Without an index, every enterprise policy refresh became a full
table scan on casbin_rule. Add ix_casbin_rule_ptype.
C3 database — Every authz timestamp column was created as sa.DateTime() with
no timezone. Inconsistent with deployment/api_key/variable models, and on
Postgres it strips tzinfo on write so audit-log ordering can drift across DST.
Switch to DateTime(timezone=True) on every authz timestamp; migration uses
postgresql_using="column AT TIME ZONE 'UTC'" to convert existing rows safely.
H1 security — uq_authz_role_assignment_global filtered on
(domain_type='global' AND domain_id IS NULL). A row with domain_type='org'
AND domain_id IS NULL was covered by neither partial index and duplicates
slipped through. Widen the filter to domain_id IS NULL (drop the domain_type
check); rename the index to uq_authz_role_assignment_unscoped to reflect that
it covers every NULL-domain_id case.
H2 database — authz_share.created_by was ondelete=CASCADE. Deleting a user
silently revoked every share they created. Mirror the SET NULL pattern from
authz_audit_log.user_id and make the column nullable.
H3 database — "list all shares for resource X" runs WHERE resource_type=? AND
resource_id=? — bitmap-AND of two single-column indexes works but a composite
index is tighter and matches the precedent set by ix_authz_audit_log_resource.
Add ix_authz_share_resource (resource_type, resource_id).
H4 python — noqa: TC002 suppressions on factory.py runtime imports were
hiding a real ruff finding: with `from __future__ import annotations`, those
imports are typecheck-only. Move them into the TYPE_CHECKING block.
H5 python — _authz_settings had no return type annotation. Mypy inferred Any
which propagated through is_enabled() and every downstream consumer. Add the
explicit AuthSettings return type.
Plus a TODO comment on AuthzAuditLog noting the retention/partitioning gap
flagged by the DB reviewer (M7 in the synthesis).
New regression tests:
- test_authz_role_assignment_blocks_duplicate_non_global_with_null_domain_id
- test_get_note_translations_is_owner_scoped_and_guarded
112 authz tests pass; migration validator output Valid: ✅; alembic upgrade
head runs clean against a fresh SQLite DB.
* refactor(authz): collapse duplicated ensure_*_permission helpers
The three public guards (flow/deployment/project) shared ~130 LOC of
copy-paste: object key assembly, action coercion, owner-override check, audit
write, and delegation to ensure_permission. Two changes from the Python
reviewer (M1, M2 in the synthesis):
- Extract a private ``_ensure_resource_permission`` core that owns the
identical structural pattern. The three public helpers become thin wrappers
that build the resource-specific extra_context (with the right key names —
``flow_user_id`` vs ``deployment_user_id`` vs ``project_user_id``) and
delegate. Adding a fourth resource type is now a five-line addition instead
of a 45-line copy.
- Rename ``_resolve_flow_domain`` to ``_resolve_casbin_domain`` because it
serves flows AND deployments (callers pass ``folder_id`` or ``project_id``
through the same helper). Parameter renamed from ``folder_id`` to
``scope_id`` to reflect the polymorphic role. A module-level alias
``_resolve_flow_domain = _resolve_casbin_domain`` preserves the old name
for downstream importers; the test asserts both names resolve to the same
function.
Drive-by: extracted ``_OWNER_CONTEXT_KEYS`` constant so the audit-detail
extraction picks up ``project_user_id`` too (previously only
``flow_user_id`` and ``deployment_user_id`` were forwarded to audit rows).
Skipped the L6 finding (collapse pass-through ``enforce``/``batch_enforce``
into ``BaseAuthorizationService``). Keeping those abstract on the base is a
deliberate safety guard — an enterprise plugin that forgets to override
``enforce`` should fail loudly at instantiation, not silently inherit
allow-all in production. The duplication between LangflowAuthorizationService
and the lfx default is the price.
112 authz tests pass; ruff clean.
* fix(authz): remove redundant owner-only check + per-domain list filter + share CHECK constraints
Three fixes from the post-merge static review:
1. ``check_flow_user_permission`` rejected any flow where
``flow.user_id != api_key_user.id`` — fine for OSS pass-through but a real
regression under enterprise enforcement, because an execute-grant on a
shared flow would still be blocked here even after the route's upstream
``ensure_flow_permission(FlowAction.EXECUTE, ...)`` passed. Remove the
helper entirely; route-level authorization is the only gate now. Added the
missing ``ensure_flow_permission(EXECUTE)`` call to ``experimental_run_flow``
so the contract holds: every ``_run_flow_internal`` caller authorizes
upstream. New ``test_check_flow_user_permission_is_gone`` regression
prevents a re-introduction.
2. ``filter_visible_resources`` sent a single ``domain`` for the entire batch,
so candidates living in different workspaces/projects all evaluated against
the same Casbin tuple — project-scoped grants became invisible. Added an
optional ``domain_extractor`` parameter that groups candidates by their
resolved domain and calls ``batch_enforce`` once per group. Output order is
preserved. ``GET /flows/`` and ``GET /projects/`` now pass extractors that
call ``_resolve_casbin_domain(item.workspace_id, item.folder_id)``. New
``test_filter_visible_resources_groups_by_extracted_domain`` covers the
per-domain dispatch.
3. ``authz_share.scope`` and ``permission_level`` were free-text columns —
typos like ``scope='PRIVATE'`` would silently bypass the partial unique
indexes (which match the lowercase form), and rows could be inserted with
``scope='public'`` + a ``target_id`` (mismatched targeted/untargeted
semantics). Added three CHECK constraints:
- ``ck_authz_share_scope_enum``
- ``ck_authz_share_permission_enum``
- ``ck_authz_share_scope_target_consistency`` (TEAM/USER → target required;
PRIVATE/PUBLIC → target forbidden)
Three new DB-level regression tests.
Phase 3 prerequisite documented (not fixed):
- ``get_flow_for_api_key_user`` and other owner-scoped lookup helpers still
filter by ``user_id``, so enterprise execute-grants on flows the caller
doesn't own would 404 at the lookup before the upstream
``ensure_flow_permission`` can permit them. The proper fix is share-aware
fetch helpers that land alongside ``authz_share`` CRUD in Phase 3.
Out-of-scope (skipped with reason):
- ``FlowAction.DEPLOY`` enum value remains unwired. Design note §5.1 plans it
for WxO publish on flow-version attach; wiring it on every deployment
create/update is Phase 4 work.
118 authz tests pass; ruff clean; migration validator output Valid: ✅;
alembic upgrade head runs clean against a fresh SQLite DB.
* refactor(authz): collapse six pending migrations into one consolidated file
None of the authz migrations have shipped yet — this branch is the first PR
introducing them. The chain had grown to six files as findings landed:
mb01b2c3d4e5
└─ f7a8b9c0d1e2 (initial tables)
└─ c8e5f4b2a9d7 (workspace_id columns)
└─ d9e8f7a6b5c4 (partial unique indexes on role_assignment)
└─ e4f5a6b7c8d9 (partial unique indexes on share)
└─ f0a1b2c3d4e5 (tz timestamps, ptype index, FK, etc.)
└─ b2c3d4e5f6a1 (CHECK constraints on share)
Replace all six with a single 7c8d9e0f1a2b migration that emits the final
schema directly. New tip: mb01b2c3d4e5 → 7c8d9e0f1a2b.
Drive-by model fixes uncovered by test_model_migration_consistency:
- _tz_column helper now accepts index=True so the audit log timestamp
column carries its index (lost in the earlier helper extraction).
- Aligned unique-index vs unique-constraint emission for the three tables
whose models use Field(index=True, unique=True) — emit a single unique
index, not a separate UniqueConstraint, so autogen sees no drift.
- Renamed CHECK constraint base names from ck_authz_share_* to * — the
alembic naming convention prepends ck_<table>_ automatically, so the
previous names were getting doubled. Final names are now correct
(ck_authz_share_scope_enum, etc.).
149 authz + alembic tests pass; ruff clean; migration validator output
Valid: ✅; alembic upgrade head runs clean against a fresh SQLite DB.
* fix: merge
* [autofix.ci] apply automated fixes
* Update Structured Data Analysis Agent.json
* fix(authz): narrow Phase 1 claims + filter paginated /flows/ branch
Two findings from the latest static review:
P1 — Owner-scoped fetch shadows enterprise grants
Route guards (ensure_flow_permission, ensure_deployment_permission,
ensure_project_permission) sit on top of fetch helpers that still scope
queries by current_user.id. So even with an enterprise plugin registered, a
share grant on a non-owned flow / folder / deployment still 404s at the
fetch layer before the guard can authorize. Cross-user reachability is
genuinely Phase 3 work (needs share-aware fetch helpers that load by id
first and convert plugin denies to 404 for UUID-privacy).
Rather than half-fix it, narrow the Phase 1 claims and document the gap
where future readers will see it:
* Module-level docstring on services/authorization/utils.py spelling out
the Phase 1 contract and why cross-user enforcement is not yet reachable.
* Inline "Phase 3 prerequisite" markers at the five lookup sites the
review flagged: flows.py:read_flow, chat.py:build_flow, projects.py:read_project,
endpoints.py:get_flow_for_api_key_user (already had one), and
mappers/deployments/helpers.py:get_deployment_row_or_404.
* AGENTS.md authz section now explicitly calls out the owner-scoped-fetch
limitation.
* PR description updated to scope the deliverables to Phases 1+2 and call
out Phase 3 prerequisites explicitly.
P2 — Paginated /flows/ branch skipped filter_visible_resources
read_flows had two return paths: get_all=True ran filter_visible_resources;
get_all=False (the paginated branch) returned apaginate result raw. So one
public mode of the same endpoint silently sidestepped authz filtering.
The fix wires filter_visible_resources into the paginated branch using the
same domain_extractor. OSS pass-through is a no-op so behavior under the
default flag is unchanged. page.total may overcount denied items under
enterprise enforcement — accurate paginated counts require SQL-level
prefiltering (Phase 3, alongside authz_share).
test_read_flows_list_uses_filter_visible_resources tightened to assert TWO
filter_visible_resources calls (one per branch) so the next regression
where someone forgets one path is caught immediately.
149 authz + alembic tests pass; ruff clean.
* test(authz): add user_id + workspace_id to _fake_deployment_row fixture
ensure_deployment_permission, wired into every guarded deployment handler,
reads deployment_row.user_id (for owner override) and
deployment_row.workspace_id (for domain resolution). The unit-test fixture
``_fake_deployment_row`` predates the authz hooks and didn't populate either
field, causing AttributeError on any test path that flows through a guarded
handler — surfaced by TestUpdateDeploymentRollback.test_rollback_called_on_commit_failure.
Add both fields with sensible defaults (random UUID for user_id matching the
production model, None for workspace_id which resolves to the wildcard domain).
Override-friendly so individual tests can pin specific values.
* fix(authz): tighten flow upload, project download, deployment list guards
Three guard placements were misshapen — coarse where they should be specific,
specific where they should be coarse, or missing context the resolver needs.
P1-1 — flows.py upload_file: remove the pre-parse coarse CREATE check
The handler runs ``ensure_flow_permission(CREATE, folder_id=folder_id)``
before the file is parsed, then runs a per-flow CREATE check after parsing
with the actual ``workspace_id``/``folder_id`` each uploaded flow targets.
The pre-parse check evaluates against ``domain="*", obj="flow:*"`` (or
``project:{folder_id}`` if the optional query param is set), which can
reject a caller who has create permission only inside the project/workspace
the uploaded flows actually target. The per-flow check is the right
enforcement point; drop the pre-parse check entirely.
P1-2 — projects.py download_file: pass owner + workspace into the guard
``ensure_project_permission(ProjectAction.READ, project_id=project_id)``
without ``project_user_id`` or ``workspace_id`` skips the owner-override
short-circuit AND resolves to the wildcard domain. An owner-only role in
the enterprise plugin would then deny the project's own owner; a
workspace-scoped role would never match. Fetch the project row first,
then pass the resolved owner and workspace.
P2-1 — deployments.py list_deployments: filter rows per item
The list endpoint had a single coarse ``deployment:*`` READ check and no
per-row filtering. That forced operators to grant global read to anyone
who should see any deployments — even those scoped to a single project.
Apply ``filter_visible_resources`` on ``rows_with_counts`` before
shaping, with a per-row ``domain_extractor`` so each deployment is
evaluated at ``project:{project_id}``/``workspace:{workspace_id}``. OSS
pass-through returns the input unchanged. ``total`` still reflects the
raw DB count — accurate paginated totals need SQL-level prefilter
(Phase 3, alongside ``authz_share``).
P2-2 (open question) — services/utils.py registration: docstring only
Confirms the OSS default registers with ``override=True`` (same as
``auth_service``) and that enterprise plugins are expected to override via
``lfx.toml`` rather than entry points. Plain entry-point registration uses
``override=False`` in the LFX manager and would lose to the OSS default —
not a bug but worth being explicit so enterprise integrators don't pick
the wrong mechanism.
83 authz + deployment-handler unit tests pass; ruff clean.
* fix(authz): guard deprecated build routes, owner-override list filter, persist workspace_id
Three findings from the latest review.
P1-1 — Deprecated build routes bypassed authorization
POST /build/{flow_id}/vertices (chat.py retrieve_vertices_order) and
POST /build/{flow_id}/vertices/{vertex_id} (chat.py build_vertex) are
hidden from the OpenAPI schema (deprecated=True, include_in_schema=False)
but still routed. Both eventually call build_graph_from_db, which does a
raw session.get(Flow, flow_id) in flow_utils.py with no owner filter, so
any authenticated user could build any other user's flow by guessing a
UUID. Apply the same owner-or-public fetch + ensure_flow_permission(EXECUTE)
the supported build_flow uses. retrieve_vertices_order now binds
current_user explicitly (was previously a dependencies-only declaration).
P1-2 — List filtering hid resources from their owners
filter_visible_resources called batch_enforce with only user context and
no per-candidate owner context. Direct guards in _ensure_resource_permission
short-circuit when owner_id == user.id and force-allow without calling
enforce. The list path did not — so a deny-all enterprise policy would
hide a user's own rows from the listing while the same user could read
each row directly. Made list and direct read disagree.
Added optional owner_extractor parameter. Items whose owner UUID equals
user.id are force-included before the enforcer is consulted; the
enforcer only sees the items that need a real decision. All three list
callers (flows GET /, projects GET /, deployments GET /) pass
``owner_extractor=lambda item: item.user_id`` (or the tuple-aware
equivalent for deployments).
New regression test asserts owner-override path bypasses a deny-all stub.
P2 — workspace_id update silently dropped
FlowUpdate accepts workspace_id and the PATCH/PUT handlers re-authorize
WRITE at the destination scope when the payload changes it. But
_UPDATABLE_FLOW_FIELDS in flows_helpers.py omitted workspace_id, so the
update passed authz, returned 200, and the row stayed in its old
workspace. Add workspace_id to the allowlist so the move actually
persists.
86 authz tests pass; ruff clean.
* test(authz): align deprecated build-route tests with 404 contract + cross-user behavioral coverage
P1 — existing tests asserted 500 for unknown flow ids on the deprecated
/build/{flow_id}/vertices and /build/{flow_id}/vertices/{vertex_id} routes.
The new ownership check returns 404 (matches /build/{flow_id}/flow and
preserves UUID-privacy). Update both assertions and document why.
P2 — the existing AST-level guard test only proves the route SOURCE contains
an ensure_flow_permission call; it does not exercise behavior. Add two
integration tests that:
1. Create a flow owned by the conftest's active_user.
2. Log in as a second, distinct user.
3. Hit /build/{flow_id}/vertices and /build/{flow_id}/vertices/{vertex_id}
with the second user's bearer token.
4. Assert 404 — the same opaque response another user's nonexistent flow
would produce.
The second-user fixture lives inline in this test file since the project's
conftest only ships single-user infrastructure (active_user /
active_super_user).
* feat: Phase 3 and 4 implementations for RBAC
* fixes from round of review
* Round 2 review fixes
* fix: Next round of review fixes
* fix: More hardening of KBs in RBAC
* fix: Third round of review feedback
* fix: Fourth round of review updates
* Update deployments.py
* Few more deployment tightens
* Update deployments.py
* Update test_deployment_route_handlers.py
* Update test_s3_endpoints.py
* [autofix.ci] apply automated fixes
* fix: Route guards and unit tests
* fix: filter paginated branch of read_project
* Update auth.py
* [autofix.ci] apply automated fixes
* [autofix.ci] apply automated fixes
* [autofix.ci] apply automated fixes (attempt 2/3)
* docs(authz): shorten docstrings and drop enterprise/Casbin comments
Use one-line module and helper docstrings across authorization code and
route guards; keep table/type identifiers unchanged.
* refactor(db): consolidate authz migrations into a single revision
Merge system-role seeding into 7c8d9e0f1a2b and drop 8d3a1f9c2e0b.
* fix: audit log retention policy and cleanup
* refactor(authz): split utils.py and address PR #13153 review
Address review feedback on the OSS authorization foundations PR:
- B1/I1: Split services/authorization/utils.py (694 lines, mixed
responsibilities) into focused modules:
- audit.py — batched audit pipeline (audit_decision, writer loop)
- guards.py — ensure_*_permission family (collapsed via _RESOURCE_SPECS
registry; nine 25-line clones become 7 thin wrappers)
- listing.py — filter_visible_resources
utils.py remains as a back-compat re-export shim so existing call sites
(api/v1/flows.py, deployments.py, etc.) keep working untouched.
- B2: Split tests/unit/services/authorization/test_utils.py (1004 lines)
into test_audit.py, test_guards.py, test_filter_visible.py,
test_domain_resolution.py. Shared stubs + monkeypatch helpers extracted
into _common.py; pytest fixtures into conftest.py.
- I2: ensure_permission default 403 detail is now "Permission denied" —
callers can opt into a richer message via the new `detail=` kwarg. The
previous default echoed flow:<uuid> in the response, leaking existence
to non-owners on any route that forgot to wrap in deny_to_404.
- I4: Document the OSS floor contract on _ensure_can_administer_share —
the early-return is dead in OSS (supports_cross_user_fetch() is False)
and the explicit 403 is the actual floor; the enterprise plugin gates
via the downstream ensure_share_permission call.
- R2: Audit drop logging is now time-based (first drop + at most every
10s during persistent saturation) instead of every 1000th drop. Low
drop rates no longer go minutes without a log line.
- R5: Seed migration's non-Postgres/SQLite fallback wraps the INSERT in
a SAVEPOINT and swallows IntegrityError. Two concurrent migration
runners can no longer race past the SELECT-then-INSERT check.
- R6: IntegrityError tests in test_authz_models.py now verify the
*specific* constraint fired — column names for unique partial indexes
(SQLite doesn't surface the index name), constraint name substring
for CHECK constraints. A generic NOT NULL regression would no longer
pass these tests.
All 180 authz tests pass; ruff clean.
* fix: One more review comments pass
* fix: remove dry run script
* fix(db): chain authz migration after api_key expires_at revision
Both 7c8d9e0f1a2b and f6b3ce6845d4 pointed at mb01b2c3d4e5, causing
MultipleHeads on startup. Authz now revises f6b3ce6845d4.
---------
Co-authored-by: Eric Hare <ericrhare@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
301 lines
4.1 KiB
Plaintext
301 lines
4.1 KiB
Plaintext
# This is to avoid Opencommit hook from getting pushed
|
|
prepare-commit-msg
|
|
# Logs
|
|
logs
|
|
*.log
|
|
npm-debug.log*
|
|
yarn-debug.log*
|
|
yarn-error.log*
|
|
lerna-debug.log*
|
|
qdrant_storage
|
|
|
|
.dspy_cache
|
|
# Mac
|
|
.DS_Store
|
|
|
|
# VSCode
|
|
.vscode/settings.json
|
|
.chroma
|
|
.ruff_cache
|
|
opendsstar_cache
|
|
|
|
# PyCharm
|
|
.idea/
|
|
|
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
|
|
# Runtime data
|
|
pids
|
|
*.pid
|
|
*.seed
|
|
*.pid.lock
|
|
|
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
lib-cov
|
|
|
|
# Coverage directory used by tools like istanbul
|
|
coverage
|
|
*.lcov
|
|
|
|
# nyc test coverage
|
|
.nyc_output
|
|
|
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
.grunt
|
|
|
|
# Bower dependency directory (https://bower.io/)
|
|
bower_components
|
|
|
|
# node-waf configuration
|
|
.lock-wscript
|
|
|
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
build/Release
|
|
|
|
# Dependency directories
|
|
node_modules/
|
|
jspm_packages/
|
|
|
|
# TypeScript v1 declaration files
|
|
typings/
|
|
|
|
# TypeScript cache
|
|
*.tsbuildinfo
|
|
|
|
# Optional npm cache directory
|
|
.npm
|
|
|
|
# Optional eslint cache
|
|
.eslintcache
|
|
|
|
# Microbundle cache
|
|
.rpt2_cache/
|
|
.rts2_cache_cjs/
|
|
.rts2_cache_es/
|
|
.rts2_cache_umd/
|
|
|
|
# Optional REPL history
|
|
.node_repl_history
|
|
|
|
# Output of 'npm pack'
|
|
*.tgz
|
|
|
|
# Yarn Integrity file
|
|
.yarn-integrity
|
|
|
|
# dotenv environment variables file
|
|
.env
|
|
.env.test
|
|
|
|
# parcel-bundler cache (https://parceljs.org/)
|
|
.cache
|
|
|
|
# Next.js build output
|
|
.next
|
|
|
|
# Nuxt.js build / generate output
|
|
.nuxt
|
|
dist
|
|
|
|
# Gatsby files
|
|
.cache/
|
|
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
# public
|
|
|
|
# vuepress build output
|
|
.vuepress/dist
|
|
|
|
# Serverless directories
|
|
.serverless/
|
|
|
|
# FuseBox cache
|
|
.fusebox/
|
|
|
|
# DynamoDB Local files
|
|
.dynamodb/
|
|
|
|
# TernJS port file
|
|
.tern-port
|
|
# Byte-compiled / optimized / DLL files
|
|
__pycache__/
|
|
*.py[cod]
|
|
*$py.class
|
|
notebooks
|
|
|
|
# C extensions
|
|
*.so
|
|
|
|
# Distribution / packaging
|
|
.Python
|
|
build/
|
|
develop-eggs/
|
|
dist/
|
|
downloads/
|
|
eggs/
|
|
.eggs/
|
|
lib/
|
|
lib64/
|
|
parts/
|
|
sdist/
|
|
var/
|
|
wheels/
|
|
pip-wheel-metadata/
|
|
share/python-wheels/
|
|
*.egg-info/
|
|
.installed.cfg
|
|
*.egg
|
|
MANIFEST
|
|
|
|
# PyInstaller
|
|
# Usually these files are written by a python script from a template
|
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
*.manifest
|
|
*.spec
|
|
|
|
# Installer logs
|
|
pip-log.txt
|
|
pip-delete-this-directory.txt
|
|
|
|
# Unit test / coverage reports
|
|
htmlcov/
|
|
.tox/
|
|
.nox/
|
|
.coverage
|
|
.coverage.*
|
|
.cache
|
|
nosetests.xml
|
|
coverage.xml
|
|
*.cover
|
|
*.py,cover
|
|
.hypothesis/
|
|
.pytest_cache/
|
|
.testmondata*
|
|
|
|
# Translations
|
|
*.mo
|
|
*.pot
|
|
|
|
# Django stuff:
|
|
*.log
|
|
local_settings.py
|
|
db.sqlite3
|
|
db.sqlite3-journal
|
|
*.db-shm
|
|
*.db-wal
|
|
|
|
# Flask stuff:
|
|
instance/
|
|
.webassets-cache
|
|
|
|
# Scrapy stuff:
|
|
.scrapy
|
|
|
|
# Sphinx documentation
|
|
docs/_build/
|
|
|
|
# PyBuilder
|
|
target/
|
|
|
|
# Jupyter Notebook
|
|
.ipynb_checkpoints
|
|
|
|
# IPython
|
|
profile_default/
|
|
ipython_config.py
|
|
|
|
# pyenv
|
|
.python-version
|
|
|
|
# pipenv
|
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
# install all needed dependencies.
|
|
#Pipfile.lock
|
|
|
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
__pypackages__/
|
|
|
|
# Celery stuff
|
|
celerybeat-schedule
|
|
celerybeat.pid
|
|
|
|
# SageMath parsed files
|
|
*.sage.py
|
|
|
|
# Environments
|
|
.env
|
|
.venv
|
|
env/
|
|
venv/
|
|
ENV/
|
|
env.bak/
|
|
venv.bak/
|
|
|
|
# Spyder project settings
|
|
.spyderproject
|
|
.spyproject
|
|
|
|
# Rope project settings
|
|
.ropeproject
|
|
|
|
# mkdocs documentation
|
|
/site
|
|
|
|
# mypy
|
|
.mypy_cache/
|
|
.dmypy.json
|
|
dmypy.json
|
|
|
|
# Poetry
|
|
.testenv/*
|
|
langflow.db
|
|
|
|
|
|
.githooks/prepare-commit-msg
|
|
.langchain.db
|
|
|
|
# docusaurus
|
|
.docusaurus/
|
|
|
|
/tmp/*
|
|
src/backend/langflow/frontend/
|
|
src/backend/base/langflow/frontend/
|
|
.docker
|
|
scratchpad*
|
|
chroma*/*
|
|
stuff/*
|
|
src/frontend/playwright-report/index.html
|
|
*.bak
|
|
prof/*
|
|
|
|
src/frontend/temp
|
|
*-shm
|
|
*-wal
|
|
.history
|
|
|
|
.dspy_cache/
|
|
*.db*
|
|
*.mcp.json
|
|
|
|
news-aggregated.json
|
|
.claude
|
|
member_servers.json
|
|
# Component index cache (user-specific)
|
|
**/.cache/lfx/
|
|
|
|
# data files used for desktop registration
|
|
data/user
|
|
|
|
sso-config.yaml
|
|
AGENTS.md
|
|
CLAUDE.local.md
|
|
langflow.log.*
|
|
tmp_toolguard/
|
|
|
|
#whitesource
|
|
whitesource/
|
|
/.codex
|