User.java 18.6 KB
Newer Older
K
kohsuke 已提交
1 2 3
/*
 * The MIT License
 * 
4
 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Tom Huybrechts
K
kohsuke 已提交
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 * 
 * 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.
 */
K
kohsuke 已提交
24 25
package hudson.model;

26
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
K
kohsuke 已提交
27
import com.thoughtworks.xstream.XStream;
K
kohsuke 已提交
28
import hudson.CopyOnWrite;
K
kohsuke 已提交
29
import hudson.FeedAdapter;
30
import hudson.Functions;
K
kohsuke 已提交
31
import hudson.Util;
32
import hudson.XmlFile;
33
import hudson.BulkChange;
K
kohsuke 已提交
34
import hudson.model.Descriptor.FormException;
35
import hudson.model.listeners.SaveableListener;
36 37 38
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.Permission;
39
import hudson.security.SecurityRealm;
K
kohsuke 已提交
40 41
import hudson.util.RunList;
import hudson.util.XStream2;
42
import jenkins.model.Jenkins;
43 44
import net.sf.json.JSONObject;

45
import org.acegisecurity.Authentication;
46 47 48
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
49
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
50
import org.acegisecurity.userdetails.UserDetails;
K
kohsuke 已提交
51 52
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
53 54
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
55
import org.apache.commons.io.filefilter.DirectoryFileFilter;
K
kohsuke 已提交
56 57

import javax.servlet.ServletException;
58
import javax.servlet.http.HttpServletResponse;
K
kohsuke 已提交
59 60
import java.io.File;
import java.io.IOException;
61
import java.io.FileFilter;
K
kohsuke 已提交
62
import java.util.ArrayList;
63
import java.util.Collection;
K
kohsuke 已提交
64
import java.util.Collections;
65 66
import java.util.HashSet;
import java.util.Iterator;
K
kohsuke 已提交
67 68
import java.util.List;
import java.util.Map;
69
import java.util.Set;
70
import java.util.TreeMap;
K
kohsuke 已提交
71 72 73 74 75
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Represents a user.
K
kohsuke 已提交
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
 *
 * <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.)
 *
 *
K
kohsuke 已提交
96 97
 * @author Kohsuke Kawaguchi
 */
K
kohsuke 已提交
98
@ExportedBean
99
public class User extends AbstractModelObject implements AccessControlled, Saveable, Comparable<User> {
K
kohsuke 已提交
100 101 102 103 104 105 106 107 108 109

    private transient final String id;

    private volatile String fullName;

    private volatile String description;

    /**
     * List of {@link UserProperty}s configured for this project.
     */
K
kohsuke 已提交
110
    @CopyOnWrite
K
kohsuke 已提交
111 112 113
    private volatile List<UserProperty> properties = new ArrayList<UserProperty>();


114
    private User(String id, String fullName) {
K
kohsuke 已提交
115
        this.id = id;
116
        this.fullName = fullName;
K
kohsuke 已提交
117 118
        load();
    }
K
kohsuke 已提交
119

120 121 122 123
    public int compareTo(User that) {
        return this.id.compareTo(that.id);
    }

K
kohsuke 已提交
124 125 126 127
    /**
     * Loads the other data from disk if it's available.
     */
    private synchronized void load() {
128 129
        properties.clear();

K
kohsuke 已提交
130 131 132 133 134 135 136 137
        XmlFile config = getConfigFile();
        try {
            if(config.exists())
                config.unmarshal(this);
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Failed to load "+config,e);
        }

138 139 140 141 142 143
        // remove nulls that have failed to load
        for (Iterator<UserProperty> itr = properties.iterator(); itr.hasNext();) {
            if(itr.next()==null)
                itr.remove();            
        }

K
kohsuke 已提交
144 145
        // allocate default instances if needed.
        // doing so after load makes sure that newly added user properties do get reflected
146
        for (UserPropertyDescriptor d : UserProperty.all()) {
K
kohsuke 已提交
147 148 149 150 151 152 153
            if(getProperty(d.clazz)==null) {
                UserProperty up = d.newInstance(this);
                if(up!=null)
                    properties.add(up);
            }
        }

K
kohsuke 已提交
154 155 156 157
        for (UserProperty p : properties)
            p.setUser(this);
    }

K
kohsuke 已提交
158
    @Exported
K
kohsuke 已提交
159 160 161 162 163
    public String getId() {
        return id;
    }

    public String getUrl() {
164
        return "user/"+Util.rawEncode(id);
165 166 167
    }

    public String getSearchUrl() {
168
        return "/user/"+Util.rawEncode(id);
K
kohsuke 已提交
169 170
    }

K
kohsuke 已提交
171 172 173
    /**
     * The URL of the user page.
     */
K
kohsuke 已提交
174
    @Exported(visibility=999)
K
kohsuke 已提交
175
    public String getAbsoluteUrl() {
176
        return Jenkins.getInstance().getRootUrl()+getUrl();
K
kohsuke 已提交
177 178
    }

K
kohsuke 已提交
179 180 181 182 183 184 185
    /**
     * Gets the human readable name of this user.
     * This is configurable by the user.
     *
     * @return
     *      never null.
     */
K
kohsuke 已提交
186
    @Exported(visibility=999)
K
kohsuke 已提交
187 188 189 190
    public String getFullName() {
        return fullName;
    }

K
kohsuke 已提交
191 192 193 194 195 196 197 198
    /**
     * Sets the human readable name of thie user.
     */
    public void setFullName(String name) {
        if(Util.fixEmptyAndTrim(name)==null)    name=id;
        this.fullName = name;
    }

K
kohsuke 已提交
199
    @Exported
K
kohsuke 已提交
200 201 202 203 204 205 206 207 208 209 210
    public String getDescription() {
        return description;
    }

    /**
     * Gets the user properties configured for this user.
     */
    public Map<Descriptor<UserProperty>,UserProperty> getProperties() {
        return Descriptor.toMap(properties);
    }

211 212 213 214 215 216 217 218 219
    /**
     * 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);
220
        p.setUser(this);
221 222 223
        properties = ps;
        save();
    }
224

K
kohsuke 已提交
225
    /**
226
     * List of all {@link UserProperty}s exposed primarily for the remoting API.
K
kohsuke 已提交
227
     */
K
kohsuke 已提交
228
    @Exported(name="property",inline=true)
229 230 231
    public List<UserProperty> getAllProperties() {
        return Collections.unmodifiableList(properties);
    }
K
kohsuke 已提交
232
    
K
kohsuke 已提交
233 234 235 236 237 238
    /**
     * Gets the specific property, or null.
     */
    public <T extends UserProperty> T getProperty(Class<T> clazz) {
        for (UserProperty p : properties) {
            if(clazz.isInstance(p))
239
                return clazz.cast(p);
K
kohsuke 已提交
240 241 242 243
        }
        return null;
    }

244 245 246 247 248 249 250 251 252 253 254 255 256 257
    /**
     * Creates an {@link Authentication} object that represents this user.
     */
    public Authentication impersonate() {
        try {
            UserDetails u = Jenkins.getInstance().getSecurityRealm().loadUserByUsername(id);
            return new UsernamePasswordAuthenticationToken(u.getUsername(), u.getPassword(), u.getAuthorities());
        } catch (AuthenticationException e) {
            // TODO: use the stored GrantedAuthorities
            return new UsernamePasswordAuthenticationToken(id, "",
                new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY});
        }
    }

K
kohsuke 已提交
258 259 260 261
    /**
     * Accepts the new description.
     */
    public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
262
        checkPermission(Jenkins.ADMINISTER);
K
kohsuke 已提交
263 264 265 266 267 268 269

        description = req.getParameter("description");
        save();
        
        rsp.sendRedirect(".");  // go to the top page
    }

K
kohsuke 已提交
270 271 272 273 274
    /**
     * Gets the fallback "unknown" user instance.
     * <p>
     * This is used to avoid null {@link User} instance.
     */
275 276 277
    public static User getUnknown() {
        return get("unknown");
    }
K
kohsuke 已提交
278

K
kohsuke 已提交
279
    /**
280
     * Gets the {@link User} object by its id or full name.
K
kohsuke 已提交
281 282 283 284 285 286 287
     *
     * @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.
     */
288 289
    public static User get(String idOrFullName, boolean create) {
        if(idOrFullName==null)
K
kohsuke 已提交
290
            return null;
291 292
        String id = idOrFullName.replace('\\', '_').replace('/', '_').replace('<','_')
                                .replace('>','_');  // 4 replace() still faster than regex
293 294
        if (Functions.isWindows()) id = id.replace(':','_');

K
kohsuke 已提交
295
        synchronized(byName) {
K
kohsuke 已提交
296
            User u = byName.get(id);
297 298 299 300 301
            if(u==null) {
                User tmp = new User(id, idOrFullName);
                if (create || tmp.getConfigFile().exists()) {
                    byName.put(id,u=tmp);
                }
K
kohsuke 已提交
302 303 304 305 306
            }
            return u;
        }
    }

K
kohsuke 已提交
307
    /**
308
     * Gets the {@link User} object by its id or full name.
K
kohsuke 已提交
309
     */
310 311
    public static User get(String idOrFullName) {
        return get(idOrFullName,true);
K
kohsuke 已提交
312 313
    }

K
kohsuke 已提交
314 315 316 317 318 319
    /**
     * 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() {
320
        Authentication a = Jenkins.getAuthentication();
K
kohsuke 已提交
321 322
        if(a instanceof AnonymousAuthenticationToken)
            return null;
323
        return get(a.getName());
K
kohsuke 已提交
324 325
    }

326
    private static volatile long lastScanned;
327

K
kohsuke 已提交
328 329 330 331
    /**
     * Gets all the users.
     */
    public static Collection<User> getAll() {
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
        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();
        }

K
kohsuke 已提交
349 350 351 352 353
        synchronized (byName) {
            return new ArrayList<User>(byName.values());
        }
    }

354 355 356 357 358 359 360 361 362
    /**
     * Reloads the configuration from disk.
     */
    public static void reload() {
        // iterate over an array to be concurrency-safe
        for( User u : byName.values().toArray(new User[0]) )
            u.load();
    }

363 364 365 366 367 368 369
    /**
     * Stop gap hack. Don't use it. To be removed in the trunk.
     */
    public static void clear() {
        byName.clear();
    }

K
kohsuke 已提交
370 371 372 373 374 375 376 377 378 379 380 381 382
    /**
     * 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?
     */
383 384
    @WithBridgeMethods(List.class)
    public RunList getBuilds() {
K
kohsuke 已提交
385
        List<AbstractBuild> r = new ArrayList<AbstractBuild>();
386
        for (AbstractProject<?,?> p : Jenkins.getInstance().getAllItems(AbstractProject.class))
387 388 389
            for (AbstractBuild<?,?> b : p.getBuilds())
                if(b.hasParticipant(this))
                    r.add(b);
390
        return RunList.fromRuns(r);
K
kohsuke 已提交
391 392
    }

393 394 395 396 397 398
    /**
     * 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<?,?>>();
399
        for (AbstractProject<?,?> p : Jenkins.getInstance().getAllItems(AbstractProject.class))
400 401 402 403 404
            if(p.hasParticipant(this))
                r.add(p);
        return r;
    }

405
    public @Override String toString() {
K
kohsuke 已提交
406 407 408 409 410 411 412
        return fullName;
    }

    /**
     * The file we save our configuration.
     */
    protected final XmlFile getConfigFile() {
413 414 415 416 417 418 419
        return new XmlFile(XSTREAM,new File(getRootDir(),id +"/config.xml"));
    }

    /**
     * Gets the directory where Hudson stores user information.
     */
    private static File getRootDir() {
420
        return new File(Jenkins.getInstance().getRootDir(), "users");
K
kohsuke 已提交
421 422 423 424 425 426
    }

    /**
     * Save the settings to a file.
     */
    public synchronized void save() throws IOException {
427
        if(BulkChange.contains(this))   return;
428
        getConfigFile().write(this);
429
        SaveableListener.fireOnChange(this, getConfigFile());
K
kohsuke 已提交
430 431
    }

432 433 434 435 436 437 438 439 440 441 442 443 444
    /**
     * 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));
        }
    }

K
kohsuke 已提交
445 446 447 448 449 450 451
    /**
     * Exposed remote API.
     */
    public Api getApi() {
        return new Api(this);
    }

K
kohsuke 已提交
452 453 454
    /**
     * Accepts submission from the configuration page.
     */
455
    public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException {
456
        checkPermission(Jenkins.ADMINISTER);
K
kohsuke 已提交
457

458 459
        fullName = req.getParameter("fullName");
        description = req.getParameter("description");
460

461
        JSONObject json = req.getSubmittedForm();
K
kohsuke 已提交
462

463 464 465 466
        List<UserProperty> props = new ArrayList<UserProperty>();
        int i = 0;
        for (UserPropertyDescriptor d : UserProperty.all()) {
            UserProperty p = getProperty(d.clazz);
467 468 469 470 471 472 473 474

            JSONObject o = json.optJSONObject("userProperty" + (i++));
            if (o!=null) {
                if (p != null) {
                    p = p.reconfigure(req, o);
                } else {
                    p = d.newInstance(req, o);
                }
K
Oops  
Kohsuke Kawaguchi 已提交
475
                p.setUser(this);
476
            }
K
kohsuke 已提交
477

478 479
            if (p!=null)
                props.add(p);
K
kohsuke 已提交
480
        }
481 482 483 484 485
        this.properties = props;

        save();

        rsp.sendRedirect(".");
K
kohsuke 已提交
486 487
    }

488 489 490 491 492
    /**
     * Deletes this user from Hudson.
     */
    public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        requirePOST();
493 494
        checkPermission(Jenkins.ADMINISTER);
        if (id.equals(Jenkins.getAuthentication().getName())) {
495 496 497 498 499 500 501 502 503
            rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Cannot delete self");
            return;
        }

        delete();

        rsp.sendRedirect2("../..");
    }

504 505
    public void doRssAll(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        rss(req, rsp, " all builds", RunList.fromRuns(getBuilds()), Run.FEED_ADAPTER);
K
kohsuke 已提交
506 507
    }

508 509
    public void doRssFailed(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        rss(req, rsp, " regression builds", RunList.fromRuns(getBuilds()).regressionOnly(), Run.FEED_ADAPTER);
K
kohsuke 已提交
510 511
    }

512 513
    public void doRssLatest(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        final List<Run> lastBuilds = new ArrayList<Run>();
514
        for (final TopLevelItem item : Jenkins.getInstance().getItems()) {
515 516 517 518 519 520 521 522 523 524 525
            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);
K
kohsuke 已提交
526 527
    }

528 529 530 531
    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);
    }
K
kohsuke 已提交
532 533

    /**
K
kohsuke 已提交
534 535
     * Keyed by {@link User#id}. This map is used to ensure
     * singleton-per-id semantics of {@link User} objects.
K
kohsuke 已提交
536
     */
537
    private static final Map<String,User> byName = new TreeMap<String,User>(String.CASE_INSENSITIVE_ORDER);
K
kohsuke 已提交
538 539 540 541 542 543 544 545 546 547 548 549

    /**
     * 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);
    }

550
    public ACL getACL() {
551
        final ACL base = Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
552
        // always allow a non-anonymous user full control of himself.
553 554
        return new ACL() {
            public boolean hasPermission(Authentication a, Permission permission) {
555 556
                return (a.getName().equals(id) && !(a instanceof AnonymousAuthenticationToken))
                        || base.hasPermission(a, permission);
557 558
            }
        };
559 560
    }

K
TAB->WS  
kohsuke 已提交
561 562 563
    public void checkPermission(Permission permission) {
        getACL().checkPermission(permission);
    }
564

K
TAB->WS  
kohsuke 已提交
565 566 567
    public boolean hasPermission(Permission permission) {
        return getACL().hasPermission(permission);
    }
568

569 570 571 572
    /**
     * With ADMINISTER permission, can delete users with persisted data but can't delete self.
     */
    public boolean canDelete() {
573
        return hasPermission(Jenkins.ADMINISTER) && !id.equals(Jenkins.getAuthentication().getName())
574 575 576
                && new File(getRootDir(), id).exists();
    }

577 578 579 580 581 582 583 584 585 586
    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;
    }
587
}