package keywhiz.service.resources.automation.v2;
import com.codahale.metrics.annotation.ExceptionMetered;
import com.codahale.metrics.annotation.Timed;
import com.google.common.collect.Sets;
import io.dropwizard.auth.Auth;
import java.net.URI;
import java.time.Instant;
import java.util.HashMap;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import keywhiz.api.automation.v2.ClientDetailResponseV2;
import keywhiz.api.automation.v2.CreateClientRequestV2;
import keywhiz.api.automation.v2.ModifyClientRequestV2;
import keywhiz.api.automation.v2.ModifyGroupsRequestV2;
import keywhiz.api.model.AutomationClient;
import keywhiz.api.model.Client;
import keywhiz.api.model.Group;
import keywhiz.api.model.SanitizedSecret;
import keywhiz.log.AuditLog;
import keywhiz.log.Event;
import keywhiz.log.EventTag;
import keywhiz.service.daos.AclDAO;
import keywhiz.service.daos.AclDAO.AclDAOFactory;
import keywhiz.service.daos.ClientDAO;
import keywhiz.service.daos.ClientDAO.ClientDAOFactory;
import keywhiz.service.daos.GroupDAO;
import keywhiz.service.daos.GroupDAO.GroupDAOFactory;
import keywhiz.service.exceptions.ConflictException;
import org.apache.commons.lang3.NotImplementedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.lang.String.format;
import static java.util.stream.Collectors.toSet;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
/**
* @parentEndpointName automation/v2-client-management
* @resourceDescription Automation endpoints to manage clients
*/
@Path("/automation/v2/clients")
public class ClientResource {
private static final Logger logger = LoggerFactory.getLogger(ClientResource.class);
private final AclDAO aclDAOReadOnly;
private final AclDAO aclDAOReadWrite;
private final ClientDAO clientDAOReadOnly;
private final ClientDAO clientDAOReadWrite;
private final GroupDAO groupDAOReadWrite;
private final AuditLog auditLog;
@Inject public ClientResource(AclDAOFactory aclDAOFactory, ClientDAOFactory clientDAOFactory,
GroupDAOFactory groupDAOFactory, AuditLog auditLog) {
this.aclDAOReadOnly = aclDAOFactory.readonly();
this.aclDAOReadWrite = aclDAOFactory.readwrite();
this.clientDAOReadOnly = clientDAOFactory.readonly();
this.clientDAOReadWrite = clientDAOFactory.readwrite();
this.groupDAOReadWrite = groupDAOFactory.readwrite();
this.auditLog = auditLog;
}
/**
* Creates a client and assigns to given groups
*
* @excludeParams automationClient
* @param request JSON request to create a client
*
* @responseMessage 201 Created client and assigned to given groups
* @responseMessage 409 Client already exists
*/
@Timed @ExceptionMetered
@POST
@Consumes(APPLICATION_JSON)
public Response createClient(@Auth AutomationClient automationClient,
@Valid CreateClientRequestV2 request) {
String creator = automationClient.getName();
String client = request.name();
clientDAOReadWrite.getClient(client).ifPresent((c) -> {
logger.info("Automation ({}) - Client {} already exists", creator, client);
throw new ConflictException("Client name already exists.");
});
// Creates new client record
long clientId = clientDAOReadWrite.createClient(client, creator, request.description());
auditLog.recordEvent(new Event(Instant.now(), EventTag.CLIENT_CREATE, creator, client));
// Enrolls client in any requested groups
groupsToGroupIds(request.groups())
.forEach((maybeGroupId) -> maybeGroupId.ifPresent(
(groupId) -> aclDAOReadWrite.findAndEnrollClient(clientId, groupId, auditLog, creator, new HashMap<>())));
URI uri = UriBuilder.fromResource(ClientResource.class).path(client).build();
return Response.created(uri).build();
}
/**
* Retrieve listing of client names
*
* @excludeParams automationClient
* @responseMessage 200 List of client names
*/
@Timed @ExceptionMetered
@GET
@Produces(APPLICATION_JSON)
public Iterable<String> clientListing(@Auth AutomationClient automationClient) {
return clientDAOReadOnly.getClients().stream()
.map(Client::getName)
.collect(toSet());
}
/**
* Retrieve information on a client
*
* @excludeParams automationClient
* @param name Client name
*
* @responseMessage 200 Client information retrieved
* @responseMessage 404 Client not found
*/
@Timed @ExceptionMetered
@GET
@Path("{name}")
@Produces(APPLICATION_JSON)
public ClientDetailResponseV2 clientInfo(@Auth AutomationClient automationClient,
@PathParam("name") String name) {
Client client = clientDAOReadOnly.getClient(name)
.orElseThrow(NotFoundException::new);
return ClientDetailResponseV2.fromClient(client);
}
/**
* Listing of groups accessible to a client
*
* @excludeParams automationClient
* @param name Client name
* @return Listing of groups the client has membership to
*
* @responseMessage 200 Listing succeeded
* @responseMessage 404 Client not found
*/
@Timed @ExceptionMetered
@GET
@Path("{name}/groups")
@Produces(APPLICATION_JSON)
public Iterable<String> clientGroupsListing(@Auth AutomationClient automationClient,
@PathParam("name") String name) {
Client client = clientDAOReadOnly.getClient(name)
.orElseThrow(NotFoundException::new);
return aclDAOReadOnly.getGroupsFor(client).stream()
.map(Group::getName)
.collect(toSet());
}
/**
* Modify groups a client has membership in
*
* @excludeParams automationClient
* @param name Client name
* @param request JSON request specifying which groups to add or remove
* @return Listing of groups client has membership in
*
* @responseMessage 201 Client modified successfully
* @responseMessage 404 Client not found
*/
@Timed @ExceptionMetered
@PUT
@Path("{name}/groups")
@Produces(APPLICATION_JSON)
public Iterable<String> modifyClientGroups(@Auth AutomationClient automationClient,
@PathParam("name") String name, @Valid ModifyGroupsRequestV2 request) {
Client client = clientDAOReadWrite.getClient(name)
.orElseThrow(NotFoundException::new);
String user = automationClient.getName();
long clientId = client.getId();
Set<String> oldGroups = aclDAOReadWrite.getGroupsFor(client).stream()
.map(Group::getName)
.collect(toSet());
Set<String> groupsToAdd = Sets.difference(request.addGroups(), oldGroups);
Set<String> groupsToRemove = Sets.intersection(request.removeGroups(), oldGroups);
// TODO: should optimize AclDAO to use names and return only name column
groupsToGroupIds(groupsToAdd)
.forEach((maybeGroupId) -> maybeGroupId.ifPresent(
(groupId) -> aclDAOReadWrite.findAndEnrollClient(clientId, groupId, auditLog, user, new HashMap<>())));
groupsToGroupIds(groupsToRemove)
.forEach((maybeGroupId) -> maybeGroupId.ifPresent(
(groupId) -> aclDAOReadWrite.findAndEvictClient(clientId, groupId, auditLog, user, new HashMap<>())));
return aclDAOReadWrite.getGroupsFor(client).stream()
.map(Group::getName)
.collect(toSet());
}
/**
* Listing of secrets accessible to a client
*
* @excludeParams automationClient
* @param name Client name
* @return Listing of secrets accessible to client
*
* @responseMessage 200 Client lookup succeeded
* @responseMessage 404 Client not found
*/
@Timed @ExceptionMetered
@GET
@Path("{name}/secrets")
@Produces(APPLICATION_JSON)
public Iterable<String> clientSecretsListing(@Auth AutomationClient automationClient,
@PathParam("name") String name) {
Client client = clientDAOReadOnly.getClient(name)
.orElseThrow(NotFoundException::new);
return aclDAOReadOnly.getSanitizedSecretsFor(client).stream()
.map(SanitizedSecret::name)
.collect(toSet());
}
/**
* Delete a client
*
* @excludeParams automationClient
* @param name Client name
*
* @responseMessage 204 Client deleted
* @responseMessage 404 Client not found
*/
@Timed @ExceptionMetered
@DELETE
@Path("{name}")
public Response deleteClient(@Auth AutomationClient automationClient,
@PathParam("name") String name) {
Client client = clientDAOReadWrite.getClient(name)
.orElseThrow(NotFoundException::new);
// Group memberships are deleted automatically by DB cascading.
clientDAOReadWrite.deleteClient(client);
auditLog.recordEvent(new Event(Instant.now(), EventTag.CLIENT_DELETE, automationClient.getName(), client.getName()));
return Response.noContent().build();
}
/**
* Modify a client
*
* @excludeParams automationClient
* @param currentName Client name
* @param request JSON request to modify the client
*
* @responseMessage 201 Client updated
* @responseMessage 404 Client not found
*/
@Timed @ExceptionMetered
@POST
@Path("{name}")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public ClientDetailResponseV2 modifyClient(@Auth AutomationClient automationClient,
@PathParam("name") String currentName, @Valid ModifyClientRequestV2 request) {
Client client = clientDAOReadWrite.getClient(currentName)
.orElseThrow(NotFoundException::new);
String newName = request.name();
// TODO: implement change client (name, updatedAt, updatedBy)
throw new NotImplementedException(format(
"Need to implement mutation methods in DAO to rename %s to %s", client.getName(), newName));
}
private Stream<Optional<Long>> groupsToGroupIds(Set<String> groupNames) {
return groupNames.stream()
.map(groupDAOReadWrite::getGroup)
.map((group) -> group.map(Group::getId));
}
}