/*
* The MIT License
*
* Copyright 2013 CloudBees.
*
* 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.hudson.plugins.folder.properties;
import com.cloudbees.hudson.plugins.folder.AbstractFolder;
import com.cloudbees.hudson.plugins.folder.AbstractFolderProperty;
import com.cloudbees.hudson.plugins.folder.AbstractFolderPropertyDescriptor;
import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsMatcher;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.CredentialsStoreAction;
import com.cloudbees.plugins.credentials.common.IdCredentials;
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.DescriptorExtensionList;
import hudson.Extension;
import hudson.model.Action;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ModelObject;
import hudson.security.ACL;
import hudson.security.AccessDeniedException2;
import hudson.security.Permission;
import hudson.util.CopyOnWriteMap;
import hudson.util.ListBoxModel;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import jenkins.model.Jenkins;
import jenkins.model.TransientActionFactory;
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.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
/**
* A store of credentials that can be used as a Stapler object.
*/
@Extension(optional = true)
public class FolderCredentialsProvider extends CredentialsProvider {
/**
* The valid scopes for this store.
*/
private static final Set<CredentialsScope> SCOPES =
Collections.<CredentialsScope>singleton(CredentialsScope.GLOBAL);
@GuardedBy("self")
private static final WeakHashMap<AbstractFolder<?>,FolderCredentialsProperty> emptyProperties =
new WeakHashMap<AbstractFolder<?>, FolderCredentialsProperty>();
/**
* {@inheritDoc}
*/
@Override
public Set<CredentialsScope> getScopes(ModelObject object) {
if (object instanceof AbstractFolder) {
return SCOPES;
}
return super.getScopes(object);
}
/**
* {@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) {
List<C> result = new ArrayList<C>();
Set<String> ids = new HashSet<String>();
if (ACL.SYSTEM.equals(authentication)) {
while (itemGroup != null) {
if (itemGroup instanceof AbstractFolder) {
final AbstractFolder<?> folder = AbstractFolder.class.cast(itemGroup);
FolderCredentialsProperty property = folder.getProperties().get(FolderCredentialsProperty.class);
if (property != null) {
for (C c : DomainCredentials.getCredentials(
property.getDomainCredentialsMap(),
type,
domainRequirements,
CredentialsMatchers.always())) {
if (!(c instanceof IdCredentials) || ids.add(((IdCredentials) c).getId())) {
// if IdCredentials, only add if we havent added already
// if not IdCredentials, always add
result.add(c);
}
}
}
}
if (itemGroup instanceof Item) {
itemGroup = Item.class.cast(itemGroup).getParent();
} else {
break;
}
}
}
return result;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> type, @NonNull Item item,
@Nullable Authentication authentication,
@NonNull List<DomainRequirement> domainRequirements) {
if (item instanceof AbstractFolder) {
// credentials defined in the folder should be available in the context of the folder
return getCredentials(type, (ItemGroup) item, authentication, domainRequirements);
}
return super.getCredentials(type, item, authentication, domainRequirements);
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public <C extends IdCredentials> ListBoxModel getCredentialIds(@NonNull Class<C> type,
@Nullable ItemGroup itemGroup,
@Nullable Authentication authentication,
@NonNull List<DomainRequirement> domainRequirements,
@NonNull CredentialsMatcher matcher) {
ListBoxModel result = new ListBoxModel();
Set<String> ids = new HashSet<String>();
if (ACL.SYSTEM.equals(authentication)) {
while (itemGroup != null) {
if (itemGroup instanceof AbstractFolder) {
final AbstractFolder<?> folder = AbstractFolder.class.cast(itemGroup);
FolderCredentialsProperty property = folder.getProperties().get(FolderCredentialsProperty.class);
if (property != null) {
for (C c : DomainCredentials.getCredentials(
property.getDomainCredentialsMap(),
type,
domainRequirements,
matcher)) {
if (ids.add(c.getId())) {
result.add(CredentialsNameProvider.name(c), c.getId());
}
}
}
}
if (itemGroup instanceof Item) {
itemGroup = ((Item)itemGroup).getParent();
} else {
break;
}
}
}
return result;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public <C extends IdCredentials> ListBoxModel getCredentialIds(@NonNull Class<C> type, @NonNull Item item,
@Nullable Authentication authentication,
@NonNull List<DomainRequirement> domainRequirements,
@NonNull CredentialsMatcher matcher) {
if (item instanceof AbstractFolder) {
// credentials defined in the folder should be available in the context of the folder
return getCredentialIds(type, (ItemGroup) item, authentication, domainRequirements, matcher);
}
return getCredentialIds(type, item.getParent(), authentication, domainRequirements, matcher);
}
/**
* {@inheritDoc}
*/
@Override
public CredentialsStore getStore(@CheckForNull ModelObject object) {
if (object instanceof AbstractFolder) {
final AbstractFolder<?> folder = AbstractFolder.class.cast(object);
FolderCredentialsProperty property = folder.getProperties().get(FolderCredentialsProperty.class);
if (property != null) {
return property.getStore();
}
synchronized (emptyProperties) {
property = emptyProperties.get(folder);
if (property == null) {
property = new FolderCredentialsProperty(folder);
emptyProperties.put(folder, property);
}
}
return property.getStore();
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public String getIconClassName() {
return "icon-credentials-folder-store";
}
/**
* Our property.
*/
public static class FolderCredentialsProperty extends AbstractFolderProperty<AbstractFolder<?>> {
/**
* Old store of credentials
*
* @deprecated
*/
@Deprecated
private transient List<Credentials> credentials;
/**
* Our credentials.
*
* @since 3.10
*/
private Map<Domain, List<Credentials>> domainCredentialsMap =
new CopyOnWriteMap.Hash<Domain, List<Credentials>>();
/**
* Our store.
*/
private transient StoreImpl store = new StoreImpl();
/*package*/ FolderCredentialsProperty(AbstractFolder<?> owner) {
setOwner(owner);
domainCredentialsMap = DomainCredentials.migrateListToMap(null, null);
}
/**
* Backwards compatibility.
*
* @param credentials the credentials.
* @deprecated
*/
@Deprecated
public FolderCredentialsProperty(List<Credentials> credentials) {
domainCredentialsMap = DomainCredentials.migrateListToMap(domainCredentialsMap, credentials);
}
/**
* Constructor for stapler.
*
* @param domainCredentials the credentials.
* @since 1.5
*/
@DataBoundConstructor
public FolderCredentialsProperty(DomainCredentials[] domainCredentials) {
domainCredentialsMap = DomainCredentials.asMap(Arrays.asList(domainCredentials));
}
/**
* Resolve old data store into new data store.
*
* @since 1.5
*/
@SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC", justification = "Only unprotected during deserialization")
@SuppressWarnings("deprecation")
private Object readResolve() throws ObjectStreamException {
if (domainCredentialsMap == null) {
domainCredentialsMap = DomainCredentials.migrateListToMap(domainCredentialsMap, credentials);
credentials = null;
}
return this;
}
public <C extends Credentials> List<C> getCredentials(Class<C> type) {
List<C> result = new ArrayList<C>();
for (Credentials credential : getCredentials()) {
if (type.isInstance(credential)) {
result.add(type.cast(credential));
}
}
return result;
}
/**
* Gets all the folder's credentials.
*
* @return all the folder's credentials.
*/
@SuppressWarnings("unused") // used by stapler
public List<Credentials> getCredentials() {
return getDomainCredentialsMap().get(Domain.global());
}
/**
* Returns the {@link com.cloudbees.plugins.credentials.domains.DomainCredentials}
*
* @return the {@link com.cloudbees.plugins.credentials.domains.DomainCredentials}
* @since 3.10
*/
@SuppressWarnings("unused") // used by stapler
public List<DomainCredentials> getDomainCredentials() {
return DomainCredentials.asList(getDomainCredentialsMap());
}
/**
* The Map of domain credentials.
*
* @since 3.10
*/
@SuppressWarnings("deprecation")
@NonNull
public synchronized Map<Domain, List<Credentials>> getDomainCredentialsMap() {
return domainCredentialsMap = DomainCredentials.migrateListToMap(domainCredentialsMap, credentials);
}
/**
* Sets the map of domain credentials.
*
* @param domainCredentialsMap the map of domain credentials.
* @since 3.10
*/
public synchronized void setDomainCredentialsMap(Map<Domain, List<Credentials>> domainCredentialsMap) {
this.domainCredentialsMap = DomainCredentials.toCopyOnWriteMap(domainCredentialsMap);
}
/**
* Returns the {@link StoreImpl}.
* @return the {@link StoreImpl}.
*/
@NonNull
public synchronized StoreImpl getStore() {
if (store == null) {
store = new StoreImpl();
}
return store;
}
/**
* Short-cut method for checking {@link CredentialsStore#hasPermission(hudson.security.Permission)}
*
* @param p the permission to check.
*/
private void checkPermission(Permission p) {
if (!store.hasPermission(p)) {
throw new AccessDeniedException2(Jenkins.getAuthentication(), p);
}
}
/**
* Short-cut method that redundantly checks the specified permission (to catch any typos) and then escalates
* authentication in order to save the {@link CredentialsStore}.
*
* @param p the permissions of the operation being performed.
* @throws IOException if something goes wrong.
*/
private void checkedSave(Permission p) throws IOException {
checkPermission(p);
SecurityContext orig = ACL.impersonate(ACL.SYSTEM);
try {
FolderCredentialsProperty property =
owner.getProperties().get(FolderCredentialsProperty.class);
if (property == null) {
synchronized (emptyProperties) {
owner.getProperties().add(this);
emptyProperties.remove(owner);
}
}
// we assume it is ourselves
owner.save();
} finally {
SecurityContextHolder.setContext(orig);
}
}
/**
* 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) {
checkedSave(CredentialsProvider.MANAGE_DOMAINS);
}
return modified;
} else {
domainCredentialsMap.put(domain, new ArrayList<Credentials>(credentials));
checkedSave(CredentialsProvider.MANAGE_DOMAINS);
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);
checkedSave(CredentialsProvider.MANAGE_DOMAINS);
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));
checkedSave(CredentialsProvider.MANAGE_DOMAINS);
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);
checkedSave(CredentialsProvider.CREATE);
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) {
if (store.hasPermission(CredentialsProvider.VIEW)) {
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);
checkedSave(CredentialsProvider.DELETE);
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);
checkedSave(CredentialsProvider.UPDATE);
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public AbstractFolderProperty<?> reconfigure(StaplerRequest req, JSONObject form) throws FormException {
return this;
}
/**
* Our {@link CredentialsStoreAction}.
* @since 5.11
*/
@Restricted(NoExternalUse.class)
public class CredentialsStoreActionImpl extends CredentialsStoreAction {
/**
* {@inheritDoc}
*/
@NonNull
@Override
public StoreImpl getStore() {
return FolderCredentialsProperty.this.getStore();
}
/**
* {@inheritDoc}
*/
@Override
public String getIconFileName() {
return isVisible()
? "/plugin/credentials/images/48x48/folder-store.png"
: null;
}
/**
* {@inheritDoc}
*/
@Override
public String getIconClassName() {
return isVisible()
? "icon-credentials-folder-store"
: null;
}
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return "Folder"; // TODO i18n
}
}
/**
* Our constructor
*/
@Extension(optional = true)
public static class DescriptorImpl extends AbstractFolderPropertyDescriptor {
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return Messages.FolderCredentialsProvider_DisplayName();
}
/**
* Gets all the {@link com.cloudbees.plugins.credentials.domains.DomainSpecification} descriptors.
*
* @return all the {@link com.cloudbees.plugins.credentials.domains.DomainSpecification} descriptors.
* @since 3.10
*/
@SuppressWarnings("unused") // used by stapler
public DescriptorExtensionList<DomainSpecification, Descriptor<DomainSpecification>>
getSpecificationDescriptors() {
return Jenkins.getActiveInstance().getDescriptorList(DomainSpecification.class);
}
}
/**
* Our actual {@link CredentialsStore}.
*/
private class StoreImpl extends CredentialsStore {
/**
* Our action.
*/
private final CredentialsStoreAction storeAction = new CredentialsStoreActionImpl();
/**
* {@inheritDoc}
*/
@Override
public ModelObject getContext() {
return owner;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasPermission(@NonNull Authentication a, @NonNull Permission permission) {
return owner.getACL().hasPermission(a, permission);
}
/**
* {@inheritDoc}
*/
@Override
public CredentialsStoreAction getStoreAction() {
return storeAction;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public List<Domain> getDomains() {
return Collections.unmodifiableList(new ArrayList<Domain>(
getDomainCredentialsMap().keySet()
));
}
/**
* {@inheritDoc}
*/
@Override
public boolean addDomain(@NonNull Domain domain, List<Credentials> credentials) throws IOException {
return FolderCredentialsProperty.this.addDomain(domain, credentials);
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeDomain(@NonNull Domain domain) throws IOException {
return FolderCredentialsProperty.this.removeDomain(domain);
}
/**
* {@inheritDoc}
*/
@Override
public boolean updateDomain(@NonNull Domain current, @NonNull Domain replacement) throws IOException {
return FolderCredentialsProperty.this.updateDomain(current, replacement);
}
/**
* {@inheritDoc}
*/
@Override
public boolean addCredentials(@NonNull Domain domain, @NonNull Credentials credentials) throws IOException {
return FolderCredentialsProperty.this.addCredentials(domain, credentials);
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public List<Credentials> getCredentials(@NonNull Domain domain) {
return FolderCredentialsProperty.this.getCredentials(domain);
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeCredentials(@NonNull Domain domain, @NonNull Credentials credentials)
throws IOException {
return FolderCredentialsProperty.this.removeCredentials(domain, credentials);
}
/**
* {@inheritDoc}
*/
@Override
public boolean updateCredentials(@NonNull Domain domain, @NonNull Credentials current,
@NonNull Credentials replacement) throws IOException {
return FolderCredentialsProperty.this.updateCredentials(domain, current, replacement);
}
}
}
}