Skip to content

Commit 2c4a3e6

Browse files
author
Mattia Roccoberton
committed
feat: authorization
1 parent c483e44 commit 2c4a3e6

File tree

8 files changed

+237
-36
lines changed

8 files changed

+237
-36
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ Plugins available:
4343

4444
- **NoAuth**: no authentication.
4545

46+
### Authorization
47+
48+
Plugins available:
49+
50+
- **Authorization**: base class to provide an authorization per action, the host application should inherit from it and override the class method `allowed?`.
51+
4652
### Repository
4753

4854
Plugin available:
@@ -134,6 +140,10 @@ authentication:
134140
password: 'f1891cea80fc05e433c943254c6bdabc159577a02a7395dfebbfbc4f7661d4af56f2d372131a45936de40160007368a56ef216a30cb202c66d3145fd24380906'
135141
```
136142

143+
`authorization_class` (String): a plugin class to use;
144+
145+
> 📚 [Wiki Authentication page](https://github.com/blocknotes/tiny_admin/wiki/Authorization) available
146+
137147
`sections` (Array of hashes): define the admin sections, properties:
138148

139149
- `slug` (String): section reference identifier;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
module TinyAdmin
4+
module Plugins
5+
class Authorization
6+
class << self
7+
def allowed?(_user, _action, _param = nil)
8+
true
9+
end
10+
end
11+
end
12+
end
13+
end

lib/tiny_admin/router.rb

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,27 @@ def render_page(page)
5454
end
5555

5656
def root_route(req)
57-
if TinyAdmin.settings.root[:redirect]
58-
req.redirect route_for(TinyAdmin.settings.root[:redirect])
57+
if authorization.allowed?(current_user, :root)
58+
if TinyAdmin.settings.root[:redirect]
59+
req.redirect route_for(TinyAdmin.settings.root[:redirect])
60+
else
61+
page_class = to_class(TinyAdmin.settings.root[:page])
62+
attributes = TinyAdmin.settings.root.slice(:content, :title, :widgets)
63+
render_page prepare_page(page_class, attributes: attributes, params: request.params)
64+
end
5965
else
60-
page_class = to_class(TinyAdmin.settings.root[:page])
61-
attributes = TinyAdmin.settings.root.slice(:content, :title, :widgets)
62-
render_page prepare_page(page_class, attributes: attributes, params: request.params)
66+
render_page prepare_page(TinyAdmin.settings.page_not_allowed)
6367
end
6468
end
6569

6670
def setup_page_route(req, slug, page_data)
6771
req.get slug do
68-
attributes = page_data.slice(:content, :title, :widgets)
69-
render_page prepare_page(page_data[:class], slug: slug, attributes: attributes, params: request.params)
72+
if authorization.allowed?(current_user, :page, slug)
73+
attributes = page_data.slice(:content, :title, :widgets)
74+
render_page prepare_page(page_data[:class], slug: slug, attributes: attributes, params: request.params)
75+
else
76+
render_page prepare_page(TinyAdmin.settings.page_not_allowed)
77+
end
7078
end
7179
end
7280

@@ -93,15 +101,19 @@ def setup_collection_routes(req, slug, options:)
93101
# Index
94102
if options[:only].include?(:index) || options[:only].include?('index')
95103
req.is do
96-
context = Context.new(
97-
actions: custom_actions,
98-
repository: repository,
99-
request: request,
100-
router: req,
101-
slug: slug
102-
)
103-
index_action = TinyAdmin::Actions::Index.new
104-
render_page index_action.call(app: self, context: context, options: action_options)
104+
if authorization.allowed?(current_user, :resource_index, slug)
105+
context = Context.new(
106+
actions: custom_actions,
107+
repository: repository,
108+
request: request,
109+
router: req,
110+
slug: slug
111+
)
112+
index_action = TinyAdmin::Actions::Index.new
113+
render_page index_action.call(app: self, context: context, options: action_options)
114+
else
115+
render_page prepare_page(TinyAdmin.settings.page_not_allowed)
116+
end
105117
end
106118
end
107119
end
@@ -124,16 +136,20 @@ def setup_member_routes(req, slug, options:)
124136
# Show
125137
if options[:only].include?(:show) || options[:only].include?('show')
126138
req.is do
127-
context = Context.new(
128-
actions: custom_actions,
129-
reference: reference,
130-
repository: repository,
131-
request: request,
132-
router: req,
133-
slug: slug
134-
)
135-
show_action = TinyAdmin::Actions::Show.new
136-
render_page show_action.call(app: self, context: context, options: action_options)
139+
if authorization.allowed?(current_user, :resource_show, slug)
140+
context = Context.new(
141+
actions: custom_actions,
142+
reference: reference,
143+
repository: repository,
144+
request: request,
145+
router: req,
146+
slug: slug
147+
)
148+
show_action = TinyAdmin::Actions::Show.new
149+
render_page show_action.call(app: self, context: context, options: action_options)
150+
else
151+
render_page prepare_page(TinyAdmin.settings.page_not_allowed)
152+
end
137153
end
138154
end
139155
end
@@ -145,20 +161,28 @@ def setup_custom_actions(req, custom_actions = nil, options:, repository:, slug:
145161
action_class = to_class(action)
146162

147163
req.get action_slug.to_s do
148-
context = Context.new(
149-
actions: {},
150-
reference: reference,
151-
repository: repository,
152-
request: request,
153-
router: req,
154-
slug: slug
155-
)
156-
custom_action = action_class.new
157-
render_page custom_action.call(app: self, context: context, options: options)
164+
if authorization.allowed?(current_user, :custom_action, action_slug.to_s)
165+
context = Context.new(
166+
actions: {},
167+
reference: reference,
168+
repository: repository,
169+
request: request,
170+
router: req,
171+
slug: slug
172+
)
173+
custom_action = action_class.new
174+
render_page custom_action.call(app: self, context: context, options: options)
175+
else
176+
render_page prepare_page(TinyAdmin.settings.page_not_allowed)
177+
end
158178
end
159179

160180
result[action_slug.to_s] = action_class
161181
end
162182
end
183+
184+
def authorization
185+
TinyAdmin.settings.authorization_class
186+
end
163187
end
164188
end

lib/tiny_admin/settings.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ class Settings
77
DEFAULTS = {
88
%i[authentication plugin] => Plugins::NoAuth,
99
%i[authentication login] => Views::Pages::SimpleAuthLogin,
10+
%i[authorization_class] => Plugins::Authorization,
1011
%i[components field_value] => Views::Components::FieldValue,
1112
%i[components flash] => Views::Components::Flash,
1213
%i[components head] => Views::Components::Head,
1314
%i[components navbar] => Views::Components::Navbar,
1415
%i[components pagination] => Views::Components::Pagination,
1516
%i[content_page] => Views::Pages::Content,
1617
%i[helper_class] => Support,
18+
%i[page_not_allowed] => Views::Pages::PageNotAllowed,
1719
%i[page_not_found] => Views::Pages::PageNotFound,
1820
%i[record_not_found] => Views::Pages::RecordNotFound,
1921
%i[repository] => Plugins::ActiveRecordRepository,
@@ -25,10 +27,12 @@ class Settings
2527

2628
OPTIONS = %i[
2729
authentication
30+
authorization_class
2831
components
2932
content_page
3033
extra_styles
3134
helper_class
35+
page_not_allowed
3236
page_not_found
3337
record_not_found
3438
repository
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
module TinyAdmin
4+
module Views
5+
module Pages
6+
class PageNotAllowed < DefaultLayout
7+
def template
8+
super do
9+
div(class: 'page_not_allowed') {
10+
h1(class: 'title') { title }
11+
}
12+
end
13+
end
14+
15+
def title
16+
'Page not allowed'
17+
end
18+
end
19+
end
20+
end
21+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module TinyAdmin
2+
module Plugins
3+
class Authorization
4+
def self.allowed?: (untyped, untyped, ?untyped?) -> bool
5+
end
6+
end
7+
end

sig/tiny_admin/router.rbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ module TinyAdmin
44

55
private
66

7+
def authorization: () -> untyped
8+
79
def render_page: (untyped) -> void
810

911
def root_route: (untyped) -> void
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# frozen_string_literal: true
2+
3+
require 'dummy_rails_app'
4+
require 'rails_helper'
5+
6+
RSpec.describe 'Authorization plugin', type: :feature do
7+
let(:root_content) { "Latest authors\nLatest posts" }
8+
9+
around do |example|
10+
prev_value = TinyAdmin.settings.authorization_class
11+
TinyAdmin.settings.authorization_class = some_class
12+
example.run
13+
TinyAdmin.settings.authorization_class = prev_value
14+
end
15+
16+
before do
17+
visit '/admin'
18+
log_in
19+
end
20+
21+
context 'with an Authorization class that restrict the root page' do
22+
let(:some_class) do
23+
Class.new(TinyAdmin::Plugins::Authorization) do
24+
class << self
25+
def allowed?(_user, action, _param = nil)
26+
return false if action == :root
27+
28+
true
29+
end
30+
end
31+
end
32+
end
33+
34+
it 'disallows the access to the root page when opened', :aggregate_failures do
35+
expect(page).to have_content 'Page not allowed'
36+
expect { click_on 'Sample page' }
37+
.to change { page.find('.main-content').text }.to("Sample page\nThis is just a sample page")
38+
end
39+
end
40+
41+
context 'with an Authorization class that restrict a specific page' do
42+
let(:some_class) do
43+
Class.new(TinyAdmin::Plugins::Authorization) do
44+
class << self
45+
def allowed?(_user, action, param = nil)
46+
return false if action == :page && param == 'sample'
47+
48+
true
49+
end
50+
end
51+
end
52+
end
53+
54+
it 'disallows the access to the page when opened' do
55+
expect { click_on 'Sample page' }
56+
.to change { page.find('.main-content').text }.from(root_content).to('Page not allowed')
57+
end
58+
end
59+
60+
context 'with an Authorization class that restrict resource index' do
61+
let(:some_class) do
62+
Class.new(TinyAdmin::Plugins::Authorization) do
63+
class << self
64+
def allowed?(_user, action, _param = nil)
65+
return false if action == :resource_index
66+
67+
true
68+
end
69+
end
70+
end
71+
end
72+
73+
it 'disallows the access to the page when opened' do
74+
expect { click_on 'Posts' }
75+
.to change { page.find('.main-content').text }.from(root_content).to('Page not allowed')
76+
end
77+
end
78+
79+
context 'with an Authorization class that restrict resource show' do
80+
let(:some_class) do
81+
Class.new(TinyAdmin::Plugins::Authorization) do
82+
class << self
83+
def allowed?(_user, action, _param = nil)
84+
return false if action == :resource_show
85+
86+
true
87+
end
88+
end
89+
end
90+
end
91+
92+
before { setup_data(posts_count: 1) }
93+
94+
it 'disallows the access to the page when opened' do
95+
click_on 'Posts'
96+
expect { click_on 'Show' }
97+
.to change { page.find('.main-content').text }.to('Page not allowed')
98+
end
99+
end
100+
101+
context 'with an Authorization class that restrict a specific custom action' do
102+
let(:some_class) do
103+
Class.new(TinyAdmin::Plugins::Authorization) do
104+
class << self
105+
def allowed?(_user, action, param = nil)
106+
return false if action == :custom_action && param == 'sample_col'
107+
108+
true
109+
end
110+
end
111+
end
112+
end
113+
114+
it 'disallows the access to the page when opened' do
115+
click_on 'Authors'
116+
expect { click_on 'sample_col' }
117+
.to change { page.find('.main-content').text }.to('Page not allowed')
118+
end
119+
end
120+
end

0 commit comments

Comments
 (0)