mirror of
https://github.com/google/nomulus.git
synced 2025-05-15 08:57:12 +02:00
Add support G-Suite group whose members have ADMIN access to registrar console
After this CL, "support" accounts (accounts that are part of the "support" G-Suite group) will the same access to the registrar console as GCP "admins". However, they don't won't have access to the GCP project itself. We could give them their own Role in the future (say SUPPORT) and give them different access than "admins", but right now we don't need it and YAGNI or something :) NOTE: we identify users by their email (they need to be logged in to a google account). I don't know if that's best practice, since I guess different google accounts might have the same email address. However, G-Suite groups' membership is by email so there's not much we can do about it if we want to use G-Suite groups. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=220804273
This commit is contained in:
parent
783c010ab4
commit
557984bb75
15 changed files with 285 additions and 25 deletions
|
@ -392,6 +392,19 @@ public final class RegistryConfig {
|
|||
return config.gSuite.adminAccountEmailAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email address of the group containing emails of support accounts.
|
||||
*
|
||||
* <p>These accounts will have "ADMIN" access to the registrar console.
|
||||
*
|
||||
* @see google.registry.groups.DirectoryGroupsConnection
|
||||
*/
|
||||
@Provides
|
||||
@Config("gSuiteSupportGroupEmailAddress")
|
||||
public static String provideGSuiteSupportGroupEmailAddress(RegistryConfigSettings config) {
|
||||
return config.gSuite.supportGroupEmailAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email address(es) that notifications of registrar and/or registrar contact
|
||||
* updates should be sent to, or the empty list if updates should not be sent.
|
||||
|
|
|
@ -66,6 +66,7 @@ public class RegistryConfigSettings {
|
|||
public String outgoingEmailAddress;
|
||||
public String outgoingEmailDisplayName;
|
||||
public String adminAccountEmailAddress;
|
||||
public String supportGroupEmailAddress;
|
||||
}
|
||||
|
||||
/** Configuration options for registry policy. */
|
||||
|
|
|
@ -32,6 +32,10 @@ gSuite:
|
|||
# logging in to perform administrative actions, not sending emails.
|
||||
adminAccountEmailAddress: admin@example.com
|
||||
|
||||
# Group containing the emails of the support accounts. These accounts will be
|
||||
# given "ADMIN" role on the registrar console.
|
||||
supportGroupEmailAddress: support@example.com
|
||||
|
||||
registryPolicy:
|
||||
# Repository identifier (ROID) suffix for contacts and hosts.
|
||||
contactAndHostRoidSuffix: ROID
|
||||
|
|
|
@ -18,6 +18,7 @@ gSuite:
|
|||
outgoingEmailDisplayName: placeholder
|
||||
outgoingEmailAddress: placeholder
|
||||
adminAccountEmailAddress: placeholder
|
||||
supportGroupEmailAddress: placeholder
|
||||
|
||||
registryPolicy:
|
||||
contactAndHostRoidSuffix: placeholder
|
||||
|
|
|
@ -46,6 +46,22 @@ public class DirectoryGroupsConnection implements GroupsConnection {
|
|||
private static final String MEMBER_NOT_FOUND_MSG = "Resource Not Found: memberKey";
|
||||
private static final String MEMBER_ALREADY_EXISTS_MSG = "Member already exists.";
|
||||
|
||||
/**
|
||||
* All possible errors from {@link Directory.Members#get} when an email doesn't belong to a group.
|
||||
*
|
||||
* <p>See {@link #isMemberOfGroup} for details.
|
||||
*
|
||||
* <p>TODO(b/119220829): remove once we transition to using hasMember
|
||||
*
|
||||
* <p>TODO(b/119221854): update error messages if and when they change
|
||||
*/
|
||||
private static final ImmutableSet<String> ERROR_MESSAGES_MEMBER_NOT_FOUND =
|
||||
ImmutableSet.of(
|
||||
// The given email corresponds to an actual account, but isn't part of this group
|
||||
"Resource Not Found: memberKey",
|
||||
// There's no account corresponding to this email
|
||||
"Missing required field: memberKey");
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final Groups defaultGroupPermissions = getDefaultGroupPermissions();
|
||||
|
||||
|
@ -163,4 +179,45 @@ public class DirectoryGroupsConnection implements GroupsConnection {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMemberOfGroup(String memberEmail, String groupKey) {
|
||||
// We're using "get" instead of "hasMember" because "hasMember" fails for emails that don't
|
||||
// belong to the G-Suite domain.
|
||||
//
|
||||
// "get" fails for users that aren't part of the group, but it also might fail for other
|
||||
// reasons (no access, group doesn't exist etc.).
|
||||
// Which error is caused by "user isn't in that group" isn't documented, and was found using
|
||||
// trial and error.
|
||||
//
|
||||
// TODO(b/119221676): transition to using hasMember
|
||||
//
|
||||
// Documentation for the API of "get":
|
||||
// https://developers.google.com/admin-sdk/directory/v1/reference/members/get
|
||||
//
|
||||
// Documentation for the API of "hasMember":
|
||||
// https://developers.google.com/admin-sdk/directory/v1/reference/members/hasMember
|
||||
try {
|
||||
Directory.Members.Get getRequest = directory.members().get(groupKey, memberEmail);
|
||||
Member getReply = getRequest.execute();
|
||||
logger.atInfo().log(
|
||||
"%s is a member of the group %s. Got reply: %s", memberEmail, groupKey, getReply);
|
||||
return true;
|
||||
} catch (GoogleJsonResponseException e) {
|
||||
if (ERROR_MESSAGES_MEMBER_NOT_FOUND.contains(e.getDetails().getMessage())) {
|
||||
// This means the "get" request failed because the email wasn't part of the group.
|
||||
// This is expected behavior for any visitor that isn't a support group member.
|
||||
logger.atInfo().log(
|
||||
"%s isn't a member of the group %s. Got reply %s",
|
||||
memberEmail, groupKey, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
// If we got here - we had an unexpected error. Rethrow.
|
||||
throw new RuntimeException(
|
||||
String.format("Error checking whether %s is in group %s", memberEmail, groupKey), e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(
|
||||
String.format("Error checking whether %s is in group %s", memberEmail, groupKey), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,4 +58,7 @@ public interface GroupsConnection {
|
|||
* automatically added as an owner.
|
||||
*/
|
||||
Group createGroup(String groupKey) throws IOException;
|
||||
|
||||
/** Checks whether the given email belongs to the "support" group. */
|
||||
boolean isMemberOfGroup(String memberEmail, String groupKey);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ java_library(
|
|||
"//java/google/registry/config",
|
||||
"//java/google/registry/dns",
|
||||
"//java/google/registry/flows",
|
||||
"//java/google/registry/groups",
|
||||
"//java/google/registry/keyring",
|
||||
"//java/google/registry/keyring/api",
|
||||
"//java/google/registry/keyring/kms",
|
||||
|
|
|
@ -21,6 +21,9 @@ import google.registry.config.CredentialModule;
|
|||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.flows.ServerTridProviderModule;
|
||||
import google.registry.flows.custom.CustomLogicFactoryModule;
|
||||
import google.registry.groups.DirectoryModule;
|
||||
import google.registry.groups.GroupsModule;
|
||||
import google.registry.groups.GroupssettingsModule;
|
||||
import google.registry.keyring.KeyringModule;
|
||||
import google.registry.keyring.api.DummyKeyringModule;
|
||||
import google.registry.keyring.api.KeyModule;
|
||||
|
@ -48,8 +51,11 @@ import javax.inject.Singleton;
|
|||
ConsoleConfigModule.class,
|
||||
CredentialModule.class,
|
||||
CustomLogicFactoryModule.class,
|
||||
DirectoryModule.class,
|
||||
DummyKeyringModule.class,
|
||||
FrontendRequestComponentModule.class,
|
||||
GroupsModule.class,
|
||||
GroupssettingsModule.class,
|
||||
Jackson2Module.class,
|
||||
KeyModule.class,
|
||||
KeyringModule.class,
|
||||
|
|
|
@ -11,6 +11,7 @@ java_library(
|
|||
"//java/google/registry/config",
|
||||
"//java/google/registry/dns",
|
||||
"//java/google/registry/flows",
|
||||
"//java/google/registry/groups",
|
||||
"//java/google/registry/keyring",
|
||||
"//java/google/registry/keyring/api",
|
||||
"//java/google/registry/keyring/kms",
|
||||
|
|
|
@ -21,6 +21,9 @@ import google.registry.config.CredentialModule;
|
|||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.flows.ServerTridProviderModule;
|
||||
import google.registry.flows.custom.CustomLogicFactoryModule;
|
||||
import google.registry.groups.DirectoryModule;
|
||||
import google.registry.groups.GroupsModule;
|
||||
import google.registry.groups.GroupssettingsModule;
|
||||
import google.registry.keyring.KeyringModule;
|
||||
import google.registry.keyring.api.DummyKeyringModule;
|
||||
import google.registry.keyring.api.KeyModule;
|
||||
|
@ -46,7 +49,10 @@ import javax.inject.Singleton;
|
|||
ConfigModule.class,
|
||||
CredentialModule.class,
|
||||
CustomLogicFactoryModule.class,
|
||||
DirectoryModule.class,
|
||||
DummyKeyringModule.class,
|
||||
GroupsModule.class,
|
||||
GroupssettingsModule.class,
|
||||
Jackson2Module.class,
|
||||
KeyModule.class,
|
||||
KeyringModule.class,
|
||||
|
|
|
@ -17,11 +17,14 @@ package google.registry.ui.server.registrar;
|
|||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
|
||||
import com.google.appengine.api.users.User;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import dagger.Lazy;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.groups.GroupsConnection;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.RegistrarContact;
|
||||
import google.registry.request.HttpException.ForbiddenException;
|
||||
|
@ -61,12 +64,44 @@ public class AuthenticatedRegistrarAccessor {
|
|||
*/
|
||||
private final ImmutableSetMultimap<String, Role> roleMap;
|
||||
|
||||
/**
|
||||
* Overriding the injected {@link GroupsConnection} for tests.
|
||||
*
|
||||
* <p>{@link GroupsConnection} needs the injected DelegatedCredential GoogleCredential. However,
|
||||
* this can't be initialized in the test environment.
|
||||
*
|
||||
* <p>The test server used in the javatests/google/registry/webdriver/ tests will hang if we try
|
||||
* to instantiate the {@link GroupsConnection}. So instead we inject a {@link Lazy} version and
|
||||
* allow tests to override the injected instace with (presumabley) a mock insteance.
|
||||
*/
|
||||
@VisibleForTesting public static GroupsConnection overrideGroupsConnection = null;
|
||||
|
||||
@Inject
|
||||
public AuthenticatedRegistrarAccessor(
|
||||
AuthResult authResult, @Config("registryAdminClientId") String registryAdminClientId) {
|
||||
AuthResult authResult,
|
||||
@Config("registryAdminClientId") String registryAdminClientId,
|
||||
@Config("gSuiteSupportGroupEmailAddress") String gSuiteSupportGroupEmailAddress,
|
||||
Lazy<GroupsConnection> groupsConnection) {
|
||||
this(
|
||||
authResult,
|
||||
registryAdminClientId,
|
||||
gSuiteSupportGroupEmailAddress,
|
||||
overrideGroupsConnection != null ? overrideGroupsConnection : groupsConnection.get());
|
||||
}
|
||||
|
||||
AuthenticatedRegistrarAccessor(
|
||||
AuthResult authResult,
|
||||
@Config("registryAdminClientId") String registryAdminClientId,
|
||||
@Config("gSuiteSupportGroupEmailAddress") String gSuiteSupportGroupEmailAddress,
|
||||
GroupsConnection groupsConnection) {
|
||||
this.authResult = authResult;
|
||||
this.registryAdminClientId = registryAdminClientId;
|
||||
this.roleMap = createRoleMap(authResult, registryAdminClientId);
|
||||
this.roleMap =
|
||||
createRoleMap(
|
||||
authResult,
|
||||
registryAdminClientId,
|
||||
groupsConnection,
|
||||
gSuiteSupportGroupEmailAddress);
|
||||
|
||||
logger.atInfo().log(
|
||||
"%s has the following roles: %s", authResult.userIdForLogging(), roleMap);
|
||||
|
@ -156,8 +191,27 @@ public class AuthenticatedRegistrarAccessor {
|
|||
return registrar;
|
||||
}
|
||||
|
||||
private static boolean checkIsSupport(
|
||||
GroupsConnection groupsConnection, String userEmail, String supportEmail) {
|
||||
if (Strings.isNullOrEmpty(supportEmail)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return groupsConnection.isMemberOfGroup(userEmail, supportEmail);
|
||||
} catch (RuntimeException e) {
|
||||
logger.atSevere().withCause(e).log(
|
||||
"Error checking whether email %s belongs to support group %s."
|
||||
+ " Skipping support role check",
|
||||
userEmail, supportEmail);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableSetMultimap<String, Role> createRoleMap(
|
||||
AuthResult authResult, String registryAdminClientId) {
|
||||
AuthResult authResult,
|
||||
String registryAdminClientId,
|
||||
GroupsConnection groupsConnection,
|
||||
String gSuiteSupportGroupEmailAddress) {
|
||||
|
||||
if (!authResult.userAuthInfo().isPresent()) {
|
||||
return ImmutableSetMultimap.of();
|
||||
|
@ -165,8 +219,10 @@ public class AuthenticatedRegistrarAccessor {
|
|||
|
||||
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
|
||||
|
||||
boolean isAdmin = userAuthInfo.isUserAdmin();
|
||||
User user = userAuthInfo.user();
|
||||
boolean isAdmin = userAuthInfo.isUserAdmin();
|
||||
boolean isSupport =
|
||||
checkIsSupport(groupsConnection, user.getEmail(), gSuiteSupportGroupEmailAddress);
|
||||
|
||||
ImmutableSetMultimap.Builder<String, Role> builder = new ImmutableSetMultimap.Builder<>();
|
||||
|
||||
|
@ -174,17 +230,13 @@ public class AuthenticatedRegistrarAccessor {
|
|||
.load()
|
||||
.type(RegistrarContact.class)
|
||||
.filter("gaeUserId", user.getUserId())
|
||||
.forEach(
|
||||
contact ->
|
||||
builder
|
||||
.put(contact.getParent().getName(), Role.OWNER));
|
||||
.forEach(contact -> builder.put(contact.getParent().getName(), Role.OWNER));
|
||||
if (isAdmin && !Strings.isNullOrEmpty(registryAdminClientId)) {
|
||||
builder
|
||||
.put(registryAdminClientId, Role.OWNER);
|
||||
builder.put(registryAdminClientId, Role.OWNER);
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
// Admins have access to all registrars
|
||||
if (isAdmin || isSupport) {
|
||||
// Admins and support have access to all registrars
|
||||
ofy()
|
||||
.load()
|
||||
.type(Registrar.class)
|
||||
|
|
|
@ -15,6 +15,7 @@ java_library(
|
|||
"//java/google/registry/config",
|
||||
"//java/google/registry/export/sheet",
|
||||
"//java/google/registry/flows",
|
||||
"//java/google/registry/groups",
|
||||
"//java/google/registry/model",
|
||||
"//java/google/registry/request",
|
||||
"//java/google/registry/request/auth",
|
||||
|
@ -24,6 +25,7 @@ java_library(
|
|||
"//java/google/registry/ui/soy:soy_java_wrappers",
|
||||
"//java/google/registry/ui/soy/registrar:soy_java_wrappers",
|
||||
"//java/google/registry/util",
|
||||
"//third_party/java/inject_common",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_auto_value",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue