/* * The MIT License * * Copyright (c) 2004-2012, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, * Tom Huybrechts, Vincent Latombe * * 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 hudson.model; import com.google.common.base.Predicate; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import hudson.BulkChange; import hudson.CopyOnWrite; import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.FeedAdapter; import hudson.Util; import hudson.XmlFile; import hudson.init.InitMilestone; import hudson.init.Initializer; import hudson.model.Descriptor.FormException; import hudson.model.listeners.SaveableListener; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.security.SecurityRealm; import hudson.security.UserMayOrMayNotExistException; import hudson.util.FormApply; import hudson.util.FormValidation; import hudson.util.RunList; import hudson.util.XStream2; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import jenkins.model.IdStrategy; import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithContextMenu; import jenkins.security.ImpersonatingUserDetailsService; import jenkins.security.LastGrantedAuthoritiesProperty; import jenkins.security.UserDetailsCache; import jenkins.util.SystemProperties; import net.sf.json.JSONObject; import org.acegisecurity.Authentication; import org.acegisecurity.GrantedAuthority; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.acegisecurity.userdetails.UserDetails; import org.acegisecurity.userdetails.UsernameNotFoundException; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.apache.commons.lang.StringUtils; import org.jenkinsci.Symbol; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; import org.springframework.dao.DataAccessException; /** * Represents a user. * *

* In Hudson, {@link User} objects are created in on-demand basis; * for example, when a build is performed, its change log is computed * and as a result commits from users who Hudson has never seen may be discovered. * When this happens, new {@link User} object is created. * *

* If the persisted record for an user exists, the information is loaded at * that point, but if there's no such record, a fresh instance is created from * thin air (this is where {@link UserPropertyDescriptor#newInstance(User)} is * called to provide initial {@link UserProperty} objects. * *

* Such newly created {@link User} objects will be simply GC-ed without * ever leaving the persisted record, unless {@link User#save()} method * is explicitly invoked (perhaps as a result of a browser submitting a * configuration.) * * * @author Kohsuke Kawaguchi */ @ExportedBean public class User extends AbstractModelObject implements AccessControlled, DescriptorByNameOwner, Saveable, Comparable, ModelObjectWithContextMenu, StaplerProxy { /** * The username of the 'unknown' user used to avoid null user references. */ private static final String UNKNOWN_USERNAME = "unknown"; /** * These usernames should not be used by real users logging into Jenkins. Therefore, we prevent * users with these names from being saved. */ private static final String[] ILLEGAL_PERSISTED_USERNAMES = new String[]{ACL.ANONYMOUS_USERNAME, ACL.SYSTEM_USERNAME, UNKNOWN_USERNAME}; private transient final String id; private volatile String fullName; private volatile String description; /** * List of {@link UserProperty}s configured for this project. */ @CopyOnWrite private volatile List properties = new ArrayList(); private User(String id, String fullName) { this.id = id; this.fullName = fullName; load(); } /** * Returns the {@link jenkins.model.IdStrategy} for use with {@link User} instances. See * {@link hudson.security.SecurityRealm#getUserIdStrategy()} * * @return the {@link jenkins.model.IdStrategy} for use with {@link User} instances. * @since 1.566 */ @Nonnull public static IdStrategy idStrategy() { Jenkins j = Jenkins.getInstance(); SecurityRealm realm = j.getSecurityRealm(); if (realm == null) { return IdStrategy.CASE_INSENSITIVE; } return realm.getUserIdStrategy(); } public int compareTo(User that) { return idStrategy().compare(this.id, that.id); } /** * Loads the other data from disk if it's available. */ private synchronized void load() { properties.clear(); XmlFile config = getConfigFile(); try { if(config.exists()) config.unmarshal(this); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to load "+config,e); } // remove nulls that have failed to load for (Iterator itr = properties.iterator(); itr.hasNext();) { if(itr.next()==null) itr.remove(); } // allocate default instances if needed. // doing so after load makes sure that newly added user properties do get reflected for (UserPropertyDescriptor d : UserProperty.all()) { if(getProperty(d.clazz)==null) { UserProperty up = d.newInstance(this); if(up!=null) properties.add(up); } } for (UserProperty p : properties) p.setUser(this); } @Exported public String getId() { return id; } public @Nonnull String getUrl() { return "user/"+Util.rawEncode(idStrategy().keyFor(id)); } public @Nonnull String getSearchUrl() { return "/user/"+Util.rawEncode(idStrategy().keyFor(id)); } /** * The URL of the user page. */ @Exported(visibility=999) public @Nonnull String getAbsoluteUrl() { return Jenkins.getInstance().getRootUrl()+getUrl(); } /** * Gets the human readable name of this user. * This is configurable by the user. */ @Exported(visibility=999) public @Nonnull String getFullName() { return fullName; } /** * Sets the human readable name of the user. * If the input parameter is empty, the user's ID will be set. */ public void setFullName(String name) { if(Util.fixEmptyAndTrim(name)==null) name=id; this.fullName = name; } @Exported public @CheckForNull String getDescription() { return description; } /** * Sets the description of the user. * @since 1.609 */ public void setDescription(String description) { this.description = description; } /** * Gets the user properties configured for this user. */ public Map,UserProperty> getProperties() { return Descriptor.toMap(properties); } /** * Updates the user object by adding a property. */ public synchronized void addProperty(@Nonnull UserProperty p) throws IOException { UserProperty old = getProperty(p.getClass()); List ps = new ArrayList(properties); if(old!=null) ps.remove(old); ps.add(p); p.setUser(this); properties = ps; save(); } /** * List of all {@link UserProperty}s exposed primarily for the remoting API. */ @Exported(name="property",inline=true) public List getAllProperties() { if (hasPermission(Jenkins.ADMINISTER)) { return Collections.unmodifiableList(properties); } return Collections.emptyList(); } /** * Gets the specific property, or null. */ public T getProperty(Class clazz) { for (UserProperty p : properties) { if(clazz.isInstance(p)) return clazz.cast(p); } return null; } /** * Creates an {@link Authentication} object that represents this user. * * This method checks with {@link SecurityRealm} if the user is a valid user that can login to the security realm. * If {@link SecurityRealm} is a kind that does not support querying information about other users, this will * use {@link LastGrantedAuthoritiesProperty} to pick up the granted authorities as of the last time the user has * logged in. * * @throws UsernameNotFoundException * If this user is not a valid user in the backend {@link SecurityRealm}. * @since 1.419 */ public @Nonnull Authentication impersonate() throws UsernameNotFoundException { try { UserDetails u = new ImpersonatingUserDetailsService( Jenkins.getInstance().getSecurityRealm().getSecurityComponents().userDetails).loadUserByUsername(id); return new UsernamePasswordAuthenticationToken(u.getUsername(), "", u.getAuthorities()); } catch (UserMayOrMayNotExistException e) { // backend can't load information about other users. so use the stored information if available } catch (UsernameNotFoundException e) { // if the user no longer exists in the backend, we need to refuse impersonating this user if (!ALLOW_NON_EXISTENT_USER_TO_LOGIN) throw e; } catch (DataAccessException e) { // seems like it's in the same boat as UserMayOrMayNotExistException } // seems like a legitimate user we have no idea about. proceed with minimum access return new UsernamePasswordAuthenticationToken(id, "", new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY}); } /** * Accepts the new description. */ @RequirePOST public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { checkPermission(Jenkins.ADMINISTER); description = req.getParameter("description"); save(); rsp.sendRedirect("."); // go to the top page } /** * Gets the fallback "unknown" user instance. *

* This is used to avoid null {@link User} instance. */ public static @Nonnull User getUnknown() { return getById(UNKNOWN_USERNAME, true); } /** * Gets the {@link User} object by its id or full name. * * @param create * If true, this method will never return null for valid input * (by creating a new {@link User} object if none exists.) * If false, this method will return null if {@link User} object * with the given name doesn't exist. * @return Requested user. May be {@code null} if a user does not exist and * {@code create} is false. * @deprecated use {@link User#get(String, boolean, java.util.Map)} */ @Deprecated public static @Nullable User get(String idOrFullName, boolean create) { return get(idOrFullName, create, Collections.emptyMap()); } /** * Gets the {@link User} object by its id or full name. * * @param create * If true, this method will never return null for valid input * (by creating a new {@link User} object if none exists.) * If false, this method will return null if {@link User} object * with the given name doesn't exist. * * @param context * contextual environment this user idOfFullName was retrieved from, * that can help resolve the user ID * * @return * An existing or created user. May be {@code null} if a user does not exist and * {@code create} is false. */ public static @Nullable User get(String idOrFullName, boolean create, Map context) { if(idOrFullName==null) return null; // sort resolvers by priority List resolvers = new ArrayList(ExtensionList.lookup(CanonicalIdResolver.class)); Collections.sort(resolvers); String id = null; for (CanonicalIdResolver resolver : resolvers) { id = resolver.resolveCanonicalId(idOrFullName, context); if (id != null) { LOGGER.log(Level.FINE, "{0} mapped {1} to {2}", new Object[] {resolver, idOrFullName, id}); break; } } // DefaultUserCanonicalIdResolver will always return a non-null id if all other CanonicalIdResolver failed if (id == null) { throw new IllegalStateException("The user id should be always non-null thanks to DefaultUserCanonicalIdResolver"); } return getOrCreate(id, idOrFullName, create); } /** * Retrieve a user by its ID, and create a new one if requested. * @return * An existing or created user. May be {@code null} if a user does not exist and * {@code create} is false. */ private static @Nullable User getOrCreate(@Nonnull String id, @Nonnull String fullName, boolean create) { return getOrCreate(id, fullName, create, getUnsanitizedLegacyConfigFileFor(id)); } private static @Nullable User getOrCreate(@Nonnull String id, @Nonnull String fullName, boolean create, File unsanitizedLegacyConfigFile) { String idkey = idStrategy().keyFor(id); byNameLock.readLock().lock(); User u; try { u = AllUsers.byName().get(idkey); } finally { byNameLock.readLock().unlock(); } final File configFile = getConfigFileFor(id); boolean mustMigrateLegacyConfig = isMigrationRequiredForLegacyConfigFile(unsanitizedLegacyConfigFile, configFile); if (mustMigrateLegacyConfig) { File ancestor = unsanitizedLegacyConfigFile.getParentFile(); if (!configFile.exists()) { try { Files.createDirectory(configFile.getParentFile().toPath()); Files.move(unsanitizedLegacyConfigFile.toPath(), configFile.toPath()); LOGGER.log(Level.INFO, "Migrated user record from {0} to {1}", new Object[] {unsanitizedLegacyConfigFile, configFile}); } catch (IOException | InvalidPathException e) { LOGGER.log( Level.WARNING, String.format("Failed to migrate user record from %s to %s", unsanitizedLegacyConfigFile, configFile), e); } } // Don't clean up ancestors with other children; the directories should be cleaned up when the last child // is migrated File tmp = ancestor; try { while (!ancestor.equals(getRootDir())) { try (DirectoryStream stream = Files.newDirectoryStream(ancestor.toPath())) { if (!stream.iterator().hasNext()) { tmp = ancestor; ancestor = tmp.getParentFile(); Files.deleteIfExists(tmp.toPath()); } else { break; } } } } catch (IOException | InvalidPathException e) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Could not delete " + tmp + " when cleaning up legacy user directories", e); } } } if (u==null && (create || configFile.exists())) { User tmp = new User(id, fullName); User prev; byNameLock.readLock().lock(); try { prev = AllUsers.byName().putIfAbsent(idkey, u = tmp); } finally { byNameLock.readLock().unlock(); } if (prev != null) { u = prev; // if some has already put a value in the map, use it if (LOGGER.isLoggable(Level.FINE) && !fullName.equals(prev.getFullName())) { LOGGER.log(Level.FINE, "mismatch on fullName (‘" + fullName + "’ vs. ‘" + prev.getFullName() + "’) for ‘" + id + "’", new Throwable()); } } else if (!id.equals(fullName) && !configFile.exists()) { // JENKINS-16332: since the fullName may not be recoverable from the id, and various code may store the id only, we must save the fullName try { u.save(); } catch (IOException x) { LOGGER.log(Level.WARNING, null, x); } } } return u; } private static boolean isMigrationRequiredForLegacyConfigFile(@Nonnull File legacyConfigFile, @Nonnull File newConfigFile){ boolean mustMigrateLegacyConfig = legacyConfigFile.exists() && !legacyConfigFile.equals(newConfigFile); if(mustMigrateLegacyConfig){ try{ // TODO Could be replace by Util.isDescendant(getRootDir(), legacyConfigFile) in 2.80+ String canonicalLegacy = legacyConfigFile.getCanonicalPath(); String canonicalUserDir = getRootDir().getCanonicalPath(); if(!canonicalLegacy.startsWith(canonicalUserDir + File.separator)){ // without that check, the application config.xml could be moved (i.e. erased from application PoV) mustMigrateLegacyConfig = false; LOGGER.log(Level.WARNING, String.format( "Attempt to escape from users directory with %s, migration aborted, see SECURITY-897 for more information", legacyConfigFile.getAbsolutePath() )); } } catch (IOException e){ mustMigrateLegacyConfig = false; LOGGER.log( Level.WARNING, String.format( "Failed to determine the canonical path of %s, migration aborted, see SECURITY-897 for more information", legacyConfigFile.getAbsolutePath() ), e ); } } return mustMigrateLegacyConfig; } /** * Gets the {@link User} object by its id or full name. * Use {@link #getById} when you know you have an ID. */ public static @Nonnull User get(String idOrFullName) { return get(idOrFullName,true); } /** * Gets the {@link User} object representing the currently logged-in user, or null * if the current user is anonymous. * @since 1.172 */ public static @CheckForNull User current() { return get(Jenkins.getAuthentication()); } /** * Gets the {@link User} object representing the supplied {@link Authentication} or * {@code null} if the supplied {@link Authentication} is either anonymous or {@code null} * @param a the supplied {@link Authentication} . * @return a {@link User} object for the supplied {@link Authentication} or {@code null} * @since 1.609 */ public static @CheckForNull User get(@CheckForNull Authentication a) { if(a == null || a instanceof AnonymousAuthenticationToken) return null; // Since we already know this is a name, we can just call getOrCreate with the name directly. String id = a.getName(); return getById(id, true); } /** * Gets the {@link User} object by its id * * @param id * the id of the user to retrieve and optionally create if it does not exist. * @param create * If true, this method will never return null for valid input (by creating a * new {@link User} object if none exists.) If false, this method will return * null if {@link User} object with the given id doesn't exist. * @return the a User whose id is id, or null if create is false * and the user does not exist. */ public static @Nullable User getById(String id, boolean create) { return getOrCreate(id, id, create); } /** * Gets all the users. */ public static @Nonnull Collection getAll() { final IdStrategy strategy = idStrategy(); byNameLock.readLock().lock(); ArrayList r; try { r = new ArrayList(AllUsers.byName().values()); } finally { byNameLock.readLock().unlock(); } Collections.sort(r,new Comparator() { public int compare(User o1, User o2) { return strategy.compare(o1.getId(), o2.getId()); } }); return r; } /** * To be called from {@link Jenkins#reload} only. */ @Restricted(NoExternalUse.class) public static void reload() { byNameLock.readLock().lock(); try { AllUsers.byName().clear(); } finally { byNameLock.readLock().unlock(); } UserDetailsCache.get().invalidateAll(); AllUsers.scanAll(); } /** * @deprecated Used to be called by test harnesses; now ignored in that case. */ @Deprecated public static void clear() { if (ExtensionList.lookup(AllUsers.class).isEmpty()) { // Historically this was called by JenkinsRule prior to startup. Ignore! return; } byNameLock.writeLock().lock(); try { AllUsers.byName().clear(); } finally { byNameLock.writeLock().unlock(); } } /** * Called when changing the {@link IdStrategy}. * @since 1.566 */ public static void rekey() { final IdStrategy strategy = idStrategy(); byNameLock.writeLock().lock(); try { ConcurrentMap byName = AllUsers.byName(); for (Map.Entry e : byName.entrySet()) { String idkey = strategy.keyFor(e.getValue().id); if (!idkey.equals(e.getKey())) { // need to remap byName.remove(e.getKey()); byName.putIfAbsent(idkey, e.getValue()); } } } finally { byNameLock.writeLock().unlock(); UserDetailsCache.get().invalidateAll(); } } /** * Returns the user name. */ public @Nonnull String getDisplayName() { return getFullName(); } /** true if {@link AbstractBuild#hasParticipant} or {@link hudson.model.Cause.UserIdCause} */ private boolean relatedTo(@Nonnull AbstractBuild b) { if (b.hasParticipant(this)) { return true; } for (Cause cause : b.getCauses()) { if (cause instanceof Cause.UserIdCause) { String userId = ((Cause.UserIdCause) cause).getUserId(); if (userId != null && idStrategy().equals(userId, getId())) { return true; } } } return false; } /** * Gets the list of {@link Build}s that include changes by this user, * by the timestamp order. */ @SuppressWarnings("unchecked") @WithBridgeMethods(List.class) public @Nonnull RunList getBuilds() { return RunList.fromJobs((Iterable)Jenkins.getInstance().allItems(Job.class)).filter(new Predicate>() { @Override public boolean apply(Run r) { return r instanceof AbstractBuild && relatedTo((AbstractBuild) r); } }); } /** * Gets all the {@link AbstractProject}s that this user has committed to. * @since 1.191 */ public @Nonnull Set> getProjects() { Set> r = new HashSet>(); for (AbstractProject p : Jenkins.getInstance().allItems(AbstractProject.class)) if(p.hasParticipant(this)) r.add(p); return r; } public @Override String toString() { return fullName; } /** * The file we save our configuration. */ protected final XmlFile getConfigFile() { return new XmlFile(XSTREAM,getConfigFileFor(id)); } private static final File getConfigFileFor(String id) { return new File(getRootDir(), idStrategy().filenameOf(id) +"/config.xml"); } private static File getUnsanitizedLegacyConfigFileFor(String id) { return new File(getRootDir(), idStrategy().legacyFilenameOf(id) + "/config.xml"); } /** * Gets the directory where Hudson stores user information. */ private static File getRootDir() { return new File(Jenkins.getInstance().getRootDir(), "users"); } /** * Is the ID allowed? Some are prohibited for security reasons. See SECURITY-166. *

* Note that this is only enforced when saving. These users are often created * via the constructor (and even listed on /asynchPeople), but our goal is to * prevent anyone from logging in as these users. Therefore, we prevent * saving a User with one of these ids. * * @param id ID to be checked * @return {@code true} if the username or fullname is valid. * For {@code null} or blank IDs returns {@code false}. * @since 1.600 */ public static boolean isIdOrFullnameAllowed(@CheckForNull String id) { //TODO: StringUtils.isBlank() checks the null value, but FindBugs is not smart enough. Remove it later if (id == null || StringUtils.isBlank(id)) { return false; } final String trimmedId = id.trim(); for (String invalidId : ILLEGAL_PERSISTED_USERNAMES) { if (trimmedId.equalsIgnoreCase(invalidId)) return false; } return true; } /** * Save the settings to a file. */ public synchronized void save() throws IOException, FormValidation { if (! isIdOrFullnameAllowed(id)) { throw FormValidation.error(Messages.User_IllegalUsername(id)); } if (! isIdOrFullnameAllowed(fullName)) { throw FormValidation.error(Messages.User_IllegalFullname(fullName)); } if(BulkChange.contains(this)) return; getConfigFile().write(this); SaveableListener.fireOnChange(this, getConfigFile()); } private Object writeReplace() { return XmlFile.replaceIfNotAtTopLevel(this, () -> new Replacer(this)); } private static class Replacer { private final String id; Replacer(User u) { id = u.getId(); } private Object readResolve() { return getById(id, false); } } /** * Deletes the data directory and removes this user from Hudson. * * @throws IOException * if we fail to delete. */ public synchronized void delete() throws IOException { final IdStrategy strategy = idStrategy(); byNameLock.readLock().lock(); try { AllUsers.byName().remove(strategy.keyFor(id)); } finally { byNameLock.readLock().unlock(); } Util.deleteRecursive(new File(getRootDir(), strategy.filenameOf(id))); UserDetailsCache.get().invalidate(strategy.keyFor(id)); } /** * Exposed remote API. */ public Api getApi() { return new Api(this); } /** * Accepts submission from the configuration page. */ @RequirePOST public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException { checkPermission(Jenkins.ADMINISTER); JSONObject json = req.getSubmittedForm(); String oldFullName = this.fullName; fullName = json.getString("fullName"); description = json.getString("description"); List props = new ArrayList(); int i = 0; for (UserPropertyDescriptor d : UserProperty.all()) { UserProperty p = getProperty(d.clazz); JSONObject o = json.optJSONObject("userProperty" + (i++)); if (o!=null) { if (p != null) { p = p.reconfigure(req, o); } else { p = d.newInstance(req, o); } p.setUser(this); } if (p!=null) props.add(p); } this.properties = props; save(); if (oldFullName != null && !oldFullName.equals(this.fullName)) { UserDetailsCache.get().invalidate(oldFullName); } FormApply.success(".").generateResponse(req,rsp,this); } /** * Deletes this user from Hudson. */ @RequirePOST public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { checkPermission(Jenkins.ADMINISTER); if (idStrategy().equals(id, Jenkins.getAuthentication().getName())) { rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Cannot delete self"); return; } delete(); rsp.sendRedirect2("../.."); } public void doRssAll(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { rss(req, rsp, " all builds", getBuilds(), Run.FEED_ADAPTER); } public void doRssFailed(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { rss(req, rsp, " regression builds", getBuilds().regressionOnly(), Run.FEED_ADAPTER); } public void doRssLatest(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { final List lastBuilds = new ArrayList(); for (AbstractProject p : Jenkins.getInstance().allItems(AbstractProject.class)) { for (AbstractBuild b = p.getLastBuild(); b != null; b = b.getPreviousBuild()) { if (relatedTo(b)) { lastBuilds.add(b); break; } } } // historically these have been reported sorted by project name, we switched to the lazy iteration // so we only have to sort the sublist of runs rather than the full list of irrelevant projects Collections.sort(lastBuilds, new Comparator() { @Override public int compare(Run o1, Run o2) { return Items.BY_FULL_NAME.compare(o1.getParent(), o2.getParent()); } }); rss(req, rsp, " latest build", RunList.fromRuns(lastBuilds), Run.FEED_ADAPTER_LATEST); } private void rss(StaplerRequest req, StaplerResponse rsp, String suffix, RunList runs, FeedAdapter adapter) throws IOException, ServletException { RSS.forwardToRss(getDisplayName()+ suffix, getUrl(), runs.newBuilds(), adapter, req, rsp); } /** * This lock is used to guard access to the {@link AllUsers#byName} map. Use * {@link java.util.concurrent.locks.ReadWriteLock#readLock()} for normal access and * {@link java.util.concurrent.locks.ReadWriteLock#writeLock()} for {@link #rekey()} or any other operation * that requires operating on the map as a whole. */ private static final ReadWriteLock byNameLock = new ReentrantReadWriteLock(); /** * Used to load/save user configuration. */ public static final XStream2 XSTREAM = new XStream2(); private static final Logger LOGGER = Logger.getLogger(User.class.getName()); static { XSTREAM.alias("user",User.class); } public ACL getACL() { final ACL base = Jenkins.getInstance().getAuthorizationStrategy().getACL(this); // always allow a non-anonymous user full control of himself. return new ACL() { public boolean hasPermission(Authentication a, Permission permission) { return (idStrategy().equals(a.getName(), id) && !(a instanceof AnonymousAuthenticationToken)) || base.hasPermission(a, permission); } }; } /** * With ADMINISTER permission, can delete users with persisted data but can't delete self. */ public boolean canDelete() { final IdStrategy strategy = idStrategy(); return hasPermission(Jenkins.ADMINISTER) && !strategy.equals(id, Jenkins.getAuthentication().getName()) && new File(getRootDir(), strategy.filenameOf(id)).exists(); } /** * Checks for authorities (groups) associated with this user. * If the caller lacks {@link Jenkins#ADMINISTER}, or any problems arise, returns an empty list. * {@link SecurityRealm#AUTHENTICATED_AUTHORITY} and the username, if present, are omitted. * @since 1.498 * @return a possibly empty list */ public @Nonnull List getAuthorities() { if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { return Collections.emptyList(); } List r = new ArrayList(); Authentication authentication; try { authentication = impersonate(); } catch (UsernameNotFoundException x) { LOGGER.log(Level.FINE, "cannot look up authorities for " + id, x); return Collections.emptyList(); } for (GrantedAuthority a : authentication.getAuthorities()) { if (a.equals(SecurityRealm.AUTHENTICATED_AUTHORITY)) { continue; } String n = a.getAuthority(); if (n != null && !idStrategy().equals(n, id)) { r.add(n); } } Collections.sort(r, String.CASE_INSENSITIVE_ORDER); return r; } public Object getDynamic(String token) { for(Action action: getTransientActions()){ if(Objects.equals(action.getUrlName(), token)) return action; } for(Action action: getPropertyActions()){ if(Objects.equals(action.getUrlName(), token)) return action; } return null; } /** * Return all properties that are also actions. * * @return the list can be empty but never null. read only. */ public List getPropertyActions() { List actions = new ArrayList(); for (UserProperty userProp : getProperties().values()) { if (userProp instanceof Action) { actions.add((Action) userProp); } } return Collections.unmodifiableList(actions); } /** * Return all transient actions associated with this user. * * @return the list can be empty but never null. read only. */ public List getTransientActions() { List actions = new ArrayList(); for (TransientUserActionFactory factory: TransientUserActionFactory.all()) { actions.addAll(factory.createFor(this)); } return Collections.unmodifiableList(actions); } public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { return new ContextMenu().from(this,request,response); } @Override @Restricted(NoExternalUse.class) public Object getTarget() { if (!SKIP_PERMISSION_CHECK) { Jenkins.getInstance().checkPermission(Jenkins.READ); } return this; } /** * Escape hatch for StaplerProxy-based access control */ @Restricted(NoExternalUse.class) public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(User.class.getName() + ".skipPermissionCheck"); /** * Gets list of Illegal usernames, for which users should not be created. * Always includes users from {@link #ILLEGAL_PERSISTED_USERNAMES} * @return List of usernames */ @Restricted(NoExternalUse.class) /*package*/ static Set getIllegalPersistedUsernames() { // TODO: This method is designed for further extensibility via system properties. To be extended in a follow-up issue final Set res = new HashSet<>(); res.addAll(Arrays.asList(ILLEGAL_PERSISTED_USERNAMES)); return res; } /** Per-{@link Jenkins} holder of all known {@link User}s. */ @Extension @Restricted(NoExternalUse.class) public static final class AllUsers { @Initializer(after = InitMilestone.JOB_LOADED) // so Jenkins.loadConfig has been called public static void scanAll() { IdStrategy strategy = idStrategy(); File[] subdirs = getRootDir().listFiles((FileFilter) DirectoryFileFilter.INSTANCE); if (subdirs != null) { for (File subdir : subdirs) { File configFile = new File(subdir, "config.xml"); if (configFile.exists()) { String name = strategy.idFromFilename(subdir.getName()); getOrCreate(name, /* calls load(), probably clobbering this anyway */name, true, configFile); } } } } @GuardedBy("User.byNameLock") private final ConcurrentMap byName = new ConcurrentHashMap(); /** * Keyed by {@link User#id}. This map is used to ensure * singleton-per-id semantics of {@link User} objects. * * The key needs to be generated by {@link IdStrategy#keyFor(String)}. */ @GuardedBy("User.byNameLock") static ConcurrentMap byName() { return ExtensionList.lookupSingleton(AllUsers.class).byName; } } public static abstract class CanonicalIdResolver extends AbstractDescribableImpl implements ExtensionPoint, Comparable { /** * context key for realm (domain) where idOrFullName has been retrieved from. * Can be used (for example) to distinguish ambiguous committer ID using the SCM URL. * Associated Value is a {@link String} */ public static final String REALM = "realm"; public int compareTo(CanonicalIdResolver o) { // reverse priority order int i = getPriority(); int j = o.getPriority(); return i>j ? -1 : (i==j ? 0:1); } /** * extract user ID from idOrFullName with help from contextual infos. * can return null if no user ID matched the input */ public abstract @CheckForNull String resolveCanonicalId(String idOrFullName, Map context); public int getPriority() { return 1; } } /** * Resolve user ID from full name */ @Extension @Symbol("fullName") public static class FullNameIdResolver extends CanonicalIdResolver { @Override public String resolveCanonicalId(String idOrFullName, Map context) { for (User user : getAll()) { if (idOrFullName.equals(user.getFullName())) return user.getId(); } return null; } @Override public int getPriority() { return -1; // lower than default } } /** * Tries to verify if an ID is valid. * If so, we do not want to even consider users who might have the same full name. */ @Extension @Restricted(NoExternalUse.class) public static class UserIDCanonicalIdResolver extends User.CanonicalIdResolver { private static /* not final */ boolean SECURITY_243_FULL_DEFENSE = SystemProperties.getBoolean(User.class.getName() + ".SECURITY_243_FULL_DEFENSE", true); private static final ThreadLocal resolving = new ThreadLocal() { @Override protected Boolean initialValue() { return false; } }; @Override public String resolveCanonicalId(String idOrFullName, Map context) { User existing = getById(idOrFullName, false); if (existing != null) { return existing.getId(); } if (SECURITY_243_FULL_DEFENSE) { if (!resolving.get()) { resolving.set(true); try { UserDetails userDetails = UserDetailsCache.get().loadUserByUsername(idOrFullName); return userDetails.getUsername(); } catch (UsernameNotFoundException x) { LOGGER.log(Level.FINE, "not sure whether " + idOrFullName + " is a valid username or not", x); } catch (DataAccessException | ExecutionException x) { LOGGER.log(Level.FINE, "could not look up " + idOrFullName, x); } finally { resolving.set(false); } } } return null; } @Override public int getPriority() { // should always come first so that ID that are ids get mapped correctly return Integer.MAX_VALUE; } } /** * Jenkins now refuses to let the user login if he/she doesn't exist in {@link SecurityRealm}, * which was necessary to make sure users removed from the backend will get removed from the frontend. *

* Unfortunately this infringed some legitimate use cases of creating Jenkins-local users for * automation purposes. This escape hatch switch can be enabled to resurrect that behaviour. * * JENKINS-22346. */ public static boolean ALLOW_NON_EXISTENT_USER_TO_LOGIN = SystemProperties.getBoolean(User.class.getName()+".allowNonExistentUserToLogin"); /** * Jenkins historically created a (usually) ephemeral user record when an user with Overall/Administer permission * accesses a /user/arbitraryName URL. *

* Unfortunately this constitutes a CSRF vulnerability, as malicious users can make admins create arbitrary numbers * of ephemeral user records, so the behavior was changed in Jenkins 2.TODO / 2.32.2. *

* As some users may be relying on the previous behavior, setting this to true restores the previous behavior. This * is not recommended. * * SECURITY-406. */ @Restricted(NoExternalUse.class) public static boolean ALLOW_USER_CREATION_VIA_URL = SystemProperties.getBoolean(User.class.getName() + ".allowUserCreationViaUrl"); }