Skip to content

fix: let org members create automations - #496

Merged
hieptl merged 2 commits into
mainfrom
hieptl/members-create-automations
Sep 22, 2026
Merged

hieptl merged 2 commits into
mainfrom
hieptl/members-create-automations

Conversation

@hieptl

@hieptl hieptl commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Org members cannot create automations. Every creation path returns 403 Requires manage_automations permission.

This dates back to #415, which split the automation permission into view_automations and manage_automations to make org members view-only. Members were moved to view_automations only, and all four creation endpoints were put behind manage_automations:

Endpoint Handler
POST /v1 create_automation
POST /v1/uploads create_upload
POST /v1/preset/prompt create_automation_from_prompt
POST /v1/preset/plugin create_automation_from_plugin

The requirement behind that split was that admins, owners and the creator of an automation have full access to it, while members can only view org automations. #415 built the creator escape hatch for that (_assert_can_manage: manage_automations or automation.user_id == user.user_id), but with creation itself gated on manage_automations a member can never become a creator, so the hatch is unreachable for them. The requirement restricts what a member can do to other people's automations; it was not meant to stop them creating their own.

The existing creator tests did not catch this because they insert the automation row directly instead of creating it through the API.

This PR gates the four creation endpoints on view_automations. The other write endpoints already depend on view_automations plus a row-level creator check. On create the caller is always the creator (user_id=user.user_id), so that check is trivially satisfied and the view-level dependency is the consistent gate.

Behaviour

Caller Create Own automation Someone else's automation
Member allowed (was 403) edit, turn on/off, run, delete (unchanged) view only (unchanged)
Admin / owner allowed (unchanged) edit, turn on/off, run, delete (unchanged) turn off, delete, run, cancel; no edit (unchanged, #427)

manage_automations keeps its meaning, so the rule from #427 still holds: an admin can turn off or delete any automation, and only the creator can edit one.

A member's automation already runs fine with view_automations alone: the run API key is minted on org membership, complete_run / report_run_phase need view plus the run owner, and KV needs org membership only. Members had full manage_automations until #415, so this restores strictly less than what they had before.

Changes

File Change
openhands/automation/router.py create_automation depends on _require_view_automations. The now-unused _require_manage_automations is removed.
openhands/automation/uploads.py create_upload depends on _require_view_automations.
openhands/automation/preset_router.py The module dependency becomes _require_view_automations, used by both preset endpoints.
tests/test_router.py TestPermissionEnforcement: a member can POST /v1.
tests/test_preset_router.py A member can create from a prompt and from plugins.
tests/test_uploads.py New TestCreateUpload: a member can upload a tarball. This is the first HTTP-level test of POST /v1/uploads.

Not changed: delete_upload (not part of creating, and already scoped to the caller's own uploads), the webhook and git-sync routers (org-level resources, still manage_automations), _assert_can_manage, update_automation, and the enterprise permission enum. Local mode is unaffected since the local user already holds both permissions.

Why not grant members manage_automations

OpenHands/enterprise#436 and #488 fix this from the other side, by giving members manage_automations and adding a new manage_all_automations for admins and owners. I went a different way because this service treats manage_automations as "is an admin or owner": _assert_can_manage returns as soon as it sees it, for any automation in the org.

Gating creation on view_automations needs no enterprise change and no new permission, and it is safe in either deploy order.

Companion PR

OpenHands/OpenHands#17526 (Agent Canvas): the automations list hides the "Add automation" / "Import" menu unless the caller has manage_automations. The companion PR shows it to members. The other create entry points in Canvas (templates, empty state, home launcher, onboarding) were never gated and currently run into the 403 above.

Either order works. If Canvas ships first, the menu appears and hits the same 403 those other entry points already hit. Shipping this first avoids that.

Test plan

Run locally (Docker is needed for the Postgres testcontainer; FILE_STORE=local keeps a local .env from pointing the suite at S3):

uv run ruff check <changed files>
uv run ruff format --check <changed files>
uv run pycodestyle --max-line-length=88 --ignore=E203,E501,W503,E704 <changed files>
uv run pyright <changed files>
FILE_STORE=local uv run pytest tests/test_router.py tests/test_preset_router.py \
  tests/test_uploads.py tests/test_auth.py tests/test_git_sync_router.py \
  tests/test_cancel_run.py tests/test_tarball_validation.py -q --no-cov
  • The four new tests fail on main, one per endpoint, each with:
    E   assert 403 == 201
    E    +  where 403 = <Response [403 Forbidden]>.status_code
    ========================= 4 failed, 1 warning in 3.70s =========================
    
  • With the change: 4 passed.
  • The suites above: 436 passed, 9 warnings in 91.29s. No existing test needed updating, as nothing asserted a 403 on a creation endpoint.
  • ruff check, ruff format, pycodestyle: clean. pyright: 0 errors, 0 warnings, 0 informations.
  • Manual against a running service, as a member:
    • POST /api/automation/v1/preset/prompt → 201, user_id is the member
    • POST /api/automation/v1/uploads?name=x then POST /api/automation/v1 with the returned oh-internal:// path → 201
    • PATCH / DELETE on a teammate's automation → 403 (unchanged)
    • As an admin, PATCH {"prompt": "x"} on a member's automation → 403, PATCH {"enabled": false} → 200 (unchanged)

Notes

  • The comment on the enterprise permission enum (server/auth/authorization.py) still says create is admin/owner only. It needs a comment-only follow-up.
  • Template enablement is deduplicated per org (find_existing_template_automation), so whoever enables a template first owns it, and a later enable returns that same automation with a 200. This already happens between two admins. It becomes a little more likely once members can create, so it is worth a look separately.

Splitting the automation permission into view and manage (#415) put every
creation endpoint behind manage_automations, which only admins and owners
hold. Members could therefore never create an automation, which left the
creator escape hatch that lets them edit, run and delete their own
automations unreachable.

Gate the four creation endpoints on view_automations instead:

- POST /v1
- POST /v1/uploads
- POST /v1/preset/prompt
- POST /v1/preset/plugin

An automation is always created under the caller's own user_id, so the
row-level creator check the other write endpoints rely on is trivially
satisfied on create.

manage_automations keeps its meaning. Admins and owners can still turn off
or delete any automation, only the creator can edit one (#427), and members
stay read-only on automations they did not create.

Add one test per endpoint asserting that a member can create.
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Coverage

Warning

Your comment is too long (maximum is 65536 characters), so the coverage report was not added. See the job log for how to reduce it.

@hieptl hieptl self-assigned this Sep 18, 2026
@all-hands-bot

Copy link
Copy Markdown
Contributor

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

@tofarr tofarr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍰

@hieptl
hieptl merged commit b2d0ce4 into main Sep 22, 2026
9 checks passed
@openhands-release-bot openhands-release-bot Bot added the released: 1.14.0 Shipped in 1.14.0 label Sep 22, 2026
@openhands-release-bot

Copy link
Copy Markdown
Contributor

🚀 Released in 1.14.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released: 1.14.0 Shipped in 1.14.0 type: fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants