# `Lightning.Policies.Sandboxes`
[🔗](https://github.com/OpenFn/lightning/blob/main/lib/lightning/policies/sandboxes.ex#L1)

The Bodyguard Policy module for sandbox project operations.

Sandbox authorization mirrors regular projects: access is decided by the
acting user's role on the project they're acting on (or the workspace
root, where the cascade applies). `User.role` (`:user` / `:superuser`)
is a user-type for global user-management screens; it is not a
project-access bypass and has no effect on sandbox policy decisions, in
line with `Lightning.Policies.ProjectUsers`.

- Sandbox owners/admins can manage their own sandboxes
- Root project owners/admins can manage any sandbox in their workspace
- Editors (and above) on the parent project can provision sandboxes

Every action except provisioning and merging acts on a sandbox, and a
workspace root is not one. A root is refused before any role is considered,
which is what keeps the root cascade from admitting the root itself.

Destructive actions on a sandbox (delete, update, merge) are scoped to
admin/owner on the sandbox itself (or the root cascade above). This
matches the rest of Lightning, where destructive actions are admin/owner
scoped, and it keeps the merge button on the sandboxes list aligned with
the cleanup step that runs after merge submission (which calls
`:delete_sandbox` and so requires admin/owner on the source).

# `actions`

```elixir
@type actions() ::
  :delete_sandbox
  | :update_sandbox
  | :provision_sandbox
  | :merge_sandbox
  | :cancel_scheduled_deletion
```

# `authorize`

```elixir
@spec authorize(
  actions(),
  Lightning.Accounts.User.t(),
  Lightning.Projects.Project.t()
) :: boolean()
```

Authorize sandbox operations based on the user's role on the project
involved.

## Authorization Rules

### `:delete_sandbox`, `:update_sandbox` and `:cancel_scheduled_deletion`
The subject must be a sandbox: a project with no parent is refused outright,
whoever is asking. Beyond that the user must be one of:
- Owner/admin of the sandbox itself
- Owner/admin of the root project (workspace)

### `:provision_sandbox`
User must be editor/admin/owner of the parent project they're creating
the sandbox under.

### `:merge_sandbox`
This check authorises the **target side** of a merge: the user must be
editor/admin/owner on the target project (the project being merged
*into*). The merge flow also requires admin/owner on the **source
sandbox** itself, enforced by `manage_permissions/3` (button
gate) and by the post-merge cleanup, which calls `:delete_sandbox`
to retire the source and so requires admin/owner there.

### Support access
No sandbox action honours it: every clause decides on `role` alone, so a
support user with no membership row is refused even on a consenting project.
Inherited behaviour, not a ruling.

## Parameters
- `action` - The action being attempted
- `user` - The user attempting the action
- `project` - The sandbox project (for delete/update), parent project (for provision),
  or target project (for merge)

# `manage_permissions`

```elixir
@spec manage_permissions(
  [Lightning.Projects.Project.t()],
  Lightning.Accounts.User.t(),
  Lightning.Projects.Project.t()
) :: %{required(binary()) =&gt; boolean()}
```

Bulk manage check for multiple sandboxes, avoiding N+1 queries.

Returns a map `sandbox_id => boolean()` where `true` means the user can
perform the destructive actions the sandbox list offers on that row: they are
an owner/admin on the sandbox itself, or an owner/admin on the root project
(cascade). The workspace root is always `false`, which is what the list relies
on to withhold edit, delete and merge from the root. Note this is the merge
*source* side; `:merge_sandbox` authorises the target and a root is a legal
target.

Assumes `root_project.project_users` and each `sandbox.project_users`
are preloaded (as ensured by `Projects.list_workspace_projects/2`).

Reads those preloaded rows rather than resolving a `Scope`, so it does not
hold the actor to the project's MFA requirement. Nothing reaches it that has
not already been held to it: the list it feeds mounts
`LightningWeb.Hooks.:project_scope`, which redirects an unenrolled member to
`/mfa_required` before the page renders, and every action a `true` enables
refuses on its own.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
