PermissionBackend: delegated permission checking

Define a new API for permission checks, splitting canFoo() type
methods into separate check() and test() methods. With this split
the implementation can handle action invocations differently from
generating results for UI button state setting.

Implement against the current ProjectControl, RefControl and
ChangeControl classes by adding private inner classes inside each
control class.  This allows migration commits to make a single
canFoo() method private and update callers in smaller steps.

Eventually PermissionBackend may become pluggable, allowing for
alternative methods of managing permissions.

Change-Id: I889f579233f2121ed19b3b9c2d8c7c18f7033f93
This commit is contained in:
Shawn Pearce 2017-02-18 12:33:33 -08:00 committed by David Pursehouse
parent 3f849afecd
commit 3a9bd79051
16 changed files with 1101 additions and 11 deletions

View File

@ -62,10 +62,9 @@ import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
import com.google.gerrit.server.notedb.NoteDbModule;
import com.google.gerrit.server.patch.DiffExecutorModule;
import com.google.gerrit.server.patch.PatchListCacheImpl;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.CommentLinkProvider;
import com.google.gerrit.server.project.DefaultPermissionBackendModule;
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SectionSortCache;
import com.google.gerrit.server.query.change.ChangeData;
@ -142,10 +141,9 @@ public class BatchProgramModule extends FactoryModule {
bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
.annotatedWith(GitReceivePackGroups.class)
.toInstance(Collections.<AccountGroup.UUID>emptySet());
bind(ChangeControl.Factory.class);
factory(ProjectControl.AssistedFactory.class);
install(new BatchGitModule());
install(new DefaultPermissionBackendModule());
install(new DefaultCacheFactory.Module());
install(new ExternalIdModule());
install(new GroupModule());

View File

@ -157,11 +157,10 @@ import com.google.gerrit.server.patch.PatchScriptFactory;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.project.AccessControlModule;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.CommentLinkProvider;
import com.google.gerrit.server.project.DefaultPermissionBackendModule;
import com.google.gerrit.server.project.PermissionCollection;
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectNode;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SectionSortCache;
@ -228,6 +227,7 @@ public class GerritGlobalModule extends FactoryModule {
install(new AccessControlModule());
install(new CmdLineParserModule());
install(new DefaultPermissionBackendModule());
install(new EmailModule());
install(new ExternalIdModule());
install(new GitModule());
@ -289,8 +289,6 @@ public class GerritGlobalModule extends FactoryModule {
bind(PatchSetInfoFactory.class);
bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
bind(ChangeControl.GenericFactory.class);
bind(ProjectControl.GenericFactory.class);
bind(AccountControl.Factory.class);
install(new AuditModule());

View File

@ -0,0 +1,55 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.permissions;
import com.google.gerrit.common.data.Permission;
import java.util.Locale;
import java.util.Optional;
public enum ChangePermission implements ChangePermissionOrLabel {
READ(Permission.READ),
RESTORE,
DELETE,
ABANDON(Permission.ABANDON),
EDIT_ASSIGNEE(Permission.EDIT_ASSIGNEE),
EDIT_DESCRIPTION,
EDIT_HASHTAGS(Permission.EDIT_HASHTAGS),
EDIT_TOPIC_NAME(Permission.EDIT_TOPIC_NAME),
REMOVE_REVIEWER(Permission.REMOVE_REVIEWER),
ADD_PATCH_SET(Permission.ADD_PATCH_SET),
REBASE(Permission.REBASE),
SUBMIT(Permission.SUBMIT);
private final String name;
ChangePermission() {
name = null;
}
ChangePermission(String name) {
this.name = name;
}
/** @return name used in {@code project.config} permissions. */
@Override
public Optional<String> permissionName() {
return Optional.ofNullable(name);
}
@Override
public String describeForException() {
return toString().toLowerCase(Locale.US).replace('_', ' ');
}
}

View File

@ -0,0 +1,26 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.permissions;
import java.util.Optional;
/** A {@link ChangePermission} or a {@link LabelPermission}. */
public interface ChangePermissionOrLabel {
/** @return name used in {@code project.config} permissions. */
public Optional<String> permissionName();
/** @return readable identifier of this permission for exception message. */
public String describeForException();
}

View File

@ -0,0 +1,168 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.permissions;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Provider;
import java.util.Collection;
import java.util.Set;
/**
* Helpers for {@link PermissionBackend} that must fail.
*
* <p>These helpers are useful to curry failure state identified inside a non-throwing factory
* method to the throwing {@code check} or {@code test} methods.
*/
public class FailedPermissionBackend {
public static ForProject project(String message) {
return project(message, null);
}
public static ForProject project(String message, Throwable cause) {
return new FailedProject(message, cause);
}
public static ForRef ref(String message) {
return ref(message, null);
}
public static ForRef ref(String message, Throwable cause) {
return new FailedRef(message, cause);
}
public static ForChange change(String message) {
return change(message, null);
}
public static ForChange change(String message, Throwable cause) {
return new FailedChange(message, cause);
}
private FailedPermissionBackend() {}
private static class FailedProject extends ForProject {
private final String message;
private final Throwable cause;
FailedProject(String message, Throwable cause) {
this.message = message;
this.cause = cause;
}
@Override
public ForProject database(Provider<ReviewDb> db) {
return this;
}
@Override
public ForProject user(CurrentUser user) {
return this;
}
@Override
public ForRef ref(String ref) {
return new FailedRef(message, cause);
}
@Override
public void check(ProjectPermission perm) throws PermissionBackendException {
throw new PermissionBackendException(message, cause);
}
@Override
public Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
throws PermissionBackendException {
throw new PermissionBackendException(message, cause);
}
}
private static class FailedRef extends ForRef {
private final String message;
private final Throwable cause;
FailedRef(String message, Throwable cause) {
this.message = message;
this.cause = cause;
}
@Override
public ForRef database(Provider<ReviewDb> db) {
return this;
}
@Override
public ForRef user(CurrentUser user) {
return this;
}
@Override
public ForChange change(ChangeData cd) {
return new FailedChange(message, cause);
}
@Override
public ForChange change(ChangeNotes cd) {
return new FailedChange(message, cause);
}
@Override
public void check(RefPermission perm) throws PermissionBackendException {
throw new PermissionBackendException(message, cause);
}
@Override
public Set<RefPermission> test(Collection<RefPermission> permSet)
throws PermissionBackendException {
throw new PermissionBackendException(message, cause);
}
}
private static class FailedChange extends ForChange {
private final String message;
private final Throwable cause;
FailedChange(String message, Throwable cause) {
this.message = message;
this.cause = cause;
}
@Override
public ForChange database(Provider<ReviewDb> db) {
return this;
}
@Override
public ForChange user(CurrentUser user) {
return this;
}
@Override
public void check(ChangePermissionOrLabel perm) throws PermissionBackendException {
throw new PermissionBackendException(message, cause);
}
@Override
public <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
throws PermissionBackendException {
throw new PermissionBackendException(message, cause);
}
}
}

View File

@ -0,0 +1,127 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.permissions;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.server.util.LabelVote;
import java.util.Optional;
/** Permission representing a label. */
public class LabelPermission implements ChangePermissionOrLabel {
private final String name;
/**
* Construct a reference to a label permission.
*
* @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
*/
public LabelPermission(String name) {
this.name = LabelType.checkName(name);
}
/** @return name of the label, e.g. {@code "Code-Review"}. */
public String label() {
return name;
}
/** @return name used in {@code project.config} permissions. */
@Override
public Optional<String> permissionName() {
return Optional.of(Permission.forLabel(label()));
}
@Override
public String describeForException() {
return "label " + label();
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public boolean equals(Object other) {
return other instanceof LabelPermission && name.equals(((LabelPermission) other).name);
}
@Override
public String toString() {
return "Label[" + name + ']';
}
/** A {@link LabelPermission} at a specific value. */
public static class WithValue implements ChangePermissionOrLabel {
private final LabelVote label;
/**
* Construct a reference to a label at a specific value.
*
* @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
* @param value numeric score assigned to the label.
*/
public WithValue(String name, short value) {
this(LabelVote.create(name, value));
}
/**
* Construct a reference to a label at a specific value.
*
* @param label label name and vote.
*/
public WithValue(LabelVote label) {
this.label = checkNotNull(label, "LabelVote");
}
/** @return name of the label, e.g. {@code "Code-Review"}. */
public String label() {
return label.label();
}
/** @return specific value of the label, e.g. 1 or 2. */
public short value() {
return label.value();
}
/** @return name used in {@code project.config} permissions. */
@Override
public Optional<String> permissionName() {
return Optional.of(Permission.forLabel(label()));
}
@Override
public String describeForException() {
return "label " + label.formatWithEquals();
}
@Override
public int hashCode() {
return label.hashCode();
}
@Override
public boolean equals(Object other) {
return other instanceof WithValue && label.equals(((WithValue) other).label);
}
@Override
public String toString() {
return "Label[" + label.format() + ']';
}
}
}

View File

@ -0,0 +1,211 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.permissions;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import com.google.inject.util.Providers;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Checks authorization to perform an action on project, ref, or change.
*
* <p>{@code PermissionBackend} should be a singleton for the server, acting as a factory for
* lightweight request instances.
*
* <p>{@code check} methods should be used during action handlers to verify the user is allowed to
* exercise the specified permission. For convenience in implementation {@code check} methods throw
* {@link AuthException} if the permission is denied.
*
* <p>{@code test} methods should be used when constructing replies to the client and the result
* object needs to include a true/false hint indicating the user's ability to exercise the
* permission. This is suitable for configuring UI button state, but should not be relied upon to
* guard handlers before making state changes.
*
* <p>Example use:
*
* <pre>
* private final PermissionBackend permissions;
* private final Provider<CurrentUser> user;
*
* @Inject
* Foo(PermissionBackend permissions, Provider<CurrentUser> user) {
* this.permissions = permissions;
* this.user = user;
* }
*
* public void apply(...) {
* permissions.user(user).change(cd).check(ChangePermission.SUBMIT);
* }
*
* public UiAction.Description getDescription(ChangeResource rsrc) {
* return new UiAction.Description()
* .setLabel("Submit")
* .setVisible(rsrc.permissions().testOrFalse(ChangePermission.SUBMIT));
* }
* </pre>
*/
public abstract class PermissionBackend {
private static final Logger logger = LoggerFactory.getLogger(PermissionBackend.class);
/** @return lightweight factory scoped to answer for the specified user. */
public abstract WithUser user(CurrentUser user);
/** @return lightweight factory scoped to answer for the specified user. */
public WithUser user(Provider<CurrentUser> user) {
return user(checkNotNull(user, "Provider<CurrentUser>").get());
}
/** PermissionBackend with an optional per-request ReviewDb handle. */
public abstract static class AcceptsReviewDb<T> {
protected Provider<ReviewDb> db;
public T database(Provider<ReviewDb> db) {
if (db != null) {
this.db = db;
}
return self();
}
public T database(ReviewDb db) {
return database(Providers.of(checkNotNull(db, "ReviewDb")));
}
@SuppressWarnings("unchecked")
private T self() {
return (T) this;
}
}
/** PermissionBackend scoped to a specific user. */
public abstract static class WithUser extends AcceptsReviewDb<WithUser> {
/** @return instance scoped for the specified project. */
public abstract ForProject project(Project.NameKey project);
/** @return instance scoped for the {@code ref}, and its parent project. */
public ForRef ref(Branch.NameKey ref) {
return project(ref.getParentKey()).ref(ref.get()).database(db);
}
/** @return instance scoped for the change, and its destination ref and project. */
public ForChange change(ChangeData cd) {
try {
return ref(cd.change().getDest()).change(cd);
} catch (OrmException e) {
return FailedPermissionBackend.change("unavailable", e);
}
}
/** @return instance scoped for the change, and its destination ref and project. */
public ForChange change(ChangeNotes notes) {
return ref(notes.getChange().getDest()).change(notes);
}
}
/** PermissionBackend scoped to a user and project. */
public abstract static class ForProject extends AcceptsReviewDb<ForProject> {
/** @return new instance rescoped to same project, but different {@code user}. */
public abstract ForProject user(CurrentUser user);
/** @return instance scoped for {@code ref} in this project. */
public abstract ForRef ref(String ref);
/** Verify scoped user can {@code perm}, throwing if denied. */
public abstract void check(ProjectPermission perm)
throws AuthException, PermissionBackendException;
/** Filter {@code permSet} to permissions scoped user might be able to perform. */
public abstract Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
throws PermissionBackendException;
public boolean test(ProjectPermission perm) throws PermissionBackendException {
return test(EnumSet.of(perm)).contains(perm);
}
}
/** PermissionBackend scoped to a user, project and reference. */
public abstract static class ForRef extends AcceptsReviewDb<ForRef> {
/** @return new instance rescoped to same reference, but different {@code user}. */
public abstract ForRef user(CurrentUser user);
/** @return instance scoped to change. */
public abstract ForChange change(ChangeData cd);
/** @return instance scoped to change. */
public abstract ForChange change(ChangeNotes notes);
/** Verify scoped user can {@code perm}, throwing if denied. */
public abstract void check(RefPermission perm) throws AuthException, PermissionBackendException;
/** Filter {@code permSet} to permissions scoped user might be able to perform. */
public abstract Set<RefPermission> test(Collection<RefPermission> permSet)
throws PermissionBackendException;
public boolean test(RefPermission perm) throws PermissionBackendException {
return test(EnumSet.of(perm)).contains(perm);
}
}
/** PermissionBackend scoped to a user, project, reference and change. */
public abstract static class ForChange extends AcceptsReviewDb<ForChange> {
/** @return new instance rescoped to same change, but different {@code user}. */
public abstract ForChange user(CurrentUser user);
/** Verify scoped user can {@code perm}, throwing if denied. */
public abstract void check(ChangePermissionOrLabel perm)
throws AuthException, PermissionBackendException;
/** Filter {@code permSet} to permissions scoped user might be able to perform. */
public abstract <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
throws PermissionBackendException;
public boolean test(ChangePermissionOrLabel perm) throws PermissionBackendException {
return test(Collections.singleton(perm)).contains(perm);
}
/**
* Test if user may be able to perform the permission.
*
* <p>Similar to {@link #test(ChangePermissionOrLabel)} except this method returns {@code false}
* instead of throwing an exception.
*
* @param perm the permission to test.
* @return true if the user might be able to perform the permission; false if the user may be
* missing the necessary grants or state, or if the backend threw an exception.
*/
public boolean testOrFalse(ChangePermissionOrLabel perm) {
try {
return test(perm);
} catch (PermissionBackendException e) {
logger.warn("Cannot test " + perm + "; assuming false", e);
return false;
}
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.permissions;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.account.GroupBackend;
/**
* Thrown when {@link PermissionBackend} cannot compute the result.
*
* <p>This is typically a transient failure, such as a required {@link GroupBackend} not responding
* to membership requests.
*/
public class PermissionBackendException extends Exception {
private static final long serialVersionUID = 1L;
public PermissionBackendException(String message) {
super(message);
}
public PermissionBackendException(@Nullable Throwable cause) {
super(cause);
}
public PermissionBackendException(String message, @Nullable Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,42 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.permissions;
import com.google.gerrit.common.data.Permission;
import java.util.Locale;
import java.util.Optional;
public enum ProjectPermission {
READ(Permission.READ);
private final String name;
ProjectPermission() {
name = null;
}
ProjectPermission(String name) {
this.name = name;
}
/** @return name used in {@code project.config} permissions. */
public Optional<String> permissionName() {
return Optional.ofNullable(name);
}
public String describeForException() {
return toString().toLowerCase(Locale.US).replace('_', ' ');
}
}

View File

@ -0,0 +1,52 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.permissions;
import com.google.gerrit.common.data.Permission;
import java.util.Locale;
import java.util.Optional;
public enum RefPermission {
READ(Permission.READ),
CREATE(Permission.CREATE),
DELETE(Permission.DELETE),
UPDATE(Permission.PUSH),
FORCE_UPDATE,
FORGE_AUTHOR(Permission.FORGE_AUTHOR),
FORGE_COMMITTER(Permission.FORGE_COMMITTER),
FORGE_SERVER(Permission.FORGE_SERVER),
CREATE_CHANGE;
private final String name;
RefPermission() {
name = null;
}
RefPermission(String name) {
this.name = name;
}
/** @return name used in {@code project.config} permissions. */
public Optional<String> permissionName() {
return Optional.ofNullable(name);
}
public String describeForException() {
return toString().toLowerCase(Locale.US).replace('_', ' ');
}
}

View File

@ -46,8 +46,5 @@ public class AccessControlModule extends FactoryModule {
.annotatedWith(GitReceivePackGroups.class)
.toProvider(GitReceivePackGroupsProvider.class)
.in(SINGLETON);
bind(ChangeControl.Factory.class);
factory(ProjectControl.AssistedFactory.class);
}
}

View File

@ -15,13 +15,17 @@
package com.google.gerrit.server.project;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.RefConfigSection;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
@ -32,13 +36,22 @@ import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.ChangePermissionOrLabel;
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Access control management for a user accessing a single change. */
public class ChangeControl {
@ -488,4 +501,137 @@ public class ChangeControl {
|| getRefControl().canViewPrivateChanges()
|| getUser().isInternalUser();
}
ForChange asForChange(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
return new ForChangeImpl(cd, db);
}
private class ForChangeImpl extends ForChange {
private ChangeData cd;
private Map<String, PermissionRange> labels;
ForChangeImpl(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
this.cd = cd;
this.db = db;
}
private ReviewDb db() {
if (db != null) {
return db.get();
} else if (cd != null) {
return cd.db();
} else {
return null;
}
}
private ChangeData changeData() {
if (cd == null) {
ReviewDb reviewDb = db();
checkState(reviewDb != null, "need ReviewDb");
cd = changeDataFactory.create(reviewDb, ChangeControl.this);
}
return cd;
}
@Override
public ForChange user(CurrentUser user) {
return getUser().equals(user) ? this : forUser(user).asForChange(cd, db);
}
@Override
public void check(ChangePermissionOrLabel perm)
throws AuthException, PermissionBackendException {
if (!can(perm)) {
throw new AuthException(perm.describeForException() + " not permitted");
}
}
@Override
public <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
throws PermissionBackendException {
Set<T> ok = newSet(permSet);
for (T perm : permSet) {
if (can(perm)) {
ok.add(perm);
}
}
return ok;
}
private boolean can(ChangePermissionOrLabel perm) throws PermissionBackendException {
if (perm instanceof ChangePermission) {
return can((ChangePermission) perm);
} else if (perm instanceof LabelPermission) {
return can((LabelPermission) perm);
} else if (perm instanceof LabelPermission.WithValue) {
return can((LabelPermission.WithValue) perm);
}
throw new PermissionBackendException(perm + " unsupported");
}
private boolean can(ChangePermission perm) throws PermissionBackendException {
try {
switch (perm) {
case READ:
return isVisible(db(), changeData());
case ABANDON:
return canAbandon(db());
case DELETE:
return canDelete(db(), getChange().getStatus());
case ADD_PATCH_SET:
return canAddPatchSet(db());
case EDIT_ASSIGNEE:
return canEditAssignee();
case EDIT_DESCRIPTION:
return canEditDescription();
case EDIT_HASHTAGS:
return canEditHashtags();
case EDIT_TOPIC_NAME:
return canEditTopicName();
case REBASE:
return canRebase(db());
case RESTORE:
return canRestore(db());
case SUBMIT:
return canSubmit();
case REMOVE_REVIEWER: // TODO Honor specific removal filters?
return getRefControl().canPerform(perm.permissionName().get());
}
} catch (OrmException e) {
throw new PermissionBackendException("unavailable", e);
}
throw new PermissionBackendException(perm + " unsupported");
}
private boolean can(LabelPermission perm) {
return !label(perm.permissionName().get()).isEmpty();
}
private boolean can(LabelPermission.WithValue perm) {
return label(perm.permissionName().get()).contains(perm.value());
}
private PermissionRange label(String permission) {
if (labels == null) {
labels = Maps.newHashMapWithExpectedSize(4);
}
PermissionRange r = labels.get(permission);
if (r == null) {
r = getRange(permission);
labels.put(permission, r);
}
return r;
}
}
static <T extends ChangePermissionOrLabel> Set<T> newSet(Collection<T> permSet) {
if (permSet instanceof EnumSet) {
@SuppressWarnings({"unchecked", "rawtypes"})
Set<T> s = ((EnumSet) permSet).clone();
s.clear();
return s;
}
return Sets.newHashSetWithExpectedSize(permSet.size());
}
}

View File

@ -0,0 +1,61 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.project;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.permissions.FailedPermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@Singleton
class DefaultPermissionBackend extends PermissionBackend {
private final ProjectCache projectCache;
@Inject
DefaultPermissionBackend(ProjectCache projectCache) {
this.projectCache = projectCache;
}
@Override
public WithUser user(CurrentUser user) {
return new WithUserImpl(checkNotNull(user, "user"));
}
class WithUserImpl extends WithUser {
private final CurrentUser user;
WithUserImpl(CurrentUser user) {
this.user = checkNotNull(user, "user");
}
@Override
public ForProject project(Project.NameKey project) {
try {
ProjectState state = projectCache.checkedGet(project);
if (state != null) {
return state.controlFor(user).asForProject().database(db);
}
return FailedPermissionBackend.project("not found");
} catch (IOException e) {
return FailedPermissionBackend.project("unavailable", e);
}
}
}
}

View File

@ -0,0 +1,33 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.project;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.inject.Scopes;
/** Binds the default {@link PermissionBackend}. */
public class DefaultPermissionBackendModule extends FactoryModule {
@Override
protected void configure() {
bind(PermissionBackend.class).to(DefaultPermissionBackend.class).in(Scopes.SINGLETON);
// TODO(sop) Hide ProjectControl, RefControl, ChangeControl related bindings.
bind(ProjectControl.GenericFactory.class);
factory(ProjectControl.AssistedFactory.class);
bind(ChangeControl.GenericFactory.class);
bind(ChangeControl.Factory.class);
}
}

View File

@ -25,6 +25,7 @@ import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.metrics.Counter0;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.MetricMaker;
@ -45,6 +46,10 @@ import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
@ -56,6 +61,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -565,4 +571,47 @@ public class ProjectControl {
Map<String, Ref> refs = filter.filter(m, true);
return !refs.isEmpty() && IncludedInResolver.includedInOne(repo, rw, commit, refs.values());
}
ForProject asForProject() {
return new ForProjectImpl();
}
private class ForProjectImpl extends ForProject {
@Override
public ForProject user(CurrentUser user) {
return forUser(user).asForProject().database(db);
}
@Override
public ForRef ref(String ref) {
return controlForRef(ref).asForRef().database(db);
}
@Override
public void check(ProjectPermission perm) throws AuthException, PermissionBackendException {
if (!can(perm)) {
throw new AuthException(perm.describeForException() + " not permitted");
}
}
@Override
public Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
throws PermissionBackendException {
EnumSet<ProjectPermission> ok = EnumSet.noneOf(ProjectPermission.class);
for (ProjectPermission perm : permSet) {
if (can(perm)) {
ok.add(perm);
}
}
return ok;
}
private boolean can(ProjectPermission perm) throws PermissionBackendException {
switch (perm) {
case READ:
return isReadable();
}
throw new PermissionBackendException(perm + " unsupported");
}
}
}

View File

@ -14,17 +14,31 @@
package com.google.gerrit.server.project;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.FailedPermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -647,4 +661,77 @@ public class RefControl {
effective.put(permissionName, mine);
return mine;
}
ForRef asForRef() {
return new ForRefImpl();
}
private class ForRefImpl extends ForRef {
@Override
public ForRef user(CurrentUser user) {
return forUser(user).asForRef().database(db);
}
@Override
public ForChange change(ChangeData cd) {
try {
return cd.changeControl().forUser(getUser()).asForChange(cd, db);
} catch (OrmException e) {
return FailedPermissionBackend.change("unavailable", e);
}
}
@Override
public ForChange change(ChangeNotes notes) {
Change change = notes.getChange();
checkArgument(
getProjectControl().getProject().getNameKey().equals(change.getProject()),
"mismatched project");
return getProjectControl().controlFor(notes).asForChange(null, db);
}
@Override
public void check(RefPermission perm) throws AuthException, PermissionBackendException {
if (!can(perm)) {
throw new AuthException(perm.describeForException() + " not permitted");
}
}
@Override
public Set<RefPermission> test(Collection<RefPermission> permSet)
throws PermissionBackendException {
EnumSet<RefPermission> ok = EnumSet.noneOf(RefPermission.class);
for (RefPermission perm : permSet) {
if (can(perm)) {
ok.add(perm);
}
}
return ok;
}
private boolean can(RefPermission perm) throws PermissionBackendException {
switch (perm) {
case READ:
return isVisible();
case CREATE:
// TODO This isn't an accurate test.
return canPerform(perm.permissionName().get());
case DELETE:
return canDelete();
case UPDATE:
return canUpdate();
case FORCE_UPDATE:
return canForceUpdate();
case FORGE_AUTHOR:
return canForgeAuthor();
case FORGE_COMMITTER:
return canForgeCommitter();
case FORGE_SERVER:
return canForgeGerritServerIdentity();
case CREATE_CHANGE:
return canUpload();
}
throw new PermissionBackendException(perm + " unsupported");
}
}
}