/*
* The MIT License
*
* Copyright (c) 2011-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 com.cloudbees.plugins.credentials.domains.DomainCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.domains.DomainSpecification;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.BulkChange;
import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.model.Descriptor;
import hudson.model.ItemGroup;
import hudson.model.ModelObject;
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
import hudson.security.ACL;
import hudson.security.AccessDeniedException2;
import hudson.security.Permission;
import hudson.util.CopyOnWriteMap;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
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;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.always;
/**
* A store of credentials tied to a specific {@link User}.
*/
@Extension
public class UserCredentialsProvider extends CredentialsProvider {
/**
* Our logger.
*/
private static final Logger LOGGER = Logger.getLogger(UserCredentialsProperty.class.getName());
/**
* We only care about {@link CredentialsScope#USER} scoped credentials.
*/
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}
*/
@Override
public Set<CredentialsScope> getScopes(ModelObject object) {
if (object instanceof User) {
return SCOPES;
}
return super.getScopes(object);
}
/**
* {@inheritDoc}
*/
@Override
public CredentialsStore getStore(@CheckForNull ModelObject object) {
if (object instanceof User) {
return new StoreImpl((User) object);
}
return null;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> type, @Nullable ItemGroup itemGroup,
@Nullable Authentication authentication) {
return getCredentials(type, itemGroup, authentication, Collections.<DomainRequirement>emptyList());
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> type,
@Nullable ItemGroup itemGroup,
@Nullable Authentication authentication,
@NonNull List<DomainRequirement> domainRequirements) {
// ignore itemGroup, as per-user credentials are available on any object
if (authentication == null) {
// assume ACL#SYSTEM
authentication = ACL.SYSTEM;
}
if (!ACL.SYSTEM.equals(authentication)) {
User user;
try {
if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
user = null;
} else if (authentication == Jenkins.getAuthentication()) {
user = User.current();
} else {
user = User.get(authentication.getName());
}
} catch (NullPointerException e) {
LogRecord lr = new LogRecord(Level.FINE,
"Could not find user for specified authentication. User credentials lookup aborted");
lr.setThrown(e);
lr.setParameters(new Object[]{authentication});
LOGGER.log(lr);
user = null;
}
if (user != null) {
UserCredentialsProperty property = user.getProperty(UserCredentialsProperty.class);
if (property != null) {
// we need to impersonate if the requesting authentication is not the current authentication.
boolean needImpersonation = !user.equals(User.current());
SecurityContext old = needImpersonation ? ACL.impersonate(user.impersonate()) : null;
try {
return DomainCredentials
.getCredentials(property.getDomainCredentialsMap(), type, domainRequirements, always());
} finally {
if (needImpersonation) {
// restore the current authentication if we impersonated.
SecurityContextHolder.setContext(old);
}
}
}
}
}
return new ArrayList<C>();
}
/**
* {@inheritDoc}
*/
@Override
public String getIconClassName() {
return "icon-credentials-user-store";
}
/**
* Need a user property to hold the user's personal credentials.
*/
public static class UserCredentialsProperty extends UserProperty {
/**
* Old store of credentials
*
* @deprecated
*/
@Deprecated
private transient List<Credentials> credentials;
/**
* Our credentials.
*
* @since 1.5
*/
@SuppressFBWarnings("IS2_INCONSISTENT_SYNC")
private Map<Domain, List<Credentials>> domainCredentialsMap;
/**
* Backwards compatibility.
*
* @param credentials the credentials.
* @deprecated
*/
@Deprecated
public UserCredentialsProperty(List<Credentials> credentials) {
domainCredentialsMap = DomainCredentials.migrateListToMap(null, credentials);
}
/**
* Constructor for stapler.
*
* @param domainCredentials the credentials.
* @since 1.5
*/
@DataBoundConstructor
public UserCredentialsProperty(DomainCredentials[] domainCredentials) {
domainCredentialsMap = DomainCredentials.asMap(Arrays.asList(domainCredentials));
}
/**
* Resolve old data store into new data store.
*
* @since 1.5
*/
@SuppressWarnings("deprecation")
private Object readResolve() throws ObjectStreamException {
if (domainCredentialsMap == null) {
return new UserCredentialsProperty(credentials);
}
return this;
}
/**
* Helper method.
*
* @param type type of credentials to get.
* @param <C> type of credentials to get.
* @return the subset of the user's credentials that are of the specified type.
*/
public <C extends Credentials> List<C> getCredentials(Class<C> type) {
checkPermission(CredentialsProvider.VIEW);
List<C> result = new ArrayList<C>();
for (Credentials credential : getCredentials()) {
if (type.isInstance(credential)) {
result.add(type.cast(credential));
}
}
return result;
}
/**
* Gets all the user's credentials.
*
* @return all the user's credentials.
*/
@SuppressWarnings("unused") // used by stapler
public List<Credentials> getCredentials() {
checkPermission(CredentialsProvider.VIEW);
return domainCredentialsMap.get(Domain.global());
}
/**
* Returns the {@link com.cloudbees.plugins.credentials.domains.DomainCredentials}
*
* @return the {@link com.cloudbees.plugins.credentials.domains.DomainCredentials}
* @since 1.5
*/
@SuppressWarnings("unused") // used by stapler
public List<DomainCredentials> getDomainCredentials() {
checkPermission(CredentialsProvider.VIEW);
return DomainCredentials.asList(getDomainCredentialsMap());
}
/**
* The map of domain credentials.
*
* @return The map of domain credentials.
* @since 1.5
*/
@SuppressWarnings("deprecation")
@NonNull
public synchronized Map<Domain, List<Credentials>> getDomainCredentialsMap() {
checkPermission(CredentialsProvider.VIEW);
return domainCredentialsMap = DomainCredentials.migrateListToMap(domainCredentialsMap, credentials);
}
/**
* Sets the map of domain credentials.
*
* @param domainCredentialsMap the map of domain credentials.
* @since 1.5
*/
public synchronized void setDomainCredentialsMap(Map<Domain, List<Credentials>> domainCredentialsMap) {
checkPermission(CredentialsProvider.MANAGE_DOMAINS);
this.domainCredentialsMap = DomainCredentials.toCopyOnWriteMap(domainCredentialsMap);
}
/**
* Implementation for {@link StoreImpl} to delegate to while keeping the lock synchronization simple.
*/
private synchronized boolean addDomain(@NonNull Domain domain, List<Credentials> credentials)
throws IOException {
checkPermission(CredentialsProvider.MANAGE_DOMAINS);
Map<Domain, List<Credentials>> domainCredentialsMap = getDomainCredentialsMap();
if (domainCredentialsMap.containsKey(domain)) {
List<Credentials> list = domainCredentialsMap.get(domain);
boolean modified = false;
for (Credentials c : credentials) {
if (list.contains(c)) {
continue;
}
list.add(c);
modified = true;
}
if (modified) {
save();
}
return modified;
} else {
domainCredentialsMap.put(domain, new ArrayList<Credentials>(credentials));
save();
return true;
}
}
/**
* Implementation for {@link StoreImpl} to delegate to while keeping the lock synchronization simple.
*/
private synchronized boolean removeDomain(@NonNull Domain domain) throws IOException {
checkPermission(CredentialsProvider.MANAGE_DOMAINS);
Map<Domain, List<Credentials>> domainCredentialsMap = getDomainCredentialsMap();
if (domainCredentialsMap.containsKey(domain)) {
domainCredentialsMap.remove(domain);
save();
return true;
}
return false;
}
/**
* Implementation for {@link StoreImpl} to delegate to while keeping the lock synchronization simple.
*/
private synchronized boolean updateDomain(@NonNull Domain current, @NonNull Domain replacement)
throws IOException {
checkPermission(CredentialsProvider.MANAGE_DOMAINS);
Map<Domain, List<Credentials>> domainCredentialsMap = getDomainCredentialsMap();
if (domainCredentialsMap.containsKey(current)) {
domainCredentialsMap.put(replacement, domainCredentialsMap.remove(current));
save();
return true;
}
return false;
}
/**
* Implementation for {@link StoreImpl} to delegate to while keeping the lock synchronization simple.
*/
private synchronized boolean addCredentials(@NonNull Domain domain, @NonNull Credentials credentials)
throws IOException {
checkPermission(CredentialsProvider.CREATE);
Map<Domain, List<Credentials>> domainCredentialsMap = getDomainCredentialsMap();
if (domainCredentialsMap.containsKey(domain)) {
List<Credentials> list = domainCredentialsMap.get(domain);
if (list.contains(credentials)) {
return false;
}
list.add(credentials);
save();
return true;
}
return false;
}
/**
* Implementation for {@link StoreImpl} to delegate to while keeping the lock synchronization simple.
*/
@NonNull
private synchronized List<Credentials> getCredentials(@NonNull Domain domain) {
// TODO switch to Jenkins.getInstance() once 2.0+ is the baseline
if (user.equals(User.current())) {
List<Credentials> list = getDomainCredentialsMap().get(domain);
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
return Collections.unmodifiableList(new ArrayList<Credentials>(list));
}
return Collections.emptyList();
}
/**
* Implementation for {@link StoreImpl} to delegate to while keeping the lock synchronization simple.
*/
private synchronized boolean removeCredentials(@NonNull Domain domain, @NonNull Credentials credentials)
throws IOException {
checkPermission(CredentialsProvider.DELETE);
Map<Domain, List<Credentials>> domainCredentialsMap = getDomainCredentialsMap();
if (domainCredentialsMap.containsKey(domain)) {
List<Credentials> list = domainCredentialsMap.get(domain);
if (!list.contains(credentials)) {
return false;
}
list.remove(credentials);
save();
return true;
}
return false;
}
/**
* Implementation for {@link StoreImpl} to delegate to while keeping the lock synchronization simple.
*/
private synchronized boolean updateCredentials(@NonNull Domain domain, @NonNull Credentials current,
@NonNull Credentials replacement) throws IOException {
checkPermission(CredentialsProvider.UPDATE);
Map<Domain, List<Credentials>> domainCredentialsMap = getDomainCredentialsMap();
if (domainCredentialsMap.containsKey(domain)) {
List<Credentials> list = domainCredentialsMap.get(domain);
int index = list.indexOf(current);
if (index == -1) {
return false;
}
list.set(index, replacement);
save();
return true;
}
return false;
}
/**
* Helper method to check the specified permission.
*
* @param p the permission to checl.
*/
private void checkPermission(Permission p) {
if (user.equals(User.current())) {
user.checkPermission(p);
} else {
throw new AccessDeniedException2(Jenkins.getAuthentication(), p);
}
}
/**
* Save all changes.
*
* @throws IOException if something goes wrong.
*/
private void save() throws IOException {
if (user.equals(User.current())) {
UserCredentialsProperty property = user.getProperty(UserCredentialsProperty.class);
if (property == null) {
Map<Domain, List<Credentials>> domainCredentialsMap;
synchronized (this) {
// peek to save manipulating the object further
domainCredentialsMap = this.domainCredentialsMap;
}
if (domainCredentialsMap == null || 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();
}
}
/**
* {@inheritDoc}
*/
@Override
public UserProperty reconfigure(StaplerRequest req, JSONObject form) throws Descriptor.FormException {
return this;
}
/**
* Allow setting the user.
* @param user the user.
*/
private void _setUser(User user) {
this.user = user;
}
/**
* Our user property descriptor.
*/
@Extension
@SuppressWarnings("unused") // used by Jenkins
public static class DescriptorImpl extends UserPropertyDescriptor {
/**
* {@inheritDoc}
*/
@Override
public UserProperty newInstance(User user) {
return new UserCredentialsProperty(new DomainCredentials[0]);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isEnabled() {
return !all().isEmpty();
}
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return Messages.UserCredentialsProvider_DisplayName();
}
/**
* Gets all the credentials descriptors.
*
* @return all the credentials descriptors.
* @since 1.5
*/
@SuppressWarnings("unused") // used by stapler
public DescriptorExtensionList<Credentials, CredentialsDescriptor> getCredentialDescriptors() {
// TODO delete me
return CredentialsProvider.allCredentialsDescriptors();
}
/**
* Gets all the {@link com.cloudbees.plugins.credentials.domains.DomainSpecification} descriptors.
*
* @return all the {@link com.cloudbees.plugins.credentials.domains.DomainSpecification} descriptors.
* @since 1.5
*/
@SuppressWarnings("unused") // used by stapler
public DescriptorExtensionList<DomainSpecification, Descriptor<DomainSpecification>>
getSpecificationDescriptors() {
// TODO switch to Jenkins.getInstance() once 2.0+ is the baseline
return Jenkins.getActiveInstance().getDescriptorList(DomainSpecification.class);
}
}
}
@ExportedBean
public static class UserFacingAction extends CredentialsStoreAction {
/**
* The user that this action belongs to.
*/
private final StoreImpl store;
/**
* Constructor.
*
* @param store the {@link CredentialsStore} that is being exposed.
*/
public UserFacingAction(StoreImpl store) {
this.store = store;
}
/**
* {@inheritDoc}
*/
@NonNull
@Exported
public CredentialsStore getStore() {
return store;
}
/**
* {@inheritDoc}
*/
@Override
public String getIconFileName() {
return isVisible()
? "/plugin/credentials/images/24x24/user-store.png"
: null;
}
/**
* {@inheritDoc}
*/
@Override
public String getIconClassName() {
return isVisible()
? "icon-credentials-user-store"
: null;
}
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return Messages.UserCredentialsProvider_UserFacingAction_DisplayName();
}
}
/**
* Our implementation
*/
public static class StoreImpl extends CredentialsStore {
/**
* The user that this store belongs to.
*/
private final User user;
/**
* Our store action.
*/
private final UserFacingAction storeAction;
/**
* The property;
*/
private transient UserCredentialsProperty property;
/**
* Constructor.
*
* @param user the user.
*/
private StoreImpl(User user) {
this.user = user;
this.storeAction = new UserFacingAction(this);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
public CredentialsStoreAction getStoreAction() {
return storeAction;
}
/**
* Looks up the {@link UserCredentialsProperty} that we store the credentials in.
*
* @return the {@link UserCredentialsProperty} that we store the credentials in.
*/
private UserCredentialsProperty getInstance() {
if (property == null) {
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;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public ModelObject getContext() {
return user;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasPermission(@NonNull Authentication a, @NonNull Permission permission) {
return getACL().hasPermission(a, permission);
}
/**
* {@inheritDoc}
*/
@Override
public ACL getACL() {
return new ACL() {
@Override
public boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission permission) {
return user.equals(User.get(a.getName())) && user.getACL().hasPermission(a, permission);
}
};
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
@Exported
public List<Domain> getDomains() {
return Collections.unmodifiableList(new ArrayList<Domain>(
getInstance().getDomainCredentialsMap().keySet()
));
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
@Exported
public List<Credentials> getCredentials(@NonNull Domain domain) {
return getInstance().getCredentials(domain);
}
/**
* {@inheritDoc}
*/
@Override
public boolean addDomain(@NonNull Domain domain, List<Credentials> credentials) throws IOException {
return getInstance().addDomain(domain, credentials);
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeDomain(@NonNull Domain domain) throws IOException {
return getInstance().removeDomain(domain);
}
/**
* {@inheritDoc}
*/
@Override
public boolean updateDomain(@NonNull Domain current, @NonNull Domain replacement) throws IOException {
return getInstance().updateDomain(current, replacement);
}
/**
* {@inheritDoc}
*/
@Override
public boolean addCredentials(@NonNull Domain domain, @NonNull Credentials credentials) throws IOException {
return getInstance().addCredentials(domain, credentials);
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeCredentials(@NonNull Domain domain, @NonNull Credentials credentials) throws IOException {
return getInstance().removeCredentials(domain, credentials);
}
/**
* {@inheritDoc}
*/
@Override
public boolean updateCredentials(@NonNull Domain domain, @NonNull Credentials current,
@NonNull Credentials replacement) throws IOException {
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();
}
}
}