mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 03:57:51 +02:00
Specifically, this prevents suspended registrars from creating domains or applications. Pending registrars already can't perform these actions because they get an error message when attempting to log in. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170481338
228 lines
8.8 KiB
Java
228 lines
8.8 KiB
Java
// Copyright 2017 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.export;
|
|
|
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
|
import static google.registry.request.Action.Method.POST;
|
|
import static google.registry.util.CollectionUtils.nullToEmpty;
|
|
import static google.registry.util.RegistrarUtils.normalizeClientId;
|
|
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
|
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
|
|
|
import com.google.common.base.Function;
|
|
import com.google.common.base.Optional;
|
|
import com.google.common.base.Predicate;
|
|
import com.google.common.collect.FluentIterable;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableMap;
|
|
import com.google.common.collect.Sets;
|
|
import com.googlecode.objectify.VoidWork;
|
|
import google.registry.config.RegistryConfig.Config;
|
|
import google.registry.groups.GroupsConnection;
|
|
import google.registry.groups.GroupsConnection.Role;
|
|
import google.registry.model.registrar.Registrar;
|
|
import google.registry.model.registrar.RegistrarContact;
|
|
import google.registry.request.Action;
|
|
import google.registry.request.Response;
|
|
import google.registry.request.auth.Auth;
|
|
import google.registry.util.FormattingLogger;
|
|
import google.registry.util.Retrier;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.Callable;
|
|
import javax.annotation.Nullable;
|
|
import javax.inject.Inject;
|
|
|
|
/**
|
|
* Action that syncs changes to {@link RegistrarContact} entities with Google Groups.
|
|
*
|
|
* <p>This uses the <a href="https://developers.google.com/admin-sdk/directory/">Directory API</a>.
|
|
*/
|
|
@Action(
|
|
path = "/_dr/task/syncGroupMembers",
|
|
method = POST,
|
|
auth = Auth.AUTH_INTERNAL_ONLY
|
|
)
|
|
public final class SyncGroupMembersAction implements Runnable {
|
|
|
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
|
|
|
private enum Result {
|
|
OK(SC_OK, "Group memberships successfully updated."),
|
|
NOT_MODIFIED(SC_OK, "No registrar contacts have been updated since the last time servlet ran."),
|
|
FAILED(SC_INTERNAL_SERVER_ERROR, "Error occurred while updating registrar contacts.") {
|
|
@Override
|
|
protected void log(Throwable cause) {
|
|
logger.severefmt(cause, "%s", message);
|
|
}};
|
|
|
|
final int statusCode;
|
|
final String message;
|
|
|
|
private Result(int statusCode, String message) {
|
|
this.statusCode = statusCode;
|
|
this.message = message;
|
|
}
|
|
|
|
/** Log an error message. Results that use log levels other than info should override this. */
|
|
void log(@Nullable Throwable cause) {
|
|
logger.infofmt(cause, "%s", message);
|
|
}
|
|
}
|
|
|
|
@Inject GroupsConnection groupsConnection;
|
|
@Inject @Config("gSuiteDomainName") String gSuiteDomainName;
|
|
@Inject Response response;
|
|
@Inject Retrier retrier;
|
|
@Inject SyncGroupMembersAction() {}
|
|
|
|
private void sendResponse(Result result, @Nullable List<Throwable> causes) {
|
|
for (Throwable cause : nullToEmpty(causes)) {
|
|
result.log(cause);
|
|
}
|
|
response.setStatus(result.statusCode);
|
|
response.setPayload(String.format("%s %s\n", result.name(), result.message));
|
|
}
|
|
|
|
/**
|
|
* Returns the Google Groups email address for the given registrar clientId and
|
|
* RegistrarContact.Type
|
|
*/
|
|
public static String getGroupEmailAddressForContactType(
|
|
String clientId, RegistrarContact.Type type, String gSuiteDomainName) {
|
|
// Take the registrar's clientId, make it lowercase, and remove all characters that aren't
|
|
// alphanumeric, hyphens, or underscores.
|
|
return String.format(
|
|
"%s-%s-contacts@%s",
|
|
normalizeClientId(clientId), type.getDisplayName(), gSuiteDomainName);
|
|
}
|
|
|
|
/**
|
|
* Loads all Registrars, and for each one that is marked dirty, grabs the existing group
|
|
* memberships and updates them to reflect the current state of the RegistrarContacts.
|
|
*/
|
|
@Override
|
|
public void run() {
|
|
List<Registrar> dirtyRegistrars = FluentIterable.from(Registrar.loadAllCached())
|
|
.filter(new Predicate<Registrar>() {
|
|
@Override
|
|
public boolean apply(Registrar registrar) {
|
|
// Only grab active registrars that require syncing and are of the correct type.
|
|
return registrar.isLive()
|
|
&& registrar.getContactsRequireSyncing()
|
|
&& registrar.getType() == Registrar.Type.REAL;
|
|
}})
|
|
.toList();
|
|
if (dirtyRegistrars.isEmpty()) {
|
|
sendResponse(Result.NOT_MODIFIED, null);
|
|
return;
|
|
}
|
|
|
|
ImmutableMap.Builder<Registrar, Optional<Throwable>> resultsBuilder =
|
|
new ImmutableMap.Builder<>();
|
|
for (final Registrar registrar : dirtyRegistrars) {
|
|
try {
|
|
retrier.callWithRetry(new Callable<Void>() {
|
|
@Override
|
|
public Void call() throws Exception {
|
|
syncRegistrarContacts(registrar);
|
|
return null;
|
|
}}, RuntimeException.class);
|
|
resultsBuilder.put(registrar, Optional.<Throwable>absent());
|
|
} catch (Throwable e) {
|
|
logger.severe(e, e.getMessage());
|
|
resultsBuilder.put(registrar, Optional.of(e));
|
|
}
|
|
}
|
|
|
|
List<Throwable> errors = getErrorsAndUpdateFlagsForSuccesses(resultsBuilder.build());
|
|
// If there were no errors, return success; otherwise return a failed status and log the errors.
|
|
if (errors.isEmpty()) {
|
|
sendResponse(Result.OK, null);
|
|
} else {
|
|
sendResponse(Result.FAILED, errors);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses the results from Google Groups for each registrar, setting the dirty flag to false in
|
|
* Datastore for the calls that succeeded and accumulating the errors for the calls that failed.
|
|
*/
|
|
private static List<Throwable> getErrorsAndUpdateFlagsForSuccesses(
|
|
ImmutableMap<Registrar, Optional<Throwable>> results) {
|
|
final ImmutableList.Builder<Registrar> registrarsToSave = new ImmutableList.Builder<>();
|
|
List<Throwable> errors = new ArrayList<>();
|
|
for (Map.Entry<Registrar, Optional<Throwable>> result : results.entrySet()) {
|
|
if (result.getValue().isPresent()) {
|
|
errors.add(result.getValue().get());
|
|
} else {
|
|
registrarsToSave.add(result.getKey().asBuilder().setContactsRequireSyncing(false).build());
|
|
}
|
|
}
|
|
ofy().transactNew(new VoidWork() {
|
|
@Override
|
|
public void vrun() {
|
|
ofy().save().entities(registrarsToSave.build());
|
|
}});
|
|
return errors;
|
|
}
|
|
|
|
/** Syncs the contacts for an individual registrar to Google Groups. */
|
|
private void syncRegistrarContacts(Registrar registrar) {
|
|
String groupKey = "";
|
|
try {
|
|
Set<RegistrarContact> registrarContacts = registrar.getContacts();
|
|
long totalAdded = 0;
|
|
long totalRemoved = 0;
|
|
for (final RegistrarContact.Type type : RegistrarContact.Type.values()) {
|
|
groupKey = getGroupEmailAddressForContactType(
|
|
registrar.getClientId(), type, gSuiteDomainName);
|
|
Set<String> currentMembers = groupsConnection.getMembersOfGroup(groupKey);
|
|
Set<String> desiredMembers = FluentIterable.from(registrarContacts)
|
|
.filter(new Predicate<RegistrarContact>() {
|
|
@Override
|
|
public boolean apply(RegistrarContact contact) {
|
|
return contact.getTypes().contains(type);
|
|
}})
|
|
.transform(new Function<RegistrarContact, String>() {
|
|
@Override
|
|
public String apply(RegistrarContact contact) {
|
|
return contact.getEmailAddress();
|
|
}})
|
|
.toSet();
|
|
for (String email : Sets.difference(desiredMembers, currentMembers)) {
|
|
groupsConnection.addMemberToGroup(groupKey, email, Role.MEMBER);
|
|
totalAdded++;
|
|
}
|
|
for (String email : Sets.difference(currentMembers, desiredMembers)) {
|
|
groupsConnection.removeMemberFromGroup(groupKey, email);
|
|
totalRemoved++;
|
|
}
|
|
}
|
|
logger.infofmt("Successfully synced contacts for registrar %s: added %d and removed %d",
|
|
registrar.getClientId(),
|
|
totalAdded,
|
|
totalRemoved);
|
|
} catch (IOException e) {
|
|
// Package up exception and re-throw with attached additional relevant info.
|
|
String msg = String.format("Couldn't sync contacts for registrar %s to group %s",
|
|
registrar.getClientId(), groupKey);
|
|
throw new RuntimeException(msg, e);
|
|
}
|
|
}
|
|
}
|