Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #37015 - add validation error message for host_owner setting #9971

Merged
merged 1 commit into from
Jan 30, 2024

Conversation

nofaralfasi
Copy link
Contributor

No description provided.

@nofaralfasi
Copy link
Contributor Author

When users attempt to update the host_owner setting through Hammer/API, they may not be aware of the accepted value format.
In the UI, we provide a selection list containing the names of users and usergroups, allowing users to make a choice. The selected value is then saved as id_and_type. For example, for the admin user with ID 1, the value would be 1-Users.

If a username is sent as the value for host_owner, an error occurs. In this PR, I propose modifying the error message that occurs in such cases to inform the user about the correct format.
As discussed with @Gauravtalreja1, we are actively seeking the best possible solution here.

Copy link
Member

@ekohl ekohl left a comment

Choose a reason for hiding this comment

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

We have the id_and_type in more places. Is this the only place where it shows up?

I wonder if OwnerClassifier should raise an exception instead of silently returning nil on invalid input.

errors.add(:value, _("Host owner is invalid")) if owner.nil?
if owner.nil?
errors.add(:value, _("Host owner is invalid. " \
"Please ensure the input follows the format 'ID-Users/Usergroups'."))
Copy link
Member

Choose a reason for hiding this comment

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

If you're doing this, I think it should be clear that ID- is also the prefix for Usergroups. Right now that isn't obvious to me. And that ID must be replaced with an actual value but Users or Usergroups. I'd either go with ${id}-[Users|Usergroups] or $id-$type where type is Users or Usergroups.

Comment on lines 238 to 240
raise StandardError, _("Host owner is invalid. Please ensure the input follows the format '${id}-[Users|Usergroups]'.") if owner.nil?
rescue StandardError => e
errors.add(:value, e.message)
Copy link
Member

Choose a reason for hiding this comment

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

Why would you raise an exception and immediately rescue it?
You can add a more detailed message to the errors array instead.

Copy link
Member

Choose a reason for hiding this comment

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

I suggested that OwnerClassifier would raise the exception, not the model.

@nofaralfasi
Copy link
Contributor Author

We have the id_and_type in more places. Is this the only place where it shows up?

I didn't find any other places except here.

I wonder if OwnerClassifier should raise an exception instead of silently returning nil on invalid input.

While I agree that this is how it should be, this change is causing a lot of errors in our testing.

@nofaralfasi
Copy link
Contributor Author

Unless someone has a better idea, I suggest moving the error messages back to the validate_host_owner method.

@stejskalleos
Copy link
Contributor

The CI is failing, not sure if it's due to the changes or just unstable pipeline

@stejskalleos stejskalleos self-assigned this Jan 8, 2024
@nofaralfasi
Copy link
Contributor Author

The CI is failing, not sure if it's due to the changes or just unstable pipeline

This is due to recent changes. These modifications are raising an error in the user_or_usergroup method, which the tests do not anticipate. This is happening because other instances, such as this, also invoke the user_or_usergroup method.

So my question is, how should we approach this? Should we consider changing the implementation? I don't believe fixing the tests is the solution, given other uses of the user_or_usergroup method. We could either raise the exceptions outside the user_or_usergroup method but still within the OwnerClassifier class, or keep it as it was, adding the error only to the validate_host_owner method.

@id_and_type.include?('Users') ? User.find_by_id(@id_and_type.to_i) : Usergroup.find_by_id(@id_and_type.to_i)
end

def valid_input?
@id_and_type =~ /^\d+-(Users|Usergroups)$/
Copy link
Member

Choose a reason for hiding this comment

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

I know you're refactoring, but while you're at it it's better to use .match? if you don't use the captured value. In Ruby 3.2+ you also can't use nil =~ anymore, or on other non-string values. So this is future proofing it.

Suggested change
@id_and_type =~ /^\d+-(Users|Usergroups)$/
@id_and_type.is_a?(String) && @id_and_type.match?(/^\d+-(Users|Usergroups)$/)

Copy link
Member

@ekohl ekohl left a comment

Choose a reason for hiding this comment

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

Implicitly these methods can now also raise exceptions:

# virtual attributes which sets the owner based on the user selection
# supports a simple user, or a usergroup
# selection parameter is expected to be an ActiveRecord id_and_type method (see Foreman's AR extentions).
def is_owned_by=(selection)
owner = OwnerClassifier.new(selection).user_or_usergroup
self.owner = owner
end
def owner_suggestion
owner_id_and_type = Setting[:host_owner]
owner = OwnerClassifier.new(owner_id_and_type).user_or_usergroup
self.owner || owner || User.current
end

I wonder if that could cause problems. Setting[:host_owner] is validated, but I doubt we have code right now that ensures the ID it uses actually exists. For example, the user or group can probably be deleted, even if it's used in the setting. I don't know if there's a good way to prevent that. Perhaps worth checking with QE how far we want to go here.


def user_or_usergroup
Copy link
Member

Choose a reason for hiding this comment

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

You're sure this isn't used anywhere? Normally we add a deprecation and remove it in a future release. Mostly for plugins.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't find any other uses in Foreman or Katello.
I can keep the old method name, but since we are changing it to a static method, we would still need to make adjustments.

Copy link
Member

Choose a reason for hiding this comment

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

Why not adding a static method in addition to the instance one:

class OwnerClassifier
  def self.classify_owner(owner_id_and_type)
    return self.new(owner_id_and_type).user_or_usergroup
  end

  def user_or_usergroup
    # ...
  end
end

This will keep the old way and also add the static shortcut. It will also be much more testable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ShimShtein, could you check the recent changes and confirm if they match your suggestion?

@nofaralfasi nofaralfasi force-pushed the 37015 branch 3 times, most recently from c7f799b to 0ce7cf1 Compare January 15, 2024 14:51
@ekohl
Copy link
Member

ekohl commented Jan 17, 2024

I think this could be merged, but please also consider this.

Currently you call the old code from the new code. You can also implement the real logic in the new code. This allows you to change the old code to first emit a deprecation and then call the new code

ShimShtein
ShimShtein previously approved these changes Jan 18, 2024
Copy link
Member

@ShimShtein ShimShtein left a comment

Choose a reason for hiding this comment

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

LGTM

Maybe we should start using globalid feature of rails instead of reinventing the wheel 🤔

@nofaralfasi
Copy link
Contributor Author

Maybe we should start using globalid feature of rails instead of reinventing the wheel 🤔

I don't see how using it will help us with the API requests. What values does the user pass when setting the host_owner via API?

Copy link
Contributor

@stejskalleos stejskalleos left a comment

Choose a reason for hiding this comment

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

🍏 LGTM

Works as expected, users and usergroup set correctly

@stejskalleos stejskalleos self-requested a review January 22, 2024 12:52
stejskalleos
stejskalleos previously approved these changes Jan 22, 2024
@stejskalleos stejskalleos requested a review from ekohl January 22, 2024 12:54
@stejskalleos
Copy link
Contributor

@ekohl could you ACK the changes so it can be released with a follow-up snap?

@ekohl
Copy link
Member

ekohl commented Jan 23, 2024

Currently you call the old code from the new code. You can also implement the real logic in the new code. This allows you to change the old code to first emit a deprecation and then call the new code

@nofaralfasi would you mind responding to this?

Copy link
Member

@ekohl ekohl left a comment

Choose a reason for hiding this comment

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

I'd want to get rid of using an instance of the class. That's the whole concept I'm urging you to deprecate. Here's now I'd implement it:

class OwnerClassifier
  def initialize(id_and_type)
    @id_and_type = id_and_type
  end

  def user_or_usergroup
    Foreman::Deprecation.deprecation_warning("3.12", "`user_or_usergroup` is deprecated, use `OwnerClassifier.classify_owner` instead.")

    self.classify_owner(@id_and_type)
  end

  def self.classify_owner(id_and_type)
    return nil if id_and_type.blank?

    unless id_and_type.is_a?(String) && id_and_type.match?(/^\d+-(Users|Usergroups)$/)
      raise ArgumentError, _("Invalid input format. Please use the format '${id}-[Users|Usergroups]'.")
    end

    cls = id_and_type.end_with?('Users') ? User : Usergroup
    cls.find_by!(id_and_type.to_i)
  end
end

First, it uses find_by_id! to let Rails raise the NotFound exception. This simplifies the code and probably gives a more accurate exception as well.

The bigger style difference is that it removes all the helper methods. This is to remove all state in the class so that eventually we can remove initialize() and the whole .new invocation. Otherwise you still maintain the instance every single time and you get all the overhead of a class, with the downsides of also having a static method.

The cls variable is just to make the code a bit easier to understand. The end_with? is an unimportant micro optimization.

@nofaralfasi
Copy link
Contributor Author

I'd want to get rid of using an instance of the class. That's the whole concept I'm urging you to deprecate.

While I was trying to keep it, due to other recommendations I received in the review :)
Thank you for the detailed explanation!

validate_input_format!(id_and_type)

owner_type = id_and_type.end_with?('Users') ? User : Usergroup
owner_type.find(id_and_type.to_i)
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be find_by_id like it was? Or find_by_id! to raise an exception?

Copy link
Member

Choose a reason for hiding this comment

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

Ok, I looked it up myself. https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find states it will call .to_i for you so it can even be:

Suggested change
owner_type.find(id_and_type.to_i)
owner_type.find(id_and_type)

But your code is also valid. I don't mind it being explicit here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

AFAIK, both find_by_id! and find raise the same exception when the record is not found (in this case). If you believe that find_by_id! enhances readability in this context, I'm open to using it instead.

@@ -2,24 +2,24 @@

class OwnerClassifierTest < ActiveSupport::TestCase
Copy link
Member

Choose a reason for hiding this comment

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

Can you keep a test for the deprecated way? That it at least still works.

- Introduce `classify_owner` class method for owner classification.
- Implement private method for input validation.
- Add error handling to raise the necessary exceptions.
- Add deprecation warning to the `user_or_usergroup` method.
@ekohl ekohl merged commit f1313e5 into theforeman:develop Jan 30, 2024
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants