Skip to content

Commit

Permalink
add superuser policy
Browse files Browse the repository at this point in the history
  • Loading branch information
yujonglee committed Oct 20, 2024
1 parent 1da5c4e commit def80b4
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 50 deletions.
3 changes: 2 additions & 1 deletion core/lib/canary/accounts/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule Canary.Accounts.Account do

attributes do
uuid_primary_key :id
attribute :super_user, :boolean, default: false
end

relationships do
Expand All @@ -20,7 +21,7 @@ defmodule Canary.Accounts.Account do
end

actions do
defaults [:read, :destroy]
defaults [:read, :destroy, update: [:super_user]]

create :create do
primary? true
Expand Down
7 changes: 7 additions & 0 deletions core/lib/canary/accounts/billing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ defmodule Canary.Accounts.Billing do
belongs_to :account, Canary.Accounts.Account
end

calculations do
calculate :membership, :atom, {
Canary.Accounts.MembershipCalculation,
stripe_subscription_attribute: :stripe_subscription
}
end

actions do
defaults [:read]

Expand Down
3 changes: 3 additions & 0 deletions core/lib/canary/accounts/membership.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Canary.Accounts.Membership do
use Ash.Type.Enum, values: [:free, :starter, :admin]
end
83 changes: 83 additions & 0 deletions core/lib/canary/accounts/membership_calculation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
defmodule Canary.Accounts.MembershipCalculation do
use Ash.Resource.Calculation

@impl true
def init(opts) do
if [
:stripe_subscription_attribute
]
|> Enum.any?(&is_nil(opts[&1])) do
{:error, :invalid_opts}
else
{:ok, opts}
end
end

@impl true
def load(_query, opts, _context) do
[
opts[:stripe_subscription_attribute]
]
end

@impl true
def calculate(records, opts, _args) do
records
|> Enum.map(fn record ->
sub = record |> Map.get(opts[:stripe_subscription_attribute])

%{
tier: tier(sub),
trial: trial?(sub),
trial_end: trial_end(sub),
will_renew: will_renew?(sub),
current_period_end: current_period_end(sub),
status: status(sub)
}
end)
end

defp tier(subscription) do
starter_price_id = Application.fetch_env!(:canary, :stripe_starter_price_id)

cond do
is_nil(subscription) ->
:free

true ->
found =
subscription["items"]["data"] |> Enum.find(&(&1["price"]["id"] == starter_price_id))

case found do
nil -> :free
%{"plan" => %{"active" => false}} -> :free
%{"plan" => %{"active" => true}} -> :starter
_ -> :free
end
end
end

defp trial?(nil), do: false
defp trial?(%{"trial_end" => v}), do: not is_nil(v)

defp will_renew?(nil), do: false
defp will_renew?(%{"cancel_at_period_end" => v}), do: not v

defp current_period_end(nil), do: nil
defp current_period_end(%{"current_period_end" => v}), do: DateTime.from_unix!(v)

defp trial_end(nil), do: nil

defp trial_end(%{
"trial_end" => trial_end,
"trial_settings" => %{"end_behavior" => %{"missing_payment_method" => _create_invoice}}
}) do
case trial_end do
nil -> nil
t -> DateTime.from_unix!(t)
end
end

defp status(nil), do: nil
defp status(%{"status" => status}), do: status
end
4 changes: 4 additions & 0 deletions core/lib/canary/accounts/project.ex
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ defmodule Canary.Accounts.Project do
end

policies do
bypass actor_attribute_equals(:super_user, true) do
authorize_if always()
end

policy action_type(:create) do
authorize_if Canary.Checks.Membership.ProjectCreate
end
Expand Down
9 changes: 3 additions & 6 deletions core/lib/canary/checks/membership_project_create.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,10 @@ defmodule Canary.Checks.Membership.ProjectCreate do
%Ash.Policy.Authorizer{resource: Canary.Accounts.Project},
_opts
) do
with {:ok, %{billing: billing, num_projects: num_projects}} <-
Ash.load(account, [:billing, :num_projects]) do
with {:ok, %{billing: _billing, num_projects: num_projects}} <-
Ash.load(account, [:num_projects, billing: [:membership]]) do
cond do
is_nil(billing.stripe_subscription) and num_projects < 1 ->
{:ok, true}

not is_nil(billing.stripe_subscription) and num_projects < 3 ->
num_projects < 1 ->
{:ok, true}

true ->
Expand Down
6 changes: 3 additions & 3 deletions core/lib/canary/checks/membership_source_create.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ defmodule Canary.Checks.Membership.SourceCreate do
},
_opts
) do
with {:ok, %{billing: billing}} <- Ash.load(account, :billing),
with {:ok, %{billing: billing}} <- Ash.load(account, billing: [:membership]),
{:ok, %{num_sources: num_sources}} <-
Ash.get(Canary.Accounts.Project, id, load: [:num_sources]) do
%Ash.Union{type: source_type} = Ash.Changeset.get_attribute(changeset, :config)

cond do
is_nil(billing.stripe_subscription) and source_type == :webpage and num_sources < 1 ->
billing.membership.tier == :free and source_type == :webpage and num_sources < 1 ->
{:ok, true}

not is_nil(billing.stripe_subscription) and num_sources < 4 ->
billing.membership.tier == :starter and num_sources < 4 ->
{:ok, true}

true ->
Expand Down
4 changes: 4 additions & 0 deletions core/lib/canary/sources/source.ex
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ defmodule Canary.Sources.Source do
end

policies do
bypass actor_attribute_equals(:super_user, true) do
authorize_if always()
end

policy action_type(:create) do
authorize_if Canary.Checks.Membership.SourceCreate
end
Expand Down
73 changes: 34 additions & 39 deletions core/lib/canary_web/live/settings_live/billing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ defmodule CanaryWeb.SettingsLive.Billing do

@impl true
def mount(_params, _session, socket) do
account = socket.assigns.current_account |> Ash.load!([:billing])
subscription = account.billing.stripe_subscription
account = socket.assigns.current_account |> Ash.load!(billing: [:membership])
membership = account.billing.membership

{:ok, usage} = Canary.Analytics.query(:last_month_usage, %{account_id: account.id})

Expand All @@ -64,9 +64,9 @@ defmodule CanaryWeb.SettingsLive.Billing do
:stripe_starter_price_id,
Application.fetch_env!(:canary, :stripe_starter_price_id)
)
|> assign(:subscription_current, subscription_current(subscription))
|> assign(:subscription_next, subscription_next(subscription))
|> assign(:subscription_trial, subscription_trial(subscription))
|> assign(:subscription_current, compute_subscription_current(membership))
|> assign(:subscription_next, compute_subscription_next(membership))
|> assign(:subscription_trial, compute_subscription_trial(membership))
|> assign(:search_usage, search_usage["sum"])
|> assign(:ask_usage, ask_usage["sum"])

Expand All @@ -91,7 +91,7 @@ defmodule CanaryWeb.SettingsLive.Billing do

defp render_action_button(
%{
current_account: %{billing: %{stripe_subscription: %{"status" => status}}}
current_account: %{billing: %{membership: %{status: status}}}
} = assigns
)
when status in [
Expand Down Expand Up @@ -119,46 +119,41 @@ defmodule CanaryWeb.SettingsLive.Billing do
"""
end

defp subscription_current(nil) do
"Free"
end

defp subscription_current(%{"items" => %{"data" => data}, "trial_end" => trial_end}) do
starter_price_id = Application.fetch_env!(:canary, :stripe_starter_price_id)

defp compute_subscription_current(membership) do
plan =
data
|> Enum.any?(&(&1["plan"]["id"] == starter_price_id))
|> then(fn starter? -> if(starter?, do: "Starter", else: "Free") end)

if is_nil(trial_end), do: plan, else: "#{plan}(trial)"
end

defp subscription_next(nil), do: nil
case membership.tier do
:admin -> "Admin"
:starter -> "Starter"
:free -> "Free"
end

defp subscription_next(%{
"cancel_at_period_end" => true,
"current_period_end" => current_period_end
}) do
"Subscription cancelled, will remain active until #{format_date(current_period_end)}."
if membership.trial do
"#{plan}(trial)"
else
plan
end
end

defp subscription_next(%{
"cancel_at_period_end" => false,
"current_period_end" => current_period_end
}) do
"Subscription will be renewed on #{format_date(current_period_end)}."
defp compute_subscription_next(membership) do
if membership.current_period_end do
if membership.will_renew do
"Subscription will be renewed on #{format_date(membership.current_period_end)}."
else
"Subscription cancelled, will remain active until #{format_date(membership.current_period_end)}."
end
else
nil
end
end

defp subscription_trial(%{
"trial_end" => trial_end,
"trial_settings" => %{"end_behavior" => %{"missing_payment_method" => _create_invoice}}
}) do
"Trial ends on #{format_date(trial_end)}."
defp compute_subscription_trial(membership) do
if membership.trial_end do
"Trial ends on #{format_date(membership.trial_end)}."
else
nil
end
end

defp subscription_trial(_), do: nil

defp format_date(nil), do: "none"
defp format_date(t), do: DateTime.from_unix!(t) |> Calendar.strftime("%B %d, %Y")
defp format_date(t), do: Calendar.strftime(t, "%B %d, %Y")
end
21 changes: 21 additions & 0 deletions core/priv/repo/migrations/20241020054720_add_super_user.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule Canary.Repo.Migrations.AddSuperUser do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""

use Ecto.Migration

def up do
alter table(:accounts) do
add :super_user, :boolean, default: false
end
end

def down do
alter table(:accounts) do
remove :super_user
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,24 @@
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "super_user",
"type": "boolean"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "CEAC75F92279A21783F82B8153C935FA7B07096F7A2F5819F94199FC16B28722",
"hash": "3EFD1D77DBB8044E6F6DFF711C61E7935AF4C26030CEDFCC66DA1C76EC985CB1",
"identities": [],
"multitenancy": {
"attribute": null,
Expand Down

0 comments on commit def80b4

Please sign in to comment.