From c0e31c9b333cf4888ec4961e2e1a235e9df498f2 Mon Sep 17 00:00:00 2001 From: Stoyan Tzenkov Date: Sun, 6 Jan 2019 12:04:12 +0200 Subject: [PATCH] NY-ZZZZ: Second_keyspace_through_datastax_test Signed-off-by: Stoyan Tzenkov --- pom.xml | 20 + .../account/accesspoints/AccessPoint.java | 19 +- .../accesspoints/AccessPointRepository.java | 55 ++- .../accesspoints/AccessPointService.java | 2 +- .../components/PreparedStatementsCache.java | 9 +- .../CassandraAccessPointConfig.java | 87 +++-- .../configuration/CassandraAccountConfig.java | 90 ----- .../configuration/CassandraBaseConfig.java | 51 --- .../configuration/CassandraConfig.java | 154 ++++---- .../permissions/PermissionsInterceptor.java | 355 +++++++++--------- 10 files changed, 403 insertions(+), 439 deletions(-) delete mode 100644 src/main/java/biz/nynja/account/configuration/CassandraAccountConfig.java delete mode 100644 src/main/java/biz/nynja/account/configuration/CassandraBaseConfig.java diff --git a/pom.xml b/pom.xml index 7a41f5d..3299435 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,19 @@ + + com.datastax.cassandra + cassandra-driver-mapping + 3.4.0 + + + + + org.springframework.data + spring-data-cassandra + 2.1.3.RELEASE + + com.google.guava guava @@ -145,6 +158,13 @@ 3.8.1 + + + org.springframework.data + spring-data-commons-core + 1.4.1.RELEASE + + com.sun.mail javax.mail diff --git a/src/main/java/biz/nynja/account/accesspoints/AccessPoint.java b/src/main/java/biz/nynja/account/accesspoints/AccessPoint.java index 978d6e1..a794beb 100644 --- a/src/main/java/biz/nynja/account/accesspoints/AccessPoint.java +++ b/src/main/java/biz/nynja/account/accesspoints/AccessPoint.java @@ -8,18 +8,27 @@ import java.io.Serializable; import java.util.UUID; import org.springframework.data.cassandra.core.cql.PrimaryKeyType; -import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn; -import org.springframework.data.cassandra.core.mapping.Table; -@Table +import com.datastax.driver.mapping.annotations.ClusteringColumn; +import com.datastax.driver.mapping.annotations.Column; +import com.datastax.driver.mapping.annotations.PartitionKey; +//import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn; +import com.datastax.driver.mapping.annotations.Table; + +@Table(name = "accesspoint") public class AccessPoint implements Serializable { - @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) + @PartitionKey + @Column(name = "accountid") private UUID accountId; - @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.CLUSTERED) + @ClusteringColumn + @Column(name = "accesstoken") private String accessToken; + @Column(name = "deviceid") private String deviceId; + @Column(name = "creationtimestamp") private Long creationTimestamp; + @Column(name = "expirationtimestamp") private Long expirationTimestamp; public String getDeviceId() { diff --git a/src/main/java/biz/nynja/account/accesspoints/AccessPointRepository.java b/src/main/java/biz/nynja/account/accesspoints/AccessPointRepository.java index 6888c6d..15a20bb 100644 --- a/src/main/java/biz/nynja/account/accesspoints/AccessPointRepository.java +++ b/src/main/java/biz/nynja/account/accesspoints/AccessPointRepository.java @@ -1,17 +1,54 @@ -/** - * Copyright (C) 2018 Nynja Inc. All rights reserved. - */ package biz.nynja.account.accesspoints; - +import java.util.List; import java.util.UUID; -import org.springframework.data.cassandra.repository.CassandraRepository; import org.springframework.stereotype.Repository; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.mapping.Mapper; +import com.datastax.driver.mapping.MappingManager; + @Repository -public interface AccessPointRepository - extends CassandraRepository { - - AccessPoint findByAccountIdAndAccessToken(UUID accountId, String accessToken); +public class AccessPointRepository { + + private Mapper mapper; + private Session session; + + private static final String TABLE = "accesspoint"; + + public AccessPointRepository(MappingManager mappingManager) { + createTable(mappingManager.getSession()); + this.mapper = mappingManager.mapper(AccessPoint.class); + this.session = mappingManager.getSession(); + } + + private void createTable(Session session) { + // use SchemaBuilder to create table + } + + public AccessPoint find(UUID accountId, String accessToken) { + return mapper.get(accountId, accessToken); + } + +// public List findAll() { +// final ResultSet result = session.execute(QueryBuilder.select().all().from(TABLE)); +// return mapper.map(result).all(); +// } + +// public AccessPoint findByAccountIdAndAccessToken(UUID accountId, String accessToken) { +// final ResultSet result = session.execute(QueryBuilder.select().all().from(TABLE) +// .where(QueryBuilder.eq("accountid", accountId)).and(QueryBuilder.eq("accesstoken", accessToken))); +// return mapper.map(result).all(); +// } + +// public void delete(String country, String firstName, String secondName, UUID id) { +// mapper.delete(country, firstName, secondName, id); +// } +// public AccessPoint save(AccessPoint person) { +// mapper.save(person); +// return person; +// } } diff --git a/src/main/java/biz/nynja/account/accesspoints/AccessPointService.java b/src/main/java/biz/nynja/account/accesspoints/AccessPointService.java index ca24bd2..8d07433 100644 --- a/src/main/java/biz/nynja/account/accesspoints/AccessPointService.java +++ b/src/main/java/biz/nynja/account/accesspoints/AccessPointService.java @@ -21,7 +21,7 @@ public class AccessPointService { } public Optional getAccessPoint(UUID accountId, String accessToken) { - AccessPoint accessPoint = accessPointRepository.findByAccountIdAndAccessToken(accountId, accessToken); + AccessPoint accessPoint = accessPointRepository.find(accountId, accessToken); if (accessPoint != null) { return Optional.of(accessPoint); } diff --git a/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java b/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java index 20be301..10032ab 100644 --- a/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java +++ b/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java @@ -18,20 +18,23 @@ 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.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 CassandraAccountConfig cassandraConfig; private final Session session; private static Map statementsCache; @Autowired - public PreparedStatementsCache(Session session, CassandraAccountConfig cassandraConfig) { +// 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 index 1c67b4d..5003493 100644 --- a/src/main/java/biz/nynja/account/configuration/CassandraAccessPointConfig.java +++ b/src/main/java/biz/nynja/account/configuration/CassandraAccessPointConfig.java @@ -1,26 +1,61 @@ -//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" }; -// } -// -//} +package biz.nynja.account.configuration; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.schemabuilder.SchemaBuilder; +import com.datastax.driver.mapping.DefaultNamingStrategy; +import com.datastax.driver.mapping.DefaultPropertyMapper; +import com.datastax.driver.mapping.MappingConfiguration; +import com.datastax.driver.mapping.MappingManager; +import com.datastax.driver.mapping.NamingConventions; +import com.datastax.driver.mapping.PropertyMapper; + +@Configuration +@EnableCassandraRepositories( basePackages = { "biz.nynja.account.accesspoints" } ) +public class CassandraAccessPointConfig { + + @Bean + public Cluster cluster( + @Value("${spring.data.cassandra.contact-points}") String host, +// @Value("${cassandra.cluster.name:cluster}") String clusterName, + @Value("${spring.data.cassandra.port}") int port) { +// return Cluster.builder().addContactPoint(host).withPort(port).withClusterName(clusterName).build(); + return Cluster.builder().addContactPoint(host).withPort(port).build(); + } + + @Bean + public Session session(Cluster cluster, + @Value("${spring.data.cassandra.accesspoint.keyspace-name}") String keyspace) throws IOException { + final Session session = cluster.connect(); + setupKeyspace(session, keyspace); + return session; + } + + private void setupKeyspace(Session session, String keyspace) throws IOException { + final Map replication = new HashMap<>(); + replication.put("class", "SimpleStrategy"); + replication.put("replication_factor", 1); +// session.execute(SchemaBuilder.createKeyspace(keyspace).ifNotExists().with().replication(replication)); + session.execute("USE " + keyspace); + // String[] statements = split(IOUtils.toString(getClass().getResourceAsStream("/cql/setup.cql")), ";"); + // Arrays.stream(statements).map(statement -> normalizeSpace(statement) + ";").forEach(session::execute); + } + + @Bean + public MappingManager mappingManager(Session session) { + final PropertyMapper propertyMapper = new DefaultPropertyMapper().setNamingStrategy( + new DefaultNamingStrategy(NamingConventions.LOWER_CAMEL_CASE, NamingConventions.LOWER_SNAKE_CASE)); + final MappingConfiguration configuration = MappingConfiguration.builder().withPropertyMapper(propertyMapper) + .build(); + return new MappingManager(session, configuration); + } +} \ No newline at end of file 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 547b535..0000000 --- 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 98dcffb..0000000 --- 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 35906d7..f5aedc7 100644 --- a/src/main/java/biz/nynja/account/configuration/CassandraConfig.java +++ b/src/main/java/biz/nynja/account/configuration/CassandraConfig.java @@ -1,77 +1,77 @@ -///** -// * 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.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(); + } +} diff --git a/src/main/java/biz/nynja/account/permissions/PermissionsInterceptor.java b/src/main/java/biz/nynja/account/permissions/PermissionsInterceptor.java index b67ce52..962d7f1 100644 --- a/src/main/java/biz/nynja/account/permissions/PermissionsInterceptor.java +++ b/src/main/java/biz/nynja/account/permissions/PermissionsInterceptor.java @@ -1,177 +1,178 @@ -///** -// * 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 -// 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; -// } -// -//} +/** + * 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.accesspoints.AccessPoint; +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 + 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; + } + +} -- GitLab