Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ and this project adheres to
is ambiguous across projects, the API returns 409 with guidance to add
`?project_id=`. Existing unscoped calls keep working for unambiguous names.
[#3548](https://github.com/OpenFn/lightning/issues/3548)
- Sandbox-aware Project Settings page. Each tab shows a banner explaining how
changes will (or will not) flow on merge: Local (sandbox-only), Editable
(syncs on merge), or Inherited (read-only, managed in the parent). The Sandbox
Identity panel links back to the parent project, the MFA toggle is read-only,
webhook authentication methods are managed from the parent project, and parent
project admins cannot be removed from a sandbox. The danger zone inside a
sandbox now deletes the sandbox through `Sandboxes.delete_sandbox/2` (matching
the Sandboxes page behaviour).
[#3398](https://github.com/OpenFn/lightning/issues/3398)

### Changed

Expand Down
14 changes: 13 additions & 1 deletion lib/lightning/projects.ex
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,10 @@ defmodule Lightning.Projects do
** (Ecto.NoResultsError)

"""
def get_project_user!(id), do: Repo.get!(ProjectUser, id)
def get_project_user!(id, opts \\ []) do
include = Keyword.get(opts, :include, [])
ProjectUser |> Repo.get!(id) |> Repo.preload(include)
end

@spec get_project_user(Ecto.UUID.t()) :: ProjectUser.t() | nil
def get_project_user(id) when is_binary(id), do: Repo.get(ProjectUser, id)
Expand Down Expand Up @@ -574,6 +577,15 @@ defmodule Lightning.Projects do
%{user_id: user_id, project_id: project_id} =
Repo.preload(project_user, [:user, :project])

if Project.sandbox?(project_user.project) and
Lightning.Projects.Sandboxes.parent_admin?(
project_user.project,
project_user.user
) do
raise ArgumentError,
"Cannot remove a parent project admin from a sandbox"
end

Repo.transaction(fn ->
from(pc in Lightning.Projects.ProjectCredential,
join: c in Lightning.Credentials.Credential,
Expand Down
29 changes: 29 additions & 0 deletions lib/lightning/projects/sandboxes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,35 @@ defmodule Lightning.Projects.Sandboxes do
end
end

@doc """
Returns `true` when `user` has an `:admin` or `:owner` role on any ancestor
of `project`, walking the parent chain.

Used to enforce the parent-admin floor rule: a user who is admin/owner on
any ancestor project cannot be removed from, or downgraded within, a
sandbox descended from that project.
"""
@spec parent_admin?(Project.t(), User.t()) :: boolean()
def parent_admin?(%Project{} = project, %User{} = user) do
project
|> ancestors()
|> Enum.any?(fn ancestor ->
Lightning.Projects.get_project_user_role(user, ancestor) in [
:admin,
:owner
]
end)
end

defp ancestors(%Project{parent_id: nil}), do: []

defp ancestors(%Project{parent_id: parent_id}) do
case Lightning.Projects.get_project(parent_id) do
nil -> []
%Project{} = parent -> [parent | ancestors(parent)]
end
end

@doc """
Deletes a sandbox and all its descendant projects.

Expand Down
4 changes: 2 additions & 2 deletions lib/lightning_web/components/layout_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ defmodule LightningWeb.LayoutComponents do

attr :title, :string, required: true
attr :subtitle, :string, required: true
attr :permissions_message, :string, required: true
attr :permissions_message, :string, default: nil
attr :can_perform_action, :boolean, default: true
attr :action_button_text, :string, default: nil
attr :action_button_click, :any, default: nil
Expand All @@ -409,7 +409,7 @@ defmodule LightningWeb.LayoutComponents do
<small class="block my-1 text-xs text-gray-600">
{@subtitle}
</small>
<%= if !@can_perform_action do %>
<%= if !@can_perform_action and @permissions_message do %>
<.permissions_message section={@permissions_message} />
<% end %>
</div>
Expand Down
71 changes: 71 additions & 0 deletions lib/lightning_web/components/sandbox_settings_banner.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule LightningWeb.Components.SandboxSettingsBanner do
@moduledoc """
Banner shown at the top of a Project Settings tab when the project is a
sandbox, communicating how changes on that tab will (or will not) flow
to the parent project on merge.

Three variants:

* `:local` — changes apply only to this sandbox and do not sync on merge
* `:editable` — changes will sync to the parent on merge
* `:inherited` — settings are read-only, managed in the parent project

## Examples

<.sandbox_settings_banner variant={:local} />

<.sandbox_settings_banner
variant={:inherited}
parent_project={@parent_project}
/>
"""
use LightningWeb, :component

alias LightningWeb.Components.Common

attr :variant, :atom, required: true, values: [:local, :editable, :inherited]
attr :id, :string, required: true
attr :parent_project, :map, default: nil

def sandbox_settings_banner(%{variant: :local} = assigns) do
~H"""
<Common.alert id={@id} type="info" class="border border-blue-300">
<:message>
Changes you make here only apply to this sandbox and do not sync to the parent project on merge.
</:message>
</Common.alert>
"""
end

def sandbox_settings_banner(%{variant: :editable} = assigns) do
~H"""
<Common.alert id={@id} type="success" class="border border-green-400">
<:message>
Changes you make here will sync to the parent project on merge.
</:message>
</Common.alert>
"""
end

def sandbox_settings_banner(%{variant: :inherited} = assigns) do
~H"""
<Common.alert id={@id} type="warning" class="border border-yellow-300">
<:message>
These settings are inherited from the parent project
<.parent_link :if={@parent_project} project={@parent_project} />and cannot be changed here.
</:message>
</Common.alert>
"""
end

attr :project, :map, required: true

defp parent_link(assigns) do
~H"""
(<.link
navigate={~p"/projects/#{@project.id}/settings"}
class="font-medium underline"
>{@project.name}</.link>)
"""
end
end
10 changes: 8 additions & 2 deletions lib/lightning_web/live/project_live/collections_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule LightningWeb.ProjectLive.CollectionsComponent do

use LightningWeb, :live_component

import LightningWeb.Components.SandboxSettingsBanner
import LightningWeb.LayoutComponents

alias Lightning.Collections
Expand Down Expand Up @@ -30,9 +31,9 @@ defmodule LightningWeb.ProjectLive.CollectionsComponent do
collections: _,
return_to: _,
project: _,
sandbox?: _,
current_user: _
} =
assigns,
} = assigns,
socket
) do
{:ok,
Expand Down Expand Up @@ -267,6 +268,11 @@ defmodule LightningWeb.ProjectLive.CollectionsComponent do
action_button_disabled={!@can_create_collection}
action_button_id="open-create-collection-modal-button"
/>
<.sandbox_settings_banner
:if={@sandbox?}
id="sandbox-banner-collections"
variant={:editable}
/>

<.form_modal_component
:if={@action == :new}
Expand Down
Loading
Loading