package org.rakam.ui.user; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.eventbus.EventBus; import com.google.common.io.Resources; import com.google.inject.Inject; import com.google.inject.name.Named; import com.lambdaworks.crypto.SCryptUtil; import io.airlift.log.Logger; import io.netty.handler.codec.http.HttpResponseStatus; import org.rakam.analysis.ApiKeyService.ProjectApiKeys; import org.rakam.analysis.JDBCPoolDataSource; import org.rakam.config.EncryptionConfig; import org.rakam.report.EmailClientConfig; import org.rakam.server.http.annotations.ApiParam; import org.rakam.ui.AuthService; import org.rakam.ui.RakamUIConfig; import org.rakam.ui.UIEvents; import org.rakam.util.AlreadyExistsException; import org.rakam.util.CryptUtil; import org.rakam.util.MailSender; import org.rakam.util.RakamException; import org.skife.jdbi.v2.DBI; import org.skife.jdbi.v2.Handle; import org.skife.jdbi.v2.ResultIterator; import org.skife.jdbi.v2.TransactionStatus; import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException; import org.skife.jdbi.v2.tweak.ResultSetMapper; import org.skife.jdbi.v2.util.BooleanMapper; import org.skife.jdbi.v2.util.IntegerMapper; import org.skife.jdbi.v2.util.StringMapper; import javax.mail.MessagingException; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.time.Instant; import java.time.ZoneId; import java.time.zone.ZoneRulesException; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import static com.google.common.base.Charsets.UTF_8; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.EXPECTATION_FAILED; import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_IMPLEMENTED; import static io.netty.handler.codec.http.HttpResponseStatus.PRECONDITION_REQUIRED; import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED; import static java.lang.String.format; import static java.time.temporal.ChronoUnit.HOURS; public class WebUserService { private final static Logger LOGGER = Logger.get(WebUserService.class); private final DBI dbi; private static final Pattern EMAIL_PATTERN = Pattern.compile("^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"); private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}$"); private final RakamUIConfig config; private final EncryptionConfig encryptionConfig; private final EventBus eventBus; private final EmailClientConfig mailConfig; private static final Mustache resetPasswordHtmlCompiler; private static final Mustache resetPasswordTxtCompiler; private static final Mustache welcomeHtmlCompiler; private static final Mustache welcomeTxtCompiler; private static final Mustache resetPasswordTitleCompiler; private static final Mustache welcomeTitleCompiler; private static final Mustache userAccessNewMemberTitleCompiler; private static final Mustache userAccessNewMemberTxtCompiler; private static final Mustache userAccessNewMemberHtmlCompiler; private static final Mustache userAccessHtmlCompiler; private static final Mustache userAccessTxtCompiler; private static final Mustache userAccessTitleCompiler; private final AuthService authService; @Inject public WebUserService( @Named("ui.metadata.jdbc") JDBCPoolDataSource dataSource, EventBus eventBus, com.google.common.base.Optional<AuthService> authService, RakamUIConfig config, EncryptionConfig encryptionConfig, EmailClientConfig mailConfig) { dbi = new DBI(dataSource); this.eventBus = eventBus; this.authService = authService.orNull(); this.config = config; this.encryptionConfig = encryptionConfig; this.mailConfig = mailConfig; } public ProjectConfiguration getProjectConfigurations(int project) { try (Connection conn = dbi.open().getConnection()) { PreparedStatement ps = conn.prepareStatement("SELECT project, timezone FROM web_user_project WHERE id = ?"); ps.setInt(1, project); ResultSet resultSet = ps.executeQuery(); if (!resultSet.next()) { throw new RakamException("API key is invalid", HttpResponseStatus.FORBIDDEN); } return new ProjectConfiguration(resultSet.getString(1), resultSet.getString(2)); } catch (SQLException e) { throw Throwables.propagate(e); } } public void updateProjectConfigurations(int userId, int project, ProjectConfiguration configuration) { try (Connection conn = dbi.open().getConnection()) { if (configuration.timezone != null) { try { ZoneId.of(configuration.timezone); } catch (Exception e) { throw new RakamException("Timezone is invalid", BAD_REQUEST); } } PreparedStatement ps = conn.prepareStatement("UPDATE web_user_project SET timezone = ? WHERE user_id = ? and id = ?"); ps.setString(1, configuration.timezone); ps.setInt(2, userId); ps.setInt(3, project); ps.executeUpdate(); } catch (SQLException e) { throw Throwables.propagate(e); } } public void setStripeId(int userId, String stripeId) { try (Handle handle = dbi.open()) { int execute = handle .createStatement("UPDATE web_user SET stripe_id = :stripeId WHERE id = :userId") .bind("stripeId", stripeId) .bind("userId", userId).execute(); if (execute != 1) { throw new IllegalStateException(); } } } public static class ProjectConfiguration { public final String name; public final String timezone; @JsonCreator public ProjectConfiguration(@ApiParam(value = "timezone", required = false) String timezone) { this(null, timezone); } public ProjectConfiguration(String name, String timezone) { this.timezone = timezone; this.name = name; } } public WebUser createUser(String email, String password, String name, String gender, String locale, String googleId, boolean external) { final String scrypt; if (password != null && !external) { if (!PASSWORD_PATTERN.matcher(password).matches()) { throw new RakamException("Password is not valid. Your password must contain at least one lowercase character, uppercase character and digit and be at least 8 characters. ", BAD_REQUEST); } if (config.getHashPassword()) { password = CryptUtil.encryptWithHMacSHA1(password, encryptionConfig.getSecretKey()); } scrypt = SCryptUtil.scrypt(password, 2 << 14, 8, 1); } else if (external) { scrypt = password; } else { if (googleId == null) { throw new RakamException("Password id empty", BAD_REQUEST); } scrypt = null; } if (!external && !EMAIL_PATTERN.matcher(email).matches()) { throw new RakamException("Email is not valid", BAD_REQUEST); } WebUser webuser = null; try ( Handle handle = dbi.open() ) { try { int id = handle.createStatement("INSERT INTO web_user (email, password, name, created_at, gender, user_locale, google_id, external) VALUES (:email, :password, :name, now(), :gender, :locale, :googleId, :external)") .bind("email", email) .bind("name", name) .bind("gender", gender) .bind("locale", locale) .bind("external", external) .bind("googleId", googleId) .bind("password", scrypt).executeAndReturnGeneratedKeys(IntegerMapper.FIRST).first(); webuser = new WebUser(id, email, name, false, ImmutableList.of()); } catch (UnableToExecuteStatementException e) { Map<String, Object> existingUser = handle.createQuery("SELECT created_at FROM web_user WHERE email = :email").bind("email", email).first(); if (existingUser != null) { if (existingUser.get("created_at") != null) { throw new AlreadyExistsException("A user with same email address", EXPECTATION_FAILED); } else { // somebody gave access for a project to this email address int id = handle.createStatement("UPDATE web_user SET password = :password, name = :name, created_at = now() WHERE email = :email") .bind("email", email) .bind("name", name) .bind("password", scrypt).executeAndReturnGeneratedKeys(IntegerMapper.FIRST).first(); if (id > 0) { webuser = new WebUser(id, email, name, false, ImmutableList.of()); } } } if (webuser == null) { throw e; } } } try { sendMail(welcomeTitleCompiler, welcomeTxtCompiler, welcomeHtmlCompiler, email, ImmutableMap.of( "name", Optional.ofNullable(name).orElse("there"), "product_name", "Rakam", "siteUrl", mailConfig.getSiteUrl().toExternalForm())); } catch ( RakamException e ) { if (e.getStatusCode() != NOT_IMPLEMENTED) { throw e; } } return webuser; } public void updateUserInfo(int id, String name) { try (Handle handle = dbi.open()) { handle.createStatement("UPDATE web_user SET name = :name WHERE id = :id") .bind("id", id) .bind("name", name) .executeAndReturnGeneratedKeys(IntegerMapper.FIRST).first(); } } public void updateUserPassword(int id, String oldPassword, String newPassword) { final String scrypt = SCryptUtil.scrypt(newPassword, 2 << 14, 8, 1); if (!PASSWORD_PATTERN.matcher(newPassword).matches()) { throw new RakamException("Password is not valid. Your password must contain at least one lowercase character, uppercase character and digit and be at least 8 characters. ", BAD_REQUEST); } if (config.getHashPassword()) { oldPassword = CryptUtil.encryptWithHMacSHA1(oldPassword, encryptionConfig.getSecretKey()); } try (Handle handle = dbi.open()) { String hashedPass = handle.createQuery("SELECT password FROM web_user WHERE id = :id") .bind("id", id).map(StringMapper.FIRST).first(); if (hashedPass == null) { throw new RakamException("User does not exist", BAD_REQUEST); } if (!SCryptUtil.check(oldPassword, hashedPass)) { throw new RakamException("Password is wrong", BAD_REQUEST); } handle.createStatement("UPDATE web_user SET password = :password WHERE id = :id") .bind("id", id) .bind("password", scrypt).execute(); } } public String getLockKeyForAPI(int user, String apiUrl) { try (Handle handle = dbi.open()) { return handle.createQuery("SELECT lock_key FROM rakam_cluster WHERE user_id = :userId AND api_url = :apiUrl") .bind("userId", user).bind("apiUrl", apiUrl) .map(StringMapper.FIRST).first(); } } public List<String> revokeUserAccess(int userId, int project, String email) { try (Handle handle = dbi.open()) { if (!hasMasterAccess(handle, project, userId)) { throw new RakamException("You do not have master key permission", UNAUTHORIZED); } List<Map<String, Object>> list = handle.createQuery("SELECT api_key.id, api_key.master_key FROM web_user_api_key_permission permission " + "JOIN web_user_api_key api_key ON (api_key.id = permission.api_key_id) " + "WHERE project_id = :project AND permission.user_id = " + "(SELECT id FROM web_user WHERE email = :email)") .bind("project", project) .bind("email", email).list(); if (list.isEmpty()) { throw new RakamException(NOT_FOUND); } handle.createStatement("DELETE FROM web_user_api_key_permission WHERE api_key_id in (" + list.stream().map(a -> a.get("id").toString()).collect(Collectors.joining(", ")) + ")") .execute(); return list.stream().map(e -> e.get("master_key").toString()).collect(Collectors.toList()); } } public void performRecoverPassword(String key, String hash, String newPassword) { if (!PASSWORD_PATTERN.matcher(newPassword).matches()) { throw new RakamException("Password is not valid. " + "Your password must contain at least one lowercase character, uppercase character and digit and be at least 8 characters. ", BAD_REQUEST); } String realKey; try { realKey = new String(Base64.getDecoder().decode(key.getBytes(UTF_8)), UTF_8); } catch (IllegalArgumentException e) { throw new RakamException("Invalid token", UNAUTHORIZED); } if (!CryptUtil.encryptWithHMacSHA1(realKey, encryptionConfig.getSecretKey()).equals(hash)) { throw new RakamException("Invalid token", UNAUTHORIZED); } String[] split = realKey.split("\\|", 2); if (split.length != 2) { throw new RakamException(BAD_REQUEST); } try { if (Instant.ofEpochSecond(Long.parseLong(split[0])).compareTo(Instant.now()) < 0) { throw new RakamException("Token expired", UNAUTHORIZED); } } catch (NumberFormatException e) { throw new RakamException("Invalid token", UNAUTHORIZED); } final String scrypt = SCryptUtil.scrypt(newPassword, 2 << 14, 8, 1); try (Handle handle = dbi.open()) { int execute = handle.createStatement("UPDATE web_user SET password = :password WHERE email = :email") .bind("email", split[1]) .bind("password", scrypt).execute(); if (execute == 0) { throw new IllegalStateException(); } } } public void prepareRecoverPassword(String email) { if (!EMAIL_PATTERN.matcher(email).matches()) { throw new RakamException("Email is not valid", BAD_REQUEST); } if (!getUserByEmail(email).isPresent()) { throw new RakamException("Email is not found", BAD_REQUEST); } Map<String, Object> scopes = ImmutableMap.of( "product_name", "Rakam", "action_url", format("%s/perform-recover-password?%s", mailConfig.getSiteUrl(), getRecoverUrl(email, 3))); sendMail(resetPasswordTitleCompiler, resetPasswordTxtCompiler, resetPasswordHtmlCompiler, email, scopes).join(); } private String getRecoverUrl(String email, int hours) { long expiration = Instant.now().plus(hours, HOURS).getEpochSecond(); String key = expiration + "|" + email; String hash = CryptUtil.encryptWithHMacSHA1(key, encryptionConfig.getSecretKey()); String encoded = new String(Base64.getEncoder().encode(key.getBytes(UTF_8)), UTF_8); try { return format("key=%s&hash=%s", URLEncoder.encode(encoded, "UTF-8"), URLEncoder.encode(hash, "UTF-8")); } catch (UnsupportedEncodingException e) { throw Throwables.propagate(e); } } private CompletableFuture sendMail(Mustache titleCompiler, Mustache contentCompiler, Mustache htmlCompiler, String email, Map<String, Object> data) { StringWriter writer; writer = new StringWriter(); contentCompiler.execute(writer, data); String txtContent = writer.toString(); writer = new StringWriter(); htmlCompiler.execute(writer, data); String htmlContent = writer.toString(); writer = new StringWriter(); titleCompiler.execute(writer, data); String title = writer.toString(); MailSender mailSender = mailConfig.getMailSender(); return CompletableFuture.runAsync(() -> { try { mailSender.sendMail(email, title, txtContent, Optional.of(htmlContent), Stream.empty()); } catch (MessagingException e) { LOGGER.error(e, "Unable to send mail"); } }); } public void deleteProject(int user, int projectId) { try (Handle handle = dbi.open()) { handle.createStatement("DELETE FROM web_user_project WHERE id = :project and user_id = :userId") .bind("userId", user) .bind("project", projectId) .execute(); } } public WebUser.UserApiKey registerProject(int user, String apiUrl, String project, String readKey, String writeKey, String masterKey) { int projectId; try (Handle handle = dbi.open()) { try { projectId = (Integer) handle.createStatement("INSERT INTO web_user_project " + "(project, api_url, user_id) " + "VALUES (:project, :apiUrl, :userId)") .bind("userId", user) .bind("project", project) .bind("apiUrl", apiUrl) .executeAndReturnGeneratedKeys().first().get("id"); } catch (Exception e) { if (e.getMessage().contains("project_check")) { throw new RakamException("Project already exists.", BAD_REQUEST); } throw e; } handle.createStatement("INSERT INTO web_user_api_key " + "(user_id, project_id, read_key, write_key, master_key) " + "VALUES (:userId, :project, :readKey, :writeKey, :masterKey)") .bind("userId", user) .bind("project", projectId) .bind("readKey", readKey) .bind("writeKey", writeKey) .bind("masterKey", masterKey) .execute(); } eventBus.post(new UIEvents.ProjectCreatedEvent(projectId)); return new WebUser.UserApiKey(projectId, readKey, writeKey, masterKey); } public static class UserAccess { @JsonProperty("project") public final int project; @JsonProperty("id") public final int id; @JsonProperty("email") public final String email; @JsonProperty("scope_expression") public final String scope_expression; @JsonProperty("read_key") public final boolean readKey; @JsonProperty("write_key") public final boolean writeKey; @JsonProperty("master_key") public final boolean masterKey; public UserAccess(int project, int id, String email, String scope_expression, boolean readKey, boolean writeKey, boolean masterKey) { this.project = project; this.id = id; this.email = email; this.scope_expression = scope_expression; this.readKey = readKey; this.writeKey = writeKey; this.masterKey = masterKey; } } public List<UserAccess> getUserAccessForProject(int user, int project) { try (Handle handle = dbi.open()) { if (!hasMasterAccess(handle, project, user)) { throw new RakamException("You do not have master key permission", UNAUTHORIZED); } return handle.createQuery("SELECT web_user.email, keys.scope_expression, " + "read_permission, write_permission, master_permission, web_user.id " + "FROM web_user_api_key_permission keys " + "JOIN web_user_api_key ON (web_user_api_key.id = keys.api_key_id) " + "JOIN web_user ON (web_user.id = keys.user_id) " + "WHERE web_user.id != :user AND web_user_api_key.project_id = :project " + "ORDER BY keys.created_at") .bind("user", user) .bind("project", project).map((i, resultSet, statementContext) -> { return new UserAccess(project, resultSet.getInt(6), resultSet.getString(1), resultSet.getString(2), resultSet.getBoolean(3), resultSet.getBoolean(4), resultSet.getBoolean(5)); }).list(); } } public void giveAccessToExistingUser(int projectId, int userId, String email, boolean readPermission, boolean writePermission, boolean masterPermisson) { try (Handle handle = dbi.open()) { if (!hasMasterAccess(handle, projectId, userId)) { throw new RakamException("You do not have master key permission", UNAUTHORIZED); } Integer newUserId = handle.createQuery("SELECT id FROM web_user WHERE email = :email").bind("email", email) .map(IntegerMapper.FIRST).first(); if (newUserId == null) { throw new RakamException(NOT_FOUND); } int exists = handle.createStatement("UPDATE web_user_api_key_permission SET " + "read_permission = :readPermission, write_permission = :writePermission, master_permission = :masterPermission " + "WHERE user_id = :newUserId") .bind("mainUser", userId) .bind("newUserId", newUserId) .bind("readPermission", readPermission) .bind("writePermission", writePermission) .bind("masterPermission", masterPermisson) .bind("project", projectId).execute(); if (exists == 0) { throw new RakamException(NOT_FOUND); } } } public void sendNewUserMail(String project, String email) { sendMail(userAccessNewMemberTitleCompiler, userAccessNewMemberTxtCompiler, userAccessNewMemberHtmlCompiler, email, ImmutableMap.of( "product_name", "Rakam", "project", project, "action_url", format("%s/perform-recover-password?%s", mailConfig.getSiteUrl(), getRecoverUrl(email, 24)))); } public static final class Access { public final List<TableAccess> tableAccessList; @JsonCreator public Access(@ApiParam("tableAccessList") List<TableAccess> tableAccessList) { this.tableAccessList = tableAccessList; } public static class TableAccess { public final String tableName; public final String expression; @JsonCreator public TableAccess(@ApiParam("tableName") String tableName, @ApiParam("expression") String expression) { this.tableName = tableName; this.expression = expression; } } } public void giveAccessToUser(int projectId, int userId, String email, ProjectApiKeys keys, String scope_expression, boolean readPermission, boolean writePermission, boolean masterPermission, Optional<Access> access) { if (masterPermission && access.isPresent()) { throw new RakamException("Scoped keys cannot have access to master_key", BAD_REQUEST); } ProjectConfiguration projectConfigurations = getProjectConfigurations(projectId); Integer newUserId; try (Handle handle = dbi.open()) { if (!hasMasterAccess(handle, projectId, userId)) { throw new RakamException("You do not have master key permission", UNAUTHORIZED); } try { newUserId = handle.createStatement("INSERT INTO web_user (email, created_at) VALUES (:email, now())") .bind("email", email).executeAndReturnGeneratedKeys(IntegerMapper.FIRST).first(); sendNewUserMail(projectConfigurations.name, email); } catch (Exception e) { Map.Entry<Integer, Boolean> element = handle.createQuery("SELECT id, password is null FROM web_user WHERE email = :email").bind("email", email) .map((ResultSetMapper<Map.Entry<Integer, Boolean>>) (index, r, ctx) -> new AbstractMap.SimpleImmutableEntry<>(r.getInt(1), r.getBoolean(2))).first(); newUserId = element.getKey(); boolean passwordIsNull = element.getValue(); if (passwordIsNull) { sendNewUserMail(projectConfigurations.name, email); } else { sendMail(userAccessTitleCompiler, userAccessTxtCompiler, userAccessHtmlCompiler, email, ImmutableMap.of( "product_name", "Rakam", "project", projectConfigurations.name, "action_url", mailConfig.getSiteUrl())); } } } if (newUserId == null) { throw new IllegalStateException("User id cannot be found."); } final int finalNewUserId = newUserId; dbi.inTransaction((Handle handle, TransactionStatus transactionStatus) -> { Integer apiKeyId = saveApiKeys(handle, userId, projectId, keys.readKey(), keys.writeKey(), keys.masterKey()); int exists = handle.createStatement("UPDATE web_user_api_key_permission SET " + "read_permission = :readPermission, write_permission = :writePermission, master_permission = :masterPermission " + "WHERE user_id = :newUserId AND (SELECT bool_or(true) FROM web_user_api_key WHERE user_id = :newUserId AND project_id = :project)") .bind("mainUser", userId) .bind("newUserId", finalNewUserId) .bind("readPermission", readPermission) .bind("writePermission", writePermission) .bind("masterPermission", masterPermission) .bind("project", projectId).execute(); if (exists == 0) { handle.createStatement("INSERT INTO web_user_api_key_permission (api_key_id, user_id, read_permission, write_permission, master_permission, scope_expression) " + " VALUES (:apiKeyId, :newUserId, :readPermission, :writePermission, :masterPermission, :scope)") .bind("apiKeyId", apiKeyId) .bind("newUserId", finalNewUserId) .bind("readPermission", readPermission) .bind("writePermission", writePermission) .bind("masterPermission", masterPermission) .bind("scope", scope_expression).execute(); } return null; }); } public Integer saveApiKeys(int user, int projectId, String readKey, String writeKey, String masterKey) { try (Handle handle = dbi.open()) { return saveApiKeys(handle, user, projectId, readKey, writeKey, masterKey); } } public Integer saveApiKeys(Handle handle, int user, int projectId, String readKey, String writeKey, String masterKey) { if (!hasMasterAccess(handle, projectId, user)) { throw new RakamException("You do not have master key permission", UNAUTHORIZED); } return handle.createStatement("INSERT INTO web_user_api_key " + "(user_id, project_id, read_key, write_key, master_key) " + "VALUES (:userId, :project, :readKey, :writeKey, :masterKey)") .bind("userId", user) .bind("project", projectId) .bind("readKey", readKey) .bind("writeKey", writeKey) .bind("masterKey", masterKey) .executeAndReturnGeneratedKeys((index, r, ctx) -> r.getInt("id")).first(); } public boolean hasMasterAccess(Handle handle, int project, int user) { return handle.createQuery("select user_id = :user or (select bool_or(master_permission) from web_user_api_key_permission p join web_user_api_key a on (p.api_key_id = a.id) where p.user_id = :user and a.project_id = :project) from web_user_project where id = :project") .bind("user", user) .bind("project", project).map(BooleanMapper.FIRST) .first() == true; } public Optional<WebUser> login(String email, String password) { if (config.getHashPassword()) { password = CryptUtil.encryptWithHMacSHA1(password, encryptionConfig.getSecretKey()); } List<WebUser.Project> projects; final Map<String, Object> data; String passwordInDb; try (Handle handle = dbi.open()) { data = handle .createQuery("SELECT id, name, password, read_only FROM web_user WHERE email = :email") .bind("email", email).first(); if (authService != null) { boolean login = authService.login(email, password); if (login && data == null) { WebUser user = createUser(email, password, null, null, null, null, true); return Optional.of(user); } else if (!login) { return Optional.empty(); } } if (data == null) { return Optional.empty(); } } passwordInDb = (String) data.get("password"); if (config.getAuthentication() != null) { if (!Objects.equals(password, passwordInDb)) { try (Handle handle = dbi.open()) { int updated = handle .createStatement("UPDATE web_user SET password = :password WHERE email = :email") .bind("password", password).execute(); if (updated == 0) { throw new IllegalStateException(); } } } } else { if (passwordInDb == null) { throw new RakamException("Your password is not set. Please reset your password in order to set it.", PRECONDITION_REQUIRED); } if (!SCryptUtil.check(password, passwordInDb)) { return Optional.empty(); } } try (Handle handle = dbi.open()) { String name = (String) data.get("name"); int id = (int) data.get("id"); boolean readOnly = (boolean) data.get("read_only"); projects = getUserApiKeys(handle, id); return Optional.of(new WebUser(id, email, name, readOnly, projects)); } } public Optional<WebUser> getUserByEmail(String email) { List<WebUser.Project> projectDefinitions; try (Handle handle = dbi.open()) { final Map<String, Object> data = handle .createQuery("SELECT id, name, read_only FROM web_user WHERE email = :email") .bind("email", email).first(); if (data == null) { return Optional.empty(); } String name = (String) data.get("name"); int id = (int) data.get("id"); boolean readOnly = (boolean) data.get("read_only"); projectDefinitions = getUserApiKeys(handle, id); return Optional.of(new WebUser(id, email, name, readOnly, projectDefinitions)); } } public Optional<WebUser> getUser(int id) { List<WebUser.Project> projectDefinitions; try (Handle handle = dbi.open()) { final Map<String, Object> data = handle .createQuery("SELECT id, name, email, read_only FROM web_user WHERE id = :id") .bind("id", id).first(); if (data == null) { return Optional.empty(); } String name = (String) data.get("name"); String email = (String) data.get("email"); id = (int) data.get("id"); projectDefinitions = getUserApiKeys(handle, id); return Optional.of(new WebUser(id, email, name, (Boolean) data.get("read_only"), projectDefinitions)); } } public String getUserStripeId(int id) { try (Handle handle = dbi.open()) { return handle .createQuery("SELECT stripe_id FROM web_user WHERE id = :id") .bind("id", id).map(StringMapper.FIRST).first(); } } private List<WebUser.Project> getUserApiKeys(Handle handle, int userId) { List<WebUser.Project> list = new ArrayList<>(); ResultIterator<Object> user = handle.createQuery("SELECT project.id, project.project, project.api_url, project.timezone, api_key.master_key, api_key.read_key, api_key.write_key " + " FROM web_user_project project " + " JOIN web_user_api_key api_key ON (api_key.project_id = project.id)" + " WHERE api_key.user_id = :user " + " UNION ALL SELECT api_key.project_id, project.project, project.api_url, project.timezone, " + "case when permission.master_permission then api_key.master_key else null end," + "case when permission.master_permission or permission.read_permission then api_key.read_key else null end," + "case when permission.master_permission or permission.write_permission then api_key.write_key else null end " + "FROM web_user_api_key_permission permission \n" + "JOIN web_user_api_key api_key ON (permission.api_key_id = api_key.id) \n" + "JOIN web_user_project project ON (project.id = api_key.project_id)\n" + "WHERE permission.user_id = :user" + " ORDER BY id NULLS LAST") .bind("user", userId) .map((index, r, ctx) -> { int id = r.getInt(1); String name = r.getString(2); String url = r.getString(3); ZoneId zoneId; try { zoneId = r.getString(4) != null ? ZoneId.of(r.getString(4)) : null; } catch (ZoneRulesException e) { zoneId = null; } ZoneId finalZoneId = zoneId; WebUser.Project p = list.stream().filter(e -> e.id == id) .findFirst() .orElseGet(() -> { WebUser.Project project = new WebUser.Project(id, name, url, finalZoneId, new ArrayList<>()); list.add(project); return project; }); p.apiKeys.add(ProjectApiKeys.create(r.getString(5), r.getString(6), r.getString(7))); return null; }).iterator(); while (user.hasNext()) { user.next(); } return list; } public void revokeApiKeys(int user, int project, String masterKey) { try (Handle handle = dbi.open()) { if (!hasMasterAccess(handle, project, user)) { throw new RakamException("You do not have master key permission", UNAUTHORIZED); } try { handle.createStatement("DELETE FROM web_user_api_key " + "WHERE user_id = :user_id AND project_id = :project AND master_key = :masterKey") .bind("user_id", user) .bind("project", project) .bind("masterKey", masterKey).execute(); } catch (Throwable e) { if (e.getMessage().contains("web_user_api_key_permission")) { List<String> list = handle.createQuery("SELECT web_user.email FROM web_user_api_key_permission permission " + "JOIN web_user ON (web_user.id = permission.user_id) " + "WHERE api_key_id in (SELECT id FROM web_user_api_key WHERE master_key = :masterKey and user_id = :userId and project_id = :project)") .bind("masterKey", masterKey).bind("userId", user).bind("project", project).map(StringMapper.FIRST).list(); if (!list.isEmpty()) { throw new RakamException("Users [" + list.stream().collect(Collectors.joining(", ")) + "] use this key." + " You need to revoke the access of the user in order to be able to delete this key", BAD_REQUEST); } } throw e; } } } static { try { MustacheFactory mf = new DefaultMustacheFactory(); resetPasswordHtmlCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/resetpassword/resetpassword.html"), UTF_8)), "resetpassword.html"); resetPasswordTxtCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/resetpassword/resetpassword.txt"), UTF_8)), "resetpassword.txt"); resetPasswordTitleCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/resetpassword/title.txt"), UTF_8)), "resetpassword_title.txt"); welcomeHtmlCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/welcome/welcome.html"), UTF_8)), "welcome.html"); welcomeTxtCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/welcome/welcome.txt"), UTF_8)), "welcome.txt"); welcomeTitleCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/welcome/title.txt"), UTF_8)), "welcome_title.txt"); userAccessNewMemberHtmlCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/teamaccess_newmember/teamaccess.html"), UTF_8)), "welcome.html"); userAccessNewMemberTxtCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/teamaccess_newmember/teamaccess.txt"), UTF_8)), "welcome.txt"); userAccessNewMemberTitleCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/teamaccess_newmember/title.txt"), UTF_8)), "welcome_title.txt"); userAccessHtmlCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/teamaccess/teamaccess.html"), UTF_8)), "welcome.html"); userAccessTxtCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/teamaccess/teamaccess.txt"), UTF_8)), "welcome.txt"); userAccessTitleCompiler = mf.compile(new StringReader(Resources.toString( WebUserService.class.getResource("/mail/teamaccess/title.txt"), UTF_8)), "welcome_title.txt"); } catch (IOException e) { throw Throwables.propagate(e); } } public static final class Project { public final String project; public final String apiUrl; public Project(String project, String apiUrl) { this.project = project; this.apiUrl = apiUrl; } } }