fe 2.3.2 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +147 -3
- data/app/controllers/concerns/fe/access_control_concern.rb +39 -0
- data/app/controllers/concerns/fe/admin/question_sheets_controller_concern.rb +2 -1
- data/app/controllers/concerns/fe/answer_pages_controller_concern.rb +3 -1
- data/app/controllers/concerns/fe/answer_sheets_controller_concern.rb +4 -1
- data/app/controllers/fe/admin/elements_controller.rb +4 -3
- data/app/controllers/fe/admin/email_templates_controller.rb +2 -1
- data/app/controllers/fe/admin/question_pages_controller.rb +2 -1
- data/app/controllers/fe/reference_pages_controller.rb +4 -0
- data/app/controllers/fe/reference_sheets_controller.rb +8 -2
- data/app/controllers/fe/references_controller.rb +4 -0
- data/app/controllers/fe/submit_pages_controller.rb +2 -0
- data/app/views/fe/answer_pages/update.js.erb +2 -1
- data/app/views/fe/questions/fe/_attachment_field.html.erb +8 -8
- data/lib/fe/access_control.rb +72 -0
- data/lib/fe/engine.rb +4 -0
- data/lib/fe/version.rb +1 -1
- data/lib/fe.rb +1 -0
- data/spec/controllers/fe/admin/elements_controller_spec.rb +29 -0
- data/spec/controllers/fe/admin/email_templates_controller_spec.rb +8 -0
- data/spec/controllers/fe/admin/question_pages_controller_spec.rb +9 -0
- data/spec/controllers/fe/admin/question_sheets_controller_spec.rb +30 -0
- data/spec/controllers/fe/answer_pages_controller_spec.rb +142 -0
- data/spec/controllers/fe/answer_sheets_controller_spec.rb +114 -0
- data/spec/controllers/fe/reference_pages_controller_spec.rb +32 -0
- data/spec/controllers/fe/reference_sheets_controller_spec.rb +88 -1
- data/spec/controllers/fe/references_controller_spec.rb +29 -1
- data/spec/controllers/fe/submit_pages_controller_spec.rb +44 -1
- data/spec/dummy/app/controllers/application_controller.rb +8 -0
- data/spec/dummy/config/initializers/fe.rb +2 -0
- data/spec/lib/fe/access_control_spec.rb +173 -0
- metadata +10 -8
- data/spec/controllers/fe/reference_pages_controller.rb +0 -4
- data/spec/dummy/log/test.log +0 -9115
- data/spec/dummy/tmp/local_secret.txt +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5901527c0d559c346f9b5260706bae3c5dca50aa53acad6e3389a9953ad3258
|
|
4
|
+
data.tar.gz: 6ce8e73924606203040a84222cf1ff2127855dff223331b9f83524a91ee8effb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d944254d553ae54b9ac39016114c797a2176fcdedaee5543e50da0fa9dc6162a81087df058f893900194c963c321141c63de5c21d3e25c3c74a103a263a7bc5
|
|
7
|
+
data.tar.gz: 120a3a78d3a8b443ca1c2cf9f43c36166e0883dfd8d075cc61655637053d02b4600100c9bcdce96a6102fe5fa40ca0e4c7ca150c13ba342234027aa10fe77c70
|
data/README.md
CHANGED
|
@@ -58,8 +58,152 @@ Run a specific spec:
|
|
|
58
58
|
|
|
59
59
|
bundle exec rails server
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
## Access control
|
|
62
|
+
|
|
63
|
+
fe denies access by default. It never decides who a person is or what they may
|
|
64
|
+
see. The host app answers two questions, and until it does, fe refuses every
|
|
65
|
+
request. fe also checks at boot: the app raises `Fe::MissingAccessHook` and
|
|
66
|
+
won't start until both hooks exist.
|
|
67
|
+
|
|
68
|
+
### Required hooks
|
|
69
|
+
|
|
70
|
+
Define both on `ApplicationController`, or on the fe controllers themselves.
|
|
71
|
+
Private, protected and public methods all count, and so do methods added by a
|
|
72
|
+
module you include or prepend in `config.to_prepare`.
|
|
73
|
+
|
|
74
|
+
class ApplicationController < ActionController::Base
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# May the current person use this answer sheet?
|
|
78
|
+
# access is :look (viewing it) or :change (anything else).
|
|
79
|
+
def fe_can_use_answer_sheet?(answer_sheet, access)
|
|
80
|
+
if answer_sheet.is_a?(Fe::ReferenceSheet)
|
|
81
|
+
# Reference writers open their sheet from a link that carries its key.
|
|
82
|
+
Fe.access_key_matches?(answer_sheet, params[:a])
|
|
83
|
+
else
|
|
84
|
+
current_user.present? && answer_sheet.owner == current_user
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# May the current person use fe's admin screens (the form builder)?
|
|
89
|
+
def fe_admin?
|
|
90
|
+
current_user&.admin?
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
`fe_can_use_answer_sheet?(answer_sheet, access)`
|
|
95
|
+
|
|
96
|
+
- `:look` is used by the `show` actions. `:change` covers every other action
|
|
97
|
+
that works on an answer sheet (saving answers, submitting, and so on).
|
|
98
|
+
- It judges whatever is in `@answer_sheet` when the check runs. If nothing has
|
|
99
|
+
loaded the sheet yet, fe calls `get_answer_sheet` first (yours, if you
|
|
100
|
+
override it), so moving, re-declaring or skipping that callback doesn't skip
|
|
101
|
+
the check. To change how the sheet loads, override `get_answer_sheet`. If the
|
|
102
|
+
loader renders or redirects, that is the response. If there is still no
|
|
103
|
+
sheet (for example a reference link with a wrong key), fe leaves the
|
|
104
|
+
response to the action.
|
|
105
|
+
- Reference sheets reach the same hook as `Fe::ReferenceSheet` records. A hook
|
|
106
|
+
that only checks the signed-in user locks reference writers out, so give
|
|
107
|
+
them their own branch, as in the example. The reference's access key is in
|
|
108
|
+
`params[:a]`, and `Fe.access_key_matches?(answer_sheet, params[:a])` is the
|
|
109
|
+
safe way to check it. Whether a key alone is enough is up to your app.
|
|
110
|
+
|
|
111
|
+
`fe_admin?` takes no arguments. It guards the admin controllers
|
|
112
|
+
(`Fe::Admin::QuestionSheetsController`, `QuestionPagesController`,
|
|
113
|
+
`ElementsController`, `EmailTemplatesController`) and the `index` and `create`
|
|
114
|
+
actions of `Fe::AnswerSheetsController`. The `index` and `create` rule follows
|
|
115
|
+
fe's own code for those actions, so it also covers any controller of yours that
|
|
116
|
+
inherits them without defining its own. If you define `index` or `create`
|
|
117
|
+
yourself (in a subclass, a reopened class or a prepended module), even one that
|
|
118
|
+
only calls `super`, fe's admin check skips it and your action owns its check.
|
|
119
|
+
|
|
120
|
+
The boot check needs `fe_can_use_answer_sheet?` on every class in
|
|
121
|
+
`Fe::ANSWER_SHEET_CONTROLLERS` and `fe_admin?` on every class in
|
|
122
|
+
`Fe::ADMIN_CONTROLLERS` plus `Fe::AnswerSheetsController`. Defining both on
|
|
123
|
+
`ApplicationController` covers all of them.
|
|
124
|
+
|
|
125
|
+
### Changing fe's controllers
|
|
126
|
+
|
|
127
|
+
Reopening an fe controller (with `class_eval`, or a module you include or
|
|
128
|
+
prepend) keeps fe's checks. Replacing one with your own class of the same name
|
|
129
|
+
doesn't: your class gets only the checks it includes or inherits. The checks
|
|
130
|
+
for answer sheets, answer pages and question sheets live in fe concerns
|
|
131
|
+
(`Fe::AnswerSheetsControllerConcern`, `Fe::AnswerPagesControllerConcern`,
|
|
132
|
+
`Fe::Admin::QuestionSheetsControllerConcern`), so a replacement that includes
|
|
133
|
+
the concern keeps them. fe's other controllers declare their checks in the
|
|
134
|
+
class itself, so a replacement for one of them loses those checks unless you
|
|
135
|
+
add them. The boot check only looks for the hook methods, so it can't tell.
|
|
136
|
+
|
|
137
|
+
### Optional: `fe_access_denied(kind)`
|
|
138
|
+
|
|
139
|
+
When a check fails, fe sends `403 Forbidden`. To do something else, define
|
|
140
|
+
`fe_access_denied(kind)`. `kind` is `:answer_sheet` or `:admin`.
|
|
141
|
+
|
|
142
|
+
def fe_access_denied(kind)
|
|
143
|
+
redirect_to root_path, alert: 'You do not have access to that.'
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
If it renders or redirects, that replaces the 403. If it does neither, fe still
|
|
147
|
+
sends 403. Exceptions raised in your hooks are not rescued.
|
|
148
|
+
|
|
149
|
+
### Other settings
|
|
150
|
+
|
|
151
|
+
- `Fe.answer_sheet_types`: the answer sheet classes fe accepts in the
|
|
152
|
+
`answer_sheet_type` param. Any other class is refused as not found. It
|
|
153
|
+
defaults to `nil`, which means `Fe.answer_sheet_class` plus
|
|
154
|
+
`Fe::ReferenceSheet`. Setting it replaces that default, so list every class
|
|
155
|
+
your app uses, including your `Fe.answer_sheet_class` and
|
|
156
|
+
`Fe::ReferenceSheet`. Because the default is `nil`, `+=` raises on it. Set it
|
|
157
|
+
where you set `Fe.answer_sheet_class` (for example in a `config.to_prepare`
|
|
158
|
+
block, if that's where your app sets the class):
|
|
159
|
+
|
|
160
|
+
Fe.answer_sheet_class = 'Fe::Application'
|
|
161
|
+
Fe.answer_sheet_types = %w[Fe::Application Fe::ReferenceSheet MyOtherSheet]
|
|
162
|
+
|
|
163
|
+
- `Fe.element_types`: the element classes the form builder accepts in the
|
|
164
|
+
`element_type` param. It defaults to fe's own element classes, so add yours
|
|
165
|
+
to it:
|
|
166
|
+
|
|
167
|
+
Fe.element_types += %w[MyRatingField]
|
|
168
|
+
|
|
169
|
+
- `Fe.access_key_matches?(answer_sheet, key)`: constant-time comparison of a
|
|
170
|
+
key against the sheet's `access_key`. False for a blank or non-string key,
|
|
171
|
+
and false when the sheet's own `access_key` is blank.
|
|
172
|
+
- `Fe.verify_access_hooks_on_boot` (default `true`): set to `false` to turn off
|
|
173
|
+
the boot check. It is meant only for fe's own test app. Leave it on in a real
|
|
174
|
+
app.
|
|
175
|
+
|
|
176
|
+
### Upgrading to 2.4
|
|
177
|
+
|
|
178
|
+
- fe no longer calls `check_valid_user`. Move that logic into
|
|
179
|
+
`fe_can_use_answer_sheet?` and `fe_admin?`.
|
|
180
|
+
- fe accepts only the classes in `Fe.answer_sheet_types` in the
|
|
181
|
+
`answer_sheet_type` param, and by default that is just
|
|
182
|
+
`Fe.answer_sheet_class` and `Fe::ReferenceSheet`. Every other sheet class is
|
|
183
|
+
refused as not found. fe's pages send the sheet's class in that param on
|
|
184
|
+
every page load and save, so if your app has any other answer sheet classes,
|
|
185
|
+
set `Fe.answer_sheet_types` to list all of them (see above).
|
|
186
|
+
- The form builder accepts only the element classes in `Fe.element_types`. Add
|
|
187
|
+
any element classes of your own.
|
|
188
|
+
- `index` and `create` on `Fe::AnswerSheetsController` are admin-only
|
|
189
|
+
(`fe_admin?`), and so are those actions in any controller of yours that
|
|
190
|
+
inherits them without defining its own.
|
|
191
|
+
- `Fe::ReferenceSheetsController` allows only `edit`, `show` and `done`. Other
|
|
192
|
+
actions return 404. `show` is checked with `:look`, and a `show` link with a
|
|
193
|
+
wrong or missing key now gets the not-found page with a 404 instead of an
|
|
194
|
+
error.
|
|
195
|
+
- If your app overrides `fe/questions/fe/_attachment_field.html.erb`, add
|
|
196
|
+
`a: params[:a]` to its upload, delete and page reload URLs, as fe's own
|
|
197
|
+
partial now does. Without the key, reference writers get 403 when they
|
|
198
|
+
attach a file.
|
|
199
|
+
- Define both hooks before deploying, or the app won't boot.
|
|
200
|
+
|
|
201
|
+
## Changelog
|
|
202
|
+
|
|
203
|
+
### 2.4
|
|
204
|
+
|
|
205
|
+
- **Deny by default**: fe refuses answer sheets and its admin screens unless the host app allows them, and the app won't boot until it defines fe's access hooks. See [Upgrading to 2.4](#upgrading-to-24).
|
|
206
|
+
|
|
207
|
+
### 2.2
|
|
64
208
|
|
|
65
209
|
- **Accidental overwrite protection**: When multiple tabs/windows have the same form open, autosave could silently overwrite another tab's answers. Now an MD5 digest of the current answers is tracked, and saves are rejected if the answers have changed since the page was loaded.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Fe::AccessControlConcern
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
# Must not rely on get_answer_sheet having run first: re-declaring a callback
|
|
7
|
+
# moves it to the end of the chain. A request that still has no sheet after
|
|
8
|
+
# loading is left to the action.
|
|
9
|
+
def fe_authorize_answer_sheet!
|
|
10
|
+
return if performed?
|
|
11
|
+
get_answer_sheet if @answer_sheet.nil? && respond_to?(:get_answer_sheet, true)
|
|
12
|
+
return if performed? || @answer_sheet.nil?
|
|
13
|
+
|
|
14
|
+
access = action_name == 'show' ? :look : :change
|
|
15
|
+
allowed = respond_to?(:fe_can_use_answer_sheet?, true) && fe_can_use_answer_sheet?(@answer_sheet, access)
|
|
16
|
+
fe_deny!(:answer_sheet) unless allowed
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def fe_require_admin!
|
|
20
|
+
return if performed?
|
|
21
|
+
|
|
22
|
+
allowed = respond_to?(:fe_admin?, true) && fe_admin?
|
|
23
|
+
fe_deny!(:admin) unless allowed
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A subclass, reopened class or prepended module that defines the action
|
|
27
|
+
# replaces fe's and owns its access control, so it must not be gated here.
|
|
28
|
+
def fe_require_admin_for_own_action!
|
|
29
|
+
return unless self.class.instance_method(action_name).owner == Fe::AnswerSheetsControllerConcern
|
|
30
|
+
|
|
31
|
+
fe_require_admin!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whatever the host's fe_access_denied does, the request must not continue.
|
|
35
|
+
def fe_deny!(kind)
|
|
36
|
+
fe_access_denied(kind) if respond_to?(:fe_access_denied, true)
|
|
37
|
+
head :forbidden unless performed?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -3,7 +3,8 @@ module Fe::Admin::QuestionSheetsControllerConcern
|
|
|
3
3
|
|
|
4
4
|
begin
|
|
5
5
|
included do
|
|
6
|
-
|
|
6
|
+
include Fe::AccessControlConcern
|
|
7
|
+
before_action :fe_require_admin!
|
|
7
8
|
before_action :get_question_sheet, only: [:show, :archive, :unarchive, :destroy, :edit, :update, :duplicate]
|
|
8
9
|
layout 'fe/fe_admin'
|
|
9
10
|
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
module Fe::AnswerPagesControllerConcern
|
|
2
2
|
extend ActiveSupport::Concern
|
|
3
|
+
include Fe::AccessControlConcern
|
|
3
4
|
|
|
4
5
|
begin
|
|
5
6
|
included do
|
|
6
7
|
before_action :get_answer_sheet, only: [:show, :edit, :update, :save_file, :delete_file]
|
|
8
|
+
before_action :fe_authorize_answer_sheet!, only: [:show, :edit, :update, :save_file, :delete_file]
|
|
7
9
|
before_action :set_quiet_reference_email_change, only: :update
|
|
8
10
|
skip_before_action :verify_authenticity_token, only: :save_file
|
|
9
11
|
end
|
|
@@ -165,7 +167,7 @@ module Fe::AnswerPagesControllerConcern
|
|
|
165
167
|
end
|
|
166
168
|
|
|
167
169
|
def answer_sheet_type
|
|
168
|
-
(params[:answer_sheet_type]
|
|
170
|
+
Fe.answer_sheet_class_for(params[:answer_sheet_type])
|
|
169
171
|
end
|
|
170
172
|
|
|
171
173
|
def answer_params
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
module Fe::AnswerSheetsControllerConcern
|
|
2
2
|
extend ActiveSupport::Concern
|
|
3
|
+
include Fe::AccessControlConcern
|
|
3
4
|
|
|
4
5
|
begin
|
|
5
6
|
included do
|
|
6
7
|
layout 'fe/application'
|
|
8
|
+
before_action :fe_require_admin_for_own_action!, only: [:index, :create]
|
|
7
9
|
before_action :get_answer_sheet, only: [:edit, :show, :send_reference_invite, :submit]
|
|
10
|
+
before_action :fe_authorize_answer_sheet!, only: [:edit, :show, :send_reference_invite, :submit]
|
|
8
11
|
after_action :inject_answers_digest_script, only: :edit
|
|
9
12
|
end
|
|
10
13
|
rescue ActiveSupport::Concern::MultipleIncludedBlocks
|
|
@@ -94,7 +97,7 @@ module Fe::AnswerSheetsControllerConcern
|
|
|
94
97
|
|
|
95
98
|
def answer_sheet_type
|
|
96
99
|
return Fe::ReferenceSheet if params[:controller] == "fe/reference_sheets"
|
|
97
|
-
(params[:answer_sheet_type]
|
|
100
|
+
Fe.answer_sheet_class_for(params[:answer_sheet_type])
|
|
98
101
|
end
|
|
99
102
|
|
|
100
103
|
def get_answer_sheet
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
class Fe::Admin::ElementsController < ApplicationController
|
|
2
|
-
|
|
2
|
+
include Fe::AccessControlConcern
|
|
3
|
+
before_action :fe_require_admin!
|
|
3
4
|
|
|
4
5
|
before_action :get_page
|
|
5
6
|
|
|
@@ -19,7 +20,7 @@ class Fe::Admin::ElementsController < ApplicationController
|
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def new
|
|
22
|
-
@questions = params[:element_type].
|
|
23
|
+
@questions = Fe.element_class_for(params[:element_type]).active.shared.order('label')
|
|
23
24
|
|
|
24
25
|
@style = element_params[:style]
|
|
25
26
|
if @style
|
|
@@ -48,7 +49,7 @@ class Fe::Admin::ElementsController < ApplicationController
|
|
|
48
49
|
|
|
49
50
|
# POST /elements
|
|
50
51
|
def create
|
|
51
|
-
@element = params[:element_type].
|
|
52
|
+
@element = Fe.element_class_for(params[:element_type]).new(element_params)
|
|
52
53
|
@element.required = true if @element.question?
|
|
53
54
|
@question_sheet = @page.question_sheet
|
|
54
55
|
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# gather reference information from Applicant
|
|
2
2
|
module Fe
|
|
3
3
|
class ReferencePagesController < ApplicationController
|
|
4
|
+
include Fe::AccessControlConcern
|
|
5
|
+
|
|
4
6
|
skip_before_action :cas_filter, raise: false
|
|
5
7
|
skip_before_action :authentication_filter, raise: false
|
|
6
8
|
|
|
7
9
|
layout nil
|
|
8
10
|
|
|
9
11
|
before_action :setup
|
|
12
|
+
before_action :fe_authorize_answer_sheet!
|
|
10
13
|
|
|
11
14
|
MONTHS_KNOWN_OPTIONS = [
|
|
12
15
|
["3 months", 3],
|
|
@@ -35,6 +38,7 @@ module Fe
|
|
|
35
38
|
private
|
|
36
39
|
def setup
|
|
37
40
|
@application = Application.find(params[:application_id])
|
|
41
|
+
@answer_sheet ||= @application
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# TODO determine how this relates to Fe::ReferencesController and if we can delete one of the two
|
|
2
2
|
class Fe::ReferenceSheetsController < Fe::AnswerSheetsController
|
|
3
3
|
skip_before_action :ssm_login_required, :login, raise: false
|
|
4
|
-
|
|
4
|
+
# Prepended so other actions stop before a sheet loads or the host's hook runs.
|
|
5
|
+
prepend_before_action :edit_only, except: [:edit, :show, :done]
|
|
5
6
|
|
|
6
7
|
def edit
|
|
7
8
|
@reference_sheet = @answer_sheet
|
|
@@ -19,6 +20,11 @@ class Fe::ReferenceSheetsController < Fe::AnswerSheetsController
|
|
|
19
20
|
render 'fe/answer_sheets/edit', layout: 'fe/application'
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
def show
|
|
24
|
+
return render(:not_found, status: :not_found) unless @answer_sheet
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
protected
|
|
23
29
|
def get_answer_sheet
|
|
24
30
|
@answer_sheet ||= Fe::ReferenceSheet.find_by_id_and_access_key(params[:id], params[:a])
|
|
@@ -26,6 +32,6 @@ class Fe::ReferenceSheetsController < Fe::AnswerSheetsController
|
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
def edit_only
|
|
29
|
-
|
|
35
|
+
head :not_found
|
|
30
36
|
end
|
|
31
37
|
end
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
# TODO determine how this relates to Fe::ReferenceSheetsController and if we can delete one of the two
|
|
3
3
|
module Fe
|
|
4
4
|
class ReferencesController < ApplicationController
|
|
5
|
+
include Fe::AccessControlConcern
|
|
6
|
+
|
|
5
7
|
skip_before_action :cas_filter, raise: false
|
|
6
8
|
skip_before_action :authentication_filter, raise: false
|
|
7
9
|
|
|
8
10
|
before_action :setup
|
|
11
|
+
before_action :fe_authorize_answer_sheet!
|
|
9
12
|
|
|
10
13
|
layout 'public'
|
|
11
14
|
|
|
@@ -70,6 +73,7 @@ module Fe
|
|
|
70
73
|
private
|
|
71
74
|
def setup
|
|
72
75
|
@application = Application.find(params[:application_id])
|
|
76
|
+
@answer_sheet ||= @application
|
|
73
77
|
end
|
|
74
78
|
end
|
|
75
79
|
end
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
$('#<%= dom_id(@answer.question) %>_filename').html('Current File: ' +
|
|
9
9
|
'<%= link_to(@answer.attachment_file_name, @answer.attachment.expiring_url) %>' +
|
|
10
10
|
'<%= link_to(image_tag("fe/icons/minus-button.png"),
|
|
11
|
-
delete_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: @answer.question.id
|
|
11
|
+
delete_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: @answer.question.id,
|
|
12
|
+
answer_sheet_type: @answer_sheet.class.name, a: params[:a]),
|
|
12
13
|
method: :delete, remote: true) %>')
|
|
13
14
|
$('#<%= dom_id(@answer.question) %>_filename').effect('highlight')
|
|
14
15
|
<% elsif @answer %>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<% if answer %>
|
|
7
7
|
Current File: <%= link_to(answer.attachment.filename, url_for(answer.attachment)) %>
|
|
8
8
|
<%= link_to(image_tag("fe/icons/minus-button.png"),
|
|
9
|
-
delete_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name),
|
|
9
|
+
delete_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name, a: params[:a]),
|
|
10
10
|
method: :delete, remote: true) %>
|
|
11
11
|
<% end %>
|
|
12
12
|
<% else %>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
if answer&.attachment&.attached? %>
|
|
20
20
|
Current File: <%= link_to(answer.attachment.filename, url_for(answer.attachment)) %>
|
|
21
21
|
<%= link_to(image_tag('fe/icons/minus-button.png'),
|
|
22
|
-
delete_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name),
|
|
22
|
+
delete_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name, a: params[:a]),
|
|
23
23
|
method: :delete, remote: true) %>
|
|
24
24
|
<% end %>
|
|
25
25
|
</span>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<% session_key_name = Rails.application.config.session_options[:key] %>
|
|
30
30
|
$('#<%= dom_id(attachment_field) %>_file_uploader').uploadify({
|
|
31
31
|
uploader : '<%= asset_path "fe/uploadify/uploadify.swf" %>',
|
|
32
|
-
script : '<%= save_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name).html_safe %>',
|
|
32
|
+
script : '<%= save_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name, a: params[:a]).html_safe %>',
|
|
33
33
|
fileDesc : 'Pick a file',
|
|
34
34
|
fileExt : '<%= attachment_field.content || "*.*" %>',
|
|
35
35
|
auto : true,
|
|
@@ -58,15 +58,15 @@
|
|
|
58
58
|
<% if answer&.attachment&.attached? %>
|
|
59
59
|
Current File: <%= link_to(answer.attachment.filename, url_for(answer.attachment)) %>
|
|
60
60
|
<%= link_to(image_tag("fe/icons/minus-button.png"),
|
|
61
|
-
delete_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name),
|
|
61
|
+
delete_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name, a: params[:a]),
|
|
62
62
|
method: :delete, remote: true, class: 'remove_current') %>
|
|
63
63
|
<script>
|
|
64
64
|
$('<%= base_element %> .remove_current').click(function() {
|
|
65
65
|
$.ajax({
|
|
66
66
|
type: 'DELETE',
|
|
67
|
-
url: '<%= delete_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name).html_safe %>',
|
|
67
|
+
url: '<%= delete_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name, a: params[:a]).html_safe %>',
|
|
68
68
|
success: function(result) {
|
|
69
|
-
$.get("
|
|
69
|
+
$.get("<%= edit_fe_answer_sheet_page_path(@answer_sheet, @page, answer_sheet_type: @answer_sheet.class.name, a: params[:a]).html_safe %>", function(data) {
|
|
70
70
|
$('<%= base_element %>').replaceWith($(data).find('<%= base_element %>'))
|
|
71
71
|
});
|
|
72
72
|
}
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
|
|
99
99
|
let $input = $("<%= base_element %> .upload_field").html5_upload({
|
|
100
100
|
url: function(number) {
|
|
101
|
-
return "<%= save_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name).html_safe %>";
|
|
101
|
+
return "<%= save_file_fe_answer_sheet_page_path(@presenter.active_answer_sheet, @page, question_id: element_id, answer_sheet_type: @answer_sheet.class.name, a: params[:a]).html_safe %>";
|
|
102
102
|
},
|
|
103
103
|
sendBoundary: window.FormData || $.browser.mozilla,
|
|
104
104
|
onStart: function(event, total) {
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
// Re-render the attachment file to show the current file; use the page view, which is not the
|
|
124
124
|
// most efficient way, but lets us avoid making an action just for this. And the amount of data
|
|
125
125
|
// on a page is not that much
|
|
126
|
-
$.get("
|
|
126
|
+
$.get("<%= edit_fe_answer_sheet_page_path(@answer_sheet, @page, answer_sheet_type: @answer_sheet.class.name, a: params[:a]).html_safe %>", function(data) {
|
|
127
127
|
$('<%= base_element %>').replaceWith($(data).find('<%= base_element %>'))
|
|
128
128
|
});
|
|
129
129
|
},
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Fe
|
|
2
|
+
# Class names accepted from request params for answer sheets. Defaults to
|
|
3
|
+
# the answer sheet class plus Fe::ReferenceSheet.
|
|
4
|
+
mattr_accessor :answer_sheet_types
|
|
5
|
+
self.answer_sheet_types = nil
|
|
6
|
+
|
|
7
|
+
mattr_accessor :element_types
|
|
8
|
+
self.element_types = %w[
|
|
9
|
+
Fe::Section Fe::Paragraph Fe::TextField Fe::ChoiceField Fe::DateField
|
|
10
|
+
Fe::QuestionGrid Fe::QuestionGridWithTotal Fe::AttachmentField
|
|
11
|
+
Fe::ReferenceQuestion Fe::StateChooser
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
def self.allowed_answer_sheet_types
|
|
15
|
+
answer_sheet_types || [answer_sheet_class, 'Fe::ReferenceSheet']
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.answer_sheet_class_for(name)
|
|
19
|
+
return answer_sheet_class.constantize if name.blank?
|
|
20
|
+
raise ActiveRecord::RecordNotFound unless allowed_answer_sheet_types.include?(name)
|
|
21
|
+
name.constantize
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.element_class_for(name)
|
|
25
|
+
raise ActiveRecord::RecordNotFound unless element_types.include?(name)
|
|
26
|
+
name.constantize
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.access_key_matches?(answer_sheet, key)
|
|
30
|
+
return false unless key.is_a?(String) && !key.empty?
|
|
31
|
+
expected = answer_sheet.access_key
|
|
32
|
+
return false if expected.blank?
|
|
33
|
+
ActiveSupport::SecurityUtils.secure_compare(expected, key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class MissingAccessHook < StandardError; end
|
|
37
|
+
|
|
38
|
+
ANSWER_SHEET_CONTROLLERS = %w[
|
|
39
|
+
Fe::AnswerSheetsController Fe::AnswerPagesController Fe::ReferenceSheetsController
|
|
40
|
+
Fe::SubmitPagesController Fe::ReferencePagesController Fe::ReferencesController
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
ADMIN_CONTROLLERS = %w[
|
|
44
|
+
Fe::Admin::QuestionSheetsController Fe::Admin::QuestionPagesController
|
|
45
|
+
Fe::Admin::ElementsController Fe::Admin::EmailTemplatesController
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
mattr_accessor :verify_access_hooks_on_boot
|
|
49
|
+
self.verify_access_hooks_on_boot = true
|
|
50
|
+
|
|
51
|
+
# Raises Fe::MissingAccessHook unless the host app defines the access hooks
|
|
52
|
+
# on every fe controller that calls them. Fe::AnswerSheetsController also
|
|
53
|
+
# calls fe_admin? (index and create), and Fe::ReferenceSheetsController
|
|
54
|
+
# inherits it. fe must never define either hook itself, or this check could
|
|
55
|
+
# not detect a missing host implementation.
|
|
56
|
+
def self.verify_access_hooks!
|
|
57
|
+
missing = {
|
|
58
|
+
fe_can_use_answer_sheet?: ANSWER_SHEET_CONTROLLERS,
|
|
59
|
+
fe_admin?: ADMIN_CONTROLLERS + %w[Fe::AnswerSheetsController]
|
|
60
|
+
}.flat_map do |hook, names|
|
|
61
|
+
names.map(&:constantize).reject { |klass|
|
|
62
|
+
klass.method_defined?(hook) || klass.private_method_defined?(hook)
|
|
63
|
+
}.map { |klass| [hook, klass.name] }
|
|
64
|
+
end
|
|
65
|
+
return if missing.empty?
|
|
66
|
+
|
|
67
|
+
lines = missing.map { |hook, name| " #{name} needs ##{hook}" }
|
|
68
|
+
raise MissingAccessHook,
|
|
69
|
+
"The host app must define fe's access hooks, and these are missing:\n#{lines.join("\n")}\n" \
|
|
70
|
+
'See the "Access control" section of the fe README.'
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/fe/engine.rb
CHANGED
|
@@ -58,6 +58,10 @@ module Fe
|
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
config.after_initialize do
|
|
62
|
+
Fe.verify_access_hooks! if Fe.verify_access_hooks_on_boot
|
|
63
|
+
end
|
|
64
|
+
|
|
61
65
|
config.generators do |g|
|
|
62
66
|
g.test_framework :rspec, fixture: false
|
|
63
67
|
g.fixture_replacement :factory_bot, dir: 'spec/factories'
|
data/lib/fe/version.rb
CHANGED
data/lib/fe.rb
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
require 'rails_helper'
|
|
2
2
|
|
|
3
3
|
describe Fe::Admin::ElementsController, type: :controller do
|
|
4
|
+
context 'element_type param' do
|
|
5
|
+
let(:page) { create(:page) }
|
|
6
|
+
let(:question_sheet) { page.question_sheet }
|
|
7
|
+
|
|
8
|
+
it 'rejects a disallowed type on #new' do
|
|
9
|
+
expect {
|
|
10
|
+
get :new, params: {element_type: 'Fe::Application', question_sheet_id: question_sheet.id, page_id: page.id}, xhr: true
|
|
11
|
+
}.to raise_error(ActiveRecord::RecordNotFound)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'rejects a disallowed type on #create without creating anything' do
|
|
15
|
+
expect {
|
|
16
|
+
expect {
|
|
17
|
+
post :create, params: {element_type: 'Fe::Application', element: { style: 'style' }, question_sheet_id: question_sheet.id, page_id: page.id}, xhr: true
|
|
18
|
+
}.to raise_error(ActiveRecord::RecordNotFound)
|
|
19
|
+
}.not_to change { Fe::Element.count }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
4
22
|
context '#show' do
|
|
5
23
|
it 'should work' do
|
|
6
24
|
answer_sheet = create(:answer_sheet)
|
|
@@ -212,3 +230,14 @@ describe Fe::Admin::ElementsController, type: :controller do
|
|
|
212
230
|
end
|
|
213
231
|
end
|
|
214
232
|
end
|
|
233
|
+
|
|
234
|
+
describe Fe::Admin::ElementsController, type: :controller do
|
|
235
|
+
it 'returns 403 on #create and creates nothing when the host says the person is not an admin' do
|
|
236
|
+
allow(controller).to receive(:fe_admin?).and_return(false)
|
|
237
|
+
page = create(:page)
|
|
238
|
+
expect {
|
|
239
|
+
post :create, params: {element_type: 'Fe::TextField', element: { style: 'style' }, question_sheet_id: page.question_sheet.id, page_id: page.id}, xhr: true
|
|
240
|
+
}.not_to change { Fe::Element.count }
|
|
241
|
+
expect(response).to have_http_status(:forbidden)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -27,3 +27,11 @@ describe Fe::Admin::EmailTemplatesController, type: :controller do
|
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
describe Fe::Admin::EmailTemplatesController, type: :controller do
|
|
32
|
+
it 'returns 403 on #index when the host says the person is not an admin' do
|
|
33
|
+
allow(controller).to receive(:fe_admin?).and_return(false)
|
|
34
|
+
get :index
|
|
35
|
+
expect(response).to have_http_status(:forbidden)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -9,3 +9,12 @@ describe Fe::Admin::QuestionPagesController, type: :controller do
|
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
describe Fe::Admin::QuestionPagesController, type: :controller do
|
|
14
|
+
it 'returns 403 on #show when the host says the person is not an admin' do
|
|
15
|
+
allow(controller).to receive(:fe_admin?).and_return(false)
|
|
16
|
+
page = create(:page)
|
|
17
|
+
get :show, params: {question_sheet_id: page.question_sheet.id, id: page.id}, xhr: true
|
|
18
|
+
expect(response).to have_http_status(:forbidden)
|
|
19
|
+
end
|
|
20
|
+
end
|