-
-
Notifications
You must be signed in to change notification settings - Fork 8.8k
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
Changes from all commits
1fe3770
f4465d1
124b84c
43b50ff
49400e5
13c8864
87b44db
5de70ac
d0a19f0
d1382a6
97bfcc3
cc75d2c
4466180
4e1c33f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,111 @@ | ||||||
/* | ||||||
* The MIT License | ||||||
* | ||||||
* | ||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
* of this software and associated documentation files (the "Software"), to deal | ||||||
* in the Software without restriction, including without limitation the rights | ||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
* copies of the Software, and to permit persons to whom the Software is | ||||||
* furnished to do so, subject to the following conditions: | ||||||
* | ||||||
* The above copyright notice and this permission notice shall be included in | ||||||
* all copies or substantial portions of the Software. | ||||||
* | ||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||||
* THE SOFTWARE. | ||||||
*/ | ||||||
|
||||||
package hudson.model; | ||||||
|
||||||
import hudson.ExtensionList; | ||||||
import hudson.ExtensionPoint; | ||||||
import hudson.model.UserProperty; | ||||||
import java.text.MessageFormat; | ||||||
import java.util.List; | ||||||
import java.util.logging.Level; | ||||||
import java.util.logging.Logger; | ||||||
import javax.annotation.Nonnull; | ||||||
|
||||||
/** | ||||||
* Listener interface which all other user property-specific event-listeners make use of. | ||||||
*/ | ||||||
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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The keywords are implied in interfaces. |
||||||
|
||||||
/** | ||||||
* Fired when a new user property has been created. | ||||||
* | ||||||
* @param username the user | ||||||
* @param value property that was newly created. | ||||||
* | ||||||
*/ | ||||||
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 commentThe reason will be displayed to describe this comment to others. Learn more. My suggestion on syntax when using
Suggested change
|
||||||
} | ||||||
|
||||||
/** | ||||||
* Fired when a new user property has been created. | ||||||
* | ||||||
* @param username the user | ||||||
* @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 commentThe 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 The password property listener might not end up being a direct implementation of this listener if there's no good way to pass two |
||||||
LOGGER.log(Level.FINE, MessageFormat.format("new {0} property created for user {1}", value.toString(), username)); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Fired when an existing user property has been changed. | ||||||
* | ||||||
* @param username the user | ||||||
* @param oldValue old property of the user | ||||||
* @param newValue new property of the user | ||||||
* | ||||||
*/ | ||||||
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 commentThe 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. |
||||||
} | ||||||
|
||||||
/** | ||||||
* Fired when an existing user property has been changed. | ||||||
* | ||||||
* @param username the user | ||||||
* @param oldValue old property of the user | ||||||
* @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 commentThe reason will be displayed to describe this comment to others. Learn more. Same issue as |
||||||
LOGGER.log(Level.FINE, MessageFormat.format("{0} property changed for user {1}", oldValue.toString(), username)); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Fired when an existing user property has been removed or deleted. | ||||||
* | ||||||
* @param username the user | ||||||
* @param value property that was removed. | ||||||
* | ||||||
*/ | ||||||
default void onDeleted(@Nonnull String username, @Nonnull UserProperty value) { | ||||||
LOGGER.log(Level.FINE, MessageFormat.format("new {0} property created for user {1}", value.getClass().toString(), username)); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Fired when an existing user property has been removed or deleted. | ||||||
* | ||||||
* @param username the user | ||||||
* @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 commentThe reason will be displayed to describe this comment to others. Learn more. Same issue as |
||||||
LOGGER.log(Level.FINE, MessageFormat.format("new {0} property created for user {1}", value.toString(), username)); | ||||||
} | ||||||
|
||||||
static List<UserPropertyListener> all() { return ExtensionList.lookup(UserPropertyListener.class); } | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -717,14 +717,21 @@ public Details newInstance(StaplerRequest req, JSONObject formData) throws FormE | |
if(!Util.fixNull(pwd).equals(Util.fixNull(pwd2))) | ||
throw new FormException("Please confirm the password by typing it twice","user.password2"); | ||
|
||
User user = Util.getNearestAncestorOfTypeOrThrow(req, User.class); | ||
UserProperty p = user.getProperty(Details.class); | ||
davidolorundare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
String currentUserHashedPassword = ((Details) p).getPassword(); | ||
|
||
String data = Protector.unprotect(pwd); | ||
if(data!=null) { | ||
String prefix = Stapler.getCurrentRequest().getSession().getId() + ':'; | ||
if(data.startsWith(prefix)) | ||
PasswordPropertyListener.fireOnChanged(user.getId(), Util.fixNull(currentUserHashedPassword), Util.fixNull(pwd)); | ||
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 commentThe 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. |
||
PasswordPropertyListener.fireOnChanged(user.getId(), Util.fixNull(currentUserHashedPassword), Util.fixNull(pwd)); | ||
} | ||
// the UserSeedProperty is not touched by the configure page | ||
UserSeedProperty userSeedProperty = user.getProperty(UserSeedProperty.class); | ||
if (userSeedProperty != null) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/* | ||
* The MIT License | ||
* | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
|
||
package hudson.security; | ||
|
||
import hudson.Extension; | ||
import hudson.ExtensionList; | ||
import hudson.model.UserPropertyListener; | ||
import java.util.List; | ||
import javax.annotation.Nonnull; | ||
|
||
import static hudson.security.HudsonPrivateSecurityRealm.PASSWORD_ENCODER; | ||
|
||
/** | ||
* 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 commentThe 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. |
||
|
||
/** | ||
* @since TODO | ||
* | ||
* Fired when an existing user password property has been changed. | ||
* | ||
* @param username the user | ||
* @param oldValue old password of the user | ||
* @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 commentThe 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 |
||
if ((!oldValue.equals(newValue)) && (!PASSWORD_ENCODER.isPasswordValid(oldValue, newValue, null))) { | ||
for (PasswordPropertyListener l : all()) { | ||
l.onChanged(username, oldValue, newValue); | ||
} | ||
} | ||
} | ||
|
||
static List<PasswordPropertyListener> all() { return ExtensionList.lookup(PasswordPropertyListener.class); } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* | ||
* The MIT License | ||
* | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
|
||
package jenkins.security; | ||
|
||
import hudson.Extension; | ||
import hudson.ExtensionList; | ||
import hudson.model.UserProperty; | ||
import hudson.model.UserPropertyListener; | ||
import java.util.List; | ||
import javax.annotation.Nonnull; | ||
|
||
/** | ||
* Listener notified of user api-token creation and deletion events from the jenkins UI. | ||
*/ | ||
@Extension | ||
public class ApiTokenPropertyListener implements UserPropertyListener { | ||
|
||
/** | ||
* @since TODO | ||
* | ||
* Fired when an api token has been created. | ||
* | ||
* @param username the user | ||
* @param value api token property of the user | ||
* | ||
* **/ | ||
static void fireOnCreated(@Nonnull String username, @Nonnull UserProperty value) { | ||
if (value instanceof ApiTokenProperty) { | ||
for (ApiTokenPropertyListener l : all()) { | ||
l.onCreated(username, value); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @since TODO | ||
* | ||
* Fired when an api token has been revoked. | ||
* | ||
* @param username the user | ||
* @param value api token property of the user | ||
* | ||
* **/ | ||
static void fireOnDeleted(@Nonnull String username, @Nonnull UserProperty value) { | ||
if (value instanceof ApiTokenProperty) { | ||
for (ApiTokenPropertyListener l : all()) { | ||
l.onDeleted(username, value); | ||
} | ||
} | ||
} | ||
|
||
static List<ApiTokenPropertyListener> all() { return ExtensionList.lookup(ApiTokenPropertyListener.class); } | ||
} |
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 usedT
instead ofUserProperty
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.