diff --git a/src/main/java/biz/nynja/account/StartupScriptsListener.java b/src/main/java/biz/nynja/account/StartupScriptsListener.java index ca66b8792a2d8424d27993222867d2dd03e11b77..88f09f532aacec51828be670b3456cabd5e372c0 100644 --- a/src/main/java/biz/nynja/account/StartupScriptsListener.java +++ b/src/main/java/biz/nynja/account/StartupScriptsListener.java @@ -32,7 +32,6 @@ public class StartupScriptsListener { private String keyspace; @Autowired - @Qualifier("accountSession") private Session session; @Autowired diff --git a/src/main/java/biz/nynja/account/accesspoints/AccessPointRepository.java b/src/main/java/biz/nynja/account/accesspoints/AccessPointRepository.java index 6888c6df5cea70596cc543c8bdda533c5ef1b7bc..9f2c3c045b616f4565b5d11840818da60e56dfc7 100644 --- a/src/main/java/biz/nynja/account/accesspoints/AccessPointRepository.java +++ b/src/main/java/biz/nynja/account/accesspoints/AccessPointRepository.java @@ -3,15 +3,23 @@ */ package biz.nynja.account.accesspoints; +import java.util.List; import java.util.UUID; - import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import biz.nynja.account.accesspoints.AccessPoint; @Repository public interface AccessPointRepository extends CassandraRepository { - - AccessPoint findByAccountIdAndAccessToken(UUID accountId, String accessToken); + + @Query("select * from auth.accesspoint where accountid=:accountId and accesstoken=:accessToken;") + AccessPoint findByAccountIdAndAccessToken(@Param("accountId") UUID accountId, + @Param("accessToken") String accessToken); + + @Query("select * from auth.accesspoint where accountid=:accountId;") + List findAllByAccountId(@Param("accountId") UUID accountId); } diff --git a/src/main/java/biz/nynja/account/accesspoints/AccessPointService.java b/src/main/java/biz/nynja/account/accesspoints/AccessPointService.java index ca24bd22e1bb812ec898871c76dd9cdd1d7793ae..2ac4a44cbe2be24a62b27c8c48e80c3bfb3ae4e8 100644 --- a/src/main/java/biz/nynja/account/accesspoints/AccessPointService.java +++ b/src/main/java/biz/nynja/account/accesspoints/AccessPointService.java @@ -21,13 +21,18 @@ public class AccessPointService { } public Optional getAccessPoint(UUID accountId, String accessToken) { - AccessPoint accessPoint = accessPointRepository.findByAccountIdAndAccessToken(accountId, accessToken); - if (accessPoint != null) { - return Optional.of(accessPoint); + try { + AccessPoint accessPoint = accessPointRepository.findByAccountIdAndAccessToken(accountId, accessToken); + if (accessPoint != null) { + return Optional.of(accessPoint); + } + } catch (Exception e) { + logger.error("Error reading accesspoint record for accoint ID {}. Error message: {}", accountId, + e.getMessage()); } return Optional.empty(); } - + public AccessPoint buildAccessPoint(String deviceId, String accessToken, String accountId, long expiresIn) { AccessPoint accessPoint = new AccessPoint(); accessPoint.setDeviceId(deviceId); diff --git a/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java b/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java index 20be301ffa8a81bd42aaea6160e46f4d00a1885e..91b9b32575dc5804591380f851ee8366510e02e4 100644 --- a/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java +++ b/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java @@ -18,20 +18,20 @@ import com.datastax.driver.core.UDTValue; import com.datastax.driver.core.UserType; import biz.nynja.account.codecs.AuthenticationProviderCodec; -import biz.nynja.account.configuration.CassandraAccountConfig; +import biz.nynja.account.configuration.CassandraConfig; import biz.nynja.account.models.AuthenticationProvider; @Configuration public class PreparedStatementsCache { - private final CassandraAccountConfig cassandraConfig; + private final CassandraConfig cassandraConfig; private final Session session; private static Map statementsCache; @Autowired - public PreparedStatementsCache(Session session, CassandraAccountConfig cassandraConfig) { + public PreparedStatementsCache(Session session, CassandraConfig cassandraConfig) { this.session = session; this.cassandraConfig = cassandraConfig; } diff --git a/src/main/java/biz/nynja/account/configuration/CassandraAccessPointConfig.java b/src/main/java/biz/nynja/account/configuration/CassandraAccessPointConfig.java deleted file mode 100644 index 1c67b4d8dcadc45e77c751658ae04a84f0fb6696..0000000000000000000000000000000000000000 --- a/src/main/java/biz/nynja/account/configuration/CassandraAccessPointConfig.java +++ /dev/null @@ -1,26 +0,0 @@ -//package biz.nynja.account.configuration; -// -//import org.springframework.beans.factory.annotation.Value; -//import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; -// -//@Configuration -//@EnableCassandraRepositories(basePackages = { "biz.nynja.account.accesspoints" }) -//@ConditionalOnMissingClass("org.springframework.test.context.junit4.SpringRunner") -//public class CassandraAccessPointConfig extends CassandraBaseConfig { -// -// @Value("${spring.data.cassandra.accesspoint.keyspace-name}") -// private String keyspaceName; -// -// @Override -// protected String getKeyspaceName() { -// return keyspaceName; -// } -// -// @Override -// public String[] getEntityBasePackages() { -// return new String[] { "biz.nynja.account.accesspoints" }; -// } -// -//} diff --git a/src/main/java/biz/nynja/account/configuration/CassandraAccountConfig.java b/src/main/java/biz/nynja/account/configuration/CassandraAccountConfig.java deleted file mode 100644 index 547b5353b18030c44129fdbed96f2594da05fdd6..0000000000000000000000000000000000000000 --- a/src/main/java/biz/nynja/account/configuration/CassandraAccountConfig.java +++ /dev/null @@ -1,90 +0,0 @@ -package biz.nynja.account.configuration; - - -import java.util.Arrays; -import java.util.List; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; -import org.springframework.data.cassandra.config.SchemaAction; -import org.springframework.data.cassandra.core.CassandraAdminOperations; -import org.springframework.data.cassandra.core.CassandraAdminTemplate; -import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecification; -import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; - -import biz.nynja.account.StartupScriptsListener; - -@Configuration -@EnableCassandraRepositories( - basePackages = { "biz.nynja.account.models", "biz.nynja.account.repositories" }, - cassandraTemplateRef = "CassandraAccountTemplate") -@ConditionalOnMissingClass("org.springframework.test.context.junit4.SpringRunner") -public class CassandraAccountConfig extends CassandraBaseConfig { - - @Value("${spring.data.cassandra.account.keyspace-name}") - protected String keyspaceName; - - @Override - protected String getKeyspaceName() { - return keyspaceName; - } - - @Override - public String[] getEntityBasePackages() { - return new String[] { "biz.nynja.account.models", "biz.nynja.account.repositories" }; - } - - @Override - public SchemaAction getSchemaAction() { - return SchemaAction.CREATE_IF_NOT_EXISTS; - } - - /** - * See {@link StartupScriptsListener} for scripts - * that require JPA annotated tables - */ - @Override - protected List getStartupScripts() { - return super.getStartupScripts(); - } - - public String getConfiguredKeyspaceName() { - return getKeyspaceName(); - } - - @Override - protected List getKeyspaceCreations() { - CreateKeyspaceSpecification specification = CreateKeyspaceSpecification.createKeyspace(getKeyspaceName()) - .ifNotExists().withSimpleReplication(); - return Arrays.asList(specification); - } - - @Override -// @Primary - @Bean(name = "CassandraAccountTemplate") - public CassandraAdminOperations cassandraTemplate( - @Qualifier("accountSession") final CassandraSessionFactoryBean session) throws Exception { - return new CassandraAdminTemplate(session().getObject(), cassandraConverter()); - } - - @Override - @Bean(name = "accountSession") - public CassandraSessionFactoryBean session() { - - CassandraSessionFactoryBean session = new CassandraSessionFactoryBean(); - - session.setCluster(cluster().getObject()); - session.setConverter(cassandraConverter()); - session.setKeyspaceName(getKeyspaceName()); - session.setSchemaAction(getSchemaAction()); - session.setStartupScripts(getStartupScripts()); - session.setShutdownScripts(getShutdownScripts()); - - return session; - } -} diff --git a/src/main/java/biz/nynja/account/configuration/CassandraBaseConfig.java b/src/main/java/biz/nynja/account/configuration/CassandraBaseConfig.java deleted file mode 100644 index 98dcffb57bdd02f56192736d5975772f2d78251a..0000000000000000000000000000000000000000 --- a/src/main/java/biz/nynja/account/configuration/CassandraBaseConfig.java +++ /dev/null @@ -1,51 +0,0 @@ -package biz.nynja.account.configuration; - -import java.util.Arrays; -import java.util.List; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.cassandra.config.AbstractCassandraConfiguration; -import org.springframework.data.cassandra.config.CassandraClusterFactoryBean; -import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; -import org.springframework.data.cassandra.config.SchemaAction; -import org.springframework.data.cassandra.core.CassandraAdminOperations; -import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecification; -import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; - -@Configuration -@EnableCassandraRepositories(basePackages = { "biz.nynja.account.accesspoints" }) // to be moved to CassandraAccessPointConfig -@ConditionalOnMissingClass("org.springframework.test.context.junit4.SpringRunner") -public abstract class CassandraBaseConfig extends AbstractCassandraConfiguration { - - @Value("${spring.data.cassandra.contact-points}") - protected String contactPoints; - - @Value("${spring.data.cassandra.port}") - protected int port; - - @Override - protected String getContactPoints() { - return contactPoints; - } - - public void setContactPoints(String contactPoints) { - this.contactPoints = contactPoints; - } - - @Override - protected int getPort() { - return port; - } - - protected int setPort() { - return port; - } - - public CassandraAdminOperations cassandraTemplate(CassandraSessionFactoryBean session) throws Exception { - return null; - } - -} diff --git a/src/main/java/biz/nynja/account/configuration/CassandraConfig.java b/src/main/java/biz/nynja/account/configuration/CassandraConfig.java index 35906d76639a696e7429abea2ecf138b195d0f81..8a5ae1b1264069b64bca7d4497959d69ad028ad9 100644 --- a/src/main/java/biz/nynja/account/configuration/CassandraConfig.java +++ b/src/main/java/biz/nynja/account/configuration/CassandraConfig.java @@ -1,77 +1,84 @@ -///** -// * Copyright (C) 2018 Nynja Inc. All rights reserved. -// */ -//package biz.nynja.account.configuration; -// -//import java.util.Arrays; -//import java.util.List; -// -//import org.springframework.beans.factory.annotation.Value; -//import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.data.cassandra.config.AbstractCassandraConfiguration; -//import org.springframework.data.cassandra.config.SchemaAction; -//import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecification; -//import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; -// -//import biz.nynja.account.StartupScriptsListener; -// -//@Configuration -//@EnableCassandraRepositories -//@ConditionalOnMissingClass("org.springframework.test.context.junit4.SpringRunner") -//public class CassandraConfig extends AbstractCassandraConfiguration { -// -// @Value("${spring.data.cassandra.account.keyspace-name}") -// private String keyspace; -// -// @Override -// protected String getKeyspaceName() { -// return keyspace; -// } -// -// @Value("${spring.data.cassandra.contact-points}") -// private String contactPoints; -// -// @Override -// protected String getContactPoints() { -// return contactPoints; -// } -// -// @Value("${spring.data.cassandra.port}") -// private int port; -// -// @Override -// protected int getPort() { -// return port; -// } -// -// @Override -// public SchemaAction getSchemaAction() { -// return SchemaAction.CREATE_IF_NOT_EXISTS; -// } -// -// @Override -// protected List getKeyspaceCreations() { -// CreateKeyspaceSpecification specification = CreateKeyspaceSpecification.createKeyspace(getKeyspaceName()) -// .ifNotExists().withSimpleReplication(); -// return Arrays.asList(specification); -// } -// -// @Override -// public String[] getEntityBasePackages() { -// return new String[] { "biz.nynja.account.models" }; -// } -// -// /** -// * See {@link StartupScriptsListener} for scripts -// * that require JPA annotated tables -// */ -// @Override -// protected List getStartupScripts() { -// return super.getStartupScripts(); -// } -// -// public String getConfiguredKeyspaceName() { -// return getKeyspaceName(); -// } -//} \ No newline at end of file +/** + * Copyright (C) 2018 Nynja Inc. All rights reserved. + */ +package biz.nynja.account.configuration; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.config.AbstractCassandraConfiguration; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecification; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; + +import biz.nynja.account.StartupScriptsListener; + +@Configuration +@EnableCassandraRepositories +@ConditionalOnMissingClass("org.springframework.test.context.junit4.SpringRunner") +public class CassandraConfig extends AbstractCassandraConfiguration { + + @Value("${spring.data.cassandra.keyspace-name}") + private String keyspace; + + @Override + protected String getKeyspaceName() { + return keyspace; + } + + @Value("${spring.data.cassandra.contact-points}") + private String contactPoints; + + @Override + protected String getContactPoints() { + return contactPoints; + } + + @Value("${spring.data.cassandra.port}") + private int port; + + @Value("${spring.data.cassandra.replication}") + private int replication; + + @Override + protected int getPort() { + return port; + } + + private int getReplication() { + return replication; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.CREATE_IF_NOT_EXISTS; + } + + @Override + protected List getKeyspaceCreations() { + CreateKeyspaceSpecification specification = CreateKeyspaceSpecification.createKeyspace(getKeyspaceName()) + .ifNotExists().withSimpleReplication(getReplication()); + return Arrays.asList(specification); + } + + @Override + public String[] getEntityBasePackages() { + return new String[] { "biz.nynja.account.models" }; + } + + /** + * See {@link StartupScriptsListener} for scripts + * that require JPA annotated tables + */ + @Override + protected List getStartupScripts() { + return super.getStartupScripts(); + } + + public String getConfiguredKeyspaceName() { + return getKeyspaceName(); + } +} diff --git a/src/main/java/biz/nynja/account/permissions/PermissionsInterceptor.java b/src/main/java/biz/nynja/account/permissions/PermissionsInterceptor.java index b67ce520a1b210f0b2d2529906c641c8fd505a1f..7a37de8e11b6aedf94ee692b6c545e67f03f8170 100644 --- a/src/main/java/biz/nynja/account/permissions/PermissionsInterceptor.java +++ b/src/main/java/biz/nynja/account/permissions/PermissionsInterceptor.java @@ -1,177 +1,177 @@ -///** -// * Copyright (C) 2018 Nynja Inc. All rights reserved. -// */ -//package biz.nynja.account.permissions; -// -//import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; -// -//import java.lang.reflect.Method; -//import java.util.Base64; -//import java.util.Optional; -//import java.util.UUID; -// -//import org.lognet.springboot.grpc.GRpcGlobalInterceptor; -//import org.slf4j.Logger; -//import org.slf4j.LoggerFactory; -//import org.springframework.beans.factory.annotation.Autowired; -// -//import com.auth0.jwt.JWT; -//import com.auth0.jwt.interfaces.Claim; -//import com.auth0.jwt.interfaces.DecodedJWT; -// -//import biz.nynja.account.services.AccountServiceImpl; -////import biz.nynja.account.accesspoints.AccessPointService; -////import biz.nynja.account.accesspoints.AccessPoint; -//import io.grpc.Context; -//import io.grpc.Contexts; -//import io.grpc.Metadata; -//import io.grpc.ServerCall; -//import io.grpc.ServerCall.Listener; -//import io.grpc.ServerCallHandler; -//import io.grpc.ServerInterceptor; -//import io.grpc.Status; -// -///** -// * @author Stoyan.Tzenkov - account-service ServerInterceptor. -// * Validates roles for granting permissions to account-service endpoints(rpcs). -// * General rules: -// * - if access token is not present - PERMISSION DENIED; -// * - if no accesspoint found for the requesting account ID and access token - PERMISSION DENIED; -// * - if no roles found in the access token - PERMISSION DENIED; -// * - if rpc not found in the account-service class - PERMISSION DENIED; -// * - if no Permitted annotation found for the rpc method - PERMISSION DENIED; -// * - if rpc has either @Permitted(role = RoleConstants.ANY) - PERMISSION GRANTED; -// * - if rpc has an annotation @PerformPermissionCheck - @Permitted annotations are checked and an additional check is performed in the rpc; -// * - if no role from the request matches any Permitted annotation for the rpc - PERMISSION DENIED -// */ -// -//@GRpcGlobalInterceptor -//public class PermissionsInterceptor implements ServerInterceptor { -// -// private static final Logger logger = LoggerFactory.getLogger(PermissionsInterceptor.class); -// private static final Class SERVICE_CLASS = AccountServiceImpl.class; -// -// public static final Metadata.Key ACCESS_TOKEN_METADATA = Metadata.Key.of("Authorization", -// ASCII_STRING_MARSHALLER); -// public static final Context.Key ACCESS_TOKEN_CTX = Context.key("accessToken"); -// -// private static final ServerCall.Listener NOOP_LISTENER = new ServerCall.Listener() { -// }; -// -// @Autowired -//// private AccessPointService accessPointService; -// -// @Override -// public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, -// ServerCallHandler next) { -// -// // WARNING: THe line bellow is to be removed and code following uncommented -// // when Istio starts sending an access token with each and every request -// return next.startCall(call, headers); -// -// /* -// * Expected metadata is "Authorization" : "Bearer --accessTokenValue--" so we can skip validation as istio won't -// * allow this request through -// */ -//// String accessToken = (headers.get(ACCESS_TOKEN_METADATA).split(" "))[1]; -//// String rpc = getRpcName(call); -//// -//// boolean permitted = false; -//// Context ctx = null; -//// String[] requestingRoles = null; -//// -//// if (accessToken == null && accessToken.isEmpty()) { -//// permissionDenied(call, headers, "Permission denied for rpc {}. Access token not in headers", rpc ); -//// } -//// ctx = Context.current().withValue(ACCESS_TOKEN_CTX, accessToken); -//// DecodedJWT decodedToken = JWT.decode(accessToken); -//// -//// if (!accessPointAvailable(accessToken, decodedToken, rpc)) { -//// permissionDenied(call, headers, "Permission denied for rpc {}. No access point available for this account and access token.", rpc ); -//// } -//// -//// requestingRoles = getRolesFromAccessToken(decodedToken); -//// if (requestingRoles == null) { -//// permissionDenied(call, headers, "Permission denied for rpc {}. No roles found for requesting account in access token.", rpc ); -//// } -//// -//// Method method = getMethod(rpc); -//// if (method == null) { -//// permissionDenied(call, headers, "Permission denied for rpc {}. Could not identify the method implementing this rpc.", rpc ); -//// } -//// -//// Permitted[] permittedRoles = method.getAnnotationsByType(Permitted.class); -//// permitted = checkPermissions(requestingRoles, permittedRoles); -//// if (permitted) { -//// logger.info("Permission granted to rpc {}.", rpc); -//// return Contexts.interceptCall(ctx, call, headers, next); -//// } else { -//// logger.error("Permission denied for rpc {}, roles {}.", rpc, requestingRoles); -//// call.close(Status.PERMISSION_DENIED.withDescription("An unauthorized call was made to " + rpc + "."), -//// headers); -//// return NOOP_LISTENER; -//// } -// } -// -// private String getRpcName(ServerCall call) { -// // Get name of endpoint/rpc called -// String callName = call.getMethodDescriptor().getFullMethodName(); -// return callName.substring(callName.lastIndexOf('/') + 1); -// } -// -//// private boolean accessPointAvailable(String accessToken, DecodedJWT decodedToken, String rpc) { -//// -//// String accountId = new String(Base64.getDecoder().decode(decodedToken.getSubject())); -//// logger.info("Verifying permissions for rpc {} for user with account id {}.", rpc, accountId); -//// -//// Optional accessPoint = accessPointService.getAccessPoint(UUID.fromString(accountId), accessToken); -//// return accessPoint.isPresent(); -//// } -// -// private String[] getRolesFromAccessToken(DecodedJWT decodedToken) { -// // Get roles from access token +/** + * Copyright (C) 2018 Nynja Inc. All rights reserved. + */ +package biz.nynja.account.permissions; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; + +import java.lang.reflect.Method; +import java.util.Base64; +import java.util.Optional; +import java.util.UUID; + +import org.lognet.springboot.grpc.GRpcGlobalInterceptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; + +import biz.nynja.account.services.AccountServiceImpl; +import biz.nynja.account.accesspoints.AccessPointService; +import biz.nynja.account.accesspoints.AccessPoint; +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; + +/** + * @author Stoyan.Tzenkov - account-service ServerInterceptor. + * Validates roles for granting permissions to account-service endpoints(rpcs). + * General rules: + * - if access token is not present - PERMISSION DENIED; + * - if no accesspoint found for the requesting account ID and access token - PERMISSION DENIED; + * - if no roles found in the access token - PERMISSION DENIED; + * - if rpc not found in the account-service class - PERMISSION DENIED; + * - if no Permitted annotation found for the rpc method - PERMISSION DENIED; + * - if rpc has either @Permitted(role = RoleConstants.ANY) - PERMISSION GRANTED; + * - if rpc has an annotation @PerformPermissionCheck - @Permitted annotations are checked and an additional check is performed in the rpc; + * - if no role from the request matches any Permitted annotation for the rpc - PERMISSION DENIED + */ + +@GRpcGlobalInterceptor +public class PermissionsInterceptor implements ServerInterceptor { + + private static final Logger logger = LoggerFactory.getLogger(PermissionsInterceptor.class); + private static final Class SERVICE_CLASS = AccountServiceImpl.class; + + public static final Metadata.Key ACCESS_TOKEN_METADATA = Metadata.Key.of("Authorization", + ASCII_STRING_MARSHALLER); + public static final Context.Key ACCESS_TOKEN_CTX = Context.key("accessToken"); + + private static final ServerCall.Listener NOOP_LISTENER = new ServerCall.Listener() { + }; + + @Autowired + private AccessPointService accessPointService; + + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + + // WARNING: THe line bellow is to be removed and code following uncommented + // when Istio starts sending an access token with each and every request + return next.startCall(call, headers); + + /* + * Expected metadata is "Authorization" : "Bearer --accessTokenValue--" so we can skip validation as istio won't + * allow this request through + */ +// String accessToken = (headers.get(ACCESS_TOKEN_METADATA).split(" "))[1]; +// String rpc = getRpcName(call); +// +// boolean permitted = false; +// Context ctx = null; // String[] requestingRoles = null; // -// Claim claim = decodedToken.getClaim("roles"); -// if (claim != null) { -// requestingRoles = claim.asArray(String.class); +// if (accessToken == null && accessToken.isEmpty()) { +// permissionDenied(call, headers, "Permission denied for rpc {}. Access token not in headers", rpc ); // } -// return requestingRoles; -// } -// -// private Method getMethod(String rpc) { -// // Get the rpc method called -// Method[] allMethods = SERVICE_CLASS.getDeclaredMethods(); +// ctx = Context.current().withValue(ACCESS_TOKEN_CTX, accessToken); +// DecodedJWT decodedToken = JWT.decode(accessToken); // -// for (Method method : allMethods) { -// if (method.getName().equals(rpc)) { -// return method; -// } +// if (!accessPointAvailable(accessToken, decodedToken, rpc)) { +// permissionDenied(call, headers, "Permission denied for rpc {}. No access point available for this account and access token.", rpc ); // } -// return null; -// } -// -// private boolean checkPermissions(String[] requestingRoles, Permitted[] permittedRoles) { // -// for (Permitted permitted : permittedRoles) { -// if (permitted.role().equals(RoleConstants.ANY)) { -// return true; -// } -// for (String role : requestingRoles) { -// if (role.equals(permitted.role()) || role.equals(RoleConstants.ACCOUNT_ADMIN) -// || role.equals(RoleConstants.AUTH_SERVICE)) { -// return true; -// } -// } +// requestingRoles = getRolesFromAccessToken(decodedToken); +// if (requestingRoles == null) { +// permissionDenied(call, headers, "Permission denied for rpc {}. No roles found for requesting account in access token.", rpc ); // } -// return false; -// } // -// private ServerCall.Listener permissionDenied(ServerCall call, Metadata headers, String message, String rpc ) { -// logger.error(message, rpc); -// call.close(Status.PERMISSION_DENIED.withDescription("An unauthorized call was made to " + rpc + "."), -// headers); -// return NOOP_LISTENER; -// } +// Method method = getMethod(rpc); +// if (method == null) { +// permissionDenied(call, headers, "Permission denied for rpc {}. Could not identify the method implementing this rpc.", rpc ); +// } // -//} +// Permitted[] permittedRoles = method.getAnnotationsByType(Permitted.class); +// permitted = checkPermissions(requestingRoles, permittedRoles); +// if (permitted) { +// logger.info("Permission granted to rpc {}.", rpc); +// return Contexts.interceptCall(ctx, call, headers, next); +// } else { +// logger.error("Permission denied for rpc {}, roles {}.", rpc, requestingRoles); +// call.close(Status.PERMISSION_DENIED.withDescription("An unauthorized call was made to " + rpc + "."), +// headers); +// return NOOP_LISTENER; +// } + } + + private String getRpcName(ServerCall call) { + // Get name of endpoint/rpc called + String callName = call.getMethodDescriptor().getFullMethodName(); + return callName.substring(callName.lastIndexOf('/') + 1); + } + + private boolean accessPointAvailable(String accessToken, DecodedJWT decodedToken, String rpc) { + + String accountId = new String(Base64.getDecoder().decode(decodedToken.getSubject())); + logger.info("Verifying permissions for rpc {} for user with account id {}.", rpc, accountId); + + Optional accessPoint = accessPointService.getAccessPoint(UUID.fromString(accountId), accessToken); + return accessPoint.isPresent(); + } + + private String[] getRolesFromAccessToken(DecodedJWT decodedToken) { + // Get roles from access token + String[] requestingRoles = null; + + Claim claim = decodedToken.getClaim("roles"); + if (claim != null) { + requestingRoles = claim.asArray(String.class); + } + return requestingRoles; + } + + private Method getMethod(String rpc) { + // Get the rpc method called + Method[] allMethods = SERVICE_CLASS.getDeclaredMethods(); + + for (Method method : allMethods) { + if (method.getName().equals(rpc)) { + return method; + } + } + return null; + } + + private boolean checkPermissions(String[] requestingRoles, Permitted[] permittedRoles) { + + for (Permitted permitted : permittedRoles) { + if (permitted.role().equals(RoleConstants.ANY)) { + return true; + } + for (String role : requestingRoles) { + if (role.equals(permitted.role()) || role.equals(RoleConstants.ACCOUNT_ADMIN) + || role.equals(RoleConstants.AUTH_SERVICE)) { + return true; + } + } + } + return false; + } + + private ServerCall.Listener permissionDenied(ServerCall call, Metadata headers, String message, String rpc ) { + logger.error(message, rpc); + call.close(Status.PERMISSION_DENIED.withDescription("An unauthorized call was made to " + rpc + "."), + headers); + return NOOP_LISTENER; + } + +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 4956ef2ba7debe2b9b755cf8708bf9b2330a8c44..4746d23ff667089f4befb23d9fbcd3bb0f7e5bc3 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -12,10 +12,8 @@ spring: cassandra: contact-points: localhost port: 9042 - account: - keyspace-name: account - accesspoint: - keyspace-name: accesspoint + keyspace-name: account + replication: 3 # To enable colors in Eclipse: # spring.output.ansi.enabled=ALWAYS and in eclipse # Help -> Install New Software... and click "Add..." to add the following URL: diff --git a/src/main/resources/application-production.yml b/src/main/resources/application-production.yml index 178b0ec470491a24fd437ddb0f84d9023d3238bd..a404f31b82aa47f18a9f777cbd441052933d0c40 100644 --- a/src/main/resources/application-production.yml +++ b/src/main/resources/application-production.yml @@ -13,10 +13,8 @@ spring: keyspace-name: ${CASSANDRA_KEYSPACE:account} contact-points: ${CASSANDRA_CONTACT_POINTS:cassandra.cassandra.svc.cluster.local} port: ${CASSANDRA_PORT:9042} - account: - keyspace-name: ${CASSANDRA_KEYSPACE:account} - accesspoint: - keyspace-name: ${CASSANDRA_ACCESS_POINT_KEYSPACE:accesspoint} + keyspace-name: ${CASSANDRA_KEYSPACE:account} + replication: 3 logging: level: