Enable create-project for non-Administrators

Allow other than 'Administrators' to use the create-project command.

Added configuration parameters in gerrit.config to denote which
group(s) are allowed to create projects, and which group(s) should
be new projects' owner(s) by default.

repository.*.createGroup specifies which group(s) are allowed to
create projects. Default is 'Administrators'.

repository.*.ownerGroup specifies which group(s) become the
owner(s) of new projects. Default is whatever is specified
by repository.*.createGroup, or 'Administrators' if no such
configuration exists. Can be overridden by create-project's
parameter --owner.

Bug: issue 269
Change-Id: Ieeb694508dd4c12578877335a63944bc90d6b553
This commit is contained in:
Hugo Josefson 2010-04-21 19:27:11 +02:00 committed by Shawn O. Pearce
parent c025627422
commit 072b470570
11 changed files with 378 additions and 54 deletions

View File

@ -11,7 +11,7 @@ SYNOPSIS
'ssh' -p <port> <host> 'gerrit create-project' \
\--name <NAME> \
[--branch <REF>] \
[\--owner <GROUP>] \
[\--owner <GROUP> ...] \
[\--description <DESC>] \
[\--submit-type <TYPE>] \
[\--use-contributor-agreements] \
@ -32,7 +32,11 @@ on the remote system to create the empty repository.
ACCESS
------
Caller must be a member of the privileged 'Administrators' group.
Caller must be a member of any of the groups defined by
repository.*.createGroup in gerrit.config.
If there is no such declaration, caller is required to be a member
of the privileged 'Administrators' group.
SCRIPTING
---------
@ -49,13 +53,14 @@ OPTIONS
Defaults to 'master'.
\--owner::
Name of the group which will initially own this repository.
The specified group must already be defined within Gerrit.
Only one group can be specified on the command line.
To specify additional owners, add the additional owners
through the web interface after project creation.
Name of the group(s) which will initially own this repository.
The specified group(s) must already be defined within Gerrit.
Several groups can be specified on the command line.
+
Defaults to `Administrators` if not specified.
Defaults to what is specified by repository.*.ownerGroup
in gerrit.config. If no such declaration(s) exist,
repository.*.createGroup will be used. If they don't exist,
`Administrators` will be used.
\--description::
Initial description of the project. If not specified,

View File

@ -1235,6 +1235,45 @@ instance are generally worked on with the repo multi-repository tool.
+
By default, false, as not all instances will deploy repo.
[[repository]]Section repository
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Repositories in this sense are the same as projects.
In the following example configuration the `Administrators` and the
`Registered Users` groups are set to be the ones to be allowed to
create projects matching `*` (any project). `Registered Users` is
set to be the default owner of new projects.
----
[repository "*"]
createGroup = Administrators
createGroup = Registered Users
ownerGroup = Registered Users
----
[NOTE]
Currently only the repository name `*` is supported.
This is a wildcard designating all repositories.
[[repository.name.createGroup]]repository.<name>.createGroup::
+
A name of a group which exists in the database. Zero, one or many
groups are allowed. Each on its own line. Groups which don't exist
in the database are ignored.
+
If no groups are declared (or only non-existing ones), the default
value `Administrators` is used.
[[repository.name.ownerGroup]]repository.<name>.ownerGroup::
+
A name of a group which exists in the database. Zero, one or many
groups are allowed. Each on its own line. Groups which don't exist
in the database are ignored.
+
If no groups are declared (or only non-existing ones), it defaults
to whatever is declared by `repository.<name>.createGroup` (including
any fallback to `Administrators`.)
[[sendemail]]Section sendemail
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -16,9 +16,18 @@ package com.google.gerrit.server.config;
import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AccountGroupName;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.SchemaFactory;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class ConfigUtil {
@ -199,6 +208,60 @@ public class ConfigUtil {
}
}
/**
* Resolve groups from group names, via the database. Group names not found in
* the database will be skipped.
*
* @param dbfactory database to resolve from.
* @param groupNames group names to resolve.
* @param log log for any warnings and errors.
* @param groupNotFoundWarning formatted message to output to the log for each
* group name which is not found in the database. <code>{0}</code> will
* be replaced with the group name.
* @return the actual groups resolved from the database. If no groups are
* found, returns an empty {@code Set}, never {@code null}.
*/
public static Set<AccountGroup.Id> groupsFor(
SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log,
String groupNotFoundWarning) {
final Set<AccountGroup.Id> result = new HashSet<AccountGroup.Id>();
try {
final ReviewDb db = dbfactory.open();
try {
for (String name : groupNames) {
AccountGroupName group =
db.accountGroupNames().get(new AccountGroup.NameKey(name));
if (group == null) {
log.warn(MessageFormat.format(groupNotFoundWarning, name));
} else {
result.add(group.getId());
}
}
} finally {
db.close();
}
} catch (OrmException e) {
log.error("Database error, cannot load groups", e);
}
return result;
}
/**
* Resolve groups from group names, via the database. Group names not found in
* the database will be skipped.
*
* @param dbfactory database to resolve from.
* @param groupNames group names to resolve.
* @param log log for any warnings and errors.
* @return the actual groups resolved from the database. If no groups are
* found, returns an empty {@code Set}, never {@code null}.
*/
public static Set<AccountGroup.Id> groupsFor(
SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log) {
return groupsFor(dbfactory, groupNames, log,
"Group \"{0}\" not in database, skipping.");
}
private static boolean match(final String a, final String... cases) {
for (final String b : cases) {
if (equalsIgnoreCase(a, b)) {

View File

@ -18,6 +18,7 @@ import static com.google.inject.Scopes.SINGLETON;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AuthType;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.server.AnonymousUser;
@ -64,9 +65,12 @@ import com.google.gerrit.server.util.IdGenerator;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.inject.Inject;
import com.google.inject.TypeLiteral;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
import java.util.Set;
/** Starts global state with standard dependencies. */
public class GerritGlobalModule extends FactoryModule {
private final AuthType loginType;
@ -93,6 +97,10 @@ public class GerritGlobalModule extends FactoryModule {
bind(Project.NameKey.class).annotatedWith(WildProjectName.class)
.toProvider(WildProjectNameProvider.class).in(SINGLETON);
bind(new TypeLiteral<Set<AccountGroup.Id>>(){}).annotatedWith(ProjectCreatorGroups.class)
.toProvider(ProjectCreatorGroupsProvider.class).in(SINGLETON);
bind(new TypeLiteral<Set<AccountGroup.Id>>(){}).annotatedWith(ProjectOwnerGroups.class)
.toProvider(ProjectOwnerGroupsProvider.class).in(SINGLETON);
bind(ApprovalTypes.class).toProvider(ApprovalTypesProvider.class).in(
SINGLETON);
bind(EmailExpander.class).toProvider(EmailExpanderProvider.class).in(

View File

@ -0,0 +1,30 @@
// Copyright (C) 2010 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.config;
import com.google.inject.BindingAnnotation;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Marker on a {@code Set&lt;AccountGroup.Id>} for the configured groups with
* permission to create projects.
*/
@Retention(RUNTIME)
@BindingAnnotation
public @interface ProjectCreatorGroups {
}

View File

@ -0,0 +1,65 @@
// Copyright (C) 2010 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.config;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.SystemConfig;
import com.google.gwtorm.client.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Set;
/**
* Provider of the group(s) which are allowed to create new projects. Currently
* only supports {@code createGroup} declarations in the {@code "*"} repository,
* like so:
*
* <pre>
* [repository &quot;*&quot;]
* createGroup = Registered Users
* createGroup = Administrators
* </pre>
*/
public class ProjectCreatorGroupsProvider implements
Provider<Set<AccountGroup.Id>> {
private static final Logger log =
LoggerFactory.getLogger(ProjectCreatorGroupsProvider.class);
private final Set<AccountGroup.Id> groupIds;
@Inject
ProjectCreatorGroupsProvider(@GerritServerConfig final Config config,
SchemaFactory<ReviewDb> db, final SystemConfig systemConfig) {
String[] names = config.getStringList("repository", "*", "createGroup");
Set<AccountGroup.Id> createGroups = ConfigUtil.groupsFor(db, names, log);
if (createGroups.isEmpty()) {
groupIds = Collections.singleton(systemConfig.adminGroupId);
} else {
groupIds = createGroups;
}
}
public Set<AccountGroup.Id> get() {
return groupIds;
}
}

View File

@ -0,0 +1,30 @@
// Copyright (C) 2010 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.config;
import com.google.inject.BindingAnnotation;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Marker on a {@code Set&lt;AccountGroup.Id>} for the configured groups which
* should become owners of a created project.
*/
@Retention(RUNTIME)
@BindingAnnotation
public @interface ProjectOwnerGroups {
}

View File

@ -0,0 +1,64 @@
// Copyright (C) 2010 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.config;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gwtorm.client.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
/**
* Provider of the group(s) which should become owners of a newly created
* project. Currently only supports {@code ownerGroup} declarations in the
* {@code "*"} repository, like so:
*
* <pre>
* [repository &quot;*&quot;]
* ownerGroup = Registered Users
* ownerGroup = Administrators
* </pre>
*/
public class ProjectOwnerGroupsProvider implements
Provider<Set<AccountGroup.Id>> {
private static final Logger log =
LoggerFactory.getLogger(ProjectOwnerGroupsProvider.class);
private final Set<AccountGroup.Id> groupIds;
@Inject
ProjectOwnerGroupsProvider(@GerritServerConfig final Config config,
SchemaFactory<ReviewDb> db,
@ProjectCreatorGroups Set<AccountGroup.Id> creatorGroups) {
String[] names = config.getStringList("repository", "*", "ownerGroup");
Set<AccountGroup.Id> ownerGroups = ConfigUtil.groupsFor(db, names, log);
if (ownerGroups.isEmpty()) {
groupIds = creatorGroups;
} else {
groupIds = ownerGroups;
}
}
public Set<AccountGroup.Id> get() {
return groupIds;
}
}

View File

@ -15,15 +15,14 @@
package com.google.gerrit.server.git;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AccountGroupName;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.ReplicationUser;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.SchemaFactory;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
@ -55,7 +54,6 @@ import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@ -322,7 +320,8 @@ public class PushReplication implements ReplicationQueue {
String[] authGroupNames =
cfg.getStringList("remote", rc.getName(), "authGroup");
authEnabled = authGroupNames.length > 0;
Set<AccountGroup.Id> authGroups = groupsFor(db, authGroupNames);
Set<AccountGroup.Id> authGroups = ConfigUtil.groupsFor(db, authGroupNames, log,
"Group \"{0}\" not in database, removing from authGroup");
final ReplicationUser remoteUser =
replicationUserFactory.create(authGroups);
@ -347,31 +346,6 @@ public class PushReplication implements ReplicationQueue {
}).getInstance(PushOp.Factory.class);
}
private static Set<AccountGroup.Id> groupsFor(
SchemaFactory<ReviewDb> dbfactory, String[] groupNames) {
final Set<AccountGroup.Id> result = new HashSet<AccountGroup.Id>();
try {
final ReviewDb db = dbfactory.open();
try {
for (String name : groupNames) {
AccountGroupName group =
db.accountGroupNames().get(new AccountGroup.NameKey(name));
if (group == null) {
log.warn("Group \"" + name + "\" not in database,"
+ " removing from authGroup");
} else {
result.add(group.getId());
}
}
} finally {
db.close();
}
} catch (OrmException e) {
log.error("Database error: " + e);
}
return result;
}
private int getInt(final RemoteConfig rc, final Config cfg,
final String name, final int defValue) {
return cfg.getInt("remote", rc.getName(), name, defValue);

View File

@ -20,10 +20,11 @@ import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.Project.SubmitType;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.ProjectCreatorGroups;
import com.google.gerrit.server.config.ProjectOwnerGroups;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.sshd.AdminCommand;
import com.google.gerrit.sshd.BaseCommand;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
@ -35,17 +36,20 @@ import org.eclipse.jgit.lib.Repository;
import org.kohsuke.args4j.Option;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Create a new project. **/
@AdminCommand
final class AdminCreateProject extends BaseCommand {
final class CreateProject extends BaseCommand {
@Option(name = "--name", required = true, aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created")
private String projectName;
@Option(name = "--owner", aliases = {"-o"}, usage = "owner of project\n"
+ "(default: Administrators)")
private AccountGroup.Id ownerId;
@Option(name = "--owner", aliases = {"-o"}, usage = "owner(s) of project")
private List<AccountGroup.Id> ownerIds;
@Option(name = "--description", aliases = {"-d"}, metaVar = "DESC", usage = "description of project")
private String projectDescription = "";
@ -71,7 +75,15 @@ final class AdminCreateProject extends BaseCommand {
private GitRepositoryManager repoManager;
@Inject
private AuthConfig authConfig;
@ProjectCreatorGroups
private Set<AccountGroup.Id> projectCreatorGroups;
@Inject
@ProjectOwnerGroups
private Set<AccountGroup.Id> projectOwnerGroups;
@Inject
private IdentifiedUser currentUser;
@Inject
private ReplicationQueue rq;
@ -83,7 +95,6 @@ final class AdminCreateProject extends BaseCommand {
public void run() throws Exception {
PrintWriter p = toPrintWriter(out);
ownerId = authConfig.getAdministratorsGroup();
parseCommandLine();
try {
@ -111,16 +122,40 @@ final class AdminCreateProject extends BaseCommand {
});
}
/**
* Checks if any of the elements in the first collection can be found in the
* second collection.
*
* @param findAnyOfThese which elements to look for.
* @param inThisCollection where to look for them.
* @param <E> type of the elements in question.
* @return {@code true} if any of the elements in {@code findAnyOfThese} can
* be found in {@code inThisCollection}, {@code false} otherwise.
*/
private static <E> boolean isAnyIncludedIn(Collection<E> findAnyOfThese,
Collection<E> inThisCollection) {
for (E findThisItem : findAnyOfThese) {
if (inThisCollection.contains(findThisItem)) {
return true;
}
}
return false;
}
private void createProject() throws OrmException {
final Project.NameKey newProjectNameKey = new Project.NameKey(projectName);
List<RefRight> access = new ArrayList<RefRight>();
for (AccountGroup.Id ownerId : ownerIds) {
final RefRight.Key prk =
new RefRight.Key(newProjectNameKey, new RefRight.RefPattern(
RefRight.ALL), ApprovalCategory.OWN, ownerId);
final RefRight pr = new RefRight(prk);
pr.setMaxValue((short) 1);
pr.setMinValue((short) 1);
db.refRights().insert(Collections.singleton(pr));
access.add(pr);
}
db.refRights().insert(access);
final Project newProject = new Project(newProjectNameKey);
newProject.setDescription(projectDescription);
@ -137,6 +172,17 @@ final class AdminCreateProject extends BaseCommand {
projectName.substring(0, projectName.length() - ".git".length());
}
if (!isAnyIncludedIn(currentUser.getEffectiveGroups(), projectCreatorGroups)) {
throw new Failure(1, "fatal: Not permitted to create " + projectName);
}
if (ownerIds != null && !ownerIds.isEmpty()) {
ownerIds =
new ArrayList<AccountGroup.Id>(new HashSet<AccountGroup.Id>(ownerIds));
} else {
ownerIds = new ArrayList<AccountGroup.Id>(projectOwnerGroups);
}
while (branch.startsWith("/")) {
branch = branch.substring(1);
}

View File

@ -27,7 +27,7 @@ public class MasterCommandModule extends CommandModule {
command(gerrit, "approve").to(ApproveCommand.class);
command(gerrit, "create-account").to(AdminCreateAccount.class);
command(gerrit, "create-project").to(AdminCreateProject.class);
command(gerrit, "create-project").to(CreateProject.class);
command(gerrit, "gsql").to(AdminQueryShell.class);
command(gerrit, "receive-pack").to(Receive.class);
command(gerrit, "replicate").to(AdminReplicate.class);