feat(roles): add editor + viewer roles with ability dispatch

pull/639/head
Bob Develop 2 weeks ago
parent a44531c6a7
commit 1b6aa56757

@ -47,9 +47,11 @@
# fk_rails_... (account_id => accounts.id) # fk_rails_... (account_id => accounts.id)
# #
class User < ApplicationRecord class User < ApplicationRecord
ROLES = [
ADMIN_ROLE = 'admin' ADMIN_ROLE = 'admin'
].freeze EDITOR_ROLE = 'editor'
VIEWER_ROLE = 'viewer'
ROLES = [ADMIN_ROLE, EDITOR_ROLE, VIEWER_ROLE].freeze
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/ EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/

@ -1,20 +1,10 @@
<div class="form-control"> <div class="form-control">
<%= f.label :role, class: 'label' %> <%= f.label :role, class: 'label' %>
<%= f.select :role, nil, {}, class: 'base-select' do %> <%= f.select :role,
<option value="admin"><%= t('admin') %></option> User::ROLES.map { |r| [t(r, default: r.titleize), r] },
<option value="editor" disabled><%= t('editor') %></option> {},
<option value="viewer" disabled><%= t('viewer') %></option> class: 'base-select' %>
<% end %>
<% if Docuseal.multitenant? %>
<label class="label"> <label class="label">
<span class="label-text-alt"> <span class="label-text-alt"><%= t('user_role_help') %></span>
<%= t('click_here_to_learn_more_about_user_roles_and_permissions_html') %>
</span>
</label> </label>
<% end %>
<a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}" %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %>
<%= t('unlock_more_user_roles_with_docuseal_pro') %>
<span class="link font-medium"><%= t('learn_more') %></span>
</a>
</div> </div>

@ -3146,6 +3146,9 @@ it: &it
detailed: "%d %B %Y %H:%M:%S" detailed: "%d %B %Y %H:%M:%S"
fr: &fr fr: &fr
user_role_help: "Les administrateurs ont un accès complet. Les éditeurs peuvent créer et modifier des modèles. Les lecteurs peuvent uniquement consulter les modèles auxquels ils ont accès."
editor: Éditeur
viewer: Lecteur
knowledge_based_authentication: Authentification basée sur la connaissance knowledge_based_authentication: Authentification basée sur la connaissance
kba_field: KBA kba_field: KBA
passed: Réussi passed: Réussi
@ -8041,3 +8044,6 @@ it-IT:
nl-NL: nl-NL:
<<: *nl <<: *nl
nl-NL:
<<: *nl

@ -4,6 +4,24 @@ class Ability
include CanCan::Ability include CanCan::Ability
def initialize(user) def initialize(user)
# Personal config is always accessible regardless of role.
can :manage, EncryptedUserConfig, user_id: user.id
can :manage, UserConfig, user_id: user.id
can :manage, AccessToken, user_id: user.id
case user.role
when User::VIEWER_ROLE
viewer_abilities(user)
when User::EDITOR_ROLE
editor_abilities(user)
else
admin_abilities(user)
end
end
private
def admin_abilities(user)
can %i[read create update], Template, Abilities::TemplateConditions.collection(user) do |template| can %i[read create update], Template, Abilities::TemplateConditions.collection(user) do |template|
Abilities::TemplateConditions.entity(template, user:, ability: 'manage') Abilities::TemplateConditions.entity(template, user:, ability: 'manage')
end end
@ -15,14 +33,42 @@ class Ability
can :manage, Submitter, account_id: user.account_id can :manage, Submitter, account_id: user.account_id
can :manage, User, account_id: user.account_id can :manage, User, account_id: user.account_id
can :manage, EncryptedConfig, account_id: user.account_id can :manage, EncryptedConfig, account_id: user.account_id
can :manage, EncryptedUserConfig, user_id: user.id
can :manage, AccountConfig, account_id: user.account_id can :manage, AccountConfig, account_id: user.account_id
can :manage, UserConfig, user_id: user.id
can :manage, Account, id: user.account_id can :manage, Account, id: user.account_id
can :manage, AccessToken, user_id: user.id
can :manage, McpToken, user_id: user.id can :manage, McpToken, user_id: user.id
can :manage, WebhookUrl, account_id: user.account_id can :manage, WebhookUrl, account_id: user.account_id
can :manage, :mcp can :manage, :mcp
end end
def editor_abilities(user)
can %i[read create update], Template, Abilities::TemplateConditions.collection(user) do |template|
Abilities::TemplateConditions.entity(template, user:, ability: 'manage')
end
can :manage, TemplateFolder, account_id: user.account_id
can :manage, TemplateSharing, template: { account_id: user.account_id }
can :create, Submission, account_id: user.account_id
can %i[read update], Submission, account_id: user.account_id, created_by_user_id: user.id
can %i[read update], Submitter, submission: { account_id: user.account_id, created_by_user_id: user.id }
can :read, User, account_id: user.account_id
can :read, Account, id: user.account_id
can :read, AccountConfig, account_id: user.account_id
end
def viewer_abilities(user)
can :read, Template, Abilities::TemplateConditions.collection(user) do |template|
Abilities::TemplateConditions.entity(template, user:, ability: 'read')
end
can :read, TemplateFolder, account_id: user.account_id
can :read, Submission, account_id: user.account_id, created_by_user_id: user.id
can :read, Submitter, submission: { account_id: user.account_id, created_by_user_id: user.id }
can :read, User, id: user.id
can :read, Account, id: user.account_id
can :read, AccountConfig, account_id: user.account_id
end
end end

@ -0,0 +1,35 @@
import { test, expect } from '@playwright/test';
import { loginAs, loginAsAdmin, adminEmail, adminPassword } from './helpers/auth';
// Phase 1.1 — User roles (admin / editor / viewer)
// Pre-seeded users required in target env:
// - editor@example.com / password (role: editor)
// - viewer@example.com / password (role: viewer)
const editorEmail = process.env.DOCUSEAL_EDITOR_EMAIL || 'editor@example.com';
const viewerEmail = process.env.DOCUSEAL_VIEWER_EMAIL || 'viewer@example.com';
const defaultPassword = process.env.DOCUSEAL_DEFAULT_PASSWORD || 'password';
test.describe('User roles', () => {
test('admin sees New Template button', async ({ page }) => {
await loginAs(page, adminEmail, adminPassword);
await page.goto('/');
await expect(page.getByRole('link', { name: /new template|create/i })).toBeVisible();
});
test('editor can access templates but not account settings', async ({ page }) => {
await loginAs(page, editorEmail, defaultPassword);
await page.goto('/templates');
await expect(page).toHaveURL(/templates|^\//);
await page.goto('/settings/account');
// Editor is denied write access; expect redirect or forbidden copy.
await expect(page).not.toHaveURL(/\/settings\/account(?:$|\?)/);
});
test('viewer cannot see New Template / create controls', async ({ page }) => {
await loginAs(page, viewerEmail, defaultPassword);
await page.goto('/templates');
await expect(page.getByRole('link', { name: /new template/i })).toHaveCount(0);
});
});
Loading…
Cancel
Save