Skip to content

Commit

Permalink
Transfer Credential Ownership (#2946)
Browse files Browse the repository at this point in the history
* Add credential transfer token validity configuration

* Add credential transfer token support

* Add credential transfer email notifications

* Update user model for validations
- Extract email validation functions
- Move project access validation

* Add credential transfer audit event

* Add core transfer functionality

* Add credential transfer UI

* Unit tests and liveview tests
- get projects by ids
- credential transfer controller
- initiate credential transfer, confirm credential transfer, revoke credential transfer
- user changesets and validations
- credential transfer modal

* Revoke credential transfer

* Tests for revoke credential transfer

* Update CL

* Remove duplicated test

* Refactor code to address review change requests
  • Loading branch information
elias-ba authored Feb 25, 2025
1 parent d4228fd commit 78df13d
Show file tree
Hide file tree
Showing 17 changed files with 1,633 additions and 217 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ and this project adheres to

- Enable project level concurrency limit
[#2906](https://github.com/OpenFn/lightning/issues/2906)
- Transfer credentials ownership to a project collaborator
[#2820](https://github.com/OpenFn/lightning/issues/2820)
- Delete unused snapshots on workorders retention cleanup
[#1832](https://github.com/OpenFn/lightning/issues/1832)

Expand Down
12 changes: 11 additions & 1 deletion lib/lightning/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,18 @@ defmodule Lightning.Accounts.User do
|> put_change(:role, :superuser)
end

def validate_email(changeset) do
def validate_email_format(changeset) do
changeset
|> validate_required(:email, message: "can't be blank")
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/,
message: "must have the @ sign and no spaces"
)
|> validate_length(:email, max: 160)
|> update_change(:email, &String.downcase/1)
end

def validate_email_exists(changeset) do
changeset
|> validate_change(:email, fn :email, email ->
if Lightning.Repo.exists?(User |> where(email: ^email)) do
[email: "has already been taken"]
Expand All @@ -181,6 +185,12 @@ defmodule Lightning.Accounts.User do
end)
end

def validate_email(changeset) do
changeset
|> validate_email_format()
|> validate_email_exists()
end

defp validate_password(changeset, opts) do
changeset
|> validate_required(:password, message: "can't be blank")
Expand Down
50 changes: 50 additions & 0 deletions lib/lightning/accounts/user_notifier.ex
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,54 @@ defmodule Lightning.Accounts.UserNotifier do

defp pluralize_with_s(1, string), do: string
defp pluralize_with_s(_integer, string), do: "#{string}s"

@doc """
Deliver instructions to confirm a credential transfer.
"""
def deliver_credential_transfer_confirmation_instructions(
owner,
receiver,
credential,
token
) do
validity_days = Lightning.Config.credential_transfer_token_validity_in_days()
validity_text = format_validity_period(validity_days)

confirmation_url =
url(~p"/credentials/transfer/#{credential.id}/#{receiver.id}/#{token}")

deliver(owner, "Confirm your credential transfer", """
Hi #{owner.first_name},
Confirm your credential transfer to #{receiver.first_name} (#{receiver.email}) by visiting the link below:
#{confirmation_url}
Note that this link is only valid for #{validity_text}. If you didn't request this change, please ignore this.
OpenFn
""")
end

defp format_validity_period(days) do
"#{days} #{pluralize_with_s(days, "day")}"
end

def deliver_credential_transfer_notification(
receiver,
owner,
credential
) do
credentials_url = url(~p"/credentials")

deliver(receiver, "You have received a credential", """
Hi #{receiver.first_name},
#{owner.first_name} has transferred the credential \"#{credential.name}\" to you.
You can find it in your list of credentials by following this URL: #{credentials_url}
OpenFn
""")
end
end
3 changes: 3 additions & 0 deletions lib/lightning/accounts/user_token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ defmodule Lightning.Accounts.UserToken do
defp days_for_context("confirm"), do: @confirm_validity_in_days
defp days_for_context("reset_password"), do: @reset_password_validity_in_days

defp days_for_context("credential_transfer"),
do: Lightning.Config.credential_transfer_token_validity_in_days()

@doc """
Checks if the token is valid and returns its underlying lookup query.
Expand Down
10 changes: 10 additions & 0 deletions lib/lightning/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ defmodule Lightning.Config do
defp ui_metrics_tracking_config do
Application.get_env(:lightning, :ui_metrics_tracking, [])
end

@impl true
def credential_transfer_token_validity_in_days do
2
end
end

@callback apollo(key :: atom() | nil) :: map()
Expand Down Expand Up @@ -300,6 +305,7 @@ defmodule Lightning.Config do
@callback worker_secret() :: binary() | nil
@callback worker_token_signer() :: Joken.Signer.t()
@callback adaptor_registry() :: Keyword.t()
@callback credential_transfer_token_validity_in_days() :: integer()

@doc """
Returns the configuration for the `Lightning.AdaptorRegistry` service
Expand Down Expand Up @@ -474,6 +480,10 @@ defmodule Lightning.Config do
impl().ui_metrics_tracking_enabled?()
end

def credential_transfer_token_validity_in_days do
impl().credential_transfer_token_validity_in_days()
end

defp impl do
Application.get_env(:lightning, __MODULE__, API)
end
Expand Down
Loading

0 comments on commit 78df13d

Please sign in to comment.