Skip to content

Commit

Permalink
[FIXED JENKINS-39317] Provide a mechanism for forcing a save of all c…
Browse files Browse the repository at this point in the history
…redential store

- This method will only be available via Groovy scripting
  • Loading branch information
stephenc committed Nov 16, 2016
1 parent 2dbde36 commit 76f7838
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 10 deletions.
Expand Up @@ -35,6 +35,7 @@
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.Util;
import hudson.init.InitMilestone;
import hudson.model.Cause;
import hudson.model.Computer;
import hudson.model.ComputerSet;
Expand All @@ -51,12 +52,14 @@
import hudson.model.ParametersAction;
import hudson.model.Queue;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.User;
import hudson.model.queue.Tasks;
import hudson.security.ACL;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.security.PermissionScope;
import hudson.security.SecurityRealm;
import hudson.util.ListBoxModel;
import java.io.IOException;
import java.io.OutputStreamWriter;
Expand All @@ -82,12 +85,19 @@
import java.util.logging.Logger;
import jenkins.model.FingerprintFacet;
import jenkins.model.Jenkins;
import jenkins.util.Timer;
import org.acegisecurity.Authentication;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang.StringUtils;
import org.jenkins.ui.icon.IconSpec;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;

Expand Down Expand Up @@ -1583,6 +1593,106 @@ public static <C extends Credentials> List<C> trackAll(@NonNull Item item, @NonN
return credentials;
}

/**
* A helper method for Groovy Scripting to address use cases such as JENKINS-39317 where all credential stores
* need to be resaved. As this is a potentially very expensive operation the method has been marked
* {@link DoNotUse} in order to ensure that no plugin attempts to call this method. If invoking this method
* from an {@code init.d} Groovy script, ensure that the call is guarded by a marker file such that
*
* @throws IOException if things go wrong.
*/
@Restricted(DoNotUse.class) // Do not use from plugins
public static void saveAll() throws IOException {
LOGGER.entering(CredentialsProvider.class.getName(), "saveAll");
try {
final Jenkins jenkins = Jenkins.getActiveInstance();
jenkins.checkPermission(Jenkins.ADMINISTER);
LOGGER.log(Level.INFO, "Forced save credentials stores: Requested by {0}",
StringUtils.defaultIfBlank(Jenkins.getAuthentication().getName(), "anonymous"));
Timer.get().execute(new Runnable() {
@Override
public void run() {
SecurityContext ctx = ACL.impersonate(ACL.SYSTEM);
try {
if (jenkins.getInitLevel().compareTo(InitMilestone.JOB_LOADED) < 0) {
LOGGER.log(Level.INFO, "Forced save credentials stores: Initialization has not completed");
while (jenkins.getInitLevel().compareTo(InitMilestone.JOB_LOADED) < 0) {
LOGGER.log(Level.INFO, "Forced save credentials stores: Sleeping for 1 second");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Forced save credentials stores: Aborting due to interrupt",
e);
return;
}
}
LOGGER.log(Level.INFO, "Forced save credentials stores: Initialization has completed");
}
LOGGER.log(Level.INFO, "Forced save credentials stores: Processing Jenkins");
for (CredentialsStore s : lookupStores(jenkins)) {
try {
s.save();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Forced save credentials stores: Could not save " + s, e);
}
}
LOGGER.log(Level.INFO, "Forced save credentials stores: Processing Items...");
int count = 0;
for (Item item : jenkins.getAllItems(Item.class)) {
count++;
if (count % 100 == 0) {
LOGGER.log(Level.INFO, "Forced save credentials stores: Processing Items ({0} processed)",
count);
}
for (CredentialsStore s : lookupStores(item)) {
if (item == s.getContext()) {
// only save if the store is associated with this context item as otherwise will
// have been saved already / later
try {
s.save();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Forced save credentials stores: Could not save " + s, e);
}
}
}
}
LOGGER.log(Level.INFO, "Forced save credentials stores: Processing Users...");
count = 0;
for (User user : User.getAll()) {
count++;
if (count % 100 == 0) {
LOGGER.log(Level.INFO, "Forced save credentials stores: Processing Users ({0} processed)",
count);
}
// HACK ALERT we just want to access the user's stores so we do just enough impersonation
// to ensure that User.current() == user
// while we could use User.impersonate() that would force a query against the backing
// SecurityRealm to revalidate
ACL.impersonate(new UsernamePasswordAuthenticationToken(user.getId(), "",
new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY}));
for (CredentialsStore s : lookupStores(user)) {
if (user == s.getContext()) {
// only save if the store is associated with this context item as otherwise will
// have been saved already / later
try {
s.save();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Forced save credentials stores: Could not save " + s, e);
}
}
}
}
} finally {
LOGGER.log(Level.INFO, "Forced save credentials stores: Completed");
SecurityContextHolder.setContext(ctx);
}
}
});
} finally {
LOGGER.exiting(CredentialsProvider.class.getName(), "saveAll");
}
}

/**
* A {@link Comparator} for {@link ListBoxModel.Option} instances.
*
Expand Down
Expand Up @@ -27,6 +27,7 @@
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.BulkChange;
import hudson.ExtensionList;
import hudson.Functions;
import hudson.Util;
Expand All @@ -36,6 +37,7 @@
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ModelObject;
import hudson.model.Saveable;
import hudson.model.User;
import hudson.security.ACL;
import hudson.security.AccessControlled;
Expand Down Expand Up @@ -64,7 +66,7 @@
* @author Stephen Connolly
* @since 1.8
*/
public abstract class CredentialsStore implements AccessControlled {
public abstract class CredentialsStore implements AccessControlled, Saveable {

/**
* The {@link CredentialsProvider} class.
Expand Down Expand Up @@ -560,4 +562,20 @@ public CredentialsStoreAction getStoreAction() {
return null;
}

/**
* Persists the state of this object into XML. Default implementation delegates to {@link #getContext()} if it
* implements {@link Saveable} otherwise dropping back to a no-op.
*
* @see Saveable#save()
* @since 2.1.9
*/
@Override
public void save() throws IOException {
if (BulkChange.contains(this)) {
return;
}
if (getContext() instanceof Saveable) {
((Saveable) getContext()).save();
}
}
}
Expand Up @@ -593,6 +593,17 @@ public boolean updateCredentials(@NonNull Domain domain, @NonNull Credentials cu
public CredentialsStoreAction getStoreAction() {
return storeAction;
}

/**
* {@inheritDoc}
*/
@Override
public void save() throws IOException {
if (BulkChange.contains(this)) {
return;
}
SystemCredentialsProvider.getInstance().save();
}
}

/**
Expand Down
Expand Up @@ -52,11 +52,13 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import net.jcip.annotations.GuardedBy;
import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContext;
Expand Down Expand Up @@ -86,6 +88,14 @@ public class UserCredentialsProvider extends CredentialsProvider {
*/
private static final Set<CredentialsScope> SCOPES = Collections.singleton(CredentialsScope.USER);

/**
* The empty properties that have not been saved yet.
*/
@GuardedBy("self")
private static final WeakHashMap<User, UserCredentialsProperty> emptyProperties =
new WeakHashMap<User, UserCredentialsProperty>();


/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -452,6 +462,24 @@ private void checkPermission(Permission p) {
*/
private void save() throws IOException {
if (user.equals(User.current())) {
UserCredentialsProperty property = user.getProperty(UserCredentialsProperty.class);
if (property == null) {
if (domainCredentialsMap.isEmpty()) {
// nothing to do here we do not want to persist the empty property and nobody
// has even called getDomainCredentialsMap so the global domain has not been populated
return;
} else if (domainCredentialsMap.size() == 1) {
List<Credentials> global = domainCredentialsMap.get(Domain.global());
if (global != null && global.isEmpty()) {
// nothing to do here we do not want to persist the empty property
return;
}
}
synchronized (emptyProperties) {
user.addProperty(this);
emptyProperties.remove(user);
}
}
user.save();
}
}
Expand All @@ -464,6 +492,14 @@ public UserProperty reconfigure(StaplerRequest req, JSONObject form) throws Desc
return this;
}

/**
* Allow setting the user.
* @param user the user.
*/
private void _setUser(User user) {
this.user = user;
}

/**
* Our user property descriptor.
*/
Expand Down Expand Up @@ -592,6 +628,11 @@ public static class StoreImpl extends CredentialsStore {
*/
private final UserFacingAction storeAction;

/**
* The property;
*/
private transient UserCredentialsProperty property;

/**
* Constructor.
*
Expand All @@ -617,17 +658,23 @@ public CredentialsStoreAction getStoreAction() {
* @return the {@link UserCredentialsProperty} that we store the credentials in.
*/
private UserCredentialsProperty getInstance() {
UserCredentialsProperty property = user.getProperty(UserCredentialsProperty.class);
if (property == null) {
BulkChange bc = new BulkChange(user);
try {
user.addProperty(property = new UserCredentialsProperty(new DomainCredentials[0]));
} catch (IOException e) {
// ignore as we are not actually saving
} finally {
// we don't want to save the user record unless the credentials are modified.
bc.abort();
UserCredentialsProperty property = user.getProperty(UserCredentialsProperty.class);
if (property == null) {
synchronized (emptyProperties) {
// need to recheck as UserCredentialsProperty#save() may have added while we awaited the lock
property = user.getProperty(UserCredentialsProperty.class);
if (property == null) {
property = emptyProperties.get(user);
if (property == null) {
property = new UserCredentialsProperty(new DomainCredentials[0]);
property._setUser(user);
emptyProperties.put(user, property);
}
}
}
}
this.property = property; // idempotent write
}
return property;
}
Expand Down Expand Up @@ -733,11 +780,25 @@ public boolean updateCredentials(@NonNull Domain domain, @NonNull Credentials cu
return getInstance().updateCredentials(domain, current, replacement);
}

/**
* {@inheritDoc}
*/
@Override
public String getRelativeLinkToContext() {
StaplerRequest request = Stapler.getCurrentRequest();
return URI.create(request.getContextPath() + "/" + user.getUrl() + "/").normalize().toString() ;
}

/**
* {@inheritDoc}
*/
@Override
public void save() throws IOException {
if (BulkChange.contains(this)) {
return;
}
getInstance().save();
}
}

}

0 comments on commit 76f7838

Please sign in to comment.