diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java index 1574131ebc..c86d5af0af 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java @@ -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>() {}) .annotatedWith(GitReceivePackGroups.class) .toInstance(Collections.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()); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index 77474371cd..5f70786ace 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java @@ -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()); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermission.java new file mode 100644 index 0000000000..90d27547b9 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermission.java @@ -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 permissionName() { + return Optional.ofNullable(name); + } + + @Override + public String describeForException() { + return toString().toLowerCase(Locale.US).replace('_', ' '); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java new file mode 100644 index 0000000000..06c0d73ded --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java @@ -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 permissionName(); + + /** @return readable identifier of this permission for exception message. */ + public String describeForException(); +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java new file mode 100644 index 0000000000..4945879707 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java @@ -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. + * + *

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 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 test(Collection 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 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 test(Collection 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 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 Set test(Collection permSet) + throws PermissionBackendException { + throw new PermissionBackendException(message, cause); + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java new file mode 100644 index 0000000000..61f7330c54 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java @@ -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 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 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() + ']'; + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java new file mode 100644 index 0000000000..9e5350b80c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java @@ -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. + * + *

{@code PermissionBackend} should be a singleton for the server, acting as a factory for + * lightweight request instances. + * + *

{@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. + * + *

{@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. + * + *

Example use: + * + *

+ *   private final PermissionBackend permissions;
+ *   private final Provider user;
+ *
+ *   @Inject
+ *   Foo(PermissionBackend permissions, Provider 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));
+ * }
+ * 
+ */ +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 user) { + return user(checkNotNull(user, "Provider").get()); + } + + /** PermissionBackend with an optional per-request ReviewDb handle. */ + public abstract static class AcceptsReviewDb { + protected Provider db; + + public T database(Provider 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 { + /** @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 { + /** @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 test(Collection 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 { + /** @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 test(Collection 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 { + /** @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 Set test(Collection 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. + * + *

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; + } + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendException.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendException.java new file mode 100644 index 0000000000..be02a6fb50 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendException.java @@ -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. + * + *

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); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java new file mode 100644 index 0000000000..81c38f4002 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java @@ -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 permissionName() { + return Optional.ofNullable(name); + } + + public String describeForException() { + return toString().toLowerCase(Locale.US).replace('_', ' '); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java new file mode 100644 index 0000000000..37744b034f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java @@ -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 permissionName() { + return Optional.ofNullable(name); + } + + public String describeForException() { + return toString().toLowerCase(Locale.US).replace('_', ' '); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java index 5c0d8d75e7..6d772676f8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java @@ -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); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java index 27e64d8295..cf9da9884f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java @@ -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 db) { + return new ForChangeImpl(cd, db); + } + + private class ForChangeImpl extends ForChange { + private ChangeData cd; + private Map labels; + + ForChangeImpl(@Nullable ChangeData cd, @Nullable Provider 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 Set test(Collection permSet) + throws PermissionBackendException { + Set 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 Set newSet(Collection permSet) { + if (permSet instanceof EnumSet) { + @SuppressWarnings({"unchecked", "rawtypes"}) + Set s = ((EnumSet) permSet).clone(); + s.clear(); + return s; + } + return Sets.newHashSetWithExpectedSize(permSet.size()); + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java new file mode 100644 index 0000000000..51fe493e4d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java @@ -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); + } + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java new file mode 100644 index 0000000000..7a30863739 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java @@ -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); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java index e9976c59c1..b188ee4a25 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java @@ -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 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 test(Collection permSet) + throws PermissionBackendException { + EnumSet 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"); + } + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java index 4c751fa775..333bc1e7ea 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java @@ -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 test(Collection permSet) + throws PermissionBackendException { + EnumSet 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"); + } + } }