Skip to content
Merged
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
8 changes: 7 additions & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

// assets/js/app.js

// Add custom hookds like "select-all" permissions
import Hooks from "./hooks";

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}
params: { _csrf_token: csrfToken },
hooks: Hooks // <-- Add imported hooks so we can use it on our page
})

// Show progress bar on live navigation and form submits
Expand Down
35 changes: 35 additions & 0 deletions assets/js/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// assets/js/hooks.js
let Hooks = {};

Hooks.SelectAllPermissions = {
mounted() {
this.el.addEventListener("change", (event) => {
const groupId = this.el.dataset.groupId;
const isChecked = event.target.checked;
const checkboxes = document.querySelectorAll(
`#access-group-permissions-${groupId} .permission-checkbox`
);
checkboxes.forEach((checkbox) => {
checkbox.checked = isChecked;
});
});
}
};

Hooks.SelectResourcePermissions = {
mounted() {
this.el.addEventListener("change", (event) => {
const resource = this.el.dataset.resource;
const groupId = this.el.dataset.groupId;
const isChecked = event.target.checked;
const checkboxes = document.querySelectorAll(
`#access-group-permissions-${groupId} .permission-checkbox[data-resource="${resource}"]`
);
checkboxes.forEach((checkbox) => {
checkbox.checked = isChecked;
});
});
}
};

export default Hooks;
3 changes: 3 additions & 0 deletions lib/helpcenter.ex
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# /home/kamaro/elixir/helpcenter/lib/helpcenter.ex
defmodule Helpcenter do
@moduledoc """
Helpcenter keeps the contexts that define your domain
Expand All @@ -6,4 +7,6 @@ defmodule Helpcenter do
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""

defdelegate permissions(), to: Helpcenter.Accounts.Permission
end
11 changes: 9 additions & 2 deletions lib/helpcenter/accounts.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
# lib/helpcenter/accounts.ex
defmodule Helpcenter.Accounts do
use Ash.Domain,
otp_app: :helpcenter
use Ash.Domain, otp_app: :helpcenter

resources do
# Authentication
resource Helpcenter.Accounts.Token
resource Helpcenter.Accounts.User
resource Helpcenter.Accounts.Team
resource Helpcenter.Accounts.UserTeam

# Authorization
resource Helpcenter.Accounts.Group
# Delete this resource Helpcenter.Accounts.Permission
resource Helpcenter.Accounts.GroupPermission
resource Helpcenter.Accounts.UserGroup
end
end
47 changes: 47 additions & 0 deletions lib/helpcenter/accounts/checks/authorized.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# lib/helpcenter/accounts/checks/authorized.ex
defmodule Helpcenter.Accounts.Checks.Authorized do
use Ash.Policy.SimpleCheck
require Ash.Query

@impl true
def describe(_opts), do: "Authorize User Access Group"

@doc """
Returns true to authorize or false to deny access
If actor is not provide, then deny access by returning false
"""
@impl true

def match?(nil = _actor, _context, _opts), do: false
def match?(actor, context, _options), do: authorized?(actor, context)

# """
# 1. If the actor is the team owner, then authorize since he's the owner
# 2. If none of the above, then check if the user has permission on the database
# """
defp authorized?(actor, context) do
cond do
is_current_team_owner?(actor) -> true
true -> can?(actor, context)
end
end

# Confirms if the actor is the owner of the current team
defp is_current_team_owner?(actor) do
Helpcenter.Accounts.Team
|> Ash.Query.filter(owner_user_id == ^actor.id)
|> Ash.Query.filter(domain == ^actor.current_team)
|> Ash.exists?()
end

# Confirms if the actor has required permissions to perform the current
# action on the current resource
defp can?(actor, context) do
Helpcenter.Accounts.User
|> Ash.Query.filter(id == ^actor.id)
|> Ash.Query.load(groups: :permissions)
|> Ash.Query.filter(groups.permissions.resource == ^context.resource)
|> Ash.Query.filter(groups.permissions.action == ^context.subject.action.type)
|> Ash.exists?(tenant: actor.current_team, authorize?: false)
end
end
74 changes: 74 additions & 0 deletions lib/helpcenter/accounts/group.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# lib/helpcenter/accounts/group.ex
defmodule Helpcenter.Accounts.Group do
use Ash.Resource,
domain: Helpcenter.Accounts,
data_layer: AshPostgres.DataLayer,
notifiers: Ash.Notifier.PubSub

postgres do
table "groups"
repo Helpcenter.Repo
end

actions do
default_accept [:name, :description]
defaults [:create, :read, :update, :destroy]
end

# Confirm how Ash will wor
pub_sub do
module HelpcenterWeb.Endpoint

prefix "groups"

publish_all :update, [[:id, nil]]
publish_all :create, [[:id, nil]]
publish_all :destroy, [[:id, nil]]
end

preparations do
prepare Helpcenter.Preparations.SetTenant
end

changes do
change Helpcenter.Changes.SetTenant
end

multitenancy do
strategy :context
end

attributes do
uuid_v7_primary_key :id

attribute :name, :string do
description "Group name unique name"
allow_nil? false
end

attribute :description, :string do
description "Describes the intention of the group"
allow_nil? false
end

timestamps()
end

relationships do
many_to_many :users, Helpcenter.Accounts.User do
through Helpcenter.Accounts.UserGroup
source_attribute_on_join_resource :group_id
destination_attribute_on_join_resource :user_id
end

# lib/helpcenter/accounts/group.ex
has_many :permissions, Helpcenter.Accounts.GroupPermission do
description "List of permission assigned to this group"
destination_attribute :group_id
end
end

identities do
identity :unique_name, [:name]
end
end
48 changes: 48 additions & 0 deletions lib/helpcenter/accounts/group_permission.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# lib/helpcenter/accounts/group_permission.ex
defmodule Helpcenter.Accounts.GroupPermission do
use Ash.Resource,
domain: Helpcenter.Accounts,
data_layer: AshPostgres.DataLayer,
notifiers: Ash.Notifier.PubSub

postgres do
table "group_permissions"
repo Helpcenter.Repo
end

actions do
default_accept [:resource, :action, :group_id]
defaults [:create, :read, :update, :destroy]
end

preparations do
prepare Helpcenter.Preparations.SetTenant
end

changes do
change Helpcenter.Changes.SetTenant
end

multitenancy do
strategy :context
end

attributes do
uuid_v7_primary_key :id
attribute :action, :string, allow_nil?: false
attribute :resource, :string, allow_nil?: false
timestamps()
end

relationships do
belongs_to :group, Helpcenter.Accounts.Group do
description "Relationshp with a group inside a tenant"
source_attribute :group_id
allow_nil? false
end
end

identities do
identity :unique_group_permission, [:group_id, :resource, :action]
end
end
30 changes: 30 additions & 0 deletions lib/helpcenter/accounts/permission.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# lib/helpcenter/accounts/permission.ex
defmodule Helpcenter.Accounts.Permission do
@doc """
Get a list of maps of resources and their actions
Example:
iex> Helpcenter.Accounts.Permission.get_permissions()
iex> [%{resource: Helpcenter.Accounts.GroupPermission, action: :create}]
"""

def permissions() do
get_all_domain_resources()
|> Enum.map(&map_resource_actions/1)
|> Enum.flat_map(& &1)
end

defp map_resource_action(action, resource) do
%{action: action.name, resource: resource}
end

defp map_resource_actions(resource) do
Ash.Resource.Info.actions(resource)
|> Enum.map(&map_resource_action(&1, resource))
end

defp get_all_domain_resources() do
Application.get_env(:helpcenter, :ash_domains)
|> Enum.map(&Ash.Domain.Info.resources(&1))
|> Enum.flat_map(& &1)
end
end
8 changes: 7 additions & 1 deletion lib/helpcenter/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule Helpcenter.Accounts.User do
authentication do
add_ons do
log_out_everywhere do
apply_on_password_change?(true)
apply_on_password_change? true
end

confirmation :confirm_new_user do
Expand Down Expand Up @@ -301,6 +301,12 @@ defmodule Helpcenter.Accounts.User do
source_attribute_on_join_resource :user_id
destination_attribute_on_join_resource :team_id
end

many_to_many :groups, Helpcenter.Accounts.Group do
through Helpcenter.Accounts.UserGroup
source_attribute_on_join_resource :user_id
destination_attribute_on_join_resource :group_id
end
end

identities do
Expand Down
53 changes: 53 additions & 0 deletions lib/helpcenter/accounts/user_group.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# lib/helpcenter/accounts/user_group.ex
defmodule Helpcenter.Accounts.UserGroup do
use Ash.Resource,
domain: Helpcenter.Accounts,
data_layer: AshPostgres.DataLayer,
notifiers: Ash.Notifier.PubSub

postgres do
table "user_groups"
repo Helpcenter.Repo
end

actions do
default_accept [:user_id, :group_id]
defaults [:create, :read, :update, :destroy]
end

preparations do
prepare Helpcenter.Preparations.SetTenant
end

changes do
change Helpcenter.Changes.SetTenant
end

multitenancy do
strategy :context
end

attributes do
uuid_v7_primary_key :id

timestamps()
end

relationships do
belongs_to :group, Helpcenter.Accounts.Group do
description "Relationshp with a group inside a tenant"
source_attribute :group_id
allow_nil? false
end

belongs_to :user, Helpcenter.Accounts.User do
description "Permission for the user access group"
source_attribute :user_id
allow_nil? false
end
end

identities do
identity :unique_name, [:group_id, :user_id]
end
end
13 changes: 11 additions & 2 deletions lib/helpcenter/knowledge_base/category.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# lib/helpcenter/knowledge_base/category.ex
defmodule Helpcenter.KnowledgeBase.Category do
use Ash.Resource,
domain: Helpcenter.KnowledgeBase,
data_layer: AshPostgres.DataLayer,
# Tell Ash to broadcast/ Emit events via pubsub
notifiers: Ash.Notifier.PubSub
notifiers: Ash.Notifier.PubSub,
# Tell Ash that this resource require authorization
authorizers: Ash.Policy.Authorizer

postgres do
# <-- Tell Ash that this resource data is stored in a table named "categories"
Expand Down Expand Up @@ -36,6 +38,13 @@ defmodule Helpcenter.KnowledgeBase.Category do
end
end

policies do
policy always() do
access_type :strict
authorize_if Helpcenter.Accounts.Checks.Authorized
end
end

# Confirm how Ash will wor
pub_sub do
# 1. Tell Ash to use HelpcenterWeb.Endpoint for publishing events
Expand Down
Loading