/*******************************************************************************
*
* Copyright (c) 2004-2012 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Winston Prakash, Erik Ramfelt, Tom Huybrechts
*
*******************************************************************************/
package hudson.model;
import com.thoughtworks.xstream.XStream;
import hudson.CopyOnWrite;
import hudson.FeedAdapter;
import hudson.Functions;
import hudson.Util;
import hudson.XmlFile;
import hudson.BulkChange;
import hudson.model.Descriptor.FormException;
import hudson.model.listeners.SaveableListener;
import hudson.security.*;
import hudson.util.RunList;
import hudson.util.XStream2;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder;
import org.eclipse.hudson.security.HudsonSecurityManager;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
/**
* Represents a user.
*
* <p> 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.
*
* <p> 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.
*
* <p> 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, Saveable, Comparable<User> {
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<UserProperty> properties = new ArrayList<UserProperty>();
private User(String id, String fullName) {
this.id = id;
this.fullName = fullName;
load();
}
public int compareTo(User that) {
return this.id.compareTo(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<UserProperty> itr = properties.iterator(); itr.hasNext();) {
if (itr.next() == null) {
itr.remove();
}
}
// In case Hudson is not yet initialized (i.e Hudson Security is used
// outside of Hudson Model (Ex. Initial Setup), don't load the extended
// properties
if (Hudson.getInstance() != null) {
// 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 String getUrl() {
return "user/" + Util.rawEncode(id);
}
public String getSearchUrl() {
return "/user/" + Util.rawEncode(id);
}
/**
* The URL of the user page.
*/
@Exported(visibility = 999)
public String getAbsoluteUrl() {
return Hudson.getInstance().getRootUrl() + getUrl();
}
/**
* Gets the human readable name of this user. This is configurable by the
* user.
*
* @return never null.
*/
@Exported(visibility = 999)
public String getFullName() {
return fullName;
}
/**
* Sets the human readable name of thie user.
*/
public void setFullName(String name) {
if (Util.fixEmptyAndTrim(name) == null) {
name = id;
}
this.fullName = name;
}
@Exported
public String getDescription() {
return description;
}
/**
* Gets the user properties configured for this user.
*/
public Map<Descriptor<UserProperty>, UserProperty> getProperties() {
return Descriptor.toMap(properties);
}
/**
* Updates the user object by adding a property.
*/
public synchronized void addProperty(UserProperty p) throws IOException {
UserProperty old = getProperty(p.getClass());
List<UserProperty> ps = new ArrayList<UserProperty>(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<UserProperty> getAllProperties() {
return Collections.unmodifiableList(properties);
}
/**
* Gets the specific property, or null.
*/
public <T extends UserProperty> T getProperty(Class<T> clazz) {
for (UserProperty p : properties) {
if (clazz.isInstance(p)) {
return clazz.cast(p);
}
}
return null;
}
/**
* Accepts the new description.
*/
public synchronized void doSubmitDescription(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
checkPermission(Hudson.ADMINISTER);
description = req.getParameter("description");
save();
rsp.sendRedirect("."); // go to the top page
}
/**
* Gets the fallback "unknown" user instance. <p> This is used to avoid null
* {@link User} instance.
*/
public static User getUnknown() {
return get("unknown");
}
/**
* 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.
*/
public static User get(String idOrFullName, boolean create) {
if (idOrFullName == null) {
return null;
}
String id = idOrFullName.replace('\\', '_').replace('/', '_').replace('<', '_')
.replace('>', '_'); // 4 replace() still faster than regex
if (Functions.isWindows()) {
id = id.replace(':', '_');
}
synchronized (byName) {
User u = byName.get(id);
if (null == u) {
User tmp = new User(id, idOrFullName);
if (create || tmp.getConfigFile().exists()) {
byName.put(id, u = tmp);
}
}
return u;
}
}
/**
* Gets the {@link User} object by its id or full name.
*/
public static 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 User current() {
Authentication a = HudsonSecurityManager.getAuthentication();
if (a instanceof AnonymousAuthenticationToken) {
return null;
}
return get(a.getName());
}
private static volatile long lastScanned;
/**
* Gets all the users.
*/
public static Collection<User> getAll() {
if (System.currentTimeMillis() - lastScanned > 10000) {
// occasionally scan the file system to check new users
// whether we should do this only once at start up or not is debatable.
// set this right away to avoid another thread from doing the same thing while we do this.
// having two threads doing the work won't cause race condition, but it's waste of time.
lastScanned = System.currentTimeMillis();
File[] subdirs = getRootDir().listFiles((FileFilter) DirectoryFileFilter.INSTANCE);
if (subdirs == null) {
return Collections.emptyList(); // shall never happen
}
for (File subdir : subdirs) {
if (new File(subdir, "config.xml").exists()) {
User.get(subdir.getName());
}
}
lastScanned = System.currentTimeMillis();
}
synchronized (byName) {
return new ArrayList<User>(byName.values());
}
}
/**
* Reloads the configuration from disk.
*/
public static void reload() {
// iterate over an array to be concurrency-safe
Collection<User> values = byName.values();
for (User u : values.toArray(new User[values.size()])) {
u.load();
}
}
/**
* Stop gap work around. Don't use it. To be removed in the trunk.
*/
public static void clear() {
byName.clear();
}
/**
* Returns the user name.
*/
public String getDisplayName() {
return getFullName();
}
/**
* Gets the list of {@link Build}s that include changes by this user, by the
* timestamp order.
*
* TODO: do we need some index for this?
*/
public RunList getBuilds() {
List<AbstractBuild> r = new ArrayList<AbstractBuild>();
for (AbstractProject<?, ?> p : Hudson.getInstance().getAllItems(AbstractProject.class)) {
for (AbstractBuild<?, ?> b : p.getBuilds()) {
if (b.hasParticipant(this)) {
r.add(b);
}
}
}
return RunList.fromRuns(r);
}
/**
* Gets all the {@link AbstractProject}s that this user has committed to.
*
* @since 1.191
*/
public Set<AbstractProject<?, ?>> getProjects() {
Set<AbstractProject<?, ?>> r = new HashSet<AbstractProject<?, ?>>();
for (AbstractProject<?, ?> p : Hudson.getInstance().getAllItems(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, new File(getRootDir(), id + "/config.xml"));
}
/**
* Gets the directory where the user information is stored.
*/
private static File getRootDir() {
return new File(HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getHudsonHome(), "users");
}
/**
* Save the settings to a file.
*/
public synchronized void save() throws IOException {
if (BulkChange.contains(this)) {
return;
}
getConfigFile().write(this);
SaveableListener.fireOnChange(this, getConfigFile());
}
/**
* Deletes the data directory and removes this user from Hudson.
*
* @throws IOException if we fail to delete.
*/
public synchronized void delete() throws IOException {
synchronized (byName) {
byName.remove(id);
Util.deleteRecursive(new File(getRootDir(), id));
}
}
/**
* Exposed remote API.
*/
public Api getApi() {
return new Api(this);
}
/**
* Accepts submission from the configuration page.
*/
public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
checkPermission(Hudson.ADMINISTER);
fullName = req.getParameter("fullName");
description = req.getParameter("description");
JSONObject json = req.getSubmittedForm();
List<UserProperty> props = new ArrayList<UserProperty>();
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);
}
props.add(p);
}
this.properties = props;
save();
rsp.sendRedirect(".");
}
/**
* Deletes this user from Hudson.
*/
public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
requirePOST();
checkPermission(Hudson.ADMINISTER);
if (id.equals(HudsonSecurityManager.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", RunList.fromRuns(getBuilds()), Run.FEED_ADAPTER);
}
public void doRssFailed(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
rss(req, rsp, " regression builds", RunList.fromRuns(getBuilds()).regressionOnly(), Run.FEED_ADAPTER);
}
public void doRssLatest(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
final List<Run> lastBuilds = new ArrayList<Run>();
for (final TopLevelItem item : Hudson.getInstance().getItems()) {
if (!(item instanceof Job)) {
continue;
}
for (Run r = ((Job) item).getLastBuild(); r != null; r = r.getPreviousBuild()) {
if (!(r instanceof AbstractBuild)) {
continue;
}
final AbstractBuild b = (AbstractBuild) r;
if (b.hasParticipant(this)) {
lastBuilds.add(b);
break;
}
}
}
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);
}
/**
* Keyed by {@link User#id}. This map is used to ensure singleton-per-id
* semantics of {@link User} objects.
*/
private static final Map<String, User> byName = new TreeMap<String, User>(String.CASE_INSENSITIVE_ORDER);
/**
* Used to load/save user configuration.
*/
private static final XStream 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 = HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getAuthorizationStrategy().getACL(this);
// always allow a non-anonymous user full control of himself.
return new ACL() {
public boolean hasPermission(Authentication a, Permission permission) {
return (a.getName().equals(id) && !(a instanceof AnonymousAuthenticationToken))
|| base.hasPermission(a, permission);
}
};
}
public void checkPermission(Permission permission) {
getACL().checkPermission(permission);
}
public boolean hasPermission(Permission permission) {
return getACL().hasPermission(permission);
}
/**
* With ADMINISTER permission, can delete users with persisted data but
* can't delete self.
*/
public boolean canDelete() {
return hasPermission(Hudson.ADMINISTER) && !id.equals(HudsonSecurityManager.getAuthentication().getName())
&& new File(getRootDir(), id).exists();
}
public Object getDynamic(String token) {
for (UserProperty property : getProperties().values()) {
if (property instanceof Action) {
Action a = (Action) property;
if (a.getUrlName().equals(token) || a.getUrlName().equals('/' + token)) {
return a;
}
}
}
return null;
}
}