Skip to content

Commit

Permalink
Merge branch 'feat/perms' into 'main'
Browse files Browse the repository at this point in the history
✨ (perms) Permissions system to access endpoints

See merge request decidim/decidim-chatbot/decidim-module-rest_full!6
  • Loading branch information
Hadrien Froger committed Nov 29, 2024
2 parents 3f29377 + da260aa commit fb5869c
Show file tree
Hide file tree
Showing 25 changed files with 298 additions and 66 deletions.
6 changes: 2 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ PATH
specs:
decidim-rest_full (0.0.2)
api-pagination (~> 6.0)
cancancan
decidim-admin (>= 0.28, < 0.30)
decidim-comments (>= 0.28, < 0.30)
decidim-core (>= 0.28, < 0.30)
doorkeeper
jsonapi-serializer
rswag-api
rswag-ui

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -108,6 +108,7 @@ GEM
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (11.1.3)
cancancan (3.6.1)
capybara (3.40.0)
addressable
matrix
Expand Down Expand Up @@ -677,9 +678,6 @@ GEM
json-schema (>= 2.2, < 6.0)
railties (>= 5.2, < 8.0)
rspec-core (>= 2.14)
rswag-ui (2.15.0)
actionpack (>= 5.2, < 8.0)
railties (>= 5.2, < 8.0)
rubocop (1.65.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require "rspec/core/rake_task"
def install_module(path)
Dir.chdir(path) do
system("bundle check || bundle install")
# system("bundle exec rake decidim_rest_full:install:migrations")
system("bundle exec rake decidim_rest_full:install:migrations")
system("bundle exec rails db:migrate")
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Api
module RestFull
class ApplicationController < ActionController::API
include Decidim::RestFull::ApiException::Handler
delegate :can?, :cannot?, :authorize!, to: :ability

protected

Expand Down Expand Up @@ -59,6 +60,10 @@ def available_locales

private

def ability
@ability ||= Decidim::RestFull::Ability.from_doorkeeper_token(doorkeeper_token)
end

def populate_params
@populate_params ||= if params[:populate].is_a?(String)
params[:populate].split(",").map(&:to_sym)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,46 @@ module Api
module RestFull
module System
class OrganizationsController < ApplicationController
before_action { doorkeeper_authorize! :system }
before_action do
doorkeeper_authorize! :system
authorize! :read, ::Decidim::Organization
end

# List all organizations
def index
# Extract only the populated fields
allowed_fields = OrganizationSerializer.db_fields
only_fields = populated_fields([], allowed_fields)

# Fetch organizations and paginate
organizations = paginate(Decidim::Organization.select(*only_fields))
organizations = paginate(collection)
# Render the response
render json: OrganizationSerializer.new(
organizations,
params: { only: only_fields, locales: available_locales },
fields: { organization: only_fields.push(:meta) }
).serializable_hash
render json: serializable_hash(organizations)
end

# Show a single organization
def show
# Extract only the populated fields
only_fields = populated_fields(OrganizationSerializer.db_fields, OrganizationSerializer.db_fields)

# Find the organization by ID
organization = Decidim::Organization.find(params[:id])

organization = collection.find(params[:id])
# Render the response
render json: OrganizationSerializer.new(
organization,
params: { only: only_fields, locales: available_locales },
fields: { organization: only_fields.map(&:to_sym) }
render json: serializable_hash(organization)
end

private

def serializable_hash(resource)
OrganizationSerializer.new(
resource,
params: { locales: available_locales }
).serializable_hash
end

def collection
Decidim::Organization.select(
:id,
:name,
:secondary_hosts,
:host,
:created_at,
:updated_at
)
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ module Api
module RestFull
module System
class UsersController < ApplicationController
before_action { doorkeeper_authorize! :system }
before_action do
doorkeeper_authorize! :system
authorize! :read, ::Decidim::User
end

# List all users
def index
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def new
def edit
@api_client = collection.find(params[:id])
@form = form(ApiClientForm).from_model(@api_client)
@perm_form = form(ApiPermissions).from_model(@api_client)
end

def create
Expand Down
27 changes: 27 additions & 0 deletions app/controllers/decidim/rest_full/system/permissions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Decidim
module RestFull
module System
class PermissionsController < Decidim::System::ApplicationController
helper Decidim::Admin::AttributesDisplayHelper
helper Decidim::Core::Engine.routes.url_helpers
helper_method :destroy_admin_session_path

def core_engine_routes
Decidim::Core::Engine.routes.url_helpers
end

def create
@form = form(ApiPermissions).from_params(params)
api_client = Decidim::RestFull::ApiClient.find(@form.api_client_id)
api_client.permissions = @form.permissions.map do |perm_string|
api_client.permissions.build(permission: perm_string)
end
api_client.save!
redirect_to core_engine_routes.edit_system_api_client_path(api_client)
end
end
end
end
end
16 changes: 16 additions & 0 deletions app/forms/decidim/rest_full/api_permissions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module Decidim
module RestFull
# The form that validates the data to construct a valid OAuthApplication.
class ApiPermissions < Decidim::Form
mimic :system_api_client
attribute :permissions, [String]
attribute :api_client_id, Integer

def organization
current_organization || Decidim::Organization.find_by(id: decidim_organization_id)
end
end
end
end
45 changes: 45 additions & 0 deletions app/models/decidim/rest_full/ability.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

require "cancan"

module Decidim
module RestFull
class Ability
include ::CanCan::Ability
attr_reader :api_client, :permissions

def initialize(api_client)
return unless api_client

@api_client = api_client
@permissions = api_client.permission_strings

can :impersonate, Decidim::RestFull::ApiClient if permissions.include? "oauth.impersonate"
can :login, Decidim::RestFull::ApiClient if permissions.include? "oauth.login"
# Switch scopes and compose permissions
scopes = api_client.scopes.to_a
perms_for_public if scopes.include? "public"
perms_for_system if scopes.include? "system"
perms_for_proposals if scopes.include? "proposals"
end

def self.from_doorkeeper_token(doorkeeper_token)
return Decidim::RestFull::Ability.new(nil) unless doorkeeper_token && doorkeeper_token.valid?
return Decidim::RestFull::Ability.new(nil) unless doorkeeper_token.application.is_a? Decidim::RestFull::ApiClient

Decidim::RestFull::Ability.new(doorkeeper_token.application)
end

private

def perms_for_public; end

def perms_for_system
can :read, ::Decidim::Organization if permissions.include? "system.organizations.read"
can :read, ::Decidim::User if permissions.include? "system.users.read"
end

def perms_for_proposals; end
end
end
end
5 changes: 5 additions & 0 deletions app/models/decidim/rest_full/api_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ class ApiClient < ::Doorkeeper::Application
include Decidim::Loggable

belongs_to :organization, foreign_key: "decidim_organization_id", class_name: "Decidim::Organization", inverse_of: :api_clients
has_many :permissions, class_name: "Decidim::RestFull::Permission", dependent: :destroy

validates :scopes, presence: true
before_validation :dummy_attributes

def permission_strings
@permission_strings ||= permissions.pluck(:permission)
end

def owner
organization
end
Expand Down
13 changes: 13 additions & 0 deletions app/models/decidim/rest_full/permission.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Decidim
module RestFull
class Permission < ::ApplicationRecord
self.table_name = "decidim_rest_full_api_client_permissions"

belongs_to :api_client, class_name: "Decidim::RestFull::ApiClient"

validates :permission, presence: true
end
end
end
73 changes: 53 additions & 20 deletions app/views/decidim/rest_full/system/api_clients/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -22,59 +22,92 @@
<strong><%= t(".permissions_scopes") %></strong><code><%= @api_client.scopes.to_a %></code>
</div>

<%= form_for(@perm_form, method: :post, url: system_api_permissions_path ) do |form| %>
<%= form.hidden_field :api_client_id, value: @api_client.id %>

<% if @api_client.scopes.to_a.include?("system") %>
<div class="border border-black ">
<h2 class="border-b-4 h5 p-4" id="system_permissions">
<%= t("scope_system", scope: "decidim.rest_full.models.api_client.fields") %>
</h2>
<div id="system_permissions" class="p-4 " >
<div class="mt-2">
<div class="mt-4">
<div class="font-bold py-2"><%= t('.auth_type') %></div>
<div class="flex gap-6 flex-wrap">
<label>
<input type="checkbox" />
<span class="pre">impersonate</span>
<% permission = "oauth.impersonate" %>
<%= form.check_box :permissions, { multiple: true, label: "", checked: @api_client.permissions.pluck(:permission).include?(permission.to_s) }, permission, nil %>
<span class="font-normal"><%= t("#{permission}", scope: "decidim.rest_full.models.api_client.permission") %></span>
</label>
<label>
<input type="checkbox" />
<span class="pre">login</span>
<% permission = "oauth.login" %>
<%= form.check_box :permissions, { multiple: true, label: "", checked: @api_client.permissions.pluck(:permission).include?(permission.to_s) }, permission, nil %>
<span class="font-normal"><%= t("#{permission}", scope: "decidim.rest_full.models.api_client.permission") %></span>
</label>
</div>
</div>
<div class="mt-2">
<div class="mt-4">
<div class="font-bold py-2"><%= t('.organization_perm') %></div>
<div class="flex gap-6 flex-wrap">
<label>
<input type="checkbox" />
<span class="pre">list</span>
<% permission = "system.organizations.read" %>
<%= form.check_box :permissions, { multiple: true, label: "", checked: @api_client.permissions.pluck(:permission).include?(permission.to_s) }, permission, nil %>
<span class="font-normal"><%= t("#{permission}", scope: "decidim.rest_full.models.api_client.permission") %></span>
</label>

<label>
<% permission = "system.organizations.update" %>
<%= form.check_box :permissions, { multiple: true, label: "", checked: @api_client.permissions.pluck(:permission).include?(permission.to_s) }, permission, nil %>
<span class="font-normal"><%= t("#{permission}", scope: "decidim.rest_full.models.api_client.permission") %></span>
</label>
<label>
<input type="checkbox" />
<span class="pre">detail</span>
<% permission = "system.organizations.destroy" %>
<%= form.check_box :permissions, { multiple: true, label: "", checked: @api_client.permissions.pluck(:permission).include?(permission.to_s) }, permission, nil %>
<span class="font-normal"><%= t("#{permission}", scope: "decidim.rest_full.models.api_client.permission") %></span>
</label>
</div>
</div>
<div class="mt-4">
<div class="font-bold py-2"><%= t('.user_perm') %></div>
<div class="flex gap-6 flex-wrap">
<label>
<input type="checkbox" />
<span class="pre">update</span>
<% permission = "system.users.read" %>
<%= form.check_box :permissions, { multiple: true, label: "", checked: @api_client.permissions.pluck(:permission).include?(permission.to_s) }, permission, nil %>
<span class="font-normal"><%= t("#{permission}", scope: "decidim.rest_full.models.api_client.permission") %></span>
</label>

<label>
<input type="checkbox" />
<span class="pre">destroy</span>
<% permission = "system.users.update" %>
<%= form.check_box :permissions, { multiple: true, label: "", checked: @api_client.permissions.pluck(:permission).include?(permission.to_s) }, permission, nil %>
<span class="font-normal"><%= t("#{permission}", scope: "decidim.rest_full.models.api_client.permission") %></span>
</label>

<label>
<% permission = "system.users.destroy" %>
<%= form.check_box :permissions, { multiple: true, label: "", checked: @api_client.permissions.pluck(:permission).include?(permission.to_s) }, permission, nil %>
<span class="font-normal"><%= t("#{permission}", scope: "decidim.rest_full.models.api_client.permission") %></span>
</label>
</div>
</div>
<div class="mt-2">
<div class="mt-4">
<div class="font-bold py-2"><%= t('.rails_perm') %></div>
<div class="flex gap-6 flex-wrap">
<label>
<input type="checkbox" />
<span class="pre">restart</span>
<% permission = "system.server.restart" %>
<%= form.check_box :permissions, { multiple: true, label: "", checked: @api_client.permissions.pluck(:permission).include?(permission.to_s) }, permission, nil %>
<span class="font-normal"><%= t("#{permission}", scope: "decidim.rest_full.models.api_client.permission") %></span>
</label>
<label>
<input type="checkbox" />
<span class="pre">execute task</span>
<% permission = "system.server.exec" %>
<%= form.check_box :permissions, { multiple: true, label: "", checked: @api_client.permissions.pluck(:permission).include?(permission.to_s) }, permission, nil %>
<span class="font-normal"><%= t("#{permission}", scope: "decidim.rest_full.models.api_client.permission") %></span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="my-4">
<%= form.submit t(".update_permissions"), class: "button button__sm md:button__lg button__primary" %>
</div>
<% end %>
<% end %>
Loading

0 comments on commit fb5869c

Please sign in to comment.