/* * The MIT License * * Copyright 2015 Jesse Glick. * * 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 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.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 java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContextHolder; import org.jvnet.hudson.test.MockFolder; /** * Analogue of <a href="https://github.com/jenkinsci/cloudbees-folder-plugin/blob/cloudbees-folder-4.2.3/src/main/java/com/cloudbees/hudson/plugins/folder/properties/FolderCredentialsProvider.java">{@code FolderCredentialsProvider}</a> for {@link MockFolder}. */ @Extension public class MockFolderCredentialsProvider extends CredentialsProvider { private static final Map<MockFolder,FolderCredentialsProperty> properties = new java.util.WeakHashMap<MockFolder,FolderCredentialsProperty>(); private static synchronized FolderCredentialsProperty getProperty(MockFolder folder) { FolderCredentialsProperty property = properties.get(folder); if (property == null) { property = new FolderCredentialsProperty(folder); properties.put(folder, property); } return property; } private static final Set<CredentialsScope> SCOPES = Collections.<CredentialsScope>singleton(CredentialsScope.GLOBAL); @Override public Set<CredentialsScope> getScopes(ModelObject object) { if (object instanceof MockFolder) { return SCOPES; } return super.getScopes(object); } @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()); } @NonNull @Override public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> type, @Nullable ItemGroup itemGroup, @Nullable Authentication authentication, @NonNull List<DomainRequirement> domainRequirements) { if (authentication == null) { authentication = ACL.SYSTEM; } List<C> result = new ArrayList<C>(); if (ACL.SYSTEM.equals(authentication)) { while (itemGroup != null) { if (itemGroup instanceof MockFolder) { final MockFolder folder = MockFolder.class.cast(itemGroup); FolderCredentialsProperty property = getProperty(folder); result.addAll(DomainCredentials.getCredentials( property.getDomainCredentialsMap(), type, domainRequirements, CredentialsMatchers.always())); } if (itemGroup instanceof Item) { itemGroup = Item.class.cast(itemGroup).getParent(); } else { break; } } } return result; } @Override public CredentialsStore getStore(@CheckForNull ModelObject object) { if (object instanceof MockFolder) { final MockFolder folder = MockFolder.class.cast(object); return getProperty(folder).getStore(); } return null; } private static class FolderCredentialsProperty { private final MockFolder owner; /** * 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(); FolderCredentialsProperty(MockFolder owner) { this.owner = owner; } 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()); } /** * 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); } public synchronized CredentialsStore 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); Authentication old = SecurityContextHolder.getContext().getAuthentication(); SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM); try { owner.save(); } finally { SecurityContextHolder.getContext().setAuthentication(old); } } /** * 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; } private class StoreImpl extends CredentialsStore { @NonNull @Override public ModelObject getContext() { return owner; } @Override public boolean hasPermission(@NonNull Authentication a, @NonNull Permission permission) { return owner.getACL().hasPermission(a, permission); } /** * {@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); } } } }