Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-18884] Add People View #9052

53 changes: 20 additions & 33 deletions core/src/main/java/hudson/model/View.java
Original file line number Diff line number Diff line change
Expand Up @@ -737,14 +737,7 @@
public People(Jenkins parent) {
this.parent = parent;
// for Hudson, really load all users
Map<User, UserInfo> users = getUserInfo(parent.getItems());
User unknown = User.getUnknown();
for (User u : User.getAll()) {
if (u == unknown) continue; // skip the special 'unknown' user
if (!users.containsKey(u))
users.put(u, new UserInfo(u, null, null));
}
this.users = toList(users);
this.users = toList(getUserInfo(parent.getItems()));
}

public People(View parent) {
Expand All @@ -755,167 +748,161 @@
private Map<User, UserInfo> getUserInfo(Collection<? extends Item> items) {
Map<User, UserInfo> users = new HashMap<>();
for (Item item : items) {
if (!item.hasPermission(Item.READ)) {
continue;
}
for (Job<?, ?> job : item.getAllJobs()) {
if (!job.hasPermission(Item.READ)) {
continue;
}
RunList<? extends Run<?, ?>> runs = job.getBuilds();
for (Run<?, ?> r : runs) {
if (r instanceof RunWithSCM) {
RunWithSCM<?, ?> runWithSCM = (RunWithSCM<?, ?>) r;

for (ChangeLogSet<? extends ChangeLogSet.Entry> c : runWithSCM.getChangeSets()) {
for (ChangeLogSet.Entry entry : c) {
User user = entry.getAuthor();

UserInfo info = users.get(user);
if (info == null)
users.put(user, new UserInfo(user, job, r.getTimestamp()));
else if (info.getLastChange().before(r.getTimestamp())) {
info.project = job;
info.lastChange = r.getTimestamp();
}
}
}
}
}
}
}
return users;
}

private List<UserInfo> toList(Map<User, UserInfo> users) {
ArrayList<UserInfo> list = new ArrayList<>(users.values());
Collections.sort(list);
return Collections.unmodifiableList(list);
}

public Api getApi() {
return new Api(this);
}

/**
* @deprecated Potentially very expensive call; do not use from Jelly views.
*/
@Deprecated
public static boolean isApplicable(Collection<? extends Item> items) {
for (Item item : items) {
for (Job job : item.getAllJobs()) {
RunList<? extends Run<?, ?>> runs = job.getBuilds();

for (Run<?, ?> r : runs) {
if (r instanceof RunWithSCM) {
RunWithSCM<?, ?> runWithSCM = (RunWithSCM<?, ?>) r;
for (ChangeLogSet<? extends ChangeLogSet.Entry> c : runWithSCM.getChangeSets()) {
for (ChangeLogSet.Entry entry : c) {
User user = entry.getAuthor();
if (user != null)
return true;
}
}
}
}
}
}
return false;
}
}

/**
* Variant of {@link People} which can be displayed progressively, since it may be slow.
* @since 1.484
*/
public static final class AsynchPeople extends ProgressiveRendering { // JENKINS-15206

private final Collection<TopLevelItem> items;
private final User unknown;
private final Map<User, UserInfo> users = new HashMap<>();
private final Set<User> modified = new HashSet<>();
private final String iconSize;
public final ModelObject parent;

/** @see Jenkins#getAsynchPeople */
public AsynchPeople(Jenkins parent) {
this.parent = parent;
items = parent.getItems();
unknown = User.getUnknown();
}

/** @see View#getAsynchPeople */
public AsynchPeople(View parent) {
this.parent = parent;
items = parent.getItems();
unknown = null;
}

{
StaplerRequest req = Stapler.getCurrentRequest();
iconSize = req != null ? Functions.validateIconSize(Functions.getCookie(req, "iconSize", "32x32")) : "32x32";
}

@Override protected void compute() throws Exception {
int itemCount = 0;
for (Item item : items) {
for (Job<?, ?> job : item.getAllJobs()) {
if (!item.hasPermission(Item.READ)) {
continue;
}
for (Job<?, ?> job : item.getAllJobs()) {
if (!item.hasPermission(Item.READ)) {
continue;
}
RunList<? extends Run<?, ?>> builds = job.getBuilds();
int buildCount = 0;
for (Run<?, ?> r : builds) {
if (canceled()) {
return;
}
if (!(r instanceof RunWithSCM)) {
continue;
}

RunWithSCM<?, ?> runWithSCM = (RunWithSCM<?, ?>) r;
for (ChangeLogSet<? extends ChangeLogSet.Entry> c : runWithSCM.getChangeSets()) {
for (ChangeLogSet.Entry entry : c) {
User user = entry.getAuthor();
UserInfo info = users.get(user);
if (info == null) {
UserInfo userInfo = new UserInfo(user, job, r.getTimestamp());
userInfo.avatar = UserAvatarResolver.resolveOrNull(user, iconSize);
synchronized (this) {
users.put(user, userInfo);
modified.add(user);
}
} else if (info.getLastChange().before(r.getTimestamp())) {
synchronized (this) {
info.project = job;
info.lastChange = r.getTimestamp();
modified.add(user);
}
}
}
}
// TODO consider also adding the user of the UserCause when applicable
buildCount++;
// TODO this defeats lazy-loading. Should rather do a breadth-first search, as in hudson.plugins.view.dashboard.builds.LatestBuilds
// (though currently there is no quick implementation of RunMap.size() ~ idOnDisk.size(), which would be needed for proper progress)
progress((itemCount + 1.0 * buildCount / builds.size()) / (items.size() + 1));
}
}
itemCount++;
progress(1.0 * itemCount / (items.size() + /* handling User.getAll */1));
}
if (unknown != null) {
if (canceled()) {
return;
}
for (User u : User.getAll()) { // TODO nice to have a method to iterate these lazily
if (canceled()) {
return;
}
if (u == unknown) {
continue;
}
if (!users.containsKey(u)) {
UserInfo userInfo = new UserInfo(u, null, null);
userInfo.avatar = UserAvatarResolver.resolveOrNull(u, iconSize);
synchronized (this) {
users.put(u, userInfo);
modified.add(u);
}
}
}
}
}

// for testing purpose
@Restricted(NoExternalUse.class)
Set<User> getModified() {
return modified;

Check warning on line 905 in core/src/main/java/hudson/model/View.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 751-905 are not covered by tests
}

@NonNull
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/java/jenkins/util/ProgressiveRendering.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import javax.servlet.http.HttpServletRequest;
import net.sf.json.JSON;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.RequestImpl;
import org.kohsuke.stapler.Stapler;
Expand Down Expand Up @@ -202,6 +204,15 @@
}
}

/**
* @return whether the computation has finished.
*/
// for testing purpose
@Restricted(NoExternalUse.class)
public boolean isFinished() {
return (status < 0 || status >= 1);

Check warning on line 213 in core/src/main/java/jenkins/util/ProgressiveRendering.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 213 is not covered by tests
}

/**
* Actually do the work.
* <p>The security context will be that in effect when the web request was made.
Expand Down
106 changes: 104 additions & 2 deletions test/src/test/java/hudson/model/AsynchPeopleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,24 @@

package hudson.model;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;

import com.google.common.collect.Collections2;
import com.google.common.collect.Sets;
import hudson.security.AuthorizationMatrixProperty;
import hudson.security.Permission;
import java.util.*;
import jenkins.model.Jenkins;
import org.htmlunit.html.DomElement;
import org.htmlunit.html.HtmlPage;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.FakeChangeLogSCM;
import org.jvnet.hudson.test.For;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.recipes.LocalData;
import org.springframework.security.core.context.SecurityContextHolder;

@For(View.AsynchPeople.class)
public class AsynchPeopleTest {
Expand Down Expand Up @@ -61,4 +69,98 @@ public class AsynchPeopleTest {
*/
}

@Issue("JENKINS-18884")
@Test
@LocalData
public void testProjectPermission() throws Exception {
User user1 = User.get("user1", false, Collections.emptyMap());
User admin = User.get("admin", false, Collections.emptyMap());

// p1 can be accessed by user1, admin
// p2 can be accessed by admin
FreeStyleProject p1 = j.createFreeStyleProject();
{
Map<Permission, Set<String>> permissions = new HashMap<>();
permissions.put(Item.READ, Sets.newHashSet(user1.getId(), admin.getId()));
p1.addProperty(new AuthorizationMatrixProperty(permissions));
}
assertFalse(p1.getACL().hasPermission(Jenkins.ANONYMOUS, Item.READ));
assertTrue(p1.getACL().hasPermission(user1.impersonate(), Item.READ));
assertTrue(p1.getACL().hasPermission(admin.impersonate(), Item.READ));

FreeStyleProject p2 = j.createFreeStyleProject();
{
Map<Permission, Set<String>> permissions = new HashMap<>();
permissions.put(Item.READ, Sets.newHashSet(admin.getId()));
p1.addProperty(new AuthorizationMatrixProperty(permissions));
}
assertFalse(p2.getACL().hasPermission(Jenkins.ANONYMOUS, Item.READ));
assertFalse(p2.getACL().hasPermission(user1.impersonate(), Item.READ));
assertTrue(p2.getACL().hasPermission(admin.impersonate(), Item.READ));

// create fake changelog
{
FakeChangeLogSCM scm = new FakeChangeLogSCM();
scm.addChange().withAuthor("author1");
scm.addChange().withAuthor("author2");
p1.setScm(scm);
}
{
FakeChangeLogSCM scm = new FakeChangeLogSCM();
scm.addChange().withAuthor("author3");
scm.addChange().withAuthor("author4");
p2.setScm(scm);
}

j.assertBuildStatusSuccess(p1.scheduleBuild2(0));
j.assertBuildStatusSuccess(p2.scheduleBuild2(0));

ListView view = new ListView("test", j.jenkins);
view.add(p1);
view.add(p2);

{
SecurityContextHolder.getContext().setAuthentication(Jenkins.ANONYMOUS2);
View.AsynchPeople people = view.getAsynchPeople();
people.start();
while(!people.isFinished()) {
Thread.sleep(100);
}
Collection<String> authors = Collections2.transform(people.getModified(), input -> input.getId());
assertFalse(authors.contains("author1"));
assertFalse(authors.contains("author2"));
assertFalse(authors.contains("author3"));
assertFalse(authors.contains("author4"));
}

{
SecurityContextHolder.getContext().setAuthentication(user1.impersonate2());
View.AsynchPeople people = view.getAsynchPeople();
people.start();
while(!people.isFinished()) {
Thread.sleep(100);
}
Collection<String> authors = Collections2.transform(people.getModified(), input -> input.getId());
assertTrue(authors.contains("author1"));
assertTrue(authors.contains("author2"));
assertFalse(authors.contains("author3"));
assertFalse(authors.contains("author4"));
}

{
SecurityContextHolder.getContext().setAuthentication(admin.impersonate2());
View.AsynchPeople people = view.getAsynchPeople();
people.start();
while(!people.isFinished()) {
Thread.sleep(100);
}
Collection<String> authors = Collections2.transform(people.getModified(), input -> input.getId());
assertTrue(authors.contains("author1"));
assertTrue(authors.contains("author2"));
assertTrue(authors.contains("author3"));
assertTrue(authors.contains("author4"));
}

}

}
11 changes: 11 additions & 0 deletions test/src/test/resources/hudson/model/AsynchPeopleTest/config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version='1.0' encoding='UTF-8'?>
<hudson>
<version>1.0</version>
<useSecurity>true</useSecurity>
<authorizationStrategy class="hudson.security.ProjectMatrixAuthorizationStrategy">
<permission>hudson.model.Hudson.Administer:admin</permission>
<permission>hudson.model.Hudson.Read:admin</permission>
<permission>hudson.model.Hudson.Read:user1</permission>
</authorizationStrategy>
<securityRealm class="hudson.security.HudsonPrivateSecurityRealm" />
</hudson>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version='1.0' encoding='UTF-8'?>
<user>
<fullName>admin</fullName>
<properties>
<hudson.security.HudsonPrivateSecurityRealm_-Details>
<!--password: admin-->
<passwordHash>#jbcrypt:$2a$10$eBXjXOF.vPfWeqmYlrsSdeupTlcjh5qAOCeKqoH2JWQGcnE3vfR9K</passwordHash>
</hudson.security.HudsonPrivateSecurityRealm_-Details>
</properties>
</user>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version='1.0' encoding='UTF-8'?>
<user>
<fullName>user1</fullName>
<properties>
<hudson.security.HudsonPrivateSecurityRealm_-Details>
<!-- password: user1 -->
<passwordHash>#jbcrypt:$2a$10$Xw3Nij/ckiVMAbVPGn24..QtngnfJUDl6KfTz45SVP3tdlkksngZy</passwordHash>
</hudson.security.HudsonPrivateSecurityRealm_-Details>
</properties>
</user>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version='1.1' encoding='UTF-8'?>
<hudson.model.UserIdMapper>
<idToDirectoryNameMap>
<entry>
<string>admin</string>
<string>admin</string>
</entry>
<entry>
<string>user1</string>
<string>user1</string>
</entry>

</idToDirectoryNameMap>
</hudson.model.UserIdMapper>