Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
34 changes: 24 additions & 10 deletions app/controllers/admin/api/buyers_users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
class Admin::Api::BuyersUsersController < Admin::Api::BuyersBaseController
representer User

before_action :find_user, except: %i[create index]
before_action :build_new_user, only: %i[create]

attr_reader :user

# User List
# GET /admin/api/accounts/{account_id}/users.xml
def index
Expand All @@ -12,11 +17,9 @@ def index
# User Create
# POST /admin/api/accounts/{account_id}/users.xml
def create
user = new_user

authorize! :create, user

user.unflattened_attributes = flat_params
user.unflattened_attributes = user_params
user.signup_type = :api

user.save
Expand All @@ -37,7 +40,7 @@ def show
def update
authorize! :update, user

user.update_with_flattened_attributes(flat_params)
user.update_with_flattened_attributes(user_params)

respond_with(user)
end
Expand Down Expand Up @@ -108,19 +111,30 @@ def authorize!(*args)
current_user ? super : logged_in?
end

def new_user
@new_user ||= buyer.users.new
end

def users
@users ||= begin
conditions = params.slice(:state, :role)
buyer.users.where(conditions)
end
end

def user
@user ||= buyer.users.find(params[:id])
private

def build_new_user
@user = buyer.users.new
end

def find_user
@user = buyer.users.find(params[:id])
end

def user_params
@user_params ||= begin
allowed_attrs = user.defined_fields_names | %i(password password_confirmation)
# TODO: are these parameters needed?
# allowed_attrs |= %i(conditions cas_identifier open_id service_conditions)
flat_params.permit(*allowed_attrs)
end
end

end
32 changes: 22 additions & 10 deletions app/controllers/admin/api/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ class Admin::Api::UsersController < Admin::Api::BaseController
representer User

before_action :can_create, only: :create
before_action :build_new_user, only: %i[create]
before_action :find_user, except: %i[create index]

attr_reader :user

# User List (provider account)
# GET /admin/api/users.xml
Expand All @@ -14,11 +18,9 @@ def index
# User Create (provider account)
# POST /admin/api/users.xml
def create
user = new_user

authorize! :create, user

user.unflattened_attributes = flat_params
user.unflattened_attributes = user_params
user.signup_type = :api

user.save
Expand All @@ -39,7 +41,7 @@ def show
def update
authorize! :update, user

user.update_with_flattened_attributes(flat_params, as: current_user.try(:role))
user.update_with_flattened_attributes(user_params)

respond_with(user)
end
Expand Down Expand Up @@ -110,19 +112,19 @@ def authorize!(*args)
current_user ? super : logged_in?
end

def new_user
@new_user ||= current_account.users.new
end

def users
@users ||= begin
conditions = params.slice(:state, :role)
current_account.users.but_impersonation_admin.where(conditions)
end
end

def user
@user ||= current_account.users.but_impersonation_admin.find(params[:id])
def build_new_user
@user = current_account.users.new
end

def find_user
@user = current_account.users.but_impersonation_admin.find(params[:id])
end

def can_create
Expand All @@ -134,4 +136,14 @@ def can_create
def flat_params
super.except(:id)
end

def user_params
@user_params ||= begin
allowed_attrs = user.defined_fields_names | %i(password password_confirmation cas_identifier)
allowed_attrs |= [member_permission_service_ids: [], member_permission_ids: [], allowed_sections: [], allowed_service_ids: []] if (provider_key.present? || current_user.admin?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is used instead of the combination of these:

user.update_with_flattened_attributes(flat_params, as: current_user.try(:role))

attr_accessible :member_permission_service_ids, :member_permission_ids, as: %i[admin]

When provider_key is used (i.e. current_user is nil), the fields can be updated (verified by a new test).

# TODO: are these parameters needed?
# allowed_attrs |= %i(conditions open_id service_conditions)
flat_params.permit(*allowed_attrs)
end
end
end
15 changes: 11 additions & 4 deletions app/controllers/buyers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def update
# TODO: I think this controller is used only on provider side
user.validate_fields! if current_account.buyer?

user.attributes = user_params
user.attributes = permitted_user_params
user.role = user_params.fetch(:role, user.role) if can?(:update_role, user)

if user.save
Expand Down Expand Up @@ -80,10 +80,17 @@ def find_user
@user = @account.users.find(params[:id]).decorate
end

DEFAULT_PARAMS = %i[username email password password_confirmation role].freeze

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic was actually problematic.
There are some fields under User model that are "standard", but not required:

optional_fields_are :title, :first_name, :last_name, :job_role

When they are added in FieldsDefinitions, the fields appear in the Edit form and can be updated. Here is what the params.require(:user) looks like in this case:

{"username"=>"uu222", "email"=>"uu12@example.com", "extra_fields"=>{"userhidden"=>"value 2", "userhiddenro"=>"a", "userro"=>"a", "userreq"=>""}, "title"=>"aa", "first_name"=>"bb", "last_name"=>"cc", "job_role"=>"dd", "password"=>"", "password_confirmation"=>"", "role"=>"member"}

However, the further permit filters them out in a way, that only the ones listed in DEFAULT_PARAMS are kept, along with the extra_fields, however, the extra_fields only contain the actually custom fields, so the user_params results in:

{"username"=>"uu222", "email"=>"uu12@example.com", "password"=>"", "password_confirmation"=>"", "role"=>"member", "extra_fields"=>#<ActionController::Parameters {"userhidden"=>"value 2", "userhiddenro"=>"a", "userro"=>"a", "userreq"=>""} permitted: true>}

Hence, title, first_name, last_name and job_role can never be updated through the UI - I think it's a bug.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, these special fields ('title', 'first_name', 'last_name' and 'job_role'), if defined as extra fields, need to be accepted at the lower level, not under extra_fields.

This looks like a bug in either UI or in the way params are permitted.

I would probably rather fix how UI pass them down but also checking for them and permitting them on the lower level is also good. I assume on the API side they already have to be passed udder extra fields?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, it depends on whether it's a UI controller or API.
In the API, all parameters (standard and custom) are accepted at the same level (as user attributes). In the UI, they are nested under extra_fields.

The comment is not about that, though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the comment about then?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the issue is not about where the parameters appear - at the "root" user level, or under extra_fields. As I explained, in the UI forms only truly custom (created by user) fields appear under extra_fields, others are at the "root" level along with username and password (they are actually normal attributes, that have a corresponding column in the DB).

The problem is that this DEFAULT_PARAMS doesn't include the "optional" fields - :title, :first_name, :last_name, :job_role. So, even if they are defined through the fields definitions, they can not be updated through the UI, because they are removed from the list of permitted user_params.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hang on, once I fix the tests and push the latest updates, I'll explain the new implementation. It has some drawbacks too, but hopefully it can work.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this explanation still sounds the same. And basically the fix is to either:

  • make the UI form send these special params under extra_fields where they will be permitted due to them being defined as extra fields
  • add these special params conditionally or undonditionally to the permitted user level params

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope this comment will clarify things 🙏 https://github.com/3scale/porta/pull/4238/changes#r2895692403


def user_params
@user_params ||= params.require(:user).permit(*DEFAULT_PARAMS, extra_fields: [*user.defined_extra_fields_names])
@user_params ||= params.require(:user)
end

def permitted_user_params
@permitted_user_params ||= begin
allowed_attrs = user.defined_builtin_fields_names | %i(password password_confirmation)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, basically, the solution I've chosen for now is to rely on the FieldsDefinitions defined for the corresponding model (User in this case).

I'll try to explain...

So, here is how the fields are defined for User.

When a provider is created, the default fields definitions are created automatically, for User these are username and email (they are also required).
There are some other fields that are listed as optional. These appear in the dropdown in FieldsDefinitions, they are standard fields (and have corresponding columns in the users table), but are not created as Fields Definitions by default.

The union of required and optional fields (plus the "internal" ones - but there is only one for the Account model, not user) is referred to as "builtin" fields, as opposed to "extra" fields, that are completely custom and created by the user.

So, I decided, rather than list the hard-coded list of fields (which, apart from username and email may or may not be among the User model fields), just take the actual builtin fields defined for the User model.

There is a small downside in this approach - and it is that I had to adjust some tests to ensure that the default Fields Definitions are actually created (for example, with :simple_provider factory they are not), because without them even the default username and email are not returned. This should not cause problems in the actual application though, where the Fields Definitions should be correctly present always.

Also, it's worth noting that for the UI controllers I am splitting the #defined_builtin_fields_names which are at "root" level under "user" param, and #defined_extra_fields_names which go nested within extra_fields - this is how the fields appear in the UI forms.

In API controllers, on the contrary, all attributes - custom and "standard" appear at the same level, so for API controller params I'm using #defined_fields_names, which includes both "builtin" and "extra"/custom.

There is also the role attribute, which can only be modified under some conditions, so it's not included in the "permitted params" list - it's always handled separately.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to accept all params flat at the user level, correct? This is fine with me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to accept all params flat at the user level, correct? This is fine with me.

No... please read again - nesting under extra_fields for UI controllers remains untouched. Because of this difference in the UI controller params I split the params list in two parts:

allowed_attrs = user.defined_builtin_fields_names | %i(password password_confirmation)
user_params.permit(*allowed_attrs, extra_fields: user.defined_extra_fields_names)

So, built-in and extra/custom treated separately. I don't see a point in changing this and resulting in many more changes - both in controllers and views.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks! That's also totally fine, I somehow looked only at this line and missed extra_fields: in the next line.

# TODO: are these parameters needed?
# allowed_attrs |= %i(conditions cas_identifier open_id service_conditions)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and in all other commented-out lines - these attributes appear in the original attr_accessible:

:conditions, :cas_identifier, :open_id, :service_conditions,

However, I couldn't find information about the intended usage for these attributes. I think most of them are legacy and unused - possibly open_id is used for the hidden legacy Partner signups, and cas_identifier appeared in one test, but is probably also dead code.

I have listed them commented out just to provide this explanation and here your thoughts on these, but my idea is actually to remove these lines, because I think we don't need them.

user_params.permit(*allowed_attrs, extra_fields: user.defined_extra_fields_names)
end
end

def redirect_back_or_show_detail(**opts)
Expand Down
13 changes: 7 additions & 6 deletions app/controllers/partners/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,8 @@ def destroy
end

def create
@user = @account.users.build
@user.email = params[:email]
@user = @account.users.build(user_params)
@user.password = SecureRandom.hex
@user.first_name = params[:first_name].presence
@user.last_name = params[:last_name].presence
@user.open_id = params[:open_id].presence
@user.username = params[:username]
@user.signup_type = :partner
@user.role = :admin
@user.activate!
Expand All @@ -42,4 +37,10 @@ def create
def find_account
@account = @partner.providers.find(params[:provider_id])
end

def user_params
# Partners controller only allows a subset of common attributes
allowed = [:email, :first_name, :last_name, :open_id, :username]
params.permit(*allowed)
end
end
2 changes: 1 addition & 1 deletion app/controllers/provider/admin/account/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def update_resource(user, attributes)

user.class.transaction do
user.assign_attributes(attributes)
user.assign_attributes(protected_attributes, without_protection: can?(:update_role, user))
user.assign_attributes(protected_attributes) if can?(:update_role, user)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, this is not exactly the same, but I guess it'll work the same way, just without the warning in the logs in the case the condition is false.

Also, maybe we should rename it, protected_attributes sounds old.


user.save
end
Expand Down
13 changes: 11 additions & 2 deletions app/controllers/provider/admin/user/personal_details_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class Provider::Admin::User::PersonalDetailsController < Provider::Admin::User::
def edit; end

def update
if current_user.update(user_params.except(:current_password))
if current_user.update(permitted_user_params)
Comment thread
jlledom marked this conversation as resolved.
if current_user.just_changed_password?
current_user.kill_user_sessions(user_session)
end
Expand All @@ -26,7 +26,16 @@ def redirect_path
end

def user_params
params.require(:user)
@user_params ||= params.require(:user)
end

def permitted_user_params
@permitted_user_params ||= begin
allowed_attrs = current_user.defined_builtin_fields_names | %i(password)
# TODO: are these parameters needed?
# allowed_attrs |= %i(conditions cas_identifier open_id service_conditions)
user_params.permit(*allowed_attrs, extra_fields: current_user.defined_extra_fields_names)
end
end

def current_password_verification
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/provider/invitee_signups_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ def can_create?
end

def build_user
@user = @invitation.make_user(params[:user] || {})
allowed_attrs = @invitation.account.users.new.defined_fields_names | %i(password password_confirmation)
user_params = params.fetch(:user, ActionController::Parameters.new).permit(*allowed_attrs)
@user = @invitation.make_user(user_params)

# This is just a sanity guard added when splitting invitation
# controllers. Remove when SURE.
Expand Down
2 changes: 0 additions & 2 deletions app/lib/authentication/by_password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ module ByPassword

validates :lost_password_token, :password_digest, length: { maximum: 255 }

attr_accessible :password, :password_confirmation

scope :with_valid_password_token, -> { where { lost_password_token_generated_at >= 24.hours.ago } }

alias_method :authenticated?, :authenticate
Expand Down
8 changes: 8 additions & 0 deletions app/lib/fields/fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ def defined_extra_fields_names
defined_extra_fields.map(&:name)
end

def defined_fields_names

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably need unit tests for these new methods.

defined_fields.map(&:name)
end

def defined_builtin_fields_names
defined_builtin_fields.map(&:name)
end

def defined_fields_hash
@defined_fields_hash ||= Hash[defined_fields.map { |f| [f.name.to_sym, f]}]
end
Expand Down
8 changes: 0 additions & 8 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,6 @@ def moderatable
validate :username_is_unique
validates :open_id, uniqueness: { case_sensitive: true }, allow_nil: true

attr_accessible :title, :username, :email, :first_name, :last_name,
:conditions, :cas_identifier, :open_id, :service_conditions,
:job_role, :extra_fields, as: %i[default member admin]

attr_accessible :member_permission_service_ids, :member_permission_ids, as: %i[admin]



def self.search_states
%w(pending active)
end
Expand Down
1 change: 0 additions & 1 deletion app/models/user/invitations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ module User::Invitations
after_commit :accept_invitation, :on => :create

attr_accessor :invitation
attr_accessible :invitation

# TODO: refactor to make this work removing above attribute.
# has_one :invitation
Expand Down
2 changes: 0 additions & 2 deletions app/models/user/permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ module User::Permissions
included do
has_many :member_permissions, dependent: :destroy, autosave: true

attr_accessible :member_permission_service_ids, :member_permission_ids, :allowed_sections, :allowed_service_ids

alias_method :allowed_sections, :member_permission_ids
alias_method :allowed_sections=, :member_permission_ids=
alias_method :allowed_service_ids, :member_permission_service_ids
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ def sso_create
redirect_to strategy.redirect_to_on_successful_login
else
user_data = strategy.user_data || {}
@user.assign_attributes(user_data.to_hash.compact)
# TODO: verify that this is the right thing to do
user_attributes = user_data.to_hash.compact.slice(:username, :email)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't figure out what would be the scenario where we'd end up in this condition branch, and still user_data would be a real hash.
But based on the following this seems to be a reasonable thing to do:

  1. From the ATTRIBUTES of user_data, only :username, :email and :authentication_id are the attributes that belong to the User model.
  2. From the 3 attributes above, only :username and :email were included in the attr_accessible previously.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If tests pass, good enough.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, as I was replying to this other comment: https://github.com/3scale/porta/pull/4249/changes#r2926129192 I realized, that this slice is not needed, because #to_hash already extracts just these two fields.

@user.assign_attributes(user_attributes)
session[:invitation_sso_uid] = user_data[:uid]
session[:invitation_sso_system_name] = strategy.authentication_provider.system_name
build_sso_authorization
Expand Down Expand Up @@ -119,7 +121,8 @@ def check_invitation!
end

def build_user
@user = @invitation.make_user(filter_readonly_params(params[:user], User))
allowed_attrs = @invitation.account.users.new.defined_fields_names | %i(password password_confirmation)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this is very ugly, I know :(
But I am not sure what's a better way to do it... An option could be to delegate the whole thing to filter_readonly_params - it goes to @invitation.account.users anyway, because it's the base collection for the new object.
However, I think that would be more obscure... Maybe refactor #make_user to first only build an empty user and return it, and then have another method to assign the required attributes?..

Or maybe the current way it not too bad?.. opinions welcome!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either modify #make_user to first generate an empty user.

Alternatively ai suggested:

account.fields_definitions.by_target('user').select { |f| f.extra? }

But I think creating the empty object and then getting and setting the values is more straightforward.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a whole user object just to get the fields just feels wrong.

@user = @invitation.make_user(filter_readonly_params(params[:user], User).permit(*allowed_attrs))
end

def invitation_token
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def show

def update
resource.validate_fields!
if resource.errors.empty? && resource.update(user_params)
if resource.errors.empty? && resource.update(permitted_user_params)
resource.kill_user_sessions(user_session) if resource.just_changed_password?

redirect_to admin_account_users_path, notice: 'User was successfully updated.'
Expand All @@ -44,9 +44,12 @@ def deny_unless_can_update
end

def user_params
filter_readonly_params(params.require(:user), User).permit([:current_password] +
resource.special_fields +
resource.defined_fields.map(&:name))
@user_params ||= params.require(:user)
end

def permitted_user_params
@permitted_user_params ||= filter_readonly_params(user_params, User).permit(resource.special_fields +
resource.defined_fields.map(&:name))
end

def verify_current_password
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def collection

def update_resource(user, attributes)
attributes.each do |attrs|
user.attributes = filter_readonly_params(attrs, User)
user.attributes = filter_readonly_params(attrs, User).permit!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we permit everything here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d8e75bc

user.role = attrs[:role] if can? :update_role, user
end
user.save
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def finish_signup_for_paid_plan
end

def filter_readonly_params(params, resource_class)
return {} unless params
return ActionController::Parameters.new unless params

read_only_fields = FieldsDefinition.by_provider(site_account).by_target(resource_class.name).read_only.pluck(:name)
params.except(*read_only_fields)
Expand Down
15 changes: 3 additions & 12 deletions spec/acceptance/api/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,9 @@

let(:account_id) { buyer.id }

let (:user) { FactoryBot.build(:user, account: buyer) }
let(:user) { FactoryBot.build(:user, account: buyer) }

let(:resource) do
FieldsDefinition.create_defaults!(master)
provider.reload
user
end
let(:resource) { user }

get '/admin/api/accounts/:account_id/users/:id.:format', action: :show
delete '/admin/api/accounts/:account_id/users/:id', action: :destroy
Expand Down Expand Up @@ -147,12 +143,7 @@

let(:user) { FactoryBot.create(:user, account: provider) }

# creating new db records for fields that are in db is pathetic as it can get
let(:resource) do
FieldsDefinition.create_defaults!(master)
provider.reload
user
end
let(:resource) { user }

it { should include('id' => user.id, 'state' => user.state, 'role' => user.role.to_s) }
it { should include('email' => user.email, 'username' => user.username) }
Expand Down
Loading