mirror of
https://github.com/google/nomulus.git
synced 2025-05-28 07:02:00 +02:00
Allow admins read-only access to all registrars
We want to be able to view / test / debug how the registrar console looks for our clients. However, we don't want to accidentally change the data for registrars, especially in a "non-accountable" way (where we later don't know who did that change) So we do 2 things here: - Add a "mode" (read-only and read-write) to the getRegistrarForUser function. We set it according to what we want to do with the registrar. Currently, read-write is only requested for the "update" RegistrarSetting action. Admins will have read-only access to all registrars, but read-write access only to the "admin registrar" (or whatever registrar they are contacts for). - Support an undocumented "clientId=XXX" query param that replaces the "guessClientIdForUser" function in the original page load. We can then set it when we want to view a different account. We also change the navigation links on the HTML page to preserve the query. ------------------------- This might be used also for a better user experience for our clients, especially those with multiple "clientId"s (some registrar entities have multiple "registrar" objects) Currently, they have to have a separate user for each clientId, and only have one user allowed which has both read and write permissions. Using this change, we can give them the possibility to add users on their own, some with read-only access (to view billing information without being able to change anything), and use a single user for all their clientIds. ------------------------- ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215480610
This commit is contained in:
parent
5038fa917c
commit
1d621bd14d
13 changed files with 267 additions and 63 deletions
|
@ -26,12 +26,14 @@ import google.registry.request.RequestComponentBuilder;
|
|||
import google.registry.request.RequestModule;
|
||||
import google.registry.request.RequestScope;
|
||||
import google.registry.ui.server.registrar.ConsoleUiAction;
|
||||
import google.registry.ui.server.registrar.RegistrarConsoleModule;
|
||||
import google.registry.ui.server.registrar.RegistrarSettingsAction;
|
||||
|
||||
/** Dagger component with per-request lifetime for "default" App Engine module. */
|
||||
@RequestScope
|
||||
@Subcomponent(
|
||||
modules = {
|
||||
RegistrarConsoleModule.class,
|
||||
DnsModule.class,
|
||||
EppTlsModule.class,
|
||||
RequestModule.class,
|
||||
|
|
|
@ -18,6 +18,7 @@ import static com.google.common.base.Charsets.UTF_8;
|
|||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
|
@ -181,7 +182,7 @@ public abstract class RdapActionBase implements Runnable {
|
|||
String clientId = sessionUtils.guessClientIdForUser(authResult);
|
||||
// We load the Registrar to make sure the user has access to it. We don't actually need it,
|
||||
// we're just checking if an exception is thrown.
|
||||
sessionUtils.getRegistrarForUserCached(clientId, authResult);
|
||||
sessionUtils.getRegistrarForUserCached(clientId, READ_ONLY, authResult);
|
||||
return Optional.of(clientId);
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().withCause(e).log(
|
||||
|
|
|
@ -160,7 +160,7 @@ registry.registrar.Console.prototype.changeNavStyle = function() {
|
|||
slashNdx = slashNdx == -1 ? hashToken.length : slashNdx;
|
||||
var regNavlist = goog.dom.getRequiredElement('reg-navlist');
|
||||
var path = hashToken.substring(0, slashNdx);
|
||||
var active = regNavlist.querySelector('a[href="/registrar#' + path + '"]');
|
||||
var active = regNavlist.querySelector('a[href="#' + path + '"]');
|
||||
if (goog.isNull(active)) {
|
||||
registry.util.log('Unknown path or path form in changeNavStyle.');
|
||||
return;
|
||||
|
|
|
@ -16,6 +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.RegistrarConsoleModule.PARAM_CLIENT_ID;
|
||||
import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
|
||||
|
@ -34,12 +36,14 @@ import google.registry.config.RegistryConfig.Config;
|
|||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.HttpException.ForbiddenException;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
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.soy.registrar.ConsoleSoyInfo;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
|
@ -79,6 +83,7 @@ public final class ConsoleUiAction implements Runnable {
|
|||
@Inject @Config("supportPhoneNumber") String supportPhoneNumber;
|
||||
@Inject @Config("technicalDocsUrl") String technicalDocsUrl;
|
||||
@Inject @Config("registrarConsoleEnabled") boolean enabled;
|
||||
@Inject @Parameter(PARAM_CLIENT_ID) Optional<String> paramClientId;
|
||||
@Inject ConsoleUiAction() {}
|
||||
|
||||
@Override
|
||||
|
@ -126,7 +131,8 @@ public final class ConsoleUiAction implements Runnable {
|
|||
data.put("logoutUrl", userService.createLogoutURL(PATH));
|
||||
data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmail()));
|
||||
try {
|
||||
String clientId = sessionUtils.guessClientIdForUser(authResult);
|
||||
String clientId =
|
||||
paramClientId.orElseGet(() -> sessionUtils.guessClientIdForUser(authResult));
|
||||
data.put("clientId", clientId);
|
||||
// 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.
|
||||
|
@ -135,7 +141,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 = sessionUtils.getRegistrarForUser(clientId, authResult);
|
||||
Registrar registrar = sessionUtils.getRegistrarForUser(clientId, READ_ONLY, authResult);
|
||||
data.put("requireFeeExtension", registrar.getPremiumPriceAckRequired());
|
||||
} catch (ForbiddenException e) {
|
||||
logger.atWarning().withCause(e).log(
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// 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.ui.server.registrar;
|
||||
|
||||
|
||||
import static google.registry.request.RequestParameters.extractOptionalParameter;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import google.registry.request.Parameter;
|
||||
import java.util.Optional;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/** Dagger module for the Registrar Console parameters. */
|
||||
@Module
|
||||
public final class RegistrarConsoleModule {
|
||||
|
||||
static final String PARAM_CLIENT_ID = "clientId";
|
||||
|
||||
@Provides
|
||||
@Parameter(PARAM_CLIENT_ID)
|
||||
static Optional<String> provideClientId(HttpServletRequest req) {
|
||||
return extractOptionalParameter(req, PARAM_CLIENT_ID);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@ 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.SessionUtils.AccessType.READ_ONLY;
|
||||
import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_WRITE;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.HashMultimap;
|
||||
|
@ -132,7 +134,9 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
|
|||
|
||||
Map<String, Object> read(String clientId) {
|
||||
return JsonResponseHelper.create(
|
||||
SUCCESS, "Success", sessionUtils.getRegistrarForUser(clientId, authResult).toJsonMap());
|
||||
SUCCESS,
|
||||
"Success",
|
||||
sessionUtils.getRegistrarForUser(clientId, READ_ONLY, authResult).toJsonMap());
|
||||
}
|
||||
|
||||
Map<String, Object> update(final Map<String, ?> args, String clientId) {
|
||||
|
@ -142,7 +146,8 @@ 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 = sessionUtils.getRegistrarForUser(clientId, authResult);
|
||||
Registrar registrar =
|
||||
sessionUtils.getRegistrarForUser(clientId, READ_WRITE, authResult);
|
||||
// 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
|
||||
|
|
|
@ -30,7 +30,7 @@ import java.util.function.Function;
|
|||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Authenticated Registrar access helper class. */
|
||||
/** HTTP session management helper class. */
|
||||
@Immutable
|
||||
public class SessionUtils {
|
||||
|
||||
|
@ -43,6 +43,9 @@ public class SessionUtils {
|
|||
@Inject
|
||||
public SessionUtils() {}
|
||||
|
||||
/** Type of access we're requesting. */
|
||||
public enum AccessType {READ_ONLY, READ_WRITE}
|
||||
|
||||
/**
|
||||
* Loads Registrar on behalf of an authorised user.
|
||||
*
|
||||
|
@ -50,10 +53,13 @@ public class SessionUtils {
|
|||
* 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)
|
||||
* @param authResult AuthResult of the user on behalf of which we want to access the data
|
||||
*/
|
||||
public Registrar getRegistrarForUser(String clientId, AuthResult authResult) {
|
||||
return getAndAuthorize(Registrar::loadByClientId, clientId, authResult);
|
||||
public Registrar getRegistrarForUser(
|
||||
String clientId, AccessType accessType, AuthResult authResult) {
|
||||
return getAndAuthorize(Registrar::loadByClientId, clientId, accessType, authResult);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,15 +69,19 @@ public class SessionUtils {
|
|||
* 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)
|
||||
* @param authResult AuthResult of the user on behalf of which we want to access the data
|
||||
*/
|
||||
public Registrar getRegistrarForUserCached(String clientId, AuthResult authResult) {
|
||||
return getAndAuthorize(Registrar::loadByClientIdCached, clientId, authResult);
|
||||
public Registrar getRegistrarForUserCached(
|
||||
String clientId, AccessType accessType, AuthResult authResult) {
|
||||
return getAndAuthorize(Registrar::loadByClientIdCached, clientId, accessType, authResult);
|
||||
}
|
||||
|
||||
Registrar getAndAuthorize(
|
||||
private Registrar getAndAuthorize(
|
||||
Function<String, Optional<Registrar>> registrarLoader,
|
||||
String clientId,
|
||||
AccessType accessType,
|
||||
AuthResult authResult) {
|
||||
UserAuthInfo userAuthInfo =
|
||||
authResult.userAuthInfo().orElseThrow(() -> new ForbiddenException("Not logged in"));
|
||||
|
@ -97,8 +107,17 @@ public class SessionUtils {
|
|||
return registrar;
|
||||
}
|
||||
|
||||
if (isAdmin && accessType == AccessType.READ_ONLY) {
|
||||
// Admins have read-only access to all registrars
|
||||
logger.atInfo().log(
|
||||
"Allowing admin %s read-only access to registrar %s.", userIdForLogging, clientId);
|
||||
return registrar;
|
||||
}
|
||||
|
||||
throw new ForbiddenException(
|
||||
String.format("User %s doesn't have access to registrar %s", userIdForLogging, clientId));
|
||||
String.format(
|
||||
"User %s doesn't have %s access to registrar %s",
|
||||
userIdForLogging, accessType, clientId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -78,21 +78,21 @@
|
|||
<div id="reg-nav" class="{css('kd-content-sidebar')}">
|
||||
<ul id="reg-navlist">
|
||||
<li>
|
||||
<a href="/registrar#">Home</a>
|
||||
<a href="#">Home</a>
|
||||
<li>
|
||||
<a href="/registrar#resources">Resources & billing</a>
|
||||
<a href="#resources">Resources & billing</a>
|
||||
<li>
|
||||
<ul>
|
||||
<span class="{css('reg-navlist-sub')}">Settings</span>
|
||||
<li>
|
||||
<a href="/registrar#whois-settings">WHOIS</a>
|
||||
<a href="#whois-settings">WHOIS</a>
|
||||
<li>
|
||||
<a href="/registrar#security-settings">Security</a>
|
||||
<a href="#security-settings">Security</a>
|
||||
<li>
|
||||
<a href="/registrar#contact-settings">Contact</a>
|
||||
<a href="#contact-settings">Contact</a>
|
||||
</ul>
|
||||
<li>
|
||||
<a href="/registrar#contact-us">Contact us</a>
|
||||
<a href="#contact-us">Contact us</a>
|
||||
</ul>
|
||||
</div>
|
||||
{/template}
|
||||
|
@ -130,19 +130,29 @@
|
|||
{@param logoutUrl: string} /** Generated URL for logging out of Google. */
|
||||
{@param logoFilename: string}
|
||||
{@param productName: string}
|
||||
{@param? clientId: string}
|
||||
{call registry.soy.console.header}
|
||||
{param app: 'registrar' /}
|
||||
{param subtitle: 'Please Login' /}
|
||||
{param subtitle: 'Not Authorized' /}
|
||||
{/call}
|
||||
<div class="{css('whoAreYou')}">
|
||||
<a class="{css('logo')}" href="/registrar">
|
||||
<img src="/assets/images/{$logoFilename}" alt="{$productName}">
|
||||
</a>
|
||||
<h1>You need permission</h1>
|
||||
<p>
|
||||
The account you are logged in as is not associated with {$productName}.
|
||||
Please contact your customer service representative or
|
||||
switch to an account associated with {$productName}.
|
||||
{if isNonnull($clientId)} // A clientId was given - but we don't have access to it
|
||||
<p>
|
||||
The account you are logged in as is not associated with the registrar
|
||||
{sp}{$clientId}. Please contact your customer service representative or
|
||||
switch to an account associated with {$clientId}. Alternatively, click
|
||||
{sp}<a href="?">here</a> to find a registrar associated with your
|
||||
account.
|
||||
{else}
|
||||
<p>
|
||||
The account you are logged in as is not associated with {$productName}.
|
||||
Please contact your customer service representative or
|
||||
switch to an account associated with {$productName}.
|
||||
{/if}
|
||||
<p>
|
||||
You are signed in as <strong>{$username}</strong>.
|
||||
<div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue