package org.rakam.ui.user;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.oauth2.Oauth2;
import com.google.api.services.oauth2.model.Userinfoplus;
import com.google.inject.Inject;
import io.airlift.log.Logger;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.util.CharsetUtil;
import org.rakam.analysis.ApiKeyService;
import org.rakam.config.EncryptionConfig;
import org.rakam.server.http.HttpService;
import org.rakam.server.http.RakamHttpRequest;
import org.rakam.server.http.Response;
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.CookieParam;
import org.rakam.server.http.annotations.IgnoreApi;
import org.rakam.server.http.annotations.JsonRequest;
import org.rakam.ui.ProtectEndpoint;
import org.rakam.ui.RakamUIConfig;
import org.rakam.ui.UIPermissionParameterProvider.Project;
import org.rakam.ui.user.WebUser.UserApiKey;
import org.rakam.ui.user.WebUserService.ProjectConfiguration;
import org.rakam.ui.user.WebUserService.UserAccess;
import org.rakam.util.CryptUtil;
import org.rakam.util.JsonHelper;
import org.rakam.util.RakamException;
import org.rakam.util.SuccessMessage;
import javax.inject.Named;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import static io.netty.buffer.Unpooled.wrappedBuffer;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaders.Names.COOKIE;
import static io.netty.handler.codec.http.HttpHeaders.Names.SET_COOKIE;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static io.netty.handler.codec.http.cookie.ServerCookieEncoder.STRICT;
@Path("/ui/user")
@IgnoreApi
public class WebUserHttpService
extends HttpService
{
private final static Logger LOGGER = Logger.get(WebUserHttpService.class);
private final WebUserService service;
private final EncryptionConfig encryptionConfig;
private final RakamUIConfig config;
@Inject
public WebUserHttpService(WebUserService service, RakamUIConfig config, EncryptionConfig encryptionConfig)
{
this.service = service;
this.encryptionConfig = encryptionConfig;
this.config = config;
}
@JsonRequest
@Path("/register")
public Response register(@ApiParam("email") String email,
@ApiParam(value = "name", required = false) String name,
@ApiParam("password") String password)
{
// TODO: implement captcha https://github.com/VividCortex/angular-recaptcha https://developers.google.com/recaptcha/docs/verify
// keep a counter for ip in local nodes
final WebUser user = service.createUser(email, password, name, null, null, null, false);
return getLoginResponseForUser(user);
}
@JsonRequest
@ProtectEndpoint(writeOperation = true, requiresProject = false)
@Path("/update/password")
public SuccessMessage updatePassword(@ApiParam("oldPassword") String oldPassword,
@ApiParam("newPassword") String newPassword,
@javax.inject.Named("user_id") Project project)
{
Optional<WebUser> webUser = service.getUser(project.userId);
if (webUser.get().readOnly) {
throw new RakamException("User is not allowed to perform this operation", UNAUTHORIZED);
}
service.updateUserPassword(project.userId, oldPassword, newPassword);
return SuccessMessage.success();
}
@JsonRequest
@ProtectEndpoint(writeOperation = true, requiresProject = false)
@Path("/update/info")
public SuccessMessage update(
@ApiParam("name") String name,
@javax.inject.Named("user_id") Project project)
{
Optional<WebUser> webUser = service.getUser(project.userId);
if (webUser.get().readOnly) {
throw new RakamException("User is not allowed to perform this operation", UNAUTHORIZED);
}
service.updateUserInfo(project.userId, name);
return SuccessMessage.success();
}
@JsonRequest
@ProtectEndpoint(writeOperation = true, requiresProject = false)
@Path("/get-lock-key")
public String getLockKey(@ApiParam("api_url") String apiUrl,
@javax.inject.Named("user_id") Project project)
{
Optional<WebUser> webUser = service.getUser(project.userId);
if (webUser.get().readOnly) {
throw new RakamException("User is not allowed to create projects", UNAUTHORIZED);
}
return service.getLockKeyForAPI(project.userId, apiUrl);
}
@JsonRequest
@ProtectEndpoint(writeOperation = true, requiresProject = false)
@Path("/register-project")
public UserApiKey registerProject(@ApiParam("name") String name,
@ApiParam("api_url") String apiUrl,
@ApiParam(value = "read_key") String readKey,
@ApiParam(value = "write_key", required = false) String writeKey,
@ApiParam(value = "master_key", required = false) String masterKey,
@javax.inject.Named("user_id") Project project)
{
Optional<WebUser> webUser = service.getUser(project.userId);
if (webUser.get().readOnly) {
throw new RakamException("User is not allowed to register projects", UNAUTHORIZED);
}
return service.registerProject(project.userId, apiUrl, name, readKey, writeKey, masterKey);
}
@JsonRequest
@ProtectEndpoint(writeOperation = true)
@Path("/delete-project")
@DELETE
public SuccessMessage deleteProject(@Named("user_id") Project project)
{
service.deleteProject(project.userId, project.project);
return SuccessMessage.success();
}
@JsonRequest
@ProtectEndpoint(writeOperation = true)
@Path("/save-api-keys")
public ApiKeyService.ProjectApiKeys createApiKeys(
@Named("user_id") Project project,
@ApiParam("read_key") String readKey,
@ApiParam("write_key") String writeKey,
@ApiParam("master_key") String masterKey)
{
service.saveApiKeys(project.userId, project.project, readKey, writeKey, masterKey);
return ApiKeyService.ProjectApiKeys.create(masterKey, readKey, writeKey);
}
@JsonRequest
@ProtectEndpoint(writeOperation = true)
@Path("/revoke-api-keys")
public SuccessMessage revokeApiKeys(@ApiParam("master_key") String key, @Named("user_id") Project project)
{
service.revokeApiKeys(project.userId, project.project, key);
return SuccessMessage.success();
}
@GET
@ApiOperation(value = "List users who can access to the project")
@Path("/user-access")
public List<UserAccess> getUserAccess(@Named("user_id") Project project)
{
return service.getUserAccessForProject(project.userId, project.project);
}
@GET
@ApiOperation(value = "Get project configurations")
@Path("/project-configuration")
public ProjectConfiguration getProjectPreferences(@Named("user_id") Project project)
{
return service.getProjectConfigurations(project.project);
}
@JsonRequest
@ApiOperation(value = "Update project configurations")
@Path("/project-configuration")
public SuccessMessage updateProjectPreferences(@Named("user_id") Project project, @BodyParam ProjectConfiguration configuration)
{
service.updateProjectConfigurations(project.userId, project.project, configuration);
return SuccessMessage.success();
}
@JsonRequest
@ApiOperation(value = "Recover my password", authorizations = @Authorization(value = "master_key"))
@Path("/prepare-recover-password")
public SuccessMessage prepareRecoverPassword(@ApiParam("email") String email)
{
service.prepareRecoverPassword(email);
return SuccessMessage.success();
}
@ApiOperation(value = "Recover my password", authorizations = @Authorization(value = "master_key"))
@JsonRequest
@Path("/perform-recover-password")
public SuccessMessage performRecoverPassword(
@ApiParam("key") String key,
@ApiParam("hash") String hash,
@ApiParam("password") String password)
{
service.performRecoverPassword(key, hash, password);
return SuccessMessage.success();
}
@JsonRequest
@ApiOperation(value = "Revoke User Access", authorizations = @Authorization(value = "master_key"))
@Path("/revoke-user-access")
@ProtectEndpoint(writeOperation = true)
public List<String> revokeUserAccess(@Named("user_id") Project project,
@ApiParam("email") String email)
{
return service.revokeUserAccess(project.userId, project.project, email);
}
@JsonRequest
@Path("/give-user-access")
@ProtectEndpoint(writeOperation = true)
public SuccessMessage giveUserAccess(@Named("user_id") Project project,
@ApiParam("email") String email,
@ApiParam(value = "scope_expression", required = false) String scopeExpression,
@ApiParam(value = "keys") ApiKeyService.ProjectApiKeys keys,
@ApiParam(value = "read_permission") boolean readPermission,
@ApiParam(value = "write_permission") boolean writePermission,
@ApiParam(value = "master_permission") boolean masterPermission)
{
Optional<WebUser> user = service.getUser(project.userId);
if (!user.get().projects.stream()
.anyMatch(e -> e.apiKeys.stream().anyMatch(a -> a.masterKey() != null))) {
throw new RakamException(FORBIDDEN);
}
service.giveAccessToUser(project.project, user.get().id, email, keys, scopeExpression,
readPermission, writePermission, masterPermission, Optional.empty());
return SuccessMessage.success();
}
@JsonRequest
@Path("/update-user-access")
@ProtectEndpoint(writeOperation = true)
public SuccessMessage updateUserAccess(@Named("user_id") Project project,
@ApiParam("email") String email,
@ApiParam(value = "read_permission") boolean readPermission,
@ApiParam(value = "write_permission") boolean writePermission,
@ApiParam(value = "master_permission") boolean masterPermission)
{
Optional<WebUser> user = service.getUser(project.userId);
if (!user.get().projects.stream()
.anyMatch(e -> e.apiKeys.stream().anyMatch(a -> a.masterKey() != null))) {
throw new RakamException(FORBIDDEN);
}
service.giveAccessToExistingUser(project.project, user.get().id, email,
readPermission, writePermission, masterPermission);
return SuccessMessage.success();
}
@GET
@JsonRequest
@Path("/me")
public void me(RakamHttpRequest request, @CookieParam(value = "session", required = false) String session)
{
List<String> jsonpParam = request.params().get("jsonp");
Optional<String> jsonp = jsonpParam == null ? Optional.empty() : jsonpParam.stream().findAny();
if (jsonp.isPresent() && !jsonp.get().matches("^[A-Za-z]+$")) {
throw new RakamException(BAD_REQUEST);
}
if (session != null) {
Integer id;
try {
id = extractUserFromCookie(session, encryptionConfig.getSecretKey());
}
catch (Exception e) {
request.response(unauthorized(jsonp)).end();
return;
}
final Optional<WebUser> user = service.getUser(id);
if (user.isPresent()) {
String encode = JsonHelper.encode(user.get());
if (jsonp.isPresent()) {
encode = jsonp.get() + "(" + encode + ")";
}
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK,
wrappedBuffer(encode.getBytes(CharsetUtil.UTF_8)));
response.headers().set(CONTENT_TYPE, "application/json; charset=utf-8");
request.response(response).end();
return;
}
}
request.response(unauthorized(jsonp)).end();
}
private FullHttpResponse unauthorized(Optional<String> jsonp)
{
String encode = JsonHelper.jsonObject()
.put("success", false)
.put("message", UNAUTHORIZED.reasonPhrase())
.put("googleApiKey", config.getGoogleClientId()).toString();
if (jsonp.isPresent()) {
encode = jsonp.get() + "(" + encode + ")";
}
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK,
wrappedBuffer(encode.getBytes(CharsetUtil.UTF_8)));
DefaultCookie cookie = new DefaultCookie("session", "");
cookie.setHttpOnly(true);
cookie.setMaxAge(0);
response.headers().add(SET_COOKIE, STRICT.encode(cookie));
response.headers().set(CONTENT_TYPE, "application/json; charset=utf-8");
return response;
}
@JsonRequest
@Path("/login")
public Response<WebUser> login(@ApiParam("email") String email,
@ApiParam("password") String password)
{
final Optional<WebUser> user = service.login(email, password);
if (user.isPresent()) {
return getLoginResponseForUser(user.get());
}
throw new RakamException("Account couldn't found.", NOT_FOUND);
}
@JsonRequest
@Path("/login_with_google")
public Response<WebUser> loginWithGoogle(@ApiParam("access_token") String accessToken)
{
GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
Oauth2 oauth2 = new Oauth2.Builder(new NetHttpTransport(),
new JacksonFactory(), credential).setApplicationName("Oauth2").build();
Userinfoplus userinfo;
try {
userinfo = oauth2.userinfo().get().execute();
if (!userinfo.getVerifiedEmail()) {
throw new RakamException("The Google email must be verified", BAD_REQUEST);
}
Optional<WebUser> userByEmail = service.getUserByEmail(userinfo.getEmail());
WebUser user = userByEmail.orElseGet(() ->
service.createUser(userinfo.getEmail(),
null, userinfo.getGivenName(),
userinfo.getGender(),
userinfo.getLocale(),
userinfo.getId(), false));
return getLoginResponseForUser(user);
}
catch (IOException e) {
LOGGER.error(e);
throw new RakamException("Unable to login", INTERNAL_SERVER_ERROR);
}
}
@GET
@Path("/logout")
public Response<SuccessMessage> logout()
{
return Response.ok(SuccessMessage.success()).addCookie("session", "", null, true, -1L, "/", null);
}
private Response getLoginResponseForUser(WebUser user)
{
return Response.ok(user).addCookie("session", getCookieForUser(user.id),
null, true, Duration.ofDays(30).getSeconds(), "/", null);
}
public String getCookieForUser(int userId) {
final long expiringTimestamp = Instant.now().plus(7, ChronoUnit.DAYS).getEpochSecond();
final StringBuilder cookieData = new StringBuilder()
.append(expiringTimestamp).append("|")
.append(userId);
final String secureKey = CryptUtil.encryptWithHMacSHA1(cookieData.toString(), encryptionConfig.getSecretKey());
cookieData.append('|').append(secureKey);
return cookieData.toString();
}
public static int extractUserFromCookie(String session, String key)
{
if (session == null) {
throw new RakamException(UNAUTHORIZED);
}
final String[] split = session.split("\\|");
if (split.length != 3) {
throw new RakamException(UNAUTHORIZED);
}
final long expiringTimestamp = Long.parseLong(split[0]);
final int id = Integer.parseInt(split[1]);
final String hash = split[2];
final StringBuilder cookieData = new StringBuilder()
.append(expiringTimestamp).append("|")
.append(id);
if (!CryptUtil.encryptWithHMacSHA1(cookieData.toString(), key).equals(hash)) {
throw new RakamException(UNAUTHORIZED);
}
return id;
}
}