// Copyright 2018 The Nomulus Authors. All Rights Reserved. // // 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 google.registry.request.auth; import static com.google.common.base.MoreObjects.toStringHelper; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.Streams.stream; 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.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.flogger.FluentLogger; import com.googlecode.objectify.Key; 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.Registrar.State; import google.registry.model.registrar.RegistrarContact; import java.util.Optional; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; /** * Allows access only to {@link Registrar}s the current user has access to. * *
A user has OWNER role on a Registrar if there exists a {@link RegistrarContact} with that * user's gaeId and the registrar as a parent. * *
An "admin" has in addition OWNER role on {@code #registryAdminClientId} and to all non-{@code * REAL} registrars (see {@link Registrar#getType}). * *
An "admin" also has ADMIN role on ALL registrars. * *
A user is an "admin" if they are a GAE-admin, or if their email is in the "Support" G Suite * group. * *
NOTE: to check whether the user is in the "Support" G Suite group, we need a connection to * G Suite. This in turn requires we have valid JsonCredentials, which not all environments have set * up. This connection will be created lazily (only if needed). * *
Specifically, we don't instantiate the connection if: (a) gSuiteSupportGroupEmailAddress isn't * defined, or (b) the user is logged out, or (c) the user is a GAE-admin, or (d) bypassAdminCheck * is true. */ @Immutable public class AuthenticatedRegistrarAccessor { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); /** The role under which access is granted. */ public enum Role { OWNER, ADMIN } private final String userIdForLogging; /** * Whether this user is an Admin, meaning either a GAE-admin or a member of the Support G Suite * group. */ private final boolean isAdmin; /** * Gives all roles a user has for a given clientId. * *
The order is significant, with "more specific to this user" coming first. * *
Logged out users have an empty roleMap.
*/
private final ImmutableSetMultimap Currently our test server doesn't let you change the user after the test server was created.
* This means we'd need multiple test files to test the same actions as both a "regular" user and
* an admin.
*
* To overcome this - we add a flag that lets you dynamically choose whether a user is an admin
* or not by creating a fake "GAE-admin" user and then bypassing the admin check if they want to
* fake a "regular" user.
*
* The reason we don't do it the other way around (have a flag that makes anyone an admin) is
* that such a flag would be a security risk, especially since VisibleForTesting is unenforced
* (and you could set it with reflection anyway).
*
* Instead of having a test flag that elevates permissions (which has security concerns) we add
* this flag that reduces permissions.
*/
@VisibleForTesting public static boolean bypassAdminCheck = false;
@Inject
public AuthenticatedRegistrarAccessor(
AuthResult authResult,
@Config("registryAdminClientId") String registryAdminClientId,
@Config("gSuiteSupportGroupEmailAddress") Optional The user will be allowed to create Registrars (and hence do OT&E setup) iff they have
* the role of ADMIN for at least one clientId.
*
* The user's "name" in logs and exception messages is "TestUserId".
*/
@VisibleForTesting
public static AuthenticatedRegistrarAccessor createForTesting(
ImmutableSetMultimap Throws a {@link RegistrarAccessDeniedException} if the user is not logged in.
*
* The result is ordered starting from "most specific to this user".
*
* If you want to load the {@link Registrar} object from these (or any other) {@code clientId},
* in order to perform actions on behalf of a user, you must use {@link #getRegistrar} which makes
* sure the user has permissions.
*
* Note that this is an OPTIONAL step in the authentication - only used if we don't have any
* other clue as to the requested {@code clientId}. It is perfectly OK to get a {@code clientId}
* from any other source, as long as the registrar is then loaded using {@link #getRegistrar}.
*/
public ImmutableSetMultimap This is syntactic sugar for {@code getAllClientIdWithRoles().get(clientId)}.
*/
public ImmutableSet This is syntactic sugar for {@code getAllClientIdWithRoles().containsEntry(clientId, role)}.
*/
public boolean hasRoleOnRegistrar(Role role, String clientId) {
return getAllClientIdWithRoles().containsEntry(clientId, role);
}
/**
* "Guesses" which client ID the user wants from all those they have access to.
*
* If no such ClientIds exist, throws a RegistrarAccessDeniedException.
*
* This should be the ClientId "most likely wanted by the user".
*
* If you want to load the {@link Registrar} object from this (or any other) {@code clientId},
* in order to perform actions on behalf of a user, you must use {@link #getRegistrar} which makes
* sure the user has permissions.
*
* Note that this is an OPTIONAL step in the authentication - only used if we don't have any
* other clue as to the requested {@code clientId}. It is perfectly OK to get a {@code clientId}
* from any other source, as long as the registrar is then loaded using {@link #getRegistrar}.
*/
public String guessClientId() throws RegistrarAccessDeniedException {
return getAllClientIdWithRoles().keySet().stream()
.findFirst()
.orElseThrow(
() ->
new RegistrarAccessDeniedException(
String.format("%s isn't associated with any registrar", userIdForLogging)));
}
/**
* Loads a Registrar IFF the user is authorized.
*
* Throws a {@link RegistrarAccessDeniedException} if the user is not logged in, or not
* authorized to access the requested registrar.
*
* @param clientId ID of the registrar we request
*/
public Registrar getRegistrar(String clientId) throws RegistrarAccessDeniedException {
Registrar registrar =
Registrar.loadByClientId(clientId)
.orElseThrow(
() ->
new RegistrarAccessDeniedException(
String.format("Registrar %s does not exist", clientId)));
verifyAccess(clientId);
if (!clientId.equals(registrar.getClientId())) {
logger.atSevere().log(
"registrarLoader.apply(clientId) returned a Registrar with a different clientId. "
+ "Requested: %s, returned: %s.",
clientId, registrar.getClientId());
throw new RegistrarAccessDeniedException("Internal error - please check logs");
}
return registrar;
}
public void verifyAccess(String clientId) throws RegistrarAccessDeniedException {
ImmutableSet