package org.rakam.analysis;
import com.google.common.collect.ImmutableMap;
import org.rakam.analysis.ApiKeyService.ProjectApiKeys;
import org.rakam.analysis.metadata.Metastore;
import org.rakam.analysis.metadata.SchemaChecker;
import org.rakam.collection.SchemaField;
import org.rakam.config.ProjectConfig;
import org.rakam.plugin.ContinuousQuery;
import org.rakam.plugin.MaterializedView;
import org.rakam.server.http.HttpService;
import org.rakam.server.http.RakamHttpRequest;
import org.rakam.server.http.annotations.Api;
import org.rakam.server.http.annotations.ApiOperation;
import org.rakam.server.http.annotations.ApiParam;
import org.rakam.server.http.annotations.Authorization;
import org.rakam.server.http.annotations.BodyParam;
import org.rakam.server.http.annotations.HeaderParam;
import org.rakam.server.http.annotations.JsonRequest;
import org.rakam.util.CryptUtil;
import org.rakam.util.SuccessMessage;
import org.rakam.util.RakamException;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static java.util.Locale.ENGLISH;
import static org.rakam.analysis.ApiKeyService.AccessKeyType.MASTER_KEY;
import static org.rakam.analysis.ApiKeyService.AccessKeyType.READ_KEY;
import static org.rakam.analysis.ApiKeyService.AccessKeyType.WRITE_KEY;
import static org.rakam.util.ValidationUtil.checkProject;
@Path("/project")
@Api(value = "/project", nickname = "project", description = "Project operations", tags = "admin")
public class ProjectHttpService
extends HttpService
{
private final Metastore metastore;
private final ContinuousQueryService continuousQueryService;
private final MaterializedViewService materializedViewService;
private final ApiKeyService apiKeyService;
private final ProjectConfig projectConfig;
private final SchemaChecker schemaChecker;
@Inject
public ProjectHttpService(Metastore metastore,
ProjectConfig projectConfig,
SchemaChecker schemaChecker,
MaterializedViewService materializedViewService,
ApiKeyService apiKeyService,
ContinuousQueryService continuousQueryService)
{
this.continuousQueryService = continuousQueryService;
this.materializedViewService = materializedViewService;
this.apiKeyService = apiKeyService;
this.metastore = metastore;
this.schemaChecker = schemaChecker;
this.projectConfig = projectConfig;
}
@ApiOperation(value = "Create project")
@JsonRequest
@Path("/create")
public ProjectApiKeys createProject(@ApiParam(value = "lock_key", required = false) String lockKey, @ApiParam("name") String name)
{
if (!Objects.equals(projectConfig.getLockKey(), lockKey)) {
throw new RakamException("Lock key is invalid", FORBIDDEN);
}
String project = checkProject(name);
if (metastore.getProjects().contains(project)) {
throw new RakamException("The project already exists.", BAD_REQUEST);
}
metastore.createProject(project);
return transformKeys(apiKeyService.createApiKeys(project));
}
@ApiOperation(value = "Delete project",
authorizations = @Authorization(value = "master_key")
)
@JsonRequest
@DELETE
@Path("/delete")
public SuccessMessage deleteProject(@Named("project") String project)
{
// TODO: we don't really want to delete project data.
if (true) {
return SuccessMessage.success();
}
checkProject(project);
metastore.deleteProject(project.toLowerCase(ENGLISH));
List<ContinuousQuery> list = continuousQueryService.list(project);
for (ContinuousQuery continuousQuery : list) {
continuousQueryService.delete(project, continuousQuery.tableName);
}
List<MaterializedView> views = materializedViewService.list(project);
for (MaterializedView view : views) {
materializedViewService.delete(project, view.tableName);
}
apiKeyService.revokeAllKeys(project);
return SuccessMessage.success();
}
@ApiOperation(value = "Get project stats")
@JsonRequest
@Path("/stats")
public Map<String, Metastore.Stats> getStats(@BodyParam List<String> apiKeys)
{
Map<String, String> keys = new LinkedHashMap<>();
for (String apiKey : apiKeys) {
String project;
try {
project = apiKeyService.getProjectOfApiKey(apiKey, READ_KEY);
}
catch (RakamException e) {
if (e.getStatusCode() == FORBIDDEN) {
continue;
}
throw e;
}
keys.put(project, apiKey);
}
Map<String, Metastore.Stats> stats = metastore.getStats(keys.keySet());
if (stats == null) {
return ImmutableMap.of();
}
return stats.entrySet().stream()
.collect(Collectors.toMap(e -> keys.get(e.getKey()), e -> e.getValue()));
}
@ApiOperation(value = "List created projects",
authorizations = @Authorization(value = "read_key")
)
@JsonRequest
@Path("/list")
public Set<String> getProjects(@ApiParam(value = "lock_key", required = false) String lockKey)
{
if (!Objects.equals(projectConfig.getLockKey(), lockKey)) {
throw new RakamException("Lock key is invalid", FORBIDDEN);
}
return metastore.getProjects();
}
@JsonRequest
@ApiOperation(value = "Add fields to collections",
authorizations = @Authorization(value = "master_key"))
@Path("/schema/add")
public List<SchemaField> addFieldsToSchema(@Named("project") String project,
@ApiParam("collection") String collection,
@ApiParam("fields") Set<SchemaField> fields)
{
return metastore.getOrCreateCollectionFieldList(project, collection,
schemaChecker.checkNewFields(collection, fields));
}
@JsonRequest
@ApiOperation(value = "Add fields to collections by transforming other schemas",
authorizations = @Authorization(value = "master_key"))
@Path("/schema/add/custom")
public List<SchemaField> addCustomFieldsToSchema(@Named("project") String project,
@ApiParam("collection") String collection,
@ApiParam("schema_type") SchemaConverter type,
@ApiParam("schema") String schema)
{
return metastore.getOrCreateCollectionFieldList(project, collection,
schemaChecker.checkNewFields(collection, type.getMapper().apply(schema)));
}
@JsonRequest
@ApiOperation(value = "Get collection schema",
authorizations = @Authorization(value = "read_key"))
@Path("/schema")
public List<Collection> schema(@Named("project") String project,
@ApiParam(value = "names", required = false) Set<String> names)
{
return metastore.getCollections(project).entrySet().stream()
.filter(entry -> names == null || names.contains(entry.getKey()))
.map(entry -> new Collection(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
@JsonRequest
@ApiOperation(value = "Create API Keys",
authorizations = @Authorization(value = "master_key"))
@Path("/create-api-keys")
public ProjectApiKeys createApiKeys(@Named("project") String project)
{
return transformKeys(apiKeyService.createApiKeys(project));
}
@JsonRequest
@ApiOperation(value = "Create API Keys")
@Path("/check-api-keys")
public List<Boolean> checkApiKeys(@ApiParam("keys") List<ProjectApiKeys> keys, @ApiParam("project") String project)
{
return keys.stream().map(key -> {
try {
Consumer<String> stringConsumer = e -> {
if (!e.equals(project.toLowerCase(Locale.ENGLISH))) {
throw new RakamException(FORBIDDEN);
}
};
Optional.ofNullable(key.masterKey()).map(k -> apiKeyService.getProjectOfApiKey(k, MASTER_KEY)).ifPresent(stringConsumer);
Optional.ofNullable(key.readKey()).map(k -> apiKeyService.getProjectOfApiKey(k, READ_KEY)).ifPresent(stringConsumer);
Optional.ofNullable(key.writeKey()).map(k -> apiKeyService.getProjectOfApiKey(k, WRITE_KEY)).ifPresent(stringConsumer);
return true;
}
catch (RakamException e) {
return false;
}
}).collect(Collectors.toList());
}
private ProjectApiKeys transformKeys(ProjectApiKeys apiKeys)
{
if (projectConfig.getPassphrase() == null) {
return ProjectApiKeys.create(apiKeys.masterKey(), apiKeys.readKey(), apiKeys.writeKey());
}
else {
return ProjectApiKeys.create(
CryptUtil.encryptAES(apiKeys.masterKey(), projectConfig.getPassphrase()),
CryptUtil.encryptAES(apiKeys.readKey(), projectConfig.getPassphrase()),
CryptUtil.encryptAES(apiKeys.writeKey(), projectConfig.getPassphrase()));
}
}
@JsonRequest
@ApiOperation(value = "Get collection names",
authorizations = @Authorization(value = "read_key"))
@Path("/collection")
public Set<String> collections(@Named("project") String project)
{
return metastore.getCollectionNames(project);
}
@JsonRequest
@ApiOperation(value = "Revoke API Keys")
@Path("/revoke-api-keys")
public SuccessMessage revokeApiKeys(@ApiParam("project") String project, @ApiParam("master_key") String masterKey)
{
apiKeyService.revokeApiKeys(project, masterKey);
return SuccessMessage.success();
}
public static class Collection
{
public final String name;
public final List<SchemaField> fields;
public Collection(String name, List<SchemaField> fields)
{
this.name = name;
this.fields = fields;
}
}
}