/* * The MIT License * * Copyright (c) 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.common.IdCredentials; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.domains.Domain; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.model.Action; import hudson.model.Api; import hudson.model.Item; import hudson.model.ItemGroup; import hudson.model.ModelObject; import hudson.model.RootAction; import hudson.model.TopLevelItem; import hudson.model.TransientUserActionFactory; import hudson.model.User; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; import java.io.IOException; import java.util.ArrayList; 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.TreeMap; import javax.annotation.Nonnull; import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithContextMenu; import jenkins.model.TransientActionFactory; import org.acegisecurity.AccessDeniedException; import org.acegisecurity.Authentication; import org.jenkins.ui.icon.IconSpec; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; /** * An {@link Action} that lets you view the available credentials for any {@link ModelObject}. */ @ExportedBean public class ViewCredentialsAction implements Action, IconSpec, AccessControlled, ModelObjectWithContextMenu { /** * Expose {@link CredentialsProvider#VIEW} for Jelly. */ public static final Permission VIEW = CredentialsProvider.VIEW; /** * The context in which this {@link ViewCredentialsAction} was created. */ private final ModelObject context; /** * Constructor. * * @param context the context. */ public ViewCredentialsAction(ModelObject context) { this.context = context; } /** * Gets the context. * * @return the context. */ public ModelObject getContext() { return context; } /** * {@inheritDoc} */ @Override public String getIconFileName() { return isVisible() ? "/plugin/credentials/images/24x24/credentials.png" : null; } /** * Exposes the {@link CredentialsStore} instances available to the {@link #getContext()}. * * @return the {@link CredentialsStore} instances available to the {@link #getContext()}. */ @NonNull public List<CredentialsStore> getParentStores() { List<CredentialsStore> result = new ArrayList<CredentialsStore>(); for (CredentialsStore s : CredentialsProvider.lookupStores(getContext())) { if (context != s.getContext() && s.hasPermission(CredentialsProvider.VIEW)) { result.add(s); } } return result; } /** * Exposes the {@link CredentialsStore} instances available to the {@link #getContext()}. * * @return the {@link CredentialsStore} instances available to the {@link #getContext()}. */ @NonNull public List<CredentialsStore> getLocalStores() { List<CredentialsStore> result = new ArrayList<CredentialsStore>(); for (CredentialsStore s : CredentialsProvider.lookupStores(getContext())) { if (context == s.getContext() && s.hasPermission(CredentialsProvider.VIEW)) { result.add(s); } } return result; } /** * Exposes the {@link #getLocalStores()} {@link CredentialsStore#getStoreAction()}. * * @return the {@link #getLocalStores()} {@link CredentialsStore#getStoreAction()}. */ @NonNull @SuppressWarnings("unused") // Jelly EL public List<CredentialsStoreAction> getStoreActions() { List<CredentialsStoreAction> result = new ArrayList<CredentialsStoreAction>(); for (final CredentialsStore s : CredentialsProvider.lookupStores(getContext())) { if (context == s.getContext() && s.hasPermission(CredentialsProvider.VIEW)) { CredentialsStoreAction action = s.getStoreAction(); if (action != null) { result.add(action); } } } return result; } /** * Exposes the {@link #getLocalStores()} for the XML API. * * @return the {@link #getLocalStores()} for the XML API. * @since 2.1.0 */ @NonNull @SuppressWarnings("unused") // Stapler XML/JSON API @Exported(name = "stores") public Map<String,CredentialsStoreAction> getStoreActionsMap() { Map<String,CredentialsStoreAction> result = new TreeMap<String, CredentialsStoreAction>(); for (CredentialsStoreAction a: getStoreActions()) { result.put(a.getUrlName(), a); } return result; } /** * Exposes the {@link #getStoreActions()} by {@link CredentialsStoreAction#getUrlName()} for Stapler. * * @param name the {@link CredentialsStoreAction#getUrlName()} to match. * @return the {@link CredentialsStoreAction} or {@code null} */ @CheckForNull @SuppressWarnings("unused") // Stapler binding public CredentialsStoreAction getStore(String name) { for (final CredentialsStore s : CredentialsProvider.lookupStores(getContext())) { if (context == s.getContext()) { // local stores only CredentialsStoreAction action = s.getStoreAction(); if (action != null && name.equals(action.getUrlName())) { return s.hasPermission(CredentialsProvider.VIEW) ? action : null; } } } return null; } /** * {@inheritDoc} */ @Override public String getDisplayName() { return Messages.CredentialsStoreAction_DisplayName(); } /** * {@inheritDoc} */ @Override public String getUrlName() { return "credentials"; } public String getStoreBaseUrl(String itUrl) { return itUrl.isEmpty() || itUrl.endsWith("/") ? itUrl + getUrlName() + "/store/" : itUrl + "/" + getUrlName() + "/store/"; } /** * Tests if the {@link ViewCredentialsAction} should be visible. * * @return {@code true} if the action should be visible. */ public boolean isVisible() { if (context instanceof AccessControlled && !((AccessControlled) context).hasPermission(CredentialsProvider.VIEW)) { // must have permission return false; } for (CredentialsProvider p : CredentialsProvider.enabled(context)) { if (p.hasCredentialsDescriptors()) { // at least one provider must have the potential for at least one type return true; } } return false; } /** * Expose a Jenkins {@link Api}. * * @return the {@link Api}. */ public Api getApi() { return new Api(this); } /** * Returns the credential entries. * * @return the credential entries. */ public List<TableEntry> getTableEntries() { List<TableEntry> result = new ArrayList<TableEntry>(); Item item = context instanceof Item ? (Item) context : null; ItemGroup group = context instanceof ItemGroup ? (ItemGroup) context : context instanceof User ? Jenkins.getActiveInstance() : null; Set<String> ids = new HashSet<String>(); for (CredentialsStore p : CredentialsProvider.lookupStores(context)) { if (p.hasPermission(CredentialsProvider.VIEW)) { for (Domain domain : p.getDomains()) { for (Credentials c : p.getCredentials(domain)) { CredentialsScope scope = c.getScope(); if (scope != null && !scope.isVisible(context)) { continue; } boolean masked; if (c instanceof IdCredentials) { String id = ((IdCredentials) c).getId(); masked = ids.contains(id); ids.add(id); } else { masked = false; } result.add(new TableEntry(p.getProvider(), p, domain, c, masked)); } } } } return result; } /** * {@inheritDoc} */ @Override public String getIconClassName() { return isVisible() ? "icon-credentials-credentials" : null; } /** * Returns the full name of this action. * * @return the full name of this action. */ public final String getFullName() { String n = getContextFullName(); if (n.length() == 0) { return getUrlName(); } else { return n + '/' + getUrlName(); } } /** * Returns the full name of the {@link #getContext()}. * * @return the full name of the {@link #getContext()}. */ public String getContextFullName() { String n; if (context instanceof Item) { n = ((Item) context).getFullName(); } else if (context instanceof ItemGroup) { n = ((ItemGroup) context).getFullName(); } else if (context instanceof User) { n = "user/" + ((User) context).getId(); } else { n = ""; } return n; } /** * Returns the full display name of this action. * * @return the full display name of this action. */ public final String getFullDisplayName() { String n = getContextFullDisplayName(); if (n.length() == 0) { return getDisplayName(); } else { return n + " \u00BB " + getDisplayName(); } } /** * Returns the full display name of the {@link #getContext()}. * * @return the full display name of the {@link #getContext()}. */ public String getContextFullDisplayName() { String n; if (context instanceof Item) { n = ((Item) context).getFullDisplayName(); } else if (context instanceof Jenkins) { n = context.getDisplayName(); } else if (context instanceof ItemGroup) { n = ((ItemGroup) context).getFullDisplayName(); } else if (context instanceof User) { n = Messages.CredentialsStoreAction_UserDisplayName(((User) context).getDisplayName()); } else { // TODO switch to Jenkins.getInstance() once 2.0+ is the baseline n = Jenkins.getActiveInstance().getFullDisplayName(); } return n; } /** * {@inheritDoc} */ @Nonnull @Override public ACL getACL() { final AccessControlled accessControlled = context instanceof AccessControlled ? (AccessControlled) context : Jenkins.getActiveInstance(); return new ACL() { @Override public boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission permission) { if (accessControlled.getACL().hasPermission(a, permission)) { for (CredentialsStore s : getLocalStores()) { if (s.hasPermission(a, permission)) { return true; } } } return false; } }; } /** * {@inheritDoc} */ @Override public void checkPermission(@Nonnull Permission permission) throws AccessDeniedException { getACL().checkPermission(permission); } /** * {@inheritDoc} */ @Override public boolean hasPermission(@Nonnull Permission permission) { return getACL().hasPermission(permission); } /** * {@inheritDoc} */ // In the general case we would implement ModelObjectWithChildren as the child actions could be viewed as children // but in this case we expose them in the sidebar, so they are more correctly part of the context menu. @Override public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { ContextMenu menu = new ContextMenu(); for (CredentialsStoreAction action : getStoreActions()) { ContextMenuIconUtils.addMenuItem( menu, "store", action, action.getContextMenu(ContextMenuIconUtils.buildUrl("store", action.getUrlName())) ); } return menu; } /** * Add the {@link ViewCredentialsAction} to all {@link TopLevelItem} instances. */ @Extension(ordinal = -1000) public static class TransientTopLevelItemActionFactoryImpl extends TransientActionFactory<TopLevelItem> { /** * {@inheritDoc} */ @Override public Class<TopLevelItem> type() { return TopLevelItem.class; } /** * {@inheritDoc} */ @Nonnull @Override public Collection<? extends Action> createFor(@Nonnull TopLevelItem target) { return Collections.singleton(new ViewCredentialsAction(target)); } } /** * Add the {@link ViewCredentialsAction} to all {@link User} instances. */ @Extension(ordinal = -1000) public static class TransientUserActionFactoryImpl extends TransientUserActionFactory { /** * {@inheritDoc} */ @Override public Collection<? extends Action> createFor(User target) { return Collections.singleton(new ViewCredentialsAction(target)); } } /** * Add the {@link ViewCredentialsAction} to the {@link Jenkins} root. */ @Extension(ordinal = -1000) public static class RootActionImpl extends ViewCredentialsAction implements RootAction { /** * Our constructor. */ public RootActionImpl() { super(Jenkins.getActiveInstance()); } } /** * Value class to simplify creating the table. */ public static class TableEntry implements IconSpec { /** * The backing {@link Credentials}. */ private final Credentials credentials; /** * The backing {@link CredentialsProvider}. */ private final CredentialsProvider provider; /** * The backing {@link CredentialsStore}. */ private final CredentialsStore store; /** * The backing {@link Domain}. */ private final Domain domain; /** * Whether this entry's ID is being masked by another entry. */ private final boolean masked; /** * Constructor. * * @param provider the backing {@link CredentialsProvider}. * @param store the backing {@link CredentialsStore}. * @param domain the backing {@link Domain}. * @param credentials the backing {@link Credentials}. * @param masked whether this entry is masked or not. */ public TableEntry(CredentialsProvider provider, CredentialsStore store, Domain domain, Credentials credentials, boolean masked) { this.provider = provider; this.store = store; this.domain = domain; this.credentials = credentials; this.masked = masked; } /** * Returns the {@link IdCredentials#getId()} of the {@link #credentials}. * * @return the {@link IdCredentials#getId()} of the {@link #credentials}. */ public String getId() { return credentials instanceof IdCredentials ? ((IdCredentials) credentials).getId() : null; } /** * Returns the {@link Credentials#getScope()} of the {@link #credentials}. * * @return the {@link Credentials#getScope()} of the {@link #credentials}. */ public CredentialsScope getScope() { return credentials.getScope(); } /** * Returns the {@link CredentialsNameProvider#name(Credentials)} of the {@link #credentials}. * * @return the {@link CredentialsNameProvider#name(Credentials)} of the {@link #credentials}. */ public String getName() { return CredentialsNameProvider.name(credentials); } /** * Returns the {@link StandardCredentials#getDescription()} of the {@link #credentials}. * * @return the {@link StandardCredentials#getDescription()} of the {@link #credentials}. * @throws IOException if there was an issue with formatting this using the markup formatter. */ public String getDescription() throws IOException { return credentials instanceof StandardCredentials ? Jenkins.getActiveInstance().getMarkupFormatter() .translate(((StandardCredentials) credentials).getDescription()) : null; } /** * Returns the {@link CredentialsDescriptor#getDisplayName()}. * * @return the {@link CredentialsDescriptor#getDisplayName()}. */ public String getKind() { return credentials.getDescriptor().getDisplayName(); } /** * Exposes the {@link CredentialsProvider}. * * @return the {@link CredentialsProvider}. */ public CredentialsProvider getProvider() { return provider; } /** * Exposes the {@link Domain}. * * @return the {@link Domain}. */ public Domain getDomain() { return domain; } /** * Exposes the {@link CredentialsStore}. * * @return the {@link CredentialsStore}. */ public CredentialsStore getStore() { return store; } /** * {@inheritDoc} */ @Override public String getIconClassName() { return credentials.getDescriptor().getIconClassName(); } /** * Exposes if this {@link Credentials}'s ID is masked by another credential. * * @return {@code true} if there is a closer credential with the same ID. */ public boolean isMasked() { return masked; } } }