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

[JENKINS-56005] Add user property-change listener #3901

Closed
wants to merge 14 commits into from

Conversation

davidolorundare
Copy link
Contributor

@davidolorundare davidolorundare commented Feb 17, 2019

See JENKINS-56005 and JENKINS-56008.

  • Revisions based on the discussions in the comments, an interface; UserPropertyListener was created, with default method all other listener implementations would make use of. Enabling support for listening to Jenkins user property-change events.
  • Two new listener implementation classes; PasswordPropertyListener and ApiTokenPropertyListener were created and added to jenkins-core. These classes handle firing notifications specific to their respective use cases (i.e. password-change and api-token creation/revocation events respectively). The rationale for the additions is discussed in each above ticket.
  • The relevant methods, in the corresponding jenkins-core classes, where these events occur; HudsonPrivateSecurityRealm.Details and ApiTokenProperty, were updated with calls to the respective new listener class methods.
  • The corresponding testsuites for each; HudsonPrivateSecurityRealmTest and ApiTokenPropertyTest were updated with tests that exercise these new listener class methods.

Proposed changelog entries

  • Enhancement: Addition of new hudson.model.UserPropertyListenerinterface to support notification of current and future Jenkins user-account property-change events (e.g. password changes, api-token key creation and deletion, etc).
  • Enhancement: Addition of new hudson.security.PasswordPropertyListenerclass to support notification of Jenkins user-account password property-change events.
  • Enhancement: Addition of new jenkins.security.ApiTokenPropertyListenerclass to support notification of Jenkins user-account api-token creation and revocation events.
  • Enhancement: Update to hudson.security.HudsonPrivateSecurityRealm.Details adding notification(s) of user-account password property-change events to its method.
  • Enhancement: Update to jenkins.security.ApiTokenProperty adding notification(s) of user-account api-token creation and revocation events to its method.

Submitter checklist

  • JIRA issue is well described
  • Changelog entry appropriate for the audience affected by the change (users or developer, depending on the change). Examples
    * Use the Internal: prefix if the change has no user-visible impact (API, test frameworks, etc.)
  • Appropriate autotests or explanation to why this change has no tests
  • For dependency updates: links to external changelogs and, if possible, full diffs

Desired reviewers

@jvz
@jeffret-b
@Wadeck
@daniel-beck

@davidolorundare
Copy link
Contributor Author

restarting build

* @param p the user object property
* @param username the username
*/
protected void userPropertyUpdated(@Nonnull UserProperty p, @Nonnull String username){}
Copy link
Member

Choose a reason for hiding this comment

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

I kind of wonder if this would be more appropriate in its own UserPropertyListener or similar. I'm thinking in terms that it may eventually be useful to have event listeners for JobProperty, NodeProperty, and others if I'm forgetting.

Also, I'm not sure if there's precedence in this class already, but I think it might make more sense to flip the ordering of the parameters here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not really convinced one way or another. The userPropertyUpdated() method doesn't quite seem to fit the other things in SecurityListener. But then, some of the other items in SecurityListener don't fit well either. The user property is more about users than specifically about security. But so are some of the other things in this file.

I guess when it comes down to it I have a slight preference for separating out a UserPropertyListener.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmh.. alright. Question though is for the new UserPropertyListener: it being an abstract-class versus interface ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Wadeck Any thoughts on the creation of a new separate UserPropertyListener ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Separation seems better for the long term, like Matt mentioned, about the other items that could/would require such listeners in the future. And also, it's not directly security related, so not really a reason to be put in SecurityListener, except for the specific case of Details. But I imagine the goal of this listener is to be triggered everytime a property change, in general.

Copy link
Contributor Author

@davidolorundare davidolorundare Feb 20, 2019

Choose a reason for hiding this comment

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

@Wadeck hmh.. abstract versus interface ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I will bet on abstract being the most natural way as you are providing some static helper methods (for the event trigger). The interfaces should be used for contract definition.

Copy link
Member

Choose a reason for hiding this comment

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

Interfaces allow static methods as of Java 8. See for example the methods added to the collections API. Also default methods.

Copy link
Contributor

@jeffret-b jeffret-b left a comment

Choose a reason for hiding this comment

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

It's looking good.

* @param p the user object property
* @param username the username
*/
protected void userPropertyUpdated(@Nonnull UserProperty p, @Nonnull String username){}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not really convinced one way or another. The userPropertyUpdated() method doesn't quite seem to fit the other things in SecurityListener. But then, some of the other items in SecurityListener don't fit well either. The user property is more about users than specifically about security. But so are some of the other things in this file.

I guess when it comes down to it I have a slight preference for separating out a UserPropertyListener.

String data = Protector.unprotect(pwd);
if(data!=null) {
String prefix = Stapler.getCurrentRequest().getSession().getId() + ':';
if(data.startsWith(prefix))
SecurityListener.fireUserPropertyUpdated(p, user.getId());
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ missing {} in the if, so you are breaking the statement there.

That's exactly the main reason why I hate the if without brackets 👿

If that code bug was not discovered by your tests => there is a missing path in your coverage.

Copy link
Contributor Author

@davidolorundare davidolorundare Feb 20, 2019

Choose a reason for hiding this comment

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

do you advise using the if for only one-liners; or to just never use a non-bracketed if at all ?

Like;

                if((data!=null) && (p != null)) {
                    String prefix = Stapler.getCurrentRequest().getSession().getId() + ':';
                    if(data.startsWith(prefix)) return Details.fromHashedPassword(data.substring(prefix.length()));
                }

versus;

                if((data!=null) && (p != null)) {
                    String prefix = Stapler.getCurrentRequest().getSession().getId() + ':';
                    if(data.startsWith(prefix)) {
                        return Details.fromHashedPassword(data.substring(prefix.length()));
                    }
                }

Copy link
Contributor

Choose a reason for hiding this comment

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

I can only advice to never use a if without the associated {}, to avoid situation where someone wants to add a line, but due to the missing brackets will break the code flow like here.

And the one-liner is worse from my PoV due to the poor readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

perks of using Python over Java :)

Copy link
Member

Choose a reason for hiding this comment

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

I only use one line ifs for things like:

if (foo == null) return null;

or

if (bar) return false;

or

if (baz) return;

Basically, only for control flow type things. Remember, forgetting your brackets can cause security problems (see the old Apple bug). If you auto-format your code before committing, you can see if you missed brackets based on how it indents things.

@Wadeck
Copy link
Contributor

Wadeck commented Feb 20, 2019

Is such approach really required? I mean to implement event firing specifically per property? I hope there is a better way to do that globally, without modification of plugins, like in UserProperty#reconfigure. Not sure it's feasible but that should be considered first.

Without a global one-time modification, you have to wait for all the plugins to implements that new event trigger, before being able to rely on it. Rough estimation ~50 UserProperty in the ecosystem.

In addition, the current approach is the opposite of DRY.

Another point, what happen if I save a user, without modification? From even listener PoV, the property was not updated, but in reality it will be recreated. That's something to take into account, ideally by not triggering the event if the property is not really changed, otherwise it will be the responsibility of the listener to check such things and that could become really cumbersome.

Finally, the new value is not really sufficient / useful if we do not have the previous value of the property. I imagine more an event with the previous value (potentially null in case of creation of the property), the new value (potentially null in case of removal of that property) and the User as it's not retrievable from the property directly.

@jvz
Copy link
Member

jvz commented Feb 20, 2019

Is such approach really required? I mean to implement event firing specifically per property? I hope there is a better way to do that globally, without modification of plugins, like in UserProperty#reconfigure. Not sure it's feasible but that should be considered first.

It could be useful to support audit logging changes to your API tokens, credentials (user creds at least; node creds need a separate NodeProperty listener), maybe even some other details. It would be nice for the UserProperty class itself to automatically fire the events rather than consumers.

Another point, what happen if I save a user, without modification? From even listener PoV, the property was not updated, but in reality it will be recreated. That's something to take into account, ideally by not triggering the event if the property is not really changed, otherwise it will be the responsibility of the listener to check such things and that could become really cumbersome.

Agreed. We should track the old and new values. These can be compared with .equals() or whatever appropriate to see if it changed, and then that would fire the event. In that case, it may be useful to include both properties as parameters (old and new).

@daniel-beck daniel-beck self-requested a review March 9, 2019 17:58
Updated the HudsonPrivateSecurityRealm newInstance()
method to use the PasswordPropertyListener class for
notifying subscribers on changes to a user's password.
Updated the ApiTokenProperty doGenerateToken() and
doRevoke() methods to use the ApiTokenPropertyListener
class for notifying subscribers of api-token creations
and revocations from the Jenkins UI, respectively.
Updated HudsonPrivateSecurityRealmTest with a unittest
that exercises the added PasswordPropertyListener class
for notifying subscribers of user password changes.
Added two unittests to the ApiTokenPropertyTest class
for exercising the ApiTokenPropertyListener class methods;
for notifying subscribers of api-token creation and revocation
events.
@davidolorundare davidolorundare changed the title [JENKINS-56005] Add user property-change listener methods to SecurityListener [JENKINS-56005] Add user property-change listener Mar 12, 2019
@davidolorundare
Copy link
Contributor Author

some updates have been made to the PR based on the earlier discussions.
cc @jeffret-b @jvz @Wadeck @daniel-beck

Copy link
Member

@jvz jvz left a comment

Choose a reason for hiding this comment

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

I like where this is going, but I'm not clear on the multiple signatures for the same events.

CC: @jenkinsci/code-reviewers for some API feedback.

@davidolorundare
Copy link
Contributor Author

Restarting the build for this pull request.

@varyvol
Copy link

varyvol commented Mar 26, 2019

@davidolorundare did you really intend to close this or was it a mistake?

@davidolorundare
Copy link
Contributor Author

davidolorundare commented Mar 26, 2019

Hi @varyvol, I'm going to reopen it soon; I need it to rebuild again. The PR needs a re-review, however, so it can be merged in.

@davidolorundare did you really intend to close this or was it a mistake?

@daniel-beck daniel-beck added the needs-more-reviews Complex change, which would benefit from more eyes label Apr 10, 2019
@oleg-nenashev oleg-nenashev requested a review from jvz May 7, 2019 10:19
*/
public interface UserPropertyListener extends ExtensionPoint {

static final Logger LOGGER = Logger.getLogger(UserPropertyListener.class.getName());
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
static final Logger LOGGER = Logger.getLogger(UserPropertyListener.class.getName());
Logger LOGGER = Logger.getLogger(UserPropertyListener.class.getName());

The keywords are implied in interfaces.

* @param value property that was newly created.
*
*/
default <T> void onCreated(@Nonnull String username, @Nonnull T value) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't believe this method will end up being useful after all. Parameterizing it this way will only affect the call site. In order to make a sort of type safe dispatcher based on T for implementations, you'd need to explicitly check the type at runtime since there's no equivalent syntactical feature for that in Java (yet). Either way, the above method that just uses UserProperty is fine.

The password property listener might not end up being a direct implementation of this listener if there's no good way to pass two Details instances for the onChanged event, though. My suggestion in such a scenario may be to make a more generic EventListener with EventObject instances like in JavaBeans.

*
*/
default void onCreated(@Nonnull String username, @Nonnull UserProperty value) {
LOGGER.log(Level.FINE, MessageFormat.format("new {0} property created for user {1}", value.getClass().toString(), username));
Copy link
Member

Choose a reason for hiding this comment

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

My suggestion on syntax when using java.util.logging is to take advantage of the lambda forms and use normal string concatenation. In this line, that would be:

Suggested change
LOGGER.log(Level.FINE, MessageFormat.format("new {0} property created for user {1}", value.getClass().toString(), username));
LOGGER.fine(() -> "new " + value.getClass().toString() + " property created for user " + username));

* @param newValue new property of the user
*
*/
default <T> void onChanged(@Nonnull String username, @Nonnull T oldValue, @Nonnull T newValue) {
Copy link
Member

Choose a reason for hiding this comment

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

Same issue as <T>onCreated here.

* @param value property that was removed
*
*/
default <T> void onDeleted(@Nonnull String username, @Nonnull T value) {
Copy link
Member

Choose a reason for hiding this comment

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

Same issue as <T>onCreated here.

* @param newValue new password of the user
*
* **/
static void fireOnChanged(@Nonnull String username, @Nonnull String oldValue, @Nonnull String newValue) {
Copy link
Member

Choose a reason for hiding this comment

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

You should probably note here somehow that the values are hashed passwords, not the actual password values. Otherwise, I was about to suggest that this method should use char[] instead of String so that you could zero out the arrays after using them, but that's overkill for hashes.

/**
* Listener interface which all other user property-specific event-listeners make use of.
*/
public interface UserPropertyListener extends ExtensionPoint {
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 made this UserPropertyListener<T extends UserProperty> extends ExtensionPoint and then used T instead of UserProperty below, you'd be able to better express that a listener is for a specific property type. Then the static methods can look up the appropriate listener based on the property type. That would work better than the existing parameterization I think.

@alecharp
Copy link
Member

@davidolorundare Do you think you could revisit this PR to address @Wadeck and @jvz comments? Thanks.

@daniel-beck daniel-beck added the plugin-api-changes Changes the API of Jenkins available for use in plugins. label Oct 11, 2019
*
*/
default void onChanged(@Nonnull String username, @Nonnull UserProperty oldValue, @Nonnull UserProperty newValue) {
LOGGER.log(Level.FINE, MessageFormat.format("{0} property changed for user {1}", oldValue.getClass().toString(), username));
Copy link
Member

Choose a reason for hiding this comment

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

Having the default implementation log will mean that every single listener implementation that doesn't implement the method will spam the log.

Copy link

@varyvol varyvol left a comment

Choose a reason for hiding this comment

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

@davidolorundare I made a couple suggestions and I also agree with @jvz 's comments. Do you think you could go through the feedback?

return Details.fromHashedPassword(data.substring(prefix.length()));
}

User user = Util.getNearestAncestorOfTypeOrThrow(req, User.class);
if (p != null) {
Copy link

Choose a reason for hiding this comment

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

You are checking here if p is not null, but you already called the getPassword() method a few lines before.

* Listener notified of user password change events from the jenkins UI.
*/
@Extension
public class PasswordPropertyListener implements UserPropertyListener {
Copy link

Choose a reason for hiding this comment

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

Which is the use case that needs receiving the password (even if hashed)? Detecting the change is fine, but this could allow retrieving the actual values.

@rsandell rsandell added the needs-comments-resolved Comments in the PR need to be addressed before it can be considered for merging label Nov 7, 2019
@jglick
Copy link
Member

jglick commented Nov 25, 2019

Closing for now as this seems to have been abandoned.

Normally we would add the stalled-pr label to the corresponding JIRA issue, to make it easier to search for patches that someone could pick up and revive, but I cannot find one in this case—the two linked issues are in the audit-log-plugin component, and marked fixed.

@jglick jglick closed this Nov 25, 2019
@zbynek
Copy link
Contributor

zbynek commented Nov 25, 2019

@jglick issues seem to be moved to GitHub jenkinsci/audit-log-plugin#37

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-comments-resolved Comments in the PR need to be addressed before it can be considered for merging needs-more-reviews Complex change, which would benefit from more eyes plugin-api-changes Changes the API of Jenkins available for use in plugins.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants