/* * The MIT License * * Copyright (c) 2016, CloudBees, Inc., Stephen Connolly. * * 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 com.cloudbees.plugins.credentials; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.ExtensionList; import hudson.XmlFile; import hudson.model.Descriptor; import hudson.model.DescriptorVisibilityFilter; import hudson.model.Saveable; import hudson.security.Permission; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import jenkins.model.Jenkins; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.StaplerRequest; /** * Manages the various {@link CredentialsProvider} implementations in a {@link Jenkins} * * @since 2.0 */ @Extension public class CredentialsProviderManager extends DescriptorVisibilityFilter implements Serializable, Saveable { /** * Our logger. */ private static final Logger LOGGER = Logger.getLogger(CredentialsProviderManager.class.getName()); /** * Ensure standardized serialization. */ private static final long serialVersionUID = 1L; /** * Our {@link CredentialsProvider} filter. */ private CredentialsProviderFilter providerFilter; /** * Our {@link CredentialsDescriptor} filter. */ private CredentialsTypeFilter typeFilter; /** * Any additional restrictions to apply. */ private List<CredentialsProviderTypeRestriction> restrictions; /** * A cache of {@link #restrictions} grouped by {@link CredentialsProviderTypeRestriction#getDescriptor()}. */ private transient Map<CredentialsProviderTypeRestrictionDescriptor, List<CredentialsProviderTypeRestriction>> restrictionGroups; /** * Our constructor. */ public CredentialsProviderManager() { try { XmlFile xml = getConfigFile(); if (xml.exists()) { xml.unmarshal(this); } } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to read the existing credentials", e); } } /** * Returns {@code true} if and only if the specified {@link CredentialsProvider} is enabled. * * @param provider the specified {@link CredentialsProvider} to check. * @return {@code true} if and only if the specified {@link CredentialsProvider} is enabled. */ public static boolean isEnabled(CredentialsProvider provider) { CredentialsProviderManager manager = getInstance(); return manager == null || manager.providerFilter == null || manager.providerFilter.filter(provider); } /** * Returns our {@link CredentialsProviderManager} singleton. * * @return {@link CredentialsProviderManager} singleton or {@code null} */ @Nullable // should never be null under normal code paths public static CredentialsProviderManager getInstance() { return ExtensionList.lookup(DescriptorVisibilityFilter.class).get(CredentialsProviderManager.class); } /** * Returns our {@link CredentialsProviderManager} singleton. * * @return {@link CredentialsProviderManager} singleton */ @NonNull public static CredentialsProviderManager getInstanceOrDie() { CredentialsProviderManager instance = getInstance(); if (instance == null) { throw new IllegalStateException("CredentialsProviderManager is not registered with Jenkins"); } return instance; } /** * {@inheritDoc} */ @Override public boolean filter(Object context, Descriptor descriptor) { Map<CredentialsProviderTypeRestrictionDescriptor, List<CredentialsProviderTypeRestriction>> restrictions = restrictions(); if (restrictions != null && descriptor instanceof CredentialsDescriptor) { CredentialsProvider provider = null; if (context instanceof CredentialsProvider) { provider = (CredentialsProvider) context; } else if (context instanceof CredentialsStore) { provider = ((CredentialsStore) context).getProvider(); } else if (context instanceof CredentialsStoreAction.DomainWrapper) { provider = ((CredentialsStoreAction.DomainWrapper) context).getStore().getProvider(); } else if (context instanceof CredentialsStoreAction.CredentialsWrapper) { provider = ((CredentialsStoreAction.CredentialsWrapper) context).getStore().getProvider(); } if (provider != null) { CredentialsDescriptor type = (CredentialsDescriptor) descriptor; for (Map.Entry<CredentialsProviderTypeRestrictionDescriptor, List<CredentialsProviderTypeRestriction>> group : restrictions.entrySet()) { if (!group.getKey().filter(group.getValue(), provider, type)) { return false; } } } } if (descriptor instanceof CredentialsDescriptor) { return typeFilter == null || typeFilter.filter((CredentialsDescriptor) descriptor); } if (descriptor instanceof CredentialsProvider && context instanceof Jenkins) { return providerFilter == null || providerFilter.filter((CredentialsProvider) descriptor); } return true; } /** * Gets the configuration file that {@link CredentialsProviderManager} uses to store its credentials. * * @return the configuration file that {@link CredentialsProviderManager} uses to store its credentials. */ public static XmlFile getConfigFile() { // TODO switch to Jenkins.getInstance() once 2.0+ is the baseline return new XmlFile(Jenkins.XSTREAM2, new File(Jenkins.getActiveInstance().getRootDir(), "credentials-configuration.xml")); } /** * Short-cut method for {@link Jenkins#checkPermission(hudson.security.Permission)} * * @param p the permission to check. */ private void checkPermission(Permission p) { // TODO switch to Jenkins.getInstance() once 2.0+ is the baseline Jenkins.getActiveInstance().checkPermission(p); } /** * {@inheritDoc} */ @Override public void save() throws IOException { checkPermission(Jenkins.ADMINISTER); getConfigFile().write(this); } /** * Gets the current {@link CredentialsProviderFilter}. * * @return the current {@link CredentialsProviderFilter}. */ @NonNull public CredentialsProviderFilter getProviderFilter() { return providerFilter == null ? new CredentialsProviderFilter.None() : providerFilter; } /** * Sets the {@link CredentialsProviderFilter}. * * @param providerFilter the new {@link CredentialsProviderFilter}. */ public void setProviderFilter(@CheckForNull CredentialsProviderFilter providerFilter) { if (providerFilter == null) { providerFilter = new CredentialsProviderFilter.None(); } if (Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER)) { if (!providerFilter.equals(this.providerFilter)) { this.providerFilter = providerFilter; try { save(); } catch (IOException e) { // ignore } } } } /** * Gets the current {@link CredentialsTypeFilter}. * * @return the current {@link CredentialsTypeFilter}. */ @NonNull public CredentialsTypeFilter getTypeFilter() { return typeFilter == null ? new CredentialsTypeFilter.None() : typeFilter; } /** * Sets the {@link CredentialsTypeFilter}. * * @param typeFilter the new {@link CredentialsTypeFilter}. */ public void setTypeFilter(@CheckForNull CredentialsTypeFilter typeFilter) { if (typeFilter == null) { typeFilter = new CredentialsTypeFilter.None(); } if (Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER)) { if (!typeFilter.equals(this.typeFilter)) { this.typeFilter = typeFilter; try { save(); } catch (IOException e) { // ignore } } } } /** * Gets the current list of {@link CredentialsProviderTypeRestriction} instances. * * @return the current list of {@link CredentialsProviderTypeRestriction} instances. */ @NonNull public List<CredentialsProviderTypeRestriction> getRestrictions() { return restrictions == null ? Collections.<CredentialsProviderTypeRestriction>emptyList() : Collections.unmodifiableList(restrictions); } /** * Sets the list of {@link CredentialsProviderTypeRestriction} instances. * * @param restrictions the new list of {@link CredentialsProviderTypeRestriction} instances. */ public void setRestrictions(List<CredentialsProviderTypeRestriction> restrictions) { if (Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER)) { if (restrictions != null) { // ensure they are sorted grouped so that it is easy to infer Collections.sort(restrictions, new Comparator<CredentialsProviderTypeRestriction>() { final ExtensionList<CredentialsProviderTypeRestrictionDescriptor> list = ExtensionList.lookup(CredentialsProviderTypeRestrictionDescriptor.class); /** {@inheritDoc} */ @Override public int compare(CredentialsProviderTypeRestriction o1, CredentialsProviderTypeRestriction o2) { int index1 = list.indexOf(o1.getDescriptor()); int index2 = list.indexOf(o2.getDescriptor()); if (index1 == -1) { return index2 == -1 ? 0 : 1; } if (index2 == -1) { return -1; } return index1 < index2 ? -1 : (index1 == index2 ? 0 : 1); } }); } if (restrictions == null ? this.restrictions != null : !restrictions.equals(this.restrictions)) { this.restrictions = restrictions; this.restrictionGroups = null; try { save(); } catch (IOException e) { // ignore } } } } /** * A repopulation-safe access for the {@link #restrictionGroups} * * @return the {@link #restrictionGroups} or {@code null} if empty. */ @CheckForNull private Map<CredentialsProviderTypeRestrictionDescriptor, List<CredentialsProviderTypeRestriction>> restrictions() { if (restrictionGroups == null && restrictions != null) { Map<CredentialsProviderTypeRestrictionDescriptor, List<CredentialsProviderTypeRestriction>> restrictionGroups = new HashMap<CredentialsProviderTypeRestrictionDescriptor, List<CredentialsProviderTypeRestriction>>(); for (CredentialsProviderTypeRestriction restriction : restrictions) { List<CredentialsProviderTypeRestriction> group = restrictionGroups.get(restriction.getDescriptor()); if (group == null) { restrictionGroups.put(restriction.getDescriptor(), group = new ArrayList<CredentialsProviderTypeRestriction>(1)); } group.add(restriction); } this.restrictionGroups = restrictionGroups; // idempotent } return restrictionGroups; } /** * Our global configuration. * * @since 2.0 */ @Extension public static class Configuration extends GlobalConfiguration { /** * A Jelly EL short-cut for {@link CredentialsProviderManager#getProviderFilter()}. * * @return the {@link CredentialsProviderFilter}. */ @SuppressWarnings("unused") // jelly EL helper @Restricted(NoExternalUse.class) public CredentialsProviderFilter getProviderFilter() { CredentialsProviderManager manager = getInstance(); return manager == null ? new CredentialsProviderFilter.None() : manager.getProviderFilter(); } /** * A Jelly form-binding short-cut for * {@link CredentialsProviderManager#setProviderFilter(CredentialsProviderFilter)}. * * @param providerFilter the {@link CredentialsProviderFilter}. */ @SuppressWarnings("unused") // jelly form binding @Restricted(NoExternalUse.class) public void setProviderFilter(CredentialsProviderFilter providerFilter) { CredentialsProviderManager manager = getInstance(); if (manager != null) { manager.setProviderFilter(providerFilter); } } /** * A Jelly EL short-cut for {@link CredentialsProviderManager#getTypeFilter()}. * * @return the {@link CredentialsTypeFilter}. */ @SuppressWarnings("unused") // jelly EL helper @Restricted(NoExternalUse.class) public CredentialsTypeFilter getTypeFilter() { CredentialsProviderManager manager = getInstance(); return manager == null ? new CredentialsTypeFilter.None() : manager.getTypeFilter(); } /** * A Jelly form-binding short-cut for * {@link CredentialsProviderManager#setTypeFilter(CredentialsTypeFilter)}. * * @param typeFilter the {@link CredentialsTypeFilter}. */ @SuppressWarnings("unused") // jelly form binding @Restricted(NoExternalUse.class) public void setTypeFilter(CredentialsTypeFilter typeFilter) { CredentialsProviderManager manager = getInstance(); if (manager != null) { manager.setTypeFilter(typeFilter); } } /** * A Jelly EL short-cut for {@link CredentialsProviderManager#getRestrictions()}. * * @return the {@link CredentialsProviderTypeRestriction} instances. */ @SuppressWarnings("unused") // jelly EL helper @Restricted(NoExternalUse.class) public List<CredentialsProviderTypeRestriction> getRestrictions() { CredentialsProviderManager manager = getInstance(); return manager == null ? Collections.<CredentialsProviderTypeRestriction>emptyList() : manager.getRestrictions(); } /** * A Jelly form-binding short-cut for * {@link CredentialsProviderManager#setRestrictions(List)}. * * @param restrictions the {@link CredentialsProviderTypeRestriction} instances. */ @SuppressWarnings("unused") // jelly form binding @Restricted(NoExternalUse.class) public void setRestrictions(List<CredentialsProviderTypeRestriction> restrictions) { CredentialsProviderManager manager = getInstance(); if (manager != null) { manager.setRestrictions(restrictions); } } /** * {@inheritDoc} */ @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { if (Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER)) { if (!json.has("restrictions")) { // JENKINS-36090 stapler "helpfully" does not submit the restrictions if there are none // and hence you can never delete the laste one json.put("restrictions", new JSONArray()); } req.bindJSON(this, json); return super.configure(req, json); } return false; } /** * {@inheritDoc} */ @Override public GlobalConfigurationCategory getCategory() { return GlobalConfigurationCategory.get(GlobalCredentialsConfiguration.Category.class); } } }