diff options
8 files changed, 605 insertions, 0 deletions
diff --git a/aai-traversal/pom.xml b/aai-traversal/pom.xml index d45af14..345b697 100644 --- a/aai-traversal/pom.xml +++ b/aai-traversal/pom.xml @@ -100,7 +100,9 @@ <micrometer-core.version>1.6.6</micrometer-core.version> <micrometer-registry-prometheus.version>1.6.6</micrometer-registry-prometheus.version> <micrometer-jersey2>1.6.6</micrometer-jersey2> + <testcontainers.version>1.6.1</testcontainers.version> <!-- End of Default ONAP Schema Properties --> + <build.helper-maven-plugin.version>3.2.0</build.helper-maven-plugin.version> </properties> <profiles> <!-- Docker profile to be used for building docker image and pushing to @@ -729,6 +731,18 @@ <groupId>org.keycloak</groupId> <artifactId>keycloak-spring-boot-starter</artifactId> </dependency> + <dependency> + <groupId>com.github.dasniko</groupId> + <artifactId>testcontainers-keycloak</artifactId> + <version>${testcontainers.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.keycloak</groupId> + <artifactId>keycloak-admin-client</artifactId> + <version>${keycloak.version}</version> + <scope>test</scope> + </dependency> </dependencies> <dependencyManagement> <dependencies> @@ -964,6 +978,7 @@ <configuration> <mainClass>${start-class}</mainClass> <layout>ZIP</layout> + <classifier>exec</classifier> </configuration> <executions> <execution> @@ -1052,6 +1067,39 @@ <skip>false</skip> </configuration> </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <version>${build.helper-maven-plugin.version}</version> + <executions> + <execution> + <id>add-integration-test-source</id> + <phase>generate-test-sources</phase> + <goals> + <goal>add-test-source</goal> + </goals> + <configuration> + <sources> + <source>src/it/java</source> + </sources> + </configuration> + </execution> + <execution> + <id>add-integration-test-resource</id> + <phase>generate-test-resources</phase> + <goals> + <goal>add-test-resource</goal> + </goals> + <configuration> + <resources> + <resource> + <directory>src/it/resources</directory> + </resource> + </resources> + </configuration> + </execution> + </executions> + </plugin> </plugins> </build> </project> diff --git a/aai-traversal/src/it/java/org/onap/aai/multitenancy/KeycloakTestConfiguration.java b/aai-traversal/src/it/java/org/onap/aai/multitenancy/KeycloakTestConfiguration.java new file mode 100644 index 0000000..01f335a --- /dev/null +++ b/aai-traversal/src/it/java/org/onap/aai/multitenancy/KeycloakTestConfiguration.java @@ -0,0 +1,73 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ +package org.onap.aai.multitenancy; + +import com.github.dockerjava.api.model.ExposedPort; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.PortBinding; +import com.github.dockerjava.api.model.Ports; +import dasniko.testcontainers.keycloak.KeycloakContainer; +import org.keycloak.adapters.springboot.KeycloakSpringBootProperties; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.representations.adapters.config.AdapterConfig; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +class KeycloakTestConfiguration { + + @Bean + public AdapterConfig adapterConfig() { + return new KeycloakSpringBootProperties(); + } + + @Bean + KeycloakContainer keycloakContainer(KeycloakTestProperties properties) { + KeycloakContainer keycloak = new KeycloakContainer("jboss/keycloak:12.0.4") + .withRealmImportFile(properties.realmJson) + .withCreateContainerCmdModifier(cmd -> cmd.withHostConfig( + new HostConfig().withPortBindings(new PortBinding(Ports.Binding.bindPort(Integer.parseInt(properties.port)), new ExposedPort(8080))) + )); + keycloak.start(); + return keycloak; + } + + @Bean + Keycloak keycloakAdminClient(KeycloakContainer keycloak, KeycloakTestProperties properties) { + return KeycloakBuilder.builder() + .serverUrl(keycloak.getAuthServerUrl()) + .realm(properties.realm) + .clientId(properties.adminCli) + .username(keycloak.getAdminUsername()) + .password(keycloak.getAdminPassword()) + .build(); + } + + @Bean + RoleHandler roleHandler(Keycloak adminClient, KeycloakTestProperties properties) { + return new RoleHandler(adminClient, properties); + } + + @Bean + KeycloakTestProperties properties() { + return new KeycloakTestProperties(); + } +} diff --git a/aai-traversal/src/it/java/org/onap/aai/multitenancy/KeycloakTestProperties.java b/aai-traversal/src/it/java/org/onap/aai/multitenancy/KeycloakTestProperties.java new file mode 100644 index 0000000..de62d2d --- /dev/null +++ b/aai-traversal/src/it/java/org/onap/aai/multitenancy/KeycloakTestProperties.java @@ -0,0 +1,44 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ +package org.onap.aai.multitenancy; + +import org.springframework.beans.factory.annotation.Value; + +class KeycloakTestProperties { + + @Value("${test.keycloak.realm.json}") + public String realmJson; + + @Value("${keycloak.realm}") + public String realm; + + @Value("${keycloak.resource}") + public String clientId; + + @Value("${test.keycloak.client.secret}") + public String clientSecret; + + @Value("${test.keycloak.admin.cli}") + public String adminCli; + + @Value("${test.keycloak.auth-server-port}") + public String port; + +} diff --git a/aai-traversal/src/it/java/org/onap/aai/multitenancy/MultiTenancyIT.java b/aai-traversal/src/it/java/org/onap/aai/multitenancy/MultiTenancyIT.java new file mode 100644 index 0000000..e34ac2b --- /dev/null +++ b/aai-traversal/src/it/java/org/onap/aai/multitenancy/MultiTenancyIT.java @@ -0,0 +1,189 @@ +/** + * ============LICENSE_START================================================== + * org.onap.aai + * =========================================================================== + * Copyright © 2017-2020 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + */ +package org.onap.aai.multitenancy; + +import com.jayway.jsonpath.JsonPath; +import dasniko.testcontainers.keycloak.KeycloakContainer; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.janusgraph.core.JanusGraphTransaction; +import org.junit.Test; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.representations.AccessTokenResponse; +import org.onap.aai.PayloadUtil; +import org.onap.aai.dbmap.AAIGraph; +import org.onap.aai.rest.AbstractSpringRestTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.http.*; +import org.springframework.test.context.TestPropertySource; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; + +@Import(KeycloakTestConfiguration.class) +@TestPropertySource(locations = "classpath:application-keycloak-test.properties") +public class MultiTenancyIT extends AbstractSpringRestTest { + + @Autowired + private KeycloakContainer keycloakContainer; + @Autowired + private RoleHandler roleHandler; + @Autowired + private KeycloakTestProperties properties; + + @Override + public void createTestGraph() { + JanusGraphTransaction transaction = AAIGraph.getInstance().getGraph().newTransaction(); + boolean success = true; + + try { + GraphTraversalSource g = transaction.traversal(); + + g.addV().property("aai-node-type", "pnf") + .property("pnf-name", "test-pnf-name-01") + .property("prov-status", "in_service") + .property("data-owner", "operator") + .property("in-maint", false) + .property("source-of-truth", "JUNIT") + .property("aai-uri", "/network/pnfs/pnf/test-pnf-name-01").next(); + + g.addV().property("aai-node-type", "pnf") + .property("pnf-name", "test-pnf-name-02") + .property("prov-status", "in_service") + .property("in-maint", false) + .property("source-of-truth", "JUNIT") + .property("aai-uri", "/network/pnfs/pnf/test-pnf-name-02").next(); + + g.addV().property("aai-node-type", "pnf") + .property("pnf-name", "test-pnf-name-03") + .property("prov-status", "in_service") + .property("data-owner", "selector") + .property("in-maint", false) + .property("source-of-truth", "JUNIT") + .property("aai-uri", "/network/pnfs/pnf/test-pnf-name-03").next(); + + g.addV().property("aai-node-type", "pnf") + .property("pnf-name", "test-pnf-name-04") + .property("prov-status", "in_service") + .property("data-owner", "selector") + .property("in-maint", false) + .property("source-of-truth", "JUNIT") + .property("aai-uri", "/network/pnfs/pnf/test-pnf-name-04").next(); + + g.addV().property("aai-node-type", "pnf") + .property("pnf-name", "test-pnf-name-05") + .property("prov-status", "in_service") + .property("data-owner", "selector") + .property("in-maint", false) + .property("source-of-truth", "JUNIT") + .property("aai-uri", "/network/pnfs/pnf/test-pnf-name-05").next(); + } catch (Exception ex) { + success = false; + } finally { + if (success) { + transaction.commit(); + } else { + transaction.rollback(); + fail("Unable to setup the graph"); + } + } + } + + @Test + public void testDslQueryWithDataOwner() throws Exception { + baseUrl = "http://localhost:" + randomPort; + String endpoint = baseUrl + "/aai/v23/dsl?format=console"; + List<Object> queryResults = null; + ResponseEntity responseEntity = null; + + Map<String, String> dslQueryMap = new HashMap<>(); + dslQueryMap.put("dsl-query", "pnf*('prov-status','in_service') "); + String payload = PayloadUtil.getTemplatePayload("dsl-query.json", dslQueryMap); + + // get pnf with ran (operator) + String username = "ran", password = "ran"; + headers = this.getHeaders(username, password); + httpEntity = new HttpEntity(payload, headers); + responseEntity = restTemplate.exchange(endpoint, HttpMethod.PUT, httpEntity, String.class); + queryResults = JsonPath.read(responseEntity.getBody().toString(), "$.results"); + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertEquals(queryResults.size(), 2); + + // get pnf with bob (operator_readOnly) + username = "bob"; password = "bob"; + headers = this.getHeaders(username, password); + httpEntity = new HttpEntity(payload, headers); + responseEntity = restTemplate.exchange(endpoint, HttpMethod.PUT, httpEntity, String.class); + queryResults = JsonPath.read(responseEntity.getBody().toString(), "$.results"); + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertEquals(queryResults.size(), 2); + + // get pnf with ted (selector) + username = "ted"; password = "ted"; + headers = this.getHeaders(username, password); + httpEntity = new HttpEntity(payload, headers); + responseEntity = restTemplate.exchange(endpoint, HttpMethod.PUT, httpEntity, String.class); + queryResults = JsonPath.read(responseEntity.getBody().toString(), "$.results"); + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertEquals(queryResults.size(), 4); + + // add role to ted and try to get pnf again + roleHandler.addToUser(RoleHandler.OPERATOR, username); + headers = this.getHeaders(username, password); + httpEntity = new HttpEntity(payload, headers); + responseEntity = restTemplate.exchange(endpoint, HttpMethod.PUT, httpEntity, String.class); + queryResults = JsonPath.read(responseEntity.getBody().toString(), "$.results"); + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertEquals(queryResults.size(), 5); + } + + private HttpHeaders getHeaders(String username, String password) { + HttpHeaders headers = new HttpHeaders(); + + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + headers.add("Real-Time", "true"); + headers.add("X-FromAppId", "JUNIT"); + headers.add("X-TransactionId", "JUNIT"); + headers.add("Authorization", "Bearer " + getStringToken(username, password)); + + return headers; + } + + private String getStringToken(String username, String password) { + Keycloak keycloakClient = KeycloakBuilder.builder() + .serverUrl(keycloakContainer.getAuthServerUrl()) + .realm(properties.realm) + .clientId(properties.clientId) + .clientSecret(properties.clientSecret) + .username(username) + .password(password) + .build(); + + AccessTokenResponse tokenResponse = keycloakClient.tokenManager().getAccessToken(); + assertNotNull(tokenResponse); + return tokenResponse.getToken(); + } +} diff --git a/aai-traversal/src/it/java/org/onap/aai/multitenancy/RoleHandler.java b/aai-traversal/src/it/java/org/onap/aai/multitenancy/RoleHandler.java new file mode 100644 index 0000000..4224c2f --- /dev/null +++ b/aai-traversal/src/it/java/org/onap/aai/multitenancy/RoleHandler.java @@ -0,0 +1,56 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ +package org.onap.aai.multitenancy; + +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.RealmResource; + +import java.util.Collections; + +class RoleHandler { + + /** + Following roles should be the same as given roles in multi-tenancy-realm json file + */ + final static String OPERATOR = "operator"; + private final Keycloak adminClient; + private final KeycloakTestProperties properties; + + RoleHandler(Keycloak adminClient, KeycloakTestProperties properties) { + this.adminClient = adminClient; + this.properties = properties; + } + + void addToUser(String role, String username) { + RealmResource realm = adminClient.realm(properties.realm); + realm.users().get(username) + .roles() + .realmLevel() + .add(Collections.singletonList(realm.roles().get(role).toRepresentation())); + } + + void removeFromUser(String role, String username) { + RealmResource realm = adminClient.realm(properties.realm); + realm.users().get(username) + .roles() + .realmLevel() + .remove(Collections.singletonList(realm.roles().get(role).toRepresentation())); + } +} diff --git a/aai-traversal/src/it/resources/application-keycloak-test.properties b/aai-traversal/src/it/resources/application-keycloak-test.properties new file mode 100644 index 0000000..0959099 --- /dev/null +++ b/aai-traversal/src/it/resources/application-keycloak-test.properties @@ -0,0 +1,17 @@ +test.keycloak.realm.json=multi-tenancy-realm.json +test.keycloak.client.secret=secret +test.keycloak.admin.cli=admin-cli +test.keycloak.auth-server-port=58181 + +keycloak.auth-server-url=http://localhost:58181/auth +keycloak.realm=aai-resources +keycloak.resource=aai-resources-app +keycloak.public-client=true +keycloak.principal-attribute=preferred_username + +keycloak.ssl-required=external +keycloak.bearer-only=true + +multi.tenancy.enabled=true +spring.profiles.active=production,keycloak +schema.version.list=v10,v11,v12,v13,v14,v15,v23 diff --git a/aai-traversal/src/it/resources/multi-tenancy-realm.json b/aai-traversal/src/it/resources/multi-tenancy-realm.json new file mode 100644 index 0000000..401187b --- /dev/null +++ b/aai-traversal/src/it/resources/multi-tenancy-realm.json @@ -0,0 +1,173 @@ +{ + "id": "aai-resources", + "realm": "aai-resources", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "users": [ + { + "username": "admin", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "admin" + } + ], + "clientRoles": { + "realm-management": ["manage-users", "view-clients", "view-realm", "view-users"] + } + }, + { + "id": "ran", + "username": "ran", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "ran" + } + ], + "realmRoles": [ + "operator" + ] + }, + { + "id": "bob", + "username": "bob", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "bob" + } + ], + "realmRoles": [ + "operator_readOnly" + ] + }, + { + "id": "ted", + "username": "ted", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "ted" + } + ], + "realmRoles": [ + "selector" + ] + } + ], + "roles": { + "realm": [ + { + "name": "operator", + "description": "Operator privileges" + }, + { + "name": "operator_readOnly", + "description": "Operator's read only privileges" + }, + { + "name": "selector", + "description": "Selector privileges" + }, + { + "name": "selector_readOnly", + "description": "Selector's read only privileges" + }, + { + "name": "admin", + "description": "Administrator privileges" + } + ] + }, + "clients": [ + { + "clientId": "aai-resources-app", + "enabled": true, + "secret": "secret", + "directAccessGrantsEnabled": true, + "authorizationServicesEnabled": true, + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING" + } + } + ], + "defaultDefaultClientScopes": [ + "roles", + "email", + "web-origins", + "profile", + "role_list" + ], + "clientScopes": [ + { + "id": "0f7dfd8b-c230-4664-8d77-da85bcc4fe2a", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "4b9f8798-8990-4c0d-87d3-034e72655e3b", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + } + ] + } + ] +}
\ No newline at end of file @@ -49,5 +49,10 @@ The REST interface logs can be found under /opt/app/aai-traversal/logs/rest. # Testing AAI Functionalities Any RESTful client such as SoapUI may be configured and setup to use for testing AAI requests. +# Integration Tests +In order to isolate integration tests from unit tests, `it` directory has been created under `src`. + +As a naming convention, All integration test classes should end with `IT` to be executed, and can be run along with all other unit tests with `mvn integration-test` + |