package org.rakam.ui; import com.google.inject.Provider; import com.google.inject.name.Named; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.Cookie; import org.rakam.analysis.CustomParameter; import org.rakam.analysis.JDBCPoolDataSource; import org.rakam.config.EncryptionConfig; import org.rakam.server.http.HttpServerBuilder; import org.rakam.server.http.IRequestParameter; import org.rakam.ui.user.WebUserHttpService; import org.rakam.util.RakamException; import org.skife.jdbi.v2.DBI; import org.skife.jdbi.v2.Handle; import org.skife.jdbi.v2.util.BooleanMapper; import javax.inject.Inject; import java.lang.reflect.Method; import java.util.Optional; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED; public class UIPermissionParameterProvider implements Provider<CustomParameter> { private final DBI dbi; private final EncryptionConfig encryptionConfig; private final AuthService authService; @Inject public UIPermissionParameterProvider(@Named("ui.metadata.jdbc") JDBCPoolDataSource dataSource, com.google.common.base.Optional<AuthService> authService, EncryptionConfig encryptionConfig) { this.dbi = new DBI(dataSource); this.encryptionConfig = encryptionConfig; this.authService = authService.orNull(); } @Override public CustomParameter get() { return new CustomParameter("user_id", new Factory(authService, encryptionConfig, dbi)); } public static class Factory implements HttpServerBuilder.IRequestParameterFactory { private final EncryptionConfig encryptionConfig; private final DBI dbi; private final AuthService authService; public Factory(AuthService authService, EncryptionConfig encryptionConfig, DBI dbi) { this.encryptionConfig = encryptionConfig; this.authService = authService; this.dbi = dbi; } @Override public IRequestParameter<Project> create(Method method) { boolean readOnly = !method.isAnnotationPresent(ProtectEndpoint.class) || !method.getAnnotation(ProtectEndpoint.class).writeOperation(); boolean requiresProject = !method.isAnnotationPresent(ProtectEndpoint.class) || method.getAnnotation(ProtectEndpoint.class).requiresProject(); return (objectNode, request) -> { String projectString = request.headers().get("project"); if (projectString == null && requiresProject) { throw new RakamException("Project header is null", BAD_REQUEST); } Optional<Cookie> session = request.cookies().stream().filter(e -> e.name().equals("session")).findAny(); int userId = WebUserHttpService.extractUserFromCookie(session.orElseThrow(() -> new RakamException(UNAUTHORIZED)).value(), encryptionConfig.getSecretKey()); if (authService != null) { authService.checkAccess(userId); } int project; if (projectString != null) { try { project = Integer.parseInt(projectString); } catch (NumberFormatException e) { throw new RakamException("Project header must be numeric", BAD_REQUEST); } if (readOnly) { try (Handle handle = dbi.open()) { boolean hasPermission = handle.createQuery("SELECT true FROM web_user_api_key key " + "JOIN web_user_project project ON (key.project_id = project.id) " + "WHERE key.user_id = :user AND project.id = :id " + " UNION ALL " + " SELECT true " + "FROM web_user_api_key_permission permission \n" + "JOIN web_user_api_key api_key ON (permission.api_key_id = api_key.id) \n" + "WHERE permission.user_id = :user AND api_key.project_id = :id AND (permission.read_permission or permission.master_permission)") .map(BooleanMapper.FIRST) .bind("user", userId) .bind("id", project) .first() != null; if (!hasPermission) { throw new RakamException(HttpResponseStatus.FORBIDDEN); } } } else { try (Handle handle = dbi.open()) { Boolean readOnlyUser = handle.createQuery("SELECT web_user.read_only FROM web_user_api_key key " + "JOIN web_user_project project ON (key.project_id = project.id) " + "JOIN web_user ON (web_user.id = key.user_id) " + "WHERE key.user_id = :user AND project.id = :id" + " UNION ALL " + " SELECT false " + "FROM web_user_api_key_permission permission \n" + "JOIN web_user_api_key api_key ON (permission.api_key_id = api_key.id) \n" + "WHERE permission.user_id = :user AND api_key.project_id = :id AND permission.master_permission") .map(BooleanMapper.FIRST) .bind("user", userId) .bind("id", project) .first(); if (readOnlyUser == null || readOnlyUser) { throw new RakamException("User is not allowed to perform this operation", UNAUTHORIZED); } } } } else { project = -1; } return new Project(project, userId); }; } } public static class Project { public final int project; public final int userId; public Project(int project, int userId) { this.project = project; this.userId = userId; } } }