mirror of
https://github.com/google/nomulus.git
synced 2025-05-15 00:47:11 +02:00
Allow admins read/write access to all registrar in web console
This CL removes the "READ vs UPDATE" feature completely. Now anyone with access has full read+write access. We still keep track of which role a user has (did they get access "explicitly" because they are an "allowed access" contact? Or do they have access because they are admins?) for the logs and UI, and also so we could in the (very near) future have features only available to admins. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=218169608
This commit is contained in:
parent
2020dcb50f
commit
d2ca67460c
12 changed files with 138 additions and 288 deletions
|
@ -18,6 +18,7 @@ import static google.registry.model.ofy.ObjectifyService.ofy;
|
|||
|
||||
import com.google.appengine.api.users.User;
|
||||
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 google.registry.config.RegistryConfig.Config;
|
||||
|
@ -26,83 +27,53 @@ import google.registry.model.registrar.RegistrarContact;
|
|||
import google.registry.request.HttpException.ForbiddenException;
|
||||
import google.registry.request.auth.AuthResult;
|
||||
import google.registry.request.auth.UserAuthInfo;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Allows access only to {@link Registrar}s the current user has access to.
|
||||
*
|
||||
* <p>A user has read+write access to a Registrar if there exists a {@link RegistrarContact} with
|
||||
* <p>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.
|
||||
*
|
||||
* <p>An admin has in addition read+write access to {@link #registryAdminClientId}.
|
||||
* <p>An admin has in addition OWNER role on {@link #registryAdminClientId}.
|
||||
*
|
||||
* <p>An admin also has read access to ALL registrars.
|
||||
* <p>An admin also has ADMIN role on ALL registrars.
|
||||
*/
|
||||
@Immutable
|
||||
public class AuthenticatedRegistrarAccessor {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
/** Type of access we're requesting. */
|
||||
public enum AccessType {
|
||||
READ,
|
||||
UPDATE
|
||||
/** The role under which access is granted. */
|
||||
public enum Role {
|
||||
OWNER,
|
||||
ADMIN
|
||||
}
|
||||
|
||||
AuthResult authResult;
|
||||
String registryAdminClientId;
|
||||
|
||||
/**
|
||||
* For any AccessType requested - all clientIDs this user is allowed that AccessType on.
|
||||
* Gives all roles a user has for a given clientId.
|
||||
*
|
||||
* <p>The order is significant, with "more specific to this user" coming first.
|
||||
*/
|
||||
private final ImmutableSetMultimap<AccessType, String> accessMap;
|
||||
private final ImmutableSetMultimap<String, Role> roleMap;
|
||||
|
||||
@Inject
|
||||
public AuthenticatedRegistrarAccessor(
|
||||
AuthResult authResult, @Config("registryAdminClientId") String registryAdminClientId) {
|
||||
this.authResult = authResult;
|
||||
this.registryAdminClientId = registryAdminClientId;
|
||||
this.accessMap = createAccessMap(authResult, registryAdminClientId);
|
||||
this.roleMap = createRoleMap(authResult, registryAdminClientId);
|
||||
|
||||
logger.atInfo().log(
|
||||
"User %s has the following accesses: %s", authResult.userIdForLogging(), accessMap);
|
||||
"%s has the following roles: %s", authResult.userIdForLogging(), roleMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a Registrar IFF the user is authorized.
|
||||
*
|
||||
* <p>Throws a {@link ForbiddenException} if the user is not logged in, or not authorized to
|
||||
* access the requested registrar.
|
||||
*
|
||||
* @param clientId ID of the registrar we request
|
||||
* @param accessType what kind of access do we want for this registrar - just read it or write as
|
||||
* well? (different users might have different access levels)
|
||||
*/
|
||||
public Registrar getRegistrar(String clientId, AccessType accessType) {
|
||||
return getAndAuthorize(Registrar::loadByClientId, clientId, accessType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a Registrar from the cache IFF the user is authorized.
|
||||
*
|
||||
* <p>Throws a {@link ForbiddenException} if the user is not logged in, or not authorized to
|
||||
* access the requested registrar.
|
||||
*
|
||||
* @param clientId ID of the registrar we request
|
||||
* @param accessType what kind of access do we want for this registrar - just read it or write as
|
||||
* well? (different users might have different access levels)
|
||||
*/
|
||||
public Registrar getRegistrarForUserCached(String clientId, AccessType accessType) {
|
||||
return getAndAuthorize(Registrar::loadByClientIdCached, clientId, accessType);
|
||||
}
|
||||
|
||||
/**
|
||||
* For all {@link AccessType}s, Returns all ClientIds this user is allowed this access.
|
||||
* A map that gives all roles a user has for a given clientId.
|
||||
*
|
||||
* <p>Throws a {@link ForbiddenException} if the user is not logged in.
|
||||
*
|
||||
|
@ -116,9 +87,8 @@ public class AuthenticatedRegistrarAccessor {
|
|||
* 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<AccessType, String> getAllClientIdWithAccess() {
|
||||
verifyLoggedIn();
|
||||
return accessMap;
|
||||
public ImmutableSetMultimap<String, Role> getAllClientIdWithRoles() {
|
||||
return roleMap;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -138,32 +108,38 @@ public class AuthenticatedRegistrarAccessor {
|
|||
*/
|
||||
public String guessClientId() {
|
||||
verifyLoggedIn();
|
||||
return getAllClientIdWithAccess().values().stream()
|
||||
return getAllClientIdWithRoles().keySet().stream()
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new ForbiddenException(
|
||||
String.format(
|
||||
"User %s isn't associated with any registrar",
|
||||
"%s isn't associated with any registrar",
|
||||
authResult.userIdForLogging())));
|
||||
}
|
||||
|
||||
private Registrar getAndAuthorize(
|
||||
Function<String, Optional<Registrar>> registrarLoader,
|
||||
String clientId,
|
||||
AccessType accessType) {
|
||||
/**
|
||||
* Loads a Registrar IFF the user is authorized.
|
||||
*
|
||||
* <p>Throws a {@link ForbiddenException} 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) {
|
||||
verifyLoggedIn();
|
||||
|
||||
if (!accessMap.containsEntry(accessType, clientId)) {
|
||||
ImmutableSet<Role> roles = getAllClientIdWithRoles().get(clientId);
|
||||
|
||||
if (roles.isEmpty()) {
|
||||
throw new ForbiddenException(
|
||||
String.format(
|
||||
"User %s doesn't have %s access to registrar %s",
|
||||
authResult.userIdForLogging(), accessType, clientId));
|
||||
"%s doesn't have access to registrar %s",
|
||||
authResult.userIdForLogging(), clientId));
|
||||
}
|
||||
|
||||
Registrar registrar =
|
||||
registrarLoader
|
||||
.apply(clientId)
|
||||
Registrar.loadByClientId(clientId)
|
||||
.orElseThrow(
|
||||
() -> new ForbiddenException(String.format("Registrar %s not found", clientId)));
|
||||
|
||||
|
@ -176,12 +152,11 @@ public class AuthenticatedRegistrarAccessor {
|
|||
}
|
||||
|
||||
logger.atInfo().log(
|
||||
"User %s has %s access to registrar %s.",
|
||||
authResult.userIdForLogging(), accessType, clientId);
|
||||
"%s has %s access to registrar %s.", authResult.userIdForLogging(), roles, clientId);
|
||||
return registrar;
|
||||
}
|
||||
|
||||
private static ImmutableSetMultimap<AccessType, String> createAccessMap(
|
||||
private static ImmutableSetMultimap<String, Role> createRoleMap(
|
||||
AuthResult authResult, String registryAdminClientId) {
|
||||
|
||||
if (!authResult.userAuthInfo().isPresent()) {
|
||||
|
@ -193,7 +168,7 @@ public class AuthenticatedRegistrarAccessor {
|
|||
boolean isAdmin = userAuthInfo.isUserAdmin();
|
||||
User user = userAuthInfo.user();
|
||||
|
||||
ImmutableSetMultimap.Builder<AccessType, String> builder = new ImmutableSetMultimap.Builder<>();
|
||||
ImmutableSetMultimap.Builder<String, Role> builder = new ImmutableSetMultimap.Builder<>();
|
||||
|
||||
ofy()
|
||||
.load()
|
||||
|
@ -202,25 +177,18 @@ public class AuthenticatedRegistrarAccessor {
|
|||
.forEach(
|
||||
contact ->
|
||||
builder
|
||||
.put(AccessType.UPDATE, contact.getParent().getName())
|
||||
.put(AccessType.READ, contact.getParent().getName()));
|
||||
.put(contact.getParent().getName(), Role.OWNER));
|
||||
if (isAdmin && !Strings.isNullOrEmpty(registryAdminClientId)) {
|
||||
logger.atInfo().log(
|
||||
"Giving admin %s read+write access to admin registrar %s",
|
||||
authResult.userIdForLogging(), registryAdminClientId);
|
||||
builder
|
||||
.put(AccessType.UPDATE, registryAdminClientId)
|
||||
.put(AccessType.READ, registryAdminClientId);
|
||||
.put(registryAdminClientId, Role.OWNER);
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
// Admins have READ access to all registrars
|
||||
logger.atInfo().log(
|
||||
"Giving admin %s read-only access to all registrars", authResult.userIdForLogging());
|
||||
// Admins have access to all registrars
|
||||
ofy()
|
||||
.load()
|
||||
.type(Registrar.class)
|
||||
.forEach(registrar -> builder.put(AccessType.READ, registrar.getClientId()));
|
||||
.forEach(registrar -> builder.put(registrar.getClientId(), Role.ADMIN));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
|
|
|
@ -16,8 +16,8 @@ package google.registry.ui.server.registrar;
|
|||
|
||||
import static com.google.common.net.HttpHeaders.LOCATION;
|
||||
import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS;
|
||||
import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.READ;
|
||||
import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.UPDATE;
|
||||
import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.Role.ADMIN;
|
||||
import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.Role.OWNER;
|
||||
import static google.registry.ui.server.registrar.RegistrarConsoleModule.PARAM_CLIENT_ID;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY;
|
||||
|
@ -27,6 +27,7 @@ import com.google.appengine.api.users.User;
|
|||
import com.google.appengine.api.users.UserService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.Resources;
|
||||
|
@ -44,6 +45,7 @@ import google.registry.request.auth.Auth;
|
|||
import google.registry.request.auth.AuthResult;
|
||||
import google.registry.security.XsrfTokenManager;
|
||||
import google.registry.ui.server.SoyTemplateUtils;
|
||||
import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.Role;
|
||||
import google.registry.ui.soy.registrar.ConsoleSoyInfo;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
|
@ -136,12 +138,10 @@ public final class ConsoleUiAction implements Runnable {
|
|||
String clientId = paramClientId.orElse(registrarAccessor.guessClientId());
|
||||
data.put("clientId", clientId);
|
||||
|
||||
data.put("readWriteClientIds", registrarAccessor.getAllClientIdWithAccess().get(UPDATE));
|
||||
data.put(
|
||||
"readOnlyClientIds",
|
||||
Sets.difference(
|
||||
registrarAccessor.getAllClientIdWithAccess().get(READ),
|
||||
registrarAccessor.getAllClientIdWithAccess().get(UPDATE)));
|
||||
ImmutableSetMultimap<Role, String> roleMap =
|
||||
registrarAccessor.getAllClientIdWithRoles().inverse();
|
||||
data.put("ownerClientIds", roleMap.get(OWNER));
|
||||
data.put("adminClientIds", Sets.difference(roleMap.get(ADMIN), roleMap.get(OWNER)));
|
||||
|
||||
// We want to load the registrar even if we won't use it later (even if we remove the
|
||||
// requireFeeExtension) - to make sure the user indeed has access to the guessed registrar.
|
||||
|
@ -150,7 +150,7 @@ public final class ConsoleUiAction implements Runnable {
|
|||
// since we double check the access to the registrar on any read / update request. We have to
|
||||
// - since the access might get revoked between the initial page load and the request! (also
|
||||
// because the requests come from the browser, and can easily be faked)
|
||||
Registrar registrar = registrarAccessor.getRegistrar(clientId, READ);
|
||||
Registrar registrar = registrarAccessor.getRegistrar(clientId);
|
||||
data.put("requireFeeExtension", registrar.getPremiumPriceAckRequired());
|
||||
} catch (ForbiddenException e) {
|
||||
logger.atWarning().withCause(e).log(
|
||||
|
|
|
@ -20,8 +20,6 @@ import static google.registry.export.sheet.SyncRegistrarsSheetAction.enqueueRegi
|
|||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.security.JsonResponseHelper.Status.ERROR;
|
||||
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
|
||||
import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.READ;
|
||||
import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.UPDATE;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Strings;
|
||||
|
@ -138,7 +136,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
|
|||
|
||||
Map<String, Object> read(String clientId) {
|
||||
return JsonResponseHelper.create(
|
||||
SUCCESS, "Success", registrarAccessor.getRegistrar(clientId, READ).toJsonMap());
|
||||
SUCCESS, "Success", registrarAccessor.getRegistrar(clientId).toJsonMap());
|
||||
}
|
||||
|
||||
Map<String, Object> update(final Map<String, ?> args, String clientId) {
|
||||
|
@ -148,7 +146,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
|
|||
// We load the registrar here rather than outside of the transaction - to make
|
||||
// sure we have the latest version. This one is loaded inside the transaction, so it's
|
||||
// guaranteed to not change before we update it.
|
||||
Registrar registrar = registrarAccessor.getRegistrar(clientId, UPDATE);
|
||||
Registrar registrar = registrarAccessor.getRegistrar(clientId);
|
||||
// Verify that the registrar hasn't been changed.
|
||||
// To do that - we find the latest update time (or null if the registrar has been
|
||||
// deleted) and compare to the update time from the args. The update time in the args
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue