Java tutorial
/* * The MIT License * * Copyright (c) 2013-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 com.cloudbees.plugins.credentials.domains.Domain; 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; import hudson.model.Actionable; import hudson.model.Descriptor; import hudson.model.DescriptorVisibilityFilter; 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; import hudson.security.AccessDeniedException2; import hudson.security.Permission; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; /** * A store of {@link Credentials}. Each {@link CredentialsStore} is associated with one and only one * {@link CredentialsProvider} though a {@link CredentialsProvider} may provide multiple {@link CredentialsStore}s * (for example a folder scoped {@link CredentialsProvider} may provide a {@link CredentialsStore} for each folder * or a user scoped {@link CredentialsProvider} may provide a {@link CredentialsStore} for each user). * * @author Stephen Connolly * @since 1.8 */ public abstract class CredentialsStore implements AccessControlled, Saveable { /** * The {@link CredentialsProvider} class. * * @since 2.0 */ private final Class<? extends CredentialsProvider> providerClass; /** * Cache for {@link #isDomainsModifiable()}. */ private transient Boolean domainsModifiable; /** * Constructor for use when the {@link CredentialsStore} is not an inner class of its {@link CredentialsProvider}. * * @param providerClass the {@link CredentialsProvider} class. * @since 2.0 */ public CredentialsStore(Class<? extends CredentialsProvider> providerClass) { this.providerClass = providerClass; } /** * Constructor that auto-detects the {@link CredentialsProvider} that this {@link CredentialsStore} is associated * with by examining the outer classes until an outer class that implements {@link CredentialsProvider} is found. * * @since 2.0 */ @SuppressWarnings("unchecked") public CredentialsStore() { // now let's infer our provider, Jesse will not like this evil Class<?> clazz = getClass().getEnclosingClass(); while (clazz != null && !CredentialsProvider.class.isAssignableFrom(clazz)) { clazz = clazz.getEnclosingClass(); } if (clazz == null) { throw new AssertionError(getClass() + " doesn't have an outer class. " + "Use the constructor that takes the Class object explicitly."); } if (!CredentialsProvider.class.isAssignableFrom(clazz)) { throw new AssertionError(getClass() + " doesn't have an outer class implementing CredentialsProvider. " + "Use the constructor that takes the Class object explicitly"); } providerClass = (Class<? extends CredentialsProvider>) clazz; } /** * Returns the {@link CredentialsProvider} or dies trying. * * @return the {@link CredentialsProvider} * @since 2.0 */ @NonNull public final CredentialsProvider getProviderOrDie() { CredentialsProvider provider = getProvider(); if (provider == null) { // we can only construct an instance if we were given the providerClass or we successfully inferred it // thus if the provider is missing it must have been removed from the extension list, e.g. by an admin // that wanted to block that provider from users before the addition of provider visibility controls throw new IllegalStateException("The credentials provider " + providerClass + " has been removed from the list of active extension points"); } return provider; } /** * Returns the {@link CredentialsProvider}. * * @return the {@link CredentialsProvider} (may be {@code null} if the admin has removed the provider from * the {@link ExtensionList}) * @since 2.0 */ @Nullable public final CredentialsProvider getProvider() { return ExtensionList.lookup(CredentialsProvider.class).get(providerClass); } /** * Returns the {@link CredentialsScope} instances that are applicable to this store. * @return the {@link CredentialsScope} instances that are applicable to this store or {@code null} if the store * instance is no longer enabled. * * @since 2.1.5 */ @Nullable public final Set<CredentialsScope> getScopes() { CredentialsProvider provider = getProvider(); return provider == null ? null : provider.getScopes(getContext()); } /** * Returns the context within which this store operates. Credentials in this store will be available to * child contexts (unless {@link CredentialsScope#SYSTEM} is valid for the store) but will not be available to * parent contexts. * * @return the context within which this store operates. */ @NonNull public abstract ModelObject getContext(); /** * Checks if the given principle has the given permission. * * @param a the principle. * @param permission the permission. * @return {@code false} if the user doesn't have the permission. */ public abstract boolean hasPermission(@NonNull Authentication a, @NonNull Permission permission); /** * {@inheritDoc} */ public ACL getACL() { // we really want people to implement this one, but in case of legacy implementations we need to provide // an effective ACL implementation. return new ACL() { @Override public boolean hasPermission(Authentication a, Permission permission) { return CredentialsStore.this.hasPermission(a, permission); } }; } /** * Checks if the current security principal has this permission. * <p> * Note: This is just a convenience function. * </p> * * @throws org.acegisecurity.AccessDeniedException if the user doesn't have the permission. */ public final void checkPermission(@NonNull Permission p) { Authentication a = Jenkins.getAuthentication(); if (!hasPermission(a, p)) { throw new AccessDeniedException2(a, p); } } /** * Checks if the current security principal has this permission. * * @return {@code false} if the user doesn't have the permission. */ public final boolean hasPermission(@NonNull Permission p) { return hasPermission(Jenkins.getAuthentication(), p); } /** * Returns all the {@link com.cloudbees.plugins.credentials.domains.Domain}s that this credential provider has. * Most implementers of {@link CredentialsStore} will probably want to override this method. * * @return the list of domains. */ @NonNull public List<Domain> getDomains() { return Collections.singletonList(Domain.global()); } /** * Retrieves the domain with the matching name. * * @param name the name (or {@code null} to match {@link Domain#global()} as that is the domain with a null name) * @return the domain or {@code null} if there is no domain with the supplied name. * @since 2.1.1 */ @CheckForNull public Domain getDomainByName(@CheckForNull String name) { for (Domain d : getDomains()) { if (StringUtils.equals(name, d.getName())) { return d; } } return null; } /** * Identifies whether this {@link CredentialsStore} supports making changes to the list of domains or * whether it only supports a fixed set of domains (which may only be one domain). * <p> * Note: in order for implementations to return {@code true} all of the following methods must be overridden: * </p> * <ul> * <li>{@link #getDomains}</li> * <li>{@link #addDomain(Domain, java.util.List)}</li> * <li>{@link #removeDomain(Domain)}</li> * <li>{@link #updateDomain(Domain, Domain)} </li> * </ul> * * @return {@code true} iff {@link #addDomain(Domain, List)} * {@link #addDomain(Domain, Credentials...)}, {@link #removeDomain(Domain)} * and {@link #updateDomain(Domain, Domain)} are expected to work */ public final boolean isDomainsModifiable() { if (domainsModifiable == null) { try { domainsModifiable = isOverridden("getDomains") && isOverridden("addDomain", Domain.class, List.class) && isOverridden("removeDomain", Domain.class) && isOverridden("updateDomain", Domain.class, Domain.class); } catch (NoSuchMethodException e) { return false; } } return domainsModifiable; } /** * Verifies if the specified method has been overridden by a subclass. * * @param name the name of the method. * @param args the arguments. * @return {@code true} if and only if the method is overridden by a subclass. * @throws NoSuchMethodException if something is seriously wrong. */ private boolean isOverridden(String name, Class... args) throws NoSuchMethodException { if (getClass().getMethod(name, args).getDeclaringClass() != CredentialsStore.class) { return true; } else { return false; } } /** * Returns an unmodifiable list of credentials for the specified domain. * * @param domain the domain. * @return the possibly empty (e.g. for an unknown {@link Domain}) unmodifiable list of credentials for the * specified domain. */ @NonNull public abstract List<Credentials> getCredentials(@NonNull Domain domain); /** * Adds a new {@link Domain} with seed credentials. * * @param domain the domain. * @param credentials the initial credentials with which to populate the domain. * @return {@code true} if the {@link CredentialsStore} was modified. * @throws java.io.IOException if the change could not be persisted. */ public final boolean addDomain(@NonNull Domain domain, Credentials... credentials) throws IOException { return addDomain(domain, Arrays.asList(credentials)); } /** * Adds a new {@link Domain} with seed credentials. * * @param domain the domain. * @param credentials the initial credentials with which to populate the domain. * @return {@code true} if the {@link CredentialsStore} was modified. * @throws IOException if the change could not be persisted. */ public boolean addDomain(@NonNull Domain domain, List<Credentials> credentials) throws IOException { throw new UnsupportedOperationException("Implementation does not support adding domains"); } /** * Removes an existing {@link Domain} and all associated {@link Credentials}. * * @param domain the domain. * @return {@code true} if the {@link CredentialsStore} was modified. * @throws IOException if the change could not be persisted. */ public boolean removeDomain(@NonNull Domain domain) throws IOException { throw new UnsupportedOperationException("Implementation does not support removing domains"); } /** * Updates an existing {@link Domain} keeping the existing associated {@link Credentials}. * * @param current the domain to update. * @param replacement the new replacement domain. * @return {@code true} if the {@link CredentialsStore} was modified. * @throws IOException if the change could not be persisted. */ public boolean updateDomain(@NonNull Domain current, @NonNull Domain replacement) throws IOException { throw new UnsupportedOperationException("Implementation does not support updating domains"); } /** * Adds the specified {@link Credentials} within the specified {@link Domain} for this {@link * CredentialsStore}. * * @param domain the domain. * @param credentials the credentials * @return {@code true} if the {@link CredentialsStore} was modified. * @throws IOException if the change could not be persisted. */ public abstract boolean addCredentials(@NonNull Domain domain, @NonNull Credentials credentials) throws IOException; /** * Removes the specified {@link Credentials} from the specified {@link Domain} for this {@link * CredentialsStore}. * * @param domain the domain. * @param credentials the credentials * @return {@code true} if the {@link CredentialsStore} was modified. * @throws IOException if the change could not be persisted. */ public abstract boolean removeCredentials(@NonNull Domain domain, @NonNull Credentials credentials) throws IOException; /** * Updates the specified {@link Credentials} from the specified {@link Domain} for this {@link * CredentialsStore} with the supplied replacement. * * @param domain the domain. * @param current the credentials to update. * @param replacement the new replacement credentials. * @return {@code true} if the {@link CredentialsStore} was modified. * @throws IOException if the change could not be persisted. */ public abstract boolean updateCredentials(@NonNull Domain domain, @NonNull Credentials current, @NonNull Credentials replacement) throws IOException; /** * Determines if the specified {@link Descriptor} is applicable to this {@link CredentialsStore}. * <p> * The default implementation consults the {@link DescriptorVisibilityFilter}s, {@link #_isApplicable(Descriptor)} * and the {@link #getProviderOrDie()}. * * @param descriptor the {@link Descriptor} to check. * @return {@code true} if the supplied {@link Descriptor} is applicable in this {@link CredentialsStore} * @since 2.0 */ public final boolean isApplicable(Descriptor<?> descriptor) { for (DescriptorVisibilityFilter filter : DescriptorVisibilityFilter.all()) { if (!filter.filter(this, descriptor)) { return false; } } CredentialsProvider provider = getProvider(); return _isApplicable(descriptor) && (provider == null || provider.isApplicable(descriptor)); } /** * {@link CredentialsStore} subtypes can override this method to veto some {@link Descriptor}s * from being available from their store. This is often useful when you are building * a custom store that holds a specific type of credentials or where you want to limit the * number of choices given to the users. * * @param descriptor the {@link Descriptor} to check. * @return {@code true} if the supplied {@link Descriptor} is applicable in this {@link CredentialsStore} * @since 2.0 */ protected boolean _isApplicable(Descriptor<?> descriptor) { return true; } /** * Returns the list of {@link CredentialsDescriptor} instances that are applicable within this * {@link CredentialsStore}. * * @return the list of {@link CredentialsDescriptor} instances that are applicable within this * {@link CredentialsStore}. * @since 2.0 */ public final List<CredentialsDescriptor> getCredentialsDescriptors() { CredentialsProvider provider = getProvider(); List<CredentialsDescriptor> result = DescriptorVisibilityFilter.apply(this, ExtensionList.lookup(CredentialsDescriptor.class)); if (provider != null && provider.isEnabled()) { if (!(result instanceof ArrayList)) { // should never happen, but let's be defensive in case the DescriptorVisibilityFilter contract changes result = new ArrayList<CredentialsDescriptor>(result); } for (Iterator<CredentialsDescriptor> iterator = result.iterator(); iterator.hasNext();) { CredentialsDescriptor d = iterator.next(); if (!_isApplicable(d) || !provider._isApplicable(d) || !d.isApplicable(provider)) { iterator.remove(); } } } return result; } /** * Computes the relative path from the current page to this store. * * @return the relative path from the current page or {@code null} * @since 2.0 */ @CheckForNull public String getRelativeLinkToContext() { ModelObject context = getContext(); if (context instanceof Item) { return Functions.getRelativeLinkTo((Item) context); } StaplerRequest request = Stapler.getCurrentRequest(); if (request == null) { return null; } if (context instanceof Jenkins) { return URI.create(request.getContextPath() + "/").normalize().toString(); } if (context instanceof User) { return URI.create(request.getContextPath() + "/" + ((User) context).getUrl() + "/").normalize() .toString(); } return null; } /** * Computes the relative path from the current page to this store. * * @return the relative path from the current page or {@code null} * @since 2.0 */ @CheckForNull public String getRelativeLinkToAction() { ModelObject context = getContext(); String relativeLink = getRelativeLinkToContext(); if (relativeLink == null) { return null; } CredentialsStoreAction a = getStoreAction(); if (a != null) { return relativeLink + "credentials/store/" + a.getUrlName() + "/"; } List<CredentialsStoreAction> actions; if (context instanceof Actionable) { actions = ((Actionable) context).getActions(CredentialsStoreAction.class); } else if (context instanceof Jenkins) { actions = Util.filter(((Jenkins) context).getActions(), CredentialsStoreAction.class); } else if (context instanceof User) { actions = Util.filter(((User) context).getTransientActions(), CredentialsStoreAction.class); } else { return null; } for (CredentialsStoreAction action : actions) { if (action.getStore() == this) { return relativeLink + action.getUrlName() + "/"; } } return null; } /** * Computes the relative path from the current page to the specified domain. * * @param domain the domain * @return the relative path from the current page or {@code null} * @since 2.0 */ @CheckForNull public String getRelativeLinkTo(Domain domain) { String relativeLink = getRelativeLinkToAction(); if (relativeLink == null) { return null; } return relativeLink + domain.getUrl(); } /** * Returns the display name of the {@link #getContext()} of this {@link CredentialsStore}. The default * implementation can handle both {@link Item} and {@link ItemGroup} as long as these are accessible from * {@link Jenkins}, and {@link User}. If the {@link CredentialsStore} provides an alternative * {@link #getContext()} that is outside of the normal tree then that implementation is responsible for * overriding this method to produce the correct display name. * * @return the display name. * @since 2.0 */ public final String getContextDisplayName() { ModelObject context = getContext(); if (context instanceof Item) { return ((Item) context).getFullDisplayName(); } else if (context instanceof Jenkins) { return ((Jenkins) context).getDisplayName(); } else if (context instanceof ItemGroup) { return ((ItemGroup) context).getFullDisplayName(); } else if (context instanceof User) { return Messages.CredentialsStoreAction_UserDisplayName(context.getDisplayName()); } else { return context.getDisplayName(); } } /** * Return the {@link CredentialsStoreAction} for this store. The action will be displayed as a sub-item of the * {@link ViewCredentialsAction}. Return {@code null} if this store will take control of displaying its action * (which will be the case for legacy implementations) * * @return the {@link CredentialsStoreAction} for this store to be rendered in {@link ViewCredentialsAction} or * {@code null} for old implementations compiled against pre 2.0 versions of credentials plugin. * @since 2.0 */ @Nullable 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(); } } }