/*
* 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();
}
}
}