/*
* 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 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.Extension;
import hudson.ExtensionList;
import hudson.XmlFile;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ModelObject;
import hudson.model.Saveable;
import hudson.model.listeners.SaveableListener;
import hudson.security.ACL;
import hudson.security.Permission;
import hudson.util.CopyOnWriteMap;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContextHolder;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.always;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.not;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.withScope;
import static com.cloudbees.plugins.credentials.CredentialsScope.GLOBAL;
import static com.cloudbees.plugins.credentials.CredentialsScope.SYSTEM;
/**
* The root store of credentials.
*/
@Extension
public class SystemCredentialsProvider extends AbstractDescribableImpl<SystemCredentialsProvider>
implements Saveable {
/**
* Our logger.
*/
private static final Logger LOGGER = Logger.getLogger(SystemCredentialsProvider.class.getName());
/**
* Old store of credentials
*
* @deprecated migrate to {@link #domainCredentialsMap}.
*/
@Deprecated
private transient List<Credentials> credentials = new CopyOnWriteArrayList<Credentials>();
/**
* Our credentials.
*
* @since 1.5
*/
private Map<Domain, List<Credentials>> domainCredentialsMap = new CopyOnWriteMap.Hash<Domain, List<Credentials>>();
/**
* Our backing store.
*/
private transient StoreImpl store = new StoreImpl();
/**
* Constructor.
*/
@SuppressWarnings("deprecation")
public SystemCredentialsProvider() {
try {
XmlFile xml = getConfigFile();
if (xml.exists()) {
xml.unmarshal(this);
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Failed to read the existing credentials", e);
}
domainCredentialsMap = DomainCredentials.migrateListToMap(domainCredentialsMap, credentials);
credentials = null;
}
/**
* Gets the configuration file that this {@link CredentialsProvider} uses to store its credentials.
*
* @return the configuration file that this {@link CredentialsProvider} uses to store its credentials.
*/
public static XmlFile getConfigFile() {
// TODO switch to Jenkins.getInstance() once 2.0+ is the baseline
return new XmlFile(Jenkins.XSTREAM2, new File(Jenkins.getActiveInstance().getRootDir(), "credentials.xml"));
}
/**
* Gets the singleton instance.
*
* @return the singleton instance.
*/
public static SystemCredentialsProvider getInstance() {
return ExtensionList.lookup(SystemCredentialsProvider.class).get(SystemCredentialsProvider.class);
}
/**
* Get all the ({@link Domain#global()}) credentials.
*
* @return all the ({@link Domain#global()}) credentials.
*/
@SuppressWarnings("unused") // used by stapler
public List<Credentials> getCredentials() {
return domainCredentialsMap.get(Domain.global());
}
/**
* Get all the credentials.
*
* @return all the credentials.
* @since 1.5
*/
@SuppressWarnings("unused") // used by stapler
public List<DomainCredentials> getDomainCredentials() {
return DomainCredentials.asList(getDomainCredentialsMap());
}
/**
* Get all the credentials.
*
* @return all the credentials.
* @since 1.5
*/
@SuppressWarnings("deprecation")
@NonNull
public synchronized Map<Domain, List<Credentials>> getDomainCredentialsMap() {
return domainCredentialsMap = DomainCredentials.migrateListToMap(domainCredentialsMap, credentials);
}
/**
* Set all the credentials.
*
* @param domainCredentialsMap all the credentials.
* @since 1.5
*/
public synchronized void setDomainCredentialsMap(Map<Domain, List<Credentials>> domainCredentialsMap) {
this.domainCredentialsMap = DomainCredentials.toCopyOnWriteMap(domainCredentialsMap);
}
/**
* Short-cut method for {@link Jenkins#checkPermission(hudson.security.Permission)}
*
* @param p the permission to check.
*/
private void checkPermission(Permission p) {
// TODO switch to Jenkins.getInstance() once 2.0+ is the baseline
Jenkins.getActiveInstance().checkPermission(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 {
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) {
// TODO switch to Jenkins.getInstance() once 2.0+ is the baseline
if (Jenkins.getActiveInstance().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;
}
/**
* Implementation for {@link ProviderImpl} to delegate to while keeping the lock synchronization simple.
*/
private synchronized StoreImpl getStore() {
if (store == null) {
store = new StoreImpl();
}
return store;
}
/**
* {@inheritDoc}
*/
public void save() throws IOException {
checkPermission(Jenkins.ADMINISTER);
if (BulkChange.contains(this)) {
return;
}
XmlFile configFile = getConfigFile();
configFile.write(this);
SaveableListener.fireOnChange(this, configFile);
}
/**
* Our management link descriptor.
*/
@Extension
@SuppressWarnings("unused") // used by Jenkins
public static final class DescriptorImpl extends Descriptor<SystemCredentialsProvider> {
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return "";
}
}
@Extension
@SuppressWarnings("unused") // used by Jenkins
public static class ProviderImpl extends CredentialsProvider {
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return Messages.SystemCredentialsProvider_ProviderImpl_DisplayName();
}
/**
* The scopes that are relevant to the store.
*/
private static final Set<CredentialsScope> SCOPES =
Collections.unmodifiableSet(new LinkedHashSet<CredentialsScope>(Arrays.asList(GLOBAL, SYSTEM)));
/**
* {@inheritDoc}
*/
@Override
public Set<CredentialsScope> getScopes(ModelObject object) {
if (object instanceof Jenkins || object instanceof SystemCredentialsProvider) {
return SCOPES;
}
return super.getScopes(object);
}
/**
* {@inheritDoc}
*/
@Override
public CredentialsStore getStore(@CheckForNull ModelObject object) {
if (object == Jenkins.getInstance()) {
return SystemCredentialsProvider.getInstance().getStore();
}
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) {
if (ACL.SYSTEM.equals(authentication)) {
CredentialsMatcher matcher = Jenkins.getInstance() == itemGroup ? always() : not(withScope(SYSTEM));
return DomainCredentials.getCredentials(SystemCredentialsProvider.getInstance()
.getDomainCredentialsMap(), type, domainRequirements, matcher);
}
return new ArrayList<C>();
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> type, @NonNull Item item,
@Nullable Authentication authentication) {
return getCredentials(type, item, authentication, Collections.<DomainRequirement>emptyList());
}
/**
* {@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 (ACL.SYSTEM.equals(authentication)) {
return DomainCredentials.getCredentials(SystemCredentialsProvider.getInstance()
.getDomainCredentialsMap(), type, domainRequirements, not(withScope(SYSTEM)));
}
return new ArrayList<C>();
}
@Override
public String getIconClassName() {
return "icon-credentials-system-store";
}
}
/**
* Our {@link CredentialsStore}.
*/
@ExportedBean
public static class StoreImpl extends CredentialsStore {
/**
* Our store action.
*/
private final UserFacingAction storeAction = new UserFacingAction();
/**
* Default constructor.
*/
public StoreImpl() {
super(ProviderImpl.class);
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public ModelObject getContext() {
// TODO switch to Jenkins.getInstance() once 2.0+ is the baseline
return Jenkins.getActiveInstance();
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasPermission(@NonNull Authentication a, @NonNull Permission permission) {
// we follow the permissions of Jenkins itself
return getACL().hasPermission(a, permission);
}
public ACL getACL() {
// TODO switch to Jenkins.getInstance() once 2.0+ is the baseline
return Jenkins.getActiveInstance().getACL();
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
@Exported
public List<Domain> getDomains() {
return Collections.unmodifiableList(new ArrayList<Domain>(
SystemCredentialsProvider.getInstance().getDomainCredentialsMap().keySet()
));
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
@Exported
public List<Credentials> getCredentials(@NonNull Domain domain) {
return SystemCredentialsProvider.getInstance().getCredentials(domain);
}
/**
* {@inheritDoc}
*/
@Override
public boolean addDomain(@NonNull Domain domain, List<Credentials> credentials) throws IOException {
return SystemCredentialsProvider.getInstance().addDomain(domain, credentials);
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeDomain(@NonNull Domain domain) throws IOException {
return SystemCredentialsProvider.getInstance().removeDomain(domain);
}
/**
* {@inheritDoc}
*/
@Override
public boolean updateDomain(@NonNull Domain current, @NonNull Domain replacement) throws IOException {
return SystemCredentialsProvider.getInstance().updateDomain(current, replacement);
}
/**
* {@inheritDoc}
*/
@Override
public boolean addCredentials(@NonNull Domain domain, @NonNull Credentials credentials) throws IOException {
return SystemCredentialsProvider.getInstance().addCredentials(domain, credentials);
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeCredentials(@NonNull Domain domain, @NonNull Credentials credentials) throws IOException {
return SystemCredentialsProvider.getInstance().removeCredentials(domain, credentials);
}
/**
* {@inheritDoc}
*/
@Override
public boolean updateCredentials(@NonNull Domain domain, @NonNull Credentials current,
@NonNull Credentials replacement) throws IOException {
return SystemCredentialsProvider.getInstance().updateCredentials(domain, current, replacement);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
public CredentialsStoreAction getStoreAction() {
return storeAction;
}
/**
* {@inheritDoc}
*/
@Override
public void save() throws IOException {
if (BulkChange.contains(this)) {
return;
}
SystemCredentialsProvider.getInstance().save();
}
}
/**
* Expose the store.
*/
@ExportedBean
public static class UserFacingAction extends CredentialsStoreAction {
/**
* {@inheritDoc}
*/
@NonNull
public CredentialsStore getStore() {
return SystemCredentialsProvider.getInstance().getStore();
}
/**
* {@inheritDoc}
*/
@Override
public String getIconFileName() {
return isVisible()
? "/plugin/credentials/images/24x24/system-store.png"
: null;
}
/**
* {@inheritDoc}
*/
@Override
public String getIconClassName() {
return isVisible()
? "icon-credentials-system-store"
: null;
}
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return Messages.SystemCredentialsProvider_UserFacingAction_DisplayName();
}
}
}