-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
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
Conversation
Added a new unit test which evaluates logging of a user property-change (update of user's password).
restarting build |
* @param p the user object property | ||
* @param username the username | ||
*/ | ||
protected void userPropertyUpdated(@Nonnull UserProperty p, @Nonnull String username){} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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){} |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{}
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.
There was a problem hiding this comment.
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()));
}
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
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 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. |
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.
Agreed. We should track the old and new values. These can be compared with |
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.
some updates have been made to the PR based on the earlier discussions. |
There was a problem hiding this 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.
Restarting the build for this pull request. |
@davidolorundare did you really intend to close this or was it a mistake? |
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.
|
*/ | ||
public interface UserPropertyListener extends ExtensionPoint { | ||
|
||
static final Logger LOGGER = Logger.getLogger(UserPropertyListener.class.getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) { |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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:
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
@davidolorundare Do you think you could revisit this PR to address @Wadeck and @jvz comments? Thanks. |
* | ||
*/ | ||
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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
Closing for now as this seems to have been abandoned. Normally we would add the |
@jglick issues seem to be moved to GitHub jenkinsci/audit-log-plugin#37 |
See JENKINS-56005 and JENKINS-56008.
UserPropertyListener
was created, withdefault
method all other listener implementations would make use of. Enabling support for listening to Jenkins user property-change events.PasswordPropertyListener
andApiTokenPropertyListener
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.HudsonPrivateSecurityRealm.Details
andApiTokenProperty
, were updated with calls to the respective new listener class methods.HudsonPrivateSecurityRealmTest
andApiTokenPropertyTest
were updated with tests that exercise these new listener class methods.Proposed changelog entries
hudson.model.UserPropertyListener
interface to support notification of current and future Jenkins user-account property-change events (e.g. password changes, api-token key creation and deletion, etc).hudson.security.PasswordPropertyListener
class to support notification of Jenkins user-account password property-change events.jenkins.security.ApiTokenPropertyListener
class to support notification of Jenkins user-account api-token creation and revocation events.hudson.security.HudsonPrivateSecurityRealm.Details
adding notification(s) of user-account password property-change events to its method.jenkins.security.ApiTokenProperty
adding notification(s) of user-account api-token creation and revocation events to its method.Submitter checklist
* Use the
Internal:
prefix if the change has no user-visible impact (API, test frameworks, etc.)Desired reviewers
@jvz
@jeffret-b
@Wadeck
@daniel-beck