diff --git a/.circleci/config.yml b/.circleci/config.yml index 263cf797f..50ef45912 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,16 +17,16 @@ jobs: # Download and cache dependencies - restore_cache: keys: - - v1-dependencies-{{ checksum "pom.xml" }} + - v2-dependencies-{{ checksum "pom.xml" }} # fallback to using the latest cache if no exact match is found - - v1-dependencies- + - v2-dependencies- - run: name: Fetch Maven Dependencies command: mvn dependency:go-offline - save_cache: paths: - ~/.m2 - key: v1-dependencies-{{ checksum "pom.xml" }} + key: v2-dependencies-{{ checksum "pom.xml" }} # Run Tests - run: @@ -35,6 +35,36 @@ jobs: mvn test \ -D spring.profiles.active=test + build-docker-image: + machine: true + + steps: + - checkout + + - run: docker login -u $DOCKER_USER -p $DOCKER_PASS + + - run: docker build -f Dockerfile.prod . -t overture/ego:$(git describe --always)-alpine + + - run: docker push overture/ego:$(git describe --always)-alpine + + deploy-staging: + machine: true + + steps: + - checkout + + - run: mkdir ~/.kube && echo $KUBE_CONFIG | base64 --decode > ~/.kube/config + + - run: wget https://storage.googleapis.com/kubernetes-helm/helm-v2.12.1-linux-amd64.tar.gz + + - run: tar -xvf helm-v2.12.1-linux-amd64.tar.gz + + - run: linux-amd64/helm init --client-only + + - run: linux-amd64/helm repo add overture https://overture-stack.github.io/charts/ + + - run: linux-amd64/helm upgrade ego-staging overture/ego --reuse-values --set image.tag=$(git describe --always)-alpine + deploy: docker: - image: circleci/openjdk:8-jdk @@ -78,10 +108,9 @@ jobs: name: Deploy Script command: ./.circleci/deploy.sh - workflows: version: 2 - test-deploy: + test-build-deploy: jobs: - test - deploy: @@ -91,4 +120,11 @@ workflows: branches: only: - develop - \ No newline at end of file + - build-docker-image: + filters: + branches: + only: + - develop + - deploy-staging: + requires: + - build-docker-image diff --git a/.gitignore b/.gitignore index c80de70f6..16c9ecbca 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ _build/ _source/ _templates/ .DS_Store + +classes/ + +local.log \ No newline at end of file diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 000000000..fa4f7b499 --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,110 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you 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. +*/ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar old mode 100644 new mode 100755 index 9cc84ea9b..01e679973 Binary files a/.mvn/wrapper/maven-wrapper.jar and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties old mode 100644 new mode 100755 index c31504370..00d32aab1 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..d83b5122a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Code Standards + +#### General +1. Do not use field injection (ie. `@Value`, `@Autowired`) + - Instead use an `@Autowired` or `@Value` annotated constructor + - Provide a static builder (ie. Lombok `@Builder` annotation) + - This helps to improves testability + - Helps to decouple from Spring + - If your constructor is feeling messy or too big - you are probably overloading the class you are working on +2. Do not use any implementation specific JPA code (ie. Hibernate-only annotations) + - Exception for when no alternative functionality exists (ie. Postgres JSON field search) +3. All of our code is auto-formatted to Google Java Format using the [fmt-maven-plugin](https://mvnrepository.com/artifact/com.coveo/fmt-maven-plugin) on build: +```xml + + com.coveo + fmt-maven-plugin + ${FMT_MVN_PLG.VERSION} + + + + format + + + + +``` +5. Constants +- must be declared in a `@NoArgsConstructor(access=PRIVATE)` annotated class with a name representative of the type of constants. For example, the class `Tables` under the package `constants` would contain sql table names. +- Constant variable names should be consistent throughout code base. For example, the text `egoUserPermissions` should be defined by the variable `EGO_USER_PERMISSION`. +6. If a method is not stateful and not an interface/abstract method, then it should be static +7. Never allow a method to return `null`. Instead, it should return `Optiona` or an empty container type (something that has `.isEmpty()`) + +#### Service Layer +1. Get * should always return Optional +2. Find * should always return a Collection + +#### JPA +1. Entity member declarations should take the following presidence: + 1. @Id (identifier) + 2. Non-relationship @Column + 3. @OneToOne + 4. @OneToMany + 5. @ManyToOne + 6. @ManyToMany +2. As explained in this [article](https://vladmihalcea.com/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate/), you should prefer bidirectional associations since they are more efficient than unidirectional ones in terms of SQL performance [source](https://vladmihalcea.com/merge-entity-collections-jpa-hibernate/) +3. Always lazy load for @OneToMany and @ManyToMany +4. Never use CascadeType.ALL or CascadeType.REMOVE becuase they are too destructive. Use CascadeType.MERGE and CascadeType.PERSIST instead +5. Name methods with `remove` indicating an entity was deleted +6. Name methods with `dissociate` indicating a child relationship with its parent will be destoryed +7. For performance reasons, @ManyToMany collections should be a Set as described [here](https://thoughts-on-java.org/association-mappings-bag-list-set/) +8. For performance reasons, @OneToMany collections should be a list as described [here](https://vladmihalcea.com/hibernate-facts-favoring-sets-vs-bags/) +9. In ManyToMany relationships, the JoinTable should only be defined on the **owning** side , and on the inverse side the `mappedBy` ManyToMany annotation parameter should be defined, as described [here](https://www.baeldung.com/hibernate-many-to-many) + +### Testing +1. Test FEATURES not methods +2. Test method names should follow this convention: `[the name of the tested feature]_[expected input / tested state]_[expected behavior]`. + +#### General +1. DB via Test Containers - no in-memory DB or OS specific services +2. No dependencies on any external services (ie. production micro-service) +3. Tests **DO NOT** clear their data between runs, meaning that no test should rely on or expect a clean DB when running + +##### Unit Testing + +##### Integration Testing diff --git a/Dockerfile b/Dockerfile index 7fbcdf221..9b4f0ceb5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,8 @@ RUN mkdir -p /srv/ego/install \ # setup required environment variables ENV EGO_INSTALL_PATH /srv/ego +ENV CONFIG_FILE /usr/src/app/src/main/resources/flyway/conf/flyway.conf # start ego server WORKDIR $EGO_INSTALL_PATH -CMD $EGO_INSTALL_PATH/exec/run.sh +CMD cd /usr/src/app;mvn "flyway:migrate" -Dflyway.configFiles=$CONFIG_FILE -Dflyway.password=password -Dflyway.url=jdbc:postgresql://postgres:5432/ego?stringtype=unspecified;$EGO_INSTALL_PATH/exec/run.sh diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 000000000..f4cebe1e5 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,16 @@ +FROM maven:3.6-jdk-8 + +WORKDIR /usr/src/app + +ADD . . + +RUN mvn package -Dmaven.test.skip=true + +FROM java:8-alpine + +COPY --from=0 /usr/src/app/target/ego-*-SNAPSHOT-exec.jar /usr/bin/ego.jar +COPY --from=0 /usr/src/app/src/main/resources/flyway/sql /usr/src/flyway-migration-sql + +ENTRYPOINT ["java", "-jar", "/usr/bin/ego.jar"] + +EXPOSE 8081/tcp diff --git a/EgoDatabaseDiagram.pdf b/EgoDatabaseDiagram.pdf new file mode 100644 index 000000000..c97e6a878 Binary files /dev/null and b/EgoDatabaseDiagram.pdf differ diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 000000000..ef4ee3961 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,38 @@ +Notes +------ + +## 1. Reason for using the `@Type` hibernate annotation + +#### Problem +In the entity `Policy`, the field `accessLevel` of type `AccessLevel` (which is an enum), the `@Type` annotation is used, which is a hibernate specific and not JPA annotation. The goal is to minimize or eliminate use of hibernate specific syntax, and just use JPA related code. + +#### Solutions +The goal is to map the enum to the database. In the end, solution 3 was chosen. + +##### 1. Middleware Level Enum Handling without Hibernate Annotations +Set the type of the field in the database to a `VARCHAR` and only use the `@Enumerated` JPA annotation +Pros: + - Hibernate will handle the logic of converting an AccessLevel to a string. This means Enum conversion is handelled by the middleware naturally. + - Simple and clean solution using only JPA annotations +Cons: + - Enum type is represented as an Enum at the application level but as a VARCHAR at the database level. If someone was to backdoor the database and update the `accessLevel` of a policy, they could potentially break the application. There is no safeguard outside of hibernate/JPA + +##### 2. Application Level Enum Handling +Set the type of the field in the postgres database to a `AccessLevelType` and in the Java DAO, represent the field as a `String`. The application level (i.e the service layers) will manage the conversion of the Policies `String` accessLevel field to the `AccessLevel` Java enum type. Hibernate will pass the string to the postgres database, and since the url ends with `?stringtype=unspecified`, postgres will cast the string to the Database enum type +Pros: + - No need for Hibernate annotations + - Since conversions are done manually at application layer, functionality is more obvious and cleaner +Cons: + - Its manual, meaning, if the developer forgets to check that the conversion from `AccessLevel->String` and `String->AccessLevel` is correct, a potentially wrong string value will be passed from hibernate to postrgres, resulting in a postgres error + + +##### 3. Middleware Level Enum Handling WITH Hibernate Annotations +Follow the instructions from this [blog post](https://vladmihalcea.com/the-best-way-to-map-an-enum-type-with-jpa-and-hiberate/) under the heading `Mapping a Java Enum to a database-specific Enumarated column type`. This results in the use of the `@Type` hibernate specific annotation for the `Policy` entity. This annotation modifies the way hibernate process the `accessLevel` field. +Pros: + - All processing is done at the middleware (hibernate) and the developer does not need to add any extra code at the application layer. + - Hibernate properly process the Java enum type to a Postgres enum type. This means **BOTH** the java code (the `Policy` entity) and the Policy table in postgres are types and protected from values outside the enumeration. + - Almost no developer effort and minimizes developer mistakes +Cons: + - The `Policy` entity is using a hibernate annotation with a custom `PostgresSQLEnumType` processor to assist hibernate in supporting Postgres enum types. + + diff --git a/README.md b/README.md index 34d259837..e006c596a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ General Availability

+[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=U3dLZnRFNWI2MWNFY2NGcXVtVTB3WDcyU2dPVjlVeEFYUEdxUnpYZlhrUT0tLTFzY0taYTA0MVFEa3ErNkRZdTBRWVE9PQ==--690f89a41a0eedf7b4975bd7df2eac162e04e775% )](https://www.browserstack.com/automate/public-build/U3dLZnRFNWI2MWNFY2NGcXVtVTB3WDcyU2dPVjlVeEFYUEdxUnpYZlhrUT0tLTFzY0taYTA0MVFEa3ErNkRZdTBRWVE9PQ==--690f89a41a0eedf7b4975bd7df2eac162e04e775%) [![Build Status](https://travis-ci.org/overture-stack/ego.svg?branch=master)](https://travis-ci.org/overture-stack/ego) [![CircleCI](https://circleci.com/gh/overture-stack/ego/tree/develop.svg?style=svg)](https://circleci.com/gh/overture-stack/ego/tree/develop) [![Slack](http://slack.overture.bio/badge.svg)](http://slack.overture.bio) @@ -23,6 +24,8 @@ - [Step 2 - Run](#step-2---run) - [Tech Specifications](#tech-specification) - [Usage](#usage) +- [Shoutouts](#shoutouts) + - [Browserstack](#browserstack) ## Introduction @@ -91,13 +94,13 @@ Database migrations and versioning is managed by [flyway](https://flywaydb.org/) Get current version information: ```bash -./flyway -configFiles=/ego/src/main/resources/flyway/conf/flyway.conf -locations=filesystem:/ego/src/main/resources/flyway/sql info +./fly ``` Run outstanding migrations: ```bash -./flyway -configFiles=/ego/src/main/resources/flyway/conf/flyway.conf -locations=filesystem:/ego/src/main/resources/flyway/sql migrate +./fly migrate ``` To see the migration naming convention, [click here.](https://flywaydb.org/documentation/migrations#naming) @@ -163,7 +166,6 @@ An example ego JWT is mentioned below: ``` #### Notes - - "aud" field can contain one or more client IDs. This field indicates the client services that are authorized to use this JWT. - "groups" will differ based on the domain of client services - each domain of service should get list of groups from that domain's ego service. - "permissions" will differ based on domain of client service - each domain of service should get list of permissions from that domain's ego service. @@ -204,3 +206,11 @@ curl example: -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=client_credentials&client_id=my-app-id&client_secret=secretpassword' ``` + +## Shoutouts + +### Browserstack +Many thanks to [Browserstack](https://www.browserstack.com/) for giving our test capabilities a powerup! +![Browserstack](https://p14.zdusercontent.com/attachment/1015988/qyPFNKIZXCbr4qKjd5oxrayZc?token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..-yKqMwgZKdCDJZmW2kcVYw.IKGbK6GBbU3uZ3B7Vapw8uZeQ-uhXDV9kANtz5OOOBl0Ceg6Oi1gS5wqBnStOsCKgb3CibgGIrYjk-odWPwaL9Ei0u3OIDuBldkxF6aJ6eGtC9G4LfbDLGtOnYkUiANvx5HNPb7HZa3QyivKxCcX_MjO5U01F0WbmJajfYBsFVHHLtO0dBqFz-eWZMmLB0yfjZEaVPAUfLk9H1TO4c6Vw91Or29FrzaoGDQmvmcP7Pg00LMoxuaLxGJuuOiUlEe6OunidzxRd_elUZxMJ_caonvHEjSCkq_yHilG67tGewY.IV6Qg9p5vE0TGk59pqZtRg) + + diff --git a/build b/build new file mode 100755 index 000000000..35b787af9 --- /dev/null +++ b/build @@ -0,0 +1,3 @@ +#!/bin/sh +./fly migrate +mvn clean package diff --git a/docker-compose.yml b/docker-compose.yml index b77648e93..3bbf3baaa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,8 +25,6 @@ services: environment: - POSTGRES_DB=ego - POSTGRES_PASSWORD=password - volumes: - - ./src/main/resources/schemas/01-psql-schema.sql:/docker-entrypoint-initdb.d/init.sql expose: - "5432" ports: diff --git a/docs/conf.py b/docs/conf.py index 0d9ed79f3..001666906 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,9 @@ # built documents. # # The short X.Y version. -version = '0.0.1' +version = '0.1.0' # The full version, including alpha/beta/rc tags. -release = '0.0.1' +release = '0.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index 7d2bf98d6..fef572200 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,47 +3,43 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -=============================== -Welcome to Ego's documentation! -=============================== - -Ego is an OAuth2 based Authorization Provider microservice. It is designed to allow users to log in with social logins such as Google and Facebook. - -Users, Groups, and Applications can be managed through Ego and allows for stateless authorization of user actions by client applications through the issuing of JWT Bearer tokens and the publishing of a public key for the verification of tokens. - -.. image:: ego-arch.png +============ +Ego Documentation +============ .. toctree:: - :maxdepth: 2 + :maxdepth: 4 + :caption: First Steps -Installation -============ -The easiest way to get up and running is with docker. - - **docker pull overture/ego** + src/introduction.rst + src/gettingstarted.rst -Otherwise, you can build from source. The prerequisites are Java 8 and Maven. +.. toctree:: + :maxdepth: 4 + :caption: User Documentation -.. code-block:: bash + src/admins.rst + src/appdevelopers.rst + src/tokens.rst - git clone https://github.com/overture-stack/ego.git - cd ego - mvn clean package +.. toctree:: + :maxdepth: 4 + :caption: Developer Documentation + src/installation.rst + src/architecture.rst + src/technology.rst + src/contribution.rst -Documentation -============= -.. toctree:: - :maxdepth: 4 - - src/quickstart - src/technology +Contribute +------------ +If you'd like to contribute to this project, it's hosted on github. +See https://github.com/overture-stack/ego Indices and tables ================== * :ref:`genindex` -* :ref:`modindex` * :ref:`search` diff --git a/docs/src/admins.rst b/docs/src/admins.rst new file mode 100644 index 000000000..0a7ac4145 --- /dev/null +++ b/docs/src/admins.rst @@ -0,0 +1,35 @@ +======================= +Ego for Administrators +======================= + +Tutorial +====================== + +To administer Ego, the admin must: + +**1. Install Ego.** + + View the installation instructions. + +**2. Insert a new user with the admin’s Oauth Id into the “egousers” table, with role ADMIN.** + +**3. A developer creates a new Ego-aware application** + + a. Admin creates a new application in Ego with the client_id and password. + b. Admin creates new policies with new policy names + c. Admin assigns permissions to users/groups to permit/deny them access to the new application and policies + +**4. Admin creates or deletes groups, assigns user/group permissions, revoke tokens, etc. as necessary.** + + For example, an administrator might want to: + + - Create a new group called **“QA”**, whose members are all the people in the “QA department” + - Create a group called “Access Denied” with access level “DENY” set for every policy in Ego + - Grant another user administrative rights (role ADMIN) + - Add a former employee to the group “AccessDenied”, and revoke all of their active tokens. + - In general, manage permissions and access controls within Ego. + +Using the Admin Portal +====================== + +Ego provides an intuitive GUI for painless user management. diff --git a/docs/src/appdevelopers.rst b/docs/src/appdevelopers.rst new file mode 100644 index 000000000..777e5e830 --- /dev/null +++ b/docs/src/appdevelopers.rst @@ -0,0 +1,13 @@ +================================ +Ego for Application Developers +================================ + +To create an Ego-aware application, a developer must: + +1. Pick a unique policy name for each type of authorization that the application requires. + +2. Write the application. Ensure that the application does it’s authorization by performing a call to Ego’s “check_token” REST endpoint, and only grants access to the service for the user id returned by “check_token” if the permissions returned by “check_token” include the required permission. + +3. Configure the program with a meaningful client_id and a secret password. + +4. Give the client_id, password, and policy names to an Ego administrator, and ask them to configure Ego for you. diff --git a/docs/src/architecture.rst b/docs/src/architecture.rst new file mode 100644 index 000000000..867e9bb60 --- /dev/null +++ b/docs/src/architecture.rst @@ -0,0 +1,2 @@ +Architecture +============================ diff --git a/docs/src/contribution.rst b/docs/src/contribution.rst new file mode 100644 index 000000000..cd3b3c248 --- /dev/null +++ b/docs/src/contribution.rst @@ -0,0 +1,2 @@ +Contributing to the Ego Project +============================ diff --git a/docs/src/gettingstarted.rst b/docs/src/gettingstarted.rst new file mode 100644 index 000000000..e4b564921 --- /dev/null +++ b/docs/src/gettingstarted.rst @@ -0,0 +1,84 @@ +Getting Started +============================ + +The easiest way to understand EGO, is to simply use it! + +Below is a description of how to get Ego quickly up and running, as well as a description of how Ego works and some important terms. + +Quick Start +---------------------------------------------------- + +The goal of this quick start is to get a working application quickly up and running. + +Using `Docker `_: + +1. Download the latest version of Ego. +2. From the Ego root directory, set the API_HOST_PORT where Ego is to be run, then run `docker-compose `_: + +.. code-block:: python + + $ API_HOST_PORT=8080 docker-compose up -d + +Ego should now be deployed locally with the Swagger UI at http://localhost:8080/swagger-ui.html + +Alternatively, see the `Installation instructions `_. + + +How Ego Works +------------------------------------------- +**1. An Ego administrator configures Ego.** + - Registers a unique client-id and application password for each application that will use Ego for Authorization. + - Creates a policy for every authorization scope that an application will use. + - Registers users and groups, and sets them up with appropriate permissions for policies and applications. + + +**2. Ego grants secret authorization tokens to individual users to represent their permissions.** + - Authorization tokens expire, and can be revoked if compromised. + - Individuals can issue tokens for part or all of their authority, and can limit the authority to specific applications. + - Users (and programs operating on their behalf) can then use these tokens to access services. + +**3. Individual services make a REST call to EGO to determine the user and authority represented by a token.** + - Makes a call to Ego's check_token endpoint and validates the user's authorization to access the requested services. + + +Terms Used in Ego +------------------------------------------- + +.. image :: terms.png + +.. glossary:: + + User + A user is any individual registered in Ego who needs to authorize themselves with Ego-aware applications. + + Admin + An admin is a power user whose role is set to 'ADMIN'. Only admins are authorized to register users, groups, applications & policies using Ego's REST endpoints. + + Group + A group of users with similar properties. Admins can create new groups and add users to them. They can then assign permissions to an entire group which will be reflected for each user in that group. + + Policy + A policy is a scope or context for which an application may want to grant a user or group READ/WRITE/DENY permissions. + + Permission + A user or group can be given READ/WRITE/DENY permissions for a particular policy. + + Application + An application is a third party service that registers itself with EGO so that EGO can authorize users on its behalf. Upon registration, the service must provide a client_id and client secret. + + Application Authentication Token + This a Basic JWT token which encodes a client id and secret, and authorizes an application to interact with Ego. This is passed in the authorization request header when an application uses the check_token endpoint in order to check a user's token. + + User Authentication Token + This is a Bearer token which encodes user information, and is passed to a user when they are authenticated through OAuth single sign-on. This Bearer token is passed in the request authorization header whenever the user wants to access Ego's resources. + If the JWT denotes that a user has an ADMIN role, they are permitted to create and modify resources (users, groups, permissions, policies). + + User Authorization Token + This is a random token which is generated to authorize a user for a specific scope, in the context of an application. + + +Play with the REST API from your browser +-------------------------------------------- +If you want to play with EGO from your browser, you can visit the Swagger UI located here : + +https://ego.overture.cancercollaboratory.org/swagger-ui.html diff --git a/docs/src/quickstart.rst b/docs/src/installation.rst similarity index 73% rename from docs/src/quickstart.rst rename to docs/src/installation.rst index 0437d05e0..fc91a5383 100644 --- a/docs/src/quickstart.rst +++ b/docs/src/installation.rst @@ -1,7 +1,7 @@ -Quick Start -=========== +.. _installation: -The goal of this quick start is to get a working application quickly up and running. +Installation +============================ Step 1 - Setup Database ----------------------- @@ -10,6 +10,11 @@ Step 1 - Setup Database 2. Create a Database: ego with user postgres and empty password 3. Execute SQL Script to setup tables. +Database Migrations with Flyway +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Database migrations and versioning is managed by `flyway `_. + Step 2 - Run ------------ @@ -23,7 +28,7 @@ Run using Maven. Maven can be used to prepare a runnable jar file, as well as th .. code-block:: bash - $ mvn clean package + $ mvn clean package ; ./fly migrate To run from command line with maven: diff --git a/docs/src/introduction.rst b/docs/src/introduction.rst new file mode 100644 index 000000000..20fa8c4dd --- /dev/null +++ b/docs/src/introduction.rst @@ -0,0 +1,47 @@ +============== +Introduction +============== + + +What is Ego? +============= + +`EGO `_ is an OAuth2 based authentication and authorization management microservice. It allows users to login and authenticate themselves using their existing logins from sites such as Google and Facebook, create and manage authorization tokens, and use those tokens to interact with Ego-aware third party applications which they are authorized for. + +OAuth single sign-on means that Ego does not need to manage users and their passwords; and similarly, none of the services that use Ego need to worry about how to manage users, logins, authentication or authorization. The end user simply sends them a token, and the service checks with Ego to learn who the token is for, and what permissions the token grants. +EGO is one of many products provided by `Overture `_ and is completely open-source and free for everyone to use. + +.. seealso:: + + For additional information on other products in the Overture stack, please visit https://overture.bio + +.. _introduction_features: + +Features +=========== + +- Single sign-on for microservices +- User authentication through federated identities such as Google, Facebook, Linkedin, Github (Coming Soon), ORCID (Coming Soon) +- Provides stateless authorization using `JSON Web Tokens (JWT) `_ +- Can scale very well to large number of users +- Provides ability to create permission lists for users and/or groups on user-defined permission entities +- Standard REST API that is easy to understand and work with +- Interactive documentation of the API is provided using Swagger UI. When run locally, this can be found at : http://localhost:8080/swagger-ui.html +- Built using well established Frameworks - Spring Boot, Spring Security + +License +========== +Copyright (c) 2018. Ontario Institute for Cancer Research + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see https://www.gnu.org/licenses. diff --git a/docs/src/jwt.png b/docs/src/jwt.png new file mode 100644 index 000000000..7feb70b7d Binary files /dev/null and b/docs/src/jwt.png differ diff --git a/docs/src/jwt.rst b/docs/src/jwt.rst deleted file mode 100644 index aad6755e1..000000000 --- a/docs/src/jwt.rst +++ /dev/null @@ -1,53 +0,0 @@ -JSON Web Token -============== - -Basics ------- - -Ego makes use of JSON Web Tokens (JWTs) for providing users with a Bearer token. - -The RFC for JWTs can be found here: https://tools.ietf.org/html/rfc7519 - -The following is a useful site for understanding JWTs: https://jwt.io/ - - -The following is the structure of an ego JWT: - -.. code-block:: guess - - { - "alg": "HS512" - } - . - { - "sub": "1234567", - "iss": "ego:56fc3842ccf2c1c7ec5c5d14", - "iat": 1459458458, - "exp": 1459487258, - "jti": "56fd919accf2c1c7ec5c5d16", - "aud": [ - "service1-id", - "service2-id", - "service3-id" - ], - "context": { - "user": { - "name": "Demo.User@example.com", - "email": "Demo.User@example.com", - "status": "Approved", - "firstName": "Demo", - "lastName": "User", - "createdAt": "2017-11-23 10:24:41", - "lastLogin": "2017-11-23 11:23:58", - "preferredLanguage": null, - "roles": ["ADMIN"] - } - } - } - . - [signature] - -Library Support ---------------- - -The Java JWT library is used in Ego for providing support for encoding, decoding, and validating JWTs: https://github.com/jwtk/jjwt diff --git a/docs/src/spring.rst b/docs/src/spring.rst deleted file mode 100644 index 21e3ab7c7..000000000 --- a/docs/src/spring.rst +++ /dev/null @@ -1,12 +0,0 @@ -Spring-Boot -=========== - -Ego is a Microservice written in Java 8 and Spring-Boot. -It makes use of the following parts of the Spring and Spring-Boot framework: - -- Web / Spring MVC w/ embedded tomcat -- Security -- JDBC -- Spring Data JPA - -Swagger docs are generated by Springfox. https://springfox.github.io/springfox/docs/current/ diff --git a/docs/src/technology.rst b/docs/src/technology.rst index ca98e4904..e36f1cb4e 100644 --- a/docs/src/technology.rst +++ b/docs/src/technology.rst @@ -1,9 +1,96 @@ -Technology -========== +Technology Stack +============================ -.. toctree:: - :maxdepth: 2 +This application is written in JAVA using Spring Boot and the Spring Security Frameworks. - jwt - spring - \ No newline at end of file +JSON Web Token +--------------- + +Basics +^^^^^^^ + +Ego makes use of JSON Web Tokens (JWTs) for providing users with a Bearer token. + +The RFC for JWTs can be found here: https://tools.ietf.org/html/rfc7519 + +The following is a useful site for understanding JWTs: https://jwt.io/ + + +The following is the structure of an ego JWT: + +.. code-block:: guess + + { + "alg": "HS512" + } + . + { + "sub": "1234567", + "iss": "ego:56fc3842ccf2c1c7ec5c5d14", + "iat": 1459458458, + "exp": 1459487258, + "jti": "56fd919accf2c1c7ec5c5d16", + "aud": [ + "service1-id", + "service2-id", + "service3-id" + ], + "context": { + "user": { + "name": "Demo.User@example.com", + "email": "Demo.User@example.com", + "status": "Approved", + "firstName": "Demo", + "lastName": "User", + "createdAt": "2017-11-23 10:24:41", + "lastLogin": "2017-11-23 11:23:58", + "preferredLanguage": null, + "roles": ["ADMIN"] + } + } + } + . + [signature] +Notes + - "aud" field can contain one or more client IDs. This field indicates the client services that are authorized to use this JWT. + - "groups" will differ based on the domain of client services - each domain of service should get list of groups from that domain's ego service. + - "permissions" will differ based on domain of client service - each domain of service should get list of permissions from that domain's ego service. + Unit Tests using testcontainers will also run flyway migrations to ensure database has the correct structure + +Library Support +^^^^^^^^^^^^^^^^ + +The Java JWT library is used in Ego for providing support for encoding, decoding, and validating JWTs: https://github.com/jwtk/jjwt + +Spring-Boot +------------ + +Ego is a microservice written in Java 8 and Spring-Boot. +It makes use of the following parts of the Spring and Spring-Boot framework: + +- Web / Spring MVC w/ embedded tomcat +- `Spring Security `_ +- JDBC +- Spring Data JPA + +Swagger docs are generated by Springfox : https://springfox.github.io/springfox/docs/current/ + +Ego Design Notes +----------------- + +1. OAuth Single Sign-On means that Ego doesn't need to manage users and their passwords; users don't need a new username or password, and don't need to trust any service other than Google / Facebook. + +2. Ego lets users be in charge of the authority they give out; so they can issue secret tokens that are limited to + the exact authority level they need to do a given task. + + Even if a such a token becomes publicly known, it can't grant an outsider accesses to services or permissions + that the token doesn't have -- regardless of whether the user has more authority that they could have granted. + + Tokens also automatically expire (by default, within 24 hours), and if a user suspects that a token may have + become known to outsiders, they can simply revoke the compromised token, removing all of it's authority, + then issue themselves a new secret token, and use it. + +3. None of the services that use Ego uses need to manage worry about how to manage users, logins, authentication, + or authorization. The end user simply sends them a token, and the service checks with Ego to learn who the + token is for, and what permissions the token grants. If the permissions granted don't include the permissions + the service needs, it denies access; otherwise, it runs the service for the given user. diff --git a/docs/src/terms.png b/docs/src/terms.png new file mode 100644 index 000000000..8d1d21ed7 Binary files /dev/null and b/docs/src/terms.png differ diff --git a/docs/src/tokens.rst b/docs/src/tokens.rst new file mode 100644 index 000000000..e807a93aa --- /dev/null +++ b/docs/src/tokens.rst @@ -0,0 +1,40 @@ +Tokens +============================ + +User Authentication Tokens +---------------------------------------------------- +Authentication concerns *who the user is*. + +User Authentication tokens are used to verify a user’s identity. + +Ego’s User Authentication tokens are signed JSON Web Tokens (see http://jwt.io) that Ego issues when a user successfully logs into Ego using their Google or Facebook credentials. + +Ego's authentication tokens confirm the user’s identity, and contain information about a user’s name, their role (user/administrator), and any applications, permissions, and groups associated with their Ego account etc. + +This data is current as of the time the token is issued, and the token is digitally signed by Ego with a publicly available signing key that applications have to use to verify that an authentication token is valid. Most of Ego’s REST endpoints require an Ego authentication token to be provided in the authorization header, in order to validate the user’s identity before operating on their data. + +.. image :: jwt.png + +User Authorization Tokens +---------------------------------------------------- +Authorization concerns *what a user is allowed to do*. + +User Authorization tokens are used to verify a user's permissions to execute on a desired scope. + +Ego’s User Authorization tokens are random numbers that Ego issues to users so they can interact with Ego-aware applications with a chosen level of authority. + +Each token is a unique secret password that is associated with a specific user, permissions, and optionally, an allowed set of applications. + +Unlike passwords, Authorization tokens automatically expire, and they can be revoked if the user suspects that they have been compromised. + +The user can then use their token with Ego-authorized applications as proof of who they are and what they are allowed to do. Typically, the user will configure a client program (such as SING, the client program used with SONG, the ICGC Metadata management service) with their secret token, and the program will then operate with the associated level of authority. + +In more detail, when an Ego-aware application wants to know if it is authorized to do something on behalf of a given user, it just sends their user authorization token to Ego, and gets back the associated information about who the user is (their user id), and what they are allowed to do (the permissions associated with their token). If the permissions that the user have include the permission the application wants, the application know it is authorized to perform the requested service on behalf of the user. + + +Application Authentication Tokens +---------------------------------------------------- + +For security reasons, applications need to be able to prove to Ego that they are the legitimate applications that Ego has been configured to work with. + +For this reason, every Ego-aware application must be configured in Ego with it’s own unique CLIENT ID and CLIENT SECRET, and the application must send a token with this information to Ego whenever it makes a request to get the identity and credentials associated with a user’s authorization token. diff --git a/fly b/fly new file mode 100755 index 000000000..3f342256e --- /dev/null +++ b/fly @@ -0,0 +1,8 @@ +#!/bin/bash +CMD=${1:-info} +CONFIG_FILE=${2:-`pwd`/src/main/resources/flyway/conf/flyway.conf} + +# To debug, use this line instead... +# mvn flyway:${CMD} -X -Dflyway.configFile=$CONFIG_FILE + +mvn "flyway:$CMD" -Dflyway.configFiles=${CONFIG_FILE} diff --git a/mvnw b/mvnw index 5bf251c07..5551fde8e 100755 --- a/mvnw +++ b/mvnw @@ -108,7 +108,7 @@ if $cygwin ; then CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" @@ -200,8 +200,69 @@ if [ -z "$BASE_DIR" ]; then exit 1; fi +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java diff --git a/mvnw.cmd b/mvnw.cmd old mode 100644 new mode 100755 index 019bd74d7..48363fa60 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,143 +1,161 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml index b8e67fe61..d8f9650aa 100644 --- a/pom.xml +++ b/pom.xml @@ -3,9 +3,9 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.overture + bio.overture ego - 1.2.3-SNAPSHOT + 2.0.0-SNAPSHOT ego OAuth 2.0 Authorization service that supports multiple OpenID Connect Providers @@ -21,6 +21,7 @@ UTF-8 UTF-8 1.8 + 1.2.0.Final @@ -31,7 +32,12 @@ org.springframework.security.oauth spring-security-oauth2 - 2.0.16.RELEASE + 2.0.17.RELEASE + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + 2.0.2.RELEASE org.springframework.boot @@ -56,6 +62,12 @@ springfox-swagger2 2.6.1 compile + + + org.mapstruct + mapstruct + + io.springfox @@ -81,6 +93,7 @@ org.springframework.security spring-security-jwt + 1.0.8.RELEASE @@ -142,7 +155,7 @@ com.google.api-client google-api-client - 1.22.0 + 1.23.0 @@ -155,7 +168,37 @@ com.fasterxml.jackson.core jackson-databind - 2.9.5 + 2.9.8 + + + + + org.assertj + assertj-core + 3.11.1 + test + + + net.javacrumbs.json-unit + json-unit-fluent + 2.1.1 + test + + + ca.andricdu + selenium-shaded + 3.141.59 + + + org.apache.commons + commons-exec + 1.3 + + + com.browserstack + browserstack-local-java + 0.1.0 + test @@ -180,27 +223,39 @@ - + - io.projectreactor - reactor-bus - 2.0.8.RELEASE + org.mapstruct + mapstruct-jdk8 + ${mapstruct.version} - io.projectreactor - reactor-core - 2.0.8.RELEASE + org.mapstruct + mapstruct-processor + ${mapstruct.version} + - org.springframework.security - spring-security-jwt - 1.0.8.RELEASE + org.springframework.boot + spring-boot-devtools + true - + + com.coveo + fmt-maven-plugin + 2.8 + + + + format + + + + org.springframework.boot spring-boot-maven-plugin @@ -257,6 +312,10 @@ + + dcc-dependencies + https://artifacts.oicr.on.ca/artifactory/dcc-dependencies + spring-snapshots Spring Snapshots diff --git a/src/main/assembly/bin.xml b/src/main/assembly/bin.xml index 668a412d5..7623db13f 100644 --- a/src/main/assembly/bin.xml +++ b/src/main/assembly/bin.xml @@ -1,5 +1,6 @@ - dist diff --git a/src/main/conf/logback.xml b/src/main/conf/logback.xml index 35c778e3f..b8ec7000c 100644 --- a/src/main/conf/logback.xml +++ b/src/main/conf/logback.xml @@ -19,9 +19,9 @@ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF S - - - + + + true @@ -54,18 +54,18 @@ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF S - + - + - + - + diff --git a/src/main/conf/wrapper.conf b/src/main/conf/wrapper.conf index 8298a2c2d..7327dc70d 100644 --- a/src/main/conf/wrapper.conf +++ b/src/main/conf/wrapper.conf @@ -30,40 +30,40 @@ #******************************************************************** # Locate the java binary on the system PATH: -wrapper.java.command=java +wrapper.java.command = java # Java Main class. This class must implement the WrapperListener interface -wrapper.java.mainclass=org.tanukisoftware.wrapper.WrapperSimpleApp +wrapper.java.mainclass = org.tanukisoftware.wrapper.WrapperSimpleApp # Java Classpath (include wrapper.jar) Add class path elements as needed starting from 1 -wrapper.java.classpath.1=../lib/ego.jar -wrapper.java.classpath.2=../lib/wrapper.jar +wrapper.java.classpath.1 = ../lib/ego.jar +wrapper.java.classpath.2 = ../lib/wrapper.jar # Java Library Path (location of Wrapper.DLL or libwrapper.so) -wrapper.java.library.path.1=../lib +wrapper.java.library.path.1 = ../lib # Java Bits. On applicable platforms, tells the JVM to run in 32 or 64-bit mode. -wrapper.java.additional.auto_bits=TRUE +wrapper.java.additional.auto_bits = TRUE # Java Additional Parameters -wrapper.java.additional.1=-Dlog.path=../logs -wrapper.java.additional.2=-Dcom.sun.management.jmxremote.port=10015 -wrapper.java.additional.3=-Dcom.sun.management.jmxremote.ssl=false -wrapper.java.additional.4=-Dcom.sun.management.jmxremote.authenticate=false -wrapper.java.additional.5=-Djava.security.egd=file:/dev/./urandom +wrapper.java.additional.1 = -Dlog.path=../logs +wrapper.java.additional.2 = -Dcom.sun.management.jmxremote.port=10015 +wrapper.java.additional.3 = -Dcom.sun.management.jmxremote.ssl=false +wrapper.java.additional.4 = -Dcom.sun.management.jmxremote.authenticate=false +wrapper.java.additional.5 = -Djava.security.egd=file:/dev/./urandom # Initial Java Heap Size (in MB) #wrapper.java.initmemory=3 # Maximum Java Heap Size (in MB) -wrapper.java.maxmemory=8192 +wrapper.java.maxmemory = 8192 # Application parameters. Add parameters as needed starting from 1 -wrapper.app.parameter.1=org.springframework.boot.loader.JarLauncher -wrapper.app.parameter.2=--spring.config.location=../conf/ -wrapper.app.parameter.3=--logging.config=../conf/logback.xml -wrapper.app.parameter.4=--spring.profiles.active=auth -wrapper.app.parameter.5=--token.key-store=src/main/resources/ego-jwt.jks +wrapper.app.parameter.1 = org.springframework.boot.loader.JarLauncher +wrapper.app.parameter.2 = --spring.config.location=../conf/ +wrapper.app.parameter.3 = --logging.config=../conf/logback.xml +wrapper.app.parameter.4 = --spring.profiles.active=auth +wrapper.app.parameter.5 = --token.key-store=src/main/resources/ego-jwt.jks #******************************************************************** # Wrapper Logging Properties @@ -72,58 +72,58 @@ wrapper.app.parameter.5=--token.key-store=src/main/resources/ego-jwt.jks # wrapper.debug=TRUE # Format of output for the console. (See docs for formats) -wrapper.console.format=PM +wrapper.console.format = PM # Log Level for console output. (See docs for log levels) -wrapper.console.loglevel=INFO +wrapper.console.loglevel = INFO # Log file to use for wrapper output logging. -wrapper.logfile=../logs/wrapper.YYYYMMDD.log +wrapper.logfile = ../logs/wrapper.YYYYMMDD.log # Format of output for the log file. (See docs for formats) -wrapper.logfile.format=LPTM +wrapper.logfile.format = LPTM # Log Level for log file output. (See docs for log levels) -wrapper.logfile.loglevel=INFO +wrapper.logfile.loglevel = INFO # Maximum number of rolled log files which will be allowed before old # files are deleted. The default value of 0 implies no limit. -wrapper.logfile.maxfiles=0 +wrapper.logfile.maxfiles = 0 # The roll mode of the log file -wrapper.logfile.rollmode=DATE +wrapper.logfile.rollmode = DATE # Log Level for sys/event log output. (See docs for log levels) -wrapper.syslog.loglevel=NONE +wrapper.syslog.loglevel = NONE #******************************************************************** # Wrapper General Properties #******************************************************************** # Allow for the use of non-contiguous numbered properties -wrapper.ignore_sequence_gaps=TRUE +wrapper.ignore_sequence_gaps = TRUE # Do not start if the pid file already exists. -wrapper.pidfile.strict=TRUE +wrapper.pidfile.strict = TRUE # Title to use when running as a console -wrapper.console.title=EGO Server +wrapper.console.title = EGO Server #******************************************************************** # Wrapper JVM Checks #******************************************************************** # Detect DeadLocked Threads in the JVM. (Requires Standard Edition) -wrapper.check.deadlock=TRUE -wrapper.check.deadlock.interval=10 -wrapper.check.deadlock.action=RESTART -wrapper.check.deadlock.output=FULL +wrapper.check.deadlock = TRUE +wrapper.check.deadlock.interval = 10 +wrapper.check.deadlock.action = RESTART +wrapper.check.deadlock.output = FULL # Out Of Memory detection. # (Ignore output from dumping the configuration to the console. This is only needed by the TestWrapper sample application.) -wrapper.filter.trigger.999=wrapper.filter.trigger.*java.lang.OutOfMemoryError -wrapper.filter.allow_wildcards.999=TRUE -wrapper.filter.action.999=NONE +wrapper.filter.trigger.999 = wrapper.filter.trigger.*java.lang.OutOfMemoryError +wrapper.filter.allow_wildcards.999 = TRUE +wrapper.filter.action.999 = NONE # Ignore -verbose:class output to avoid false positives. -wrapper.filter.trigger.1000=[Loaded java.lang.OutOfMemoryError +wrapper.filter.trigger.1000 = [Loaded java.lang.OutOfMemoryError wrapper.filter.action.1000=NONE # (Simple match) wrapper.filter.trigger.1001=java.lang.OutOfMemoryError @@ -142,4 +142,4 @@ wrapper.filter.trigger.1000=[Loaded java.lang.OutOfMemoryError wrapper.filter.message.1002=An application restart request has been detected. wrapper.shutdown.timeoutMs=60 - wrapper.ping.timeoutMs.action=DUMP,RESTART \ No newline at end of file + wrapper.ping.timeoutMs.action=DUMP, RESTART \ No newline at end of file diff --git a/src/main/java/org/overture/ego/AuthorizationServiceMain.java b/src/main/java/bio/overture/ego/AuthorizationServiceMain.java similarity index 97% rename from src/main/java/org/overture/ego/AuthorizationServiceMain.java rename to src/main/java/bio/overture/ego/AuthorizationServiceMain.java index c8b74725c..1fbc9b94a 100644 --- a/src/main/java/org/overture/ego/AuthorizationServiceMain.java +++ b/src/main/java/bio/overture/ego/AuthorizationServiceMain.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.overture.ego; +package bio.overture.ego; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/org/overture/ego/config/AuthConfig.java b/src/main/java/bio/overture/ego/config/AuthConfig.java similarity index 71% rename from src/main/java/org/overture/ego/config/AuthConfig.java rename to src/main/java/bio/overture/ego/config/AuthConfig.java index 9e623ce79..5957ce382 100644 --- a/src/main/java/org/overture/ego/config/AuthConfig.java +++ b/src/main/java/bio/overture/ego/config/AuthConfig.java @@ -14,20 +14,21 @@ * limitations under the License. */ -package org.overture.ego.config; +package bio.overture.ego.config; +import bio.overture.ego.security.CorsFilter; +import bio.overture.ego.service.ApplicationService; +import bio.overture.ego.service.TokenService; +import bio.overture.ego.token.CustomTokenEnhancer; +import bio.overture.ego.token.signer.TokenSigner; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.TimeZone; import lombok.extern.slf4j.Slf4j; -import org.overture.ego.security.CorsFilter; -import org.overture.ego.service.ApplicationService; -import org.overture.ego.token.CustomTokenEnhancer; -import org.overture.ego.token.signer.TokenSigner; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; @@ -40,23 +41,14 @@ import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.TimeZone; - @Slf4j @Configuration @EnableAuthorizationServer public class AuthConfig extends AuthorizationServerConfigurerAdapter { - @Autowired - private ApplicationService clientDetailsService; - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - TokenSigner tokenSigner; + @Autowired TokenSigner tokenSigner; + @Autowired TokenService tokenService; + @Autowired private ApplicationService clientDetailsService; @Bean @Primary @@ -65,9 +57,8 @@ public CorsFilter corsFilter() { } @Bean - public SimpleDateFormat formatter(){ - SimpleDateFormat formatter = - new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); + public SimpleDateFormat formatter() { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); formatter.setTimeZone(TimeZone.getTimeZone("UTC")); return formatter; } @@ -77,15 +68,10 @@ public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); - if(tokenSigner.getKeyPair().isPresent()) { + if (tokenSigner.getKeyPair().isPresent()) { converter.setKeyPair(tokenSigner.getKeyPair().get()); } return converter; @@ -101,8 +87,7 @@ public DefaultTokenServices tokenServices() { } @Override - public void configure(ClientDetailsServiceConfigurer clients) - throws Exception { + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); } @@ -112,19 +97,18 @@ public TokenEnhancer tokenEnhancer() { } @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + public void configure(AuthorizationServerEndpointsConfigurer endpoints) { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); - tokenEnhancerChain.setTokenEnhancers( - Arrays.asList(tokenEnhancer())); - endpoints.tokenStore(tokenStore()) - .tokenEnhancer(tokenEnhancerChain) - .accessTokenConverter(accessTokenConverter()); - endpoints.authenticationManager(this.authenticationManager); + tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(tokenEnhancer())); + endpoints + .tokenStore(tokenStore()) + .tokenEnhancer(tokenEnhancerChain) + .accessTokenConverter(accessTokenConverter()); } @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { + public void configure(AuthorizationServerSecurityConfigurer security) { security.allowFormAuthenticationForClients(); } } diff --git a/src/main/java/bio/overture/ego/config/EncoderConfig.java b/src/main/java/bio/overture/ego/config/EncoderConfig.java new file mode 100644 index 000000000..3d300006c --- /dev/null +++ b/src/main/java/bio/overture/ego/config/EncoderConfig.java @@ -0,0 +1,15 @@ +package bio.overture.ego.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class EncoderConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/bio/overture/ego/config/OAuth2ClientConfig.java b/src/main/java/bio/overture/ego/config/OAuth2ClientConfig.java new file mode 100644 index 000000000..6db5b89dd --- /dev/null +++ b/src/main/java/bio/overture/ego/config/OAuth2ClientConfig.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.config; + +import bio.overture.ego.security.OAuth2ClientResources; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OAuth2ClientConfig { + + @Bean + @ConfigurationProperties("google") + public OAuth2ClientResources google() { + return new OAuth2ClientResources(); + } + + @Bean + @ConfigurationProperties("facebook") + public OAuth2ClientResources facebook() { + return new OAuth2ClientResources(); + } + + @Bean + @ConfigurationProperties("github") + public OAuth2ClientResources github() { + return new OAuth2ClientResources(); + } + + @Bean + @ConfigurationProperties("linkedin") + public OAuth2ClientResources linkedin() { + return new OAuth2ClientResources(); + } +} diff --git a/src/main/java/bio/overture/ego/config/RequestLoggingFilterConfig.java b/src/main/java/bio/overture/ego/config/RequestLoggingFilterConfig.java new file mode 100644 index 000000000..d119f99cd --- /dev/null +++ b/src/main/java/bio/overture/ego/config/RequestLoggingFilterConfig.java @@ -0,0 +1,20 @@ +package bio.overture.ego.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; + +@Configuration +public class RequestLoggingFilterConfig { + + @Bean + public CommonsRequestLoggingFilter logFilter() { + CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); + filter.setIncludeQueryString(true); + filter.setIncludePayload(true); + filter.setMaxPayloadLength(10000); + filter.setIncludeHeaders(false); + filter.setIncludeClientInfo(true); + return filter; + } +} diff --git a/src/main/java/bio/overture/ego/config/SecureServerConfig.java b/src/main/java/bio/overture/ego/config/SecureServerConfig.java new file mode 100644 index 000000000..8bb19497e --- /dev/null +++ b/src/main/java/bio/overture/ego/config/SecureServerConfig.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.config; + +import bio.overture.ego.security.AuthorizationManager; +import bio.overture.ego.security.JWTAuthorizationFilter; +import bio.overture.ego.security.OAuth2SsoFilter; +import bio.overture.ego.security.SecureAuthorizationManager; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableOAuth2Client +@Profile("auth") +public class SecureServerConfig { + + /** Constants */ + private final String[] PUBLIC_ENDPOINTS = + new String[] { + "/oauth/token", + "/oauth/google/token", + "/oauth/facebook/token", + "/oauth/token/public_key", + "/oauth/token/verify", + "/oauth/ego-token" + }; + + /** Dependencies */ + private AuthenticationManager authenticationManager; + + private OAuth2SsoFilter oAuth2SsoFilter; + + @SneakyThrows + @Autowired + public SecureServerConfig( + AuthenticationManager authenticationManager, OAuth2SsoFilter oAuth2SsoFilter) { + this.authenticationManager = authenticationManager; + this.oAuth2SsoFilter = oAuth2SsoFilter; + } + + @Bean + @SneakyThrows + public JWTAuthorizationFilter authorizationFilter() { + return new JWTAuthorizationFilter(authenticationManager, PUBLIC_ENDPOINTS); + } + + // Do not register JWTAuthorizationFilter in global scope + @Bean + public FilterRegistrationBean jwtAuthorizationFilterRegistration(JWTAuthorizationFilter filter) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); + registration.setEnabled(false); + return registration; + } + + // Do not register OAuth2SsoFilter in global scope + @Bean + public FilterRegistrationBean oAuth2SsoFilterRegistration(OAuth2SsoFilter filter) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); + registration.setEnabled(false); + return registration; + } + + @Bean + public AuthorizationManager authorizationManager() { + return new SecureAuthorizationManager(); + } + + // Register oauth2 filter earlier so it can handle redirects signaled by exceptions in + // authentication requests. + @Bean + public FilterRegistrationBean oauth2ClientFilterRegistration( + OAuth2ClientContextFilter filter) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(filter); + registration.setOrder(-100); + return registration; + } + + // int LOWEST_PRECEDENCE = Integer.MAX_VALUE; + @Configuration + @Order(SecurityProperties.BASIC_AUTH_ORDER - 3) + public class OAuthConfigurerAdapter extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.requestMatchers() + .antMatchers("/oauth/login/*", "/oauth/ego-token") + .and() + .csrf() + .disable() + .authorizeRequests() + .anyRequest() + .permitAll() + .and() + .addFilterAfter(oAuth2SsoFilter, BasicAuthenticationFilter.class); + } + } + + @Configuration + @Order(SecurityProperties.BASIC_AUTH_ORDER + 3) + public class AppConfigurerAdapter extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf() + .disable() + .authorizeRequests() + .antMatchers( + "/", + "/favicon.ico", + "/swagger**", + "/swagger-resources/**", + "/configuration/ui", + "/configuration/**", + "/v2/api**", + "/webjars/**", + "/oauth/token/verify", + "/oauth/token/public_key") + .permitAll() + .antMatchers(HttpMethod.OPTIONS, "/**") + .permitAll() + .anyRequest() + .authenticated() + .and() + .addFilterBefore(authorizationFilter(), BasicAuthenticationFilter.class) + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + } +} diff --git a/src/main/java/org/overture/ego/config/ServerConfig.java b/src/main/java/bio/overture/ego/config/ServerConfig.java similarity index 78% rename from src/main/java/org/overture/ego/config/ServerConfig.java rename to src/main/java/bio/overture/ego/config/ServerConfig.java index 1e2be461d..eedf2c803 100644 --- a/src/main/java/org/overture/ego/config/ServerConfig.java +++ b/src/main/java/bio/overture/ego/config/ServerConfig.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.overture.ego.config; +package bio.overture.ego.config; -import org.overture.ego.security.AuthorizationManager; -import org.overture.ego.security.DefaultAuthorizationManager; +import bio.overture.ego.security.AuthorizationManager; +import bio.overture.ego.security.DefaultAuthorizationManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -38,12 +38,17 @@ public AuthorizationManager authorizationManager() { @Override protected void configure(HttpSecurity http) throws Exception { - http.csrf().disable() + http.csrf() + .disable() .authorizeRequests() - .antMatchers("/**").permitAll() - .anyRequest().authenticated().and().authorizeRequests() + .antMatchers("/**") + .permitAll() + .anyRequest() + .authenticated() .and() - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + .authorizeRequests() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } - } diff --git a/src/main/java/bio/overture/ego/config/SwaggerConfig.java b/src/main/java/bio/overture/ego/config/SwaggerConfig.java new file mode 100644 index 000000000..d4939337d --- /dev/null +++ b/src/main/java/bio/overture/ego/config/SwaggerConfig.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.val; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.paths.RelativePathProvider; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@EnableSwagger2 +@Configuration +public class SwaggerConfig { + + @Component + @ConfigurationProperties(prefix = "swagger") + class SwaggerProperties { + /** Specify host if ego is running behind proxy. */ + @Setter @Getter private String host = ""; + + /** + * If there is url write rule, you may want to set this variable. This value requires host to be + * not empty. + */ + @Setter @Getter private String baseUrl = ""; + } + + @Bean + public Docket productApi(SwaggerProperties properties) { + val docket = + new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.basePackage("bio.overture.ego.controller")) + .build() + .host(properties.host) + .pathProvider( + new RelativePathProvider(null) { + @Override + public String getApplicationBasePath() { + return properties.getBaseUrl(); + } + }) + .apiInfo(metaInfo()); + + return docket; + } + + private ApiInfo metaInfo() { + + return new ApiInfo( + "ego Service API", + "ego API Documentation", + "0.02", + "", + new Contact("", "", ""), + "Apache License Version 2.0", + ""); + } +} diff --git a/src/main/java/bio/overture/ego/config/UserDefaultsConfig.java b/src/main/java/bio/overture/ego/config/UserDefaultsConfig.java new file mode 100644 index 000000000..96358bde1 --- /dev/null +++ b/src/main/java/bio/overture/ego/config/UserDefaultsConfig.java @@ -0,0 +1,28 @@ +package bio.overture.ego.config; + +import static bio.overture.ego.model.enums.StatusType.PENDING; +import static bio.overture.ego.model.enums.StatusType.resolveStatusType; +import static bio.overture.ego.model.enums.UserType.USER; +import static bio.overture.ego.model.enums.UserType.resolveUserType; +import static org.springframework.util.StringUtils.isEmpty; + +import bio.overture.ego.model.enums.StatusType; +import bio.overture.ego.model.enums.UserType; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class UserDefaultsConfig { + + @Getter private final UserType defaultUserType; + + @Getter private final StatusType defaultUserStatus; + + public UserDefaultsConfig( + @Value("${default.user.type}") String userType, + @Value("${default.user.status}") String userStatus) { + this.defaultUserType = isEmpty(userType) ? USER : resolveUserType(userType); + this.defaultUserStatus = isEmpty(userStatus) ? PENDING : resolveStatusType(userStatus); + } +} diff --git a/src/main/java/org/overture/ego/config/WebRequestConfig.java b/src/main/java/bio/overture/ego/config/WebRequestConfig.java similarity index 82% rename from src/main/java/org/overture/ego/config/WebRequestConfig.java rename to src/main/java/bio/overture/ego/config/WebRequestConfig.java index 546b312a2..e99bcd589 100644 --- a/src/main/java/org/overture/ego/config/WebRequestConfig.java +++ b/src/main/java/bio/overture/ego/config/WebRequestConfig.java @@ -14,24 +14,23 @@ * limitations under the License. */ -package org.overture.ego.config; +package bio.overture.ego.config; -import org.overture.ego.controller.resolver.FilterResolver; -import org.overture.ego.controller.resolver.PageableResolver; -import org.overture.ego.model.enums.Fields; -import org.overture.ego.utils.FieldUtils; +import bio.overture.ego.controller.resolver.FilterResolver; +import bio.overture.ego.controller.resolver.PageableResolver; +import bio.overture.ego.model.enums.Fields; +import bio.overture.ego.utils.FieldUtils; +import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; -import java.util.List; - @Configuration public class WebRequestConfig extends WebMvcConfigurerAdapter { @Bean - public List fieldValues(){ + public List fieldValues() { return FieldUtils.getStaticFieldValueList(Fields.class); } diff --git a/src/main/java/bio/overture/ego/controller/ApplicationController.java b/src/main/java/bio/overture/ego/controller/ApplicationController.java new file mode 100644 index 000000000..42e25bb89 --- /dev/null +++ b/src/main/java/bio/overture/ego/controller/ApplicationController.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.controller; + +import static bio.overture.ego.controller.resolver.PageableResolver.LIMIT; +import static bio.overture.ego.controller.resolver.PageableResolver.OFFSET; +import static bio.overture.ego.controller.resolver.PageableResolver.SORT; +import static bio.overture.ego.controller.resolver.PageableResolver.SORTORDER; +import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.web.bind.annotation.RequestMethod.DELETE; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +import bio.overture.ego.model.dto.CreateApplicationRequest; +import bio.overture.ego.model.dto.PageDTO; +import bio.overture.ego.model.dto.UpdateApplicationRequest; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.Fields; +import bio.overture.ego.model.search.Filters; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.security.AdminScoped; +import bio.overture.ego.service.ApplicationService; +import bio.overture.ego.service.GroupService; +import bio.overture.ego.service.UserService; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import java.util.List; +import java.util.UUID; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.annotations.ApiIgnore; + +@Slf4j +@RestController +@RequestMapping("/applications") +public class ApplicationController { + + /** Dependencies */ + private final ApplicationService applicationService; + + private final GroupService groupService; + private final UserService userService; + + @Autowired + public ApplicationController( + @NonNull ApplicationService applicationService, + @NonNull GroupService groupService, + @NonNull UserService userService) { + this.applicationService = applicationService; + this.groupService = groupService; + this.userService = userService; + } + + @AdminScoped + @RequestMapping(method = GET, value = "") + @ApiImplicitParams({ + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page Applications")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO findApplications( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @RequestParam(value = "query", required = false) String query, + @ApiIgnore @Filters List filters, + Pageable pageable) { + if (isEmpty(query)) { + return new PageDTO<>(applicationService.listApps(filters, pageable)); + } else { + return new PageDTO<>(applicationService.findApps(query, filters, pageable)); + } + } + + @AdminScoped + @RequestMapping(method = POST, value = "") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "New Application", response = Application.class)}) + public @ResponseBody Application createApplication( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @RequestBody(required = true) CreateApplicationRequest request) { + return applicationService.create(request); + } + + @AdminScoped + @RequestMapping(method = GET, value = "/{id}") + @ApiResponses( + value = { + @ApiResponse(code = 200, message = "Application Details", response = Application.class) + }) + @JsonView(Views.REST.class) + public @ResponseBody Application getApplication( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id) { + return applicationService.getById(id); + } + + @AdminScoped + @RequestMapping(method = PUT, value = "/{id}") + @ApiResponses( + value = { + @ApiResponse(code = 200, message = "Updated application info", response = Application.class) + }) + public @ResponseBody Application updateApplication( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(name = "id", required = true) UUID id, + @RequestBody(required = true) UpdateApplicationRequest updateRequest) { + return applicationService.partialUpdate(id, updateRequest); + } + + @AdminScoped + @RequestMapping(method = DELETE, value = "/{id}") + @ResponseStatus(value = HttpStatus.OK) + public void deleteApplication( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id) { + applicationService.delete(id); + } + + @AdminScoped + @RequestMapping(method = GET, value = "/{id}/users") + @ApiImplicitParams({ + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = Fields.ID, + required = false, + dataType = "string", + paramType = "query", + value = "Search for ids containing this text"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page Users for an Application")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO getUsersForApplication( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestParam(value = "query", required = false) String query, + @ApiIgnore @Filters List filters, + Pageable pageable) { + if (isEmpty(query)) { + return new PageDTO<>(userService.findUsersForApplication(id, filters, pageable)); + } else { + return new PageDTO<>(userService.findUsersForApplication(id, query, filters, pageable)); + } + } + + @AdminScoped + @RequestMapping(method = GET, value = "/{id}/groups") + @ApiImplicitParams({ + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = Fields.ID, + required = false, + dataType = "string", + paramType = "query", + value = "Search for ids containing this text"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page Groups for an Application")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO getGroupsForApplication( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestParam(value = "query", required = false) String query, + @ApiIgnore @Filters List filters, + Pageable pageable) { + if (isEmpty(query)) { + return new PageDTO<>(groupService.findGroupsForApplication(id, filters, pageable)); + } else { + return new PageDTO<>(groupService.findGroupsForApplication(id, query, filters, pageable)); + } + } +} diff --git a/src/main/java/bio/overture/ego/controller/AuthController.java b/src/main/java/bio/overture/ego/controller/AuthController.java new file mode 100644 index 000000000..eedefb97e --- /dev/null +++ b/src/main/java/bio/overture/ego/controller/AuthController.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.controller; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +import bio.overture.ego.provider.facebook.FacebookTokenService; +import bio.overture.ego.provider.google.GoogleTokenService; +import bio.overture.ego.service.TokenService; +import bio.overture.ego.token.IDToken; +import bio.overture.ego.token.signer.TokenSigner; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping("/oauth") +public class AuthController { + + private final TokenService tokenService; + private final GoogleTokenService googleTokenService; + private final FacebookTokenService facebookTokenService; + private final TokenSigner tokenSigner; + + @Autowired + public AuthController( + @NonNull TokenService tokenService, + @NonNull GoogleTokenService googleTokenService, + @NonNull FacebookTokenService facebookTokenService, + @NonNull TokenSigner tokenSigner) { + this.tokenService = tokenService; + this.googleTokenService = googleTokenService; + this.facebookTokenService = facebookTokenService; + this.tokenSigner = tokenSigner; + } + + @RequestMapping(method = GET, value = "/google/token") + @ResponseStatus(value = OK) + @SneakyThrows + public @ResponseBody String exchangeGoogleTokenForAuth( + @RequestHeader(value = "token") final String idToken) { + if (!googleTokenService.validToken(idToken)) + throw new InvalidTokenException("Invalid user token:" + idToken); + val authInfo = googleTokenService.decode(idToken); + return tokenService.generateUserToken(authInfo); + } + + @RequestMapping(method = GET, value = "/facebook/token") + @ResponseStatus(value = OK) + @SneakyThrows + public @ResponseBody String exchangeFacebookTokenForAuth( + @RequestHeader(value = "token") final String idToken) { + if (!facebookTokenService.validToken(idToken)) + throw new InvalidTokenException("Invalid user token:" + idToken); + val authInfo = facebookTokenService.getAuthInfo(idToken); + if (authInfo.isPresent()) { + return tokenService.generateUserToken(authInfo.get()); + } else { + throw new InvalidTokenException("Unable to generate auth token for this user"); + } + } + + @RequestMapping(method = GET, value = "/token/verify") + @ResponseStatus(value = OK) + @SneakyThrows + public @ResponseBody boolean verifyJWToken(@RequestHeader(value = "token") final String token) { + if (StringUtils.isEmpty(token)) { + throw new InvalidTokenException("ScopedAccessToken is empty"); + } + + if (!tokenService.isValidToken(token)) { + throw new InvalidTokenException("ScopedAccessToken failed validation"); + } + return true; + } + + @RequestMapping(method = GET, value = "/token/public_key") + @ResponseStatus(value = OK) + public @ResponseBody String getPublicKey() { + val pubKey = tokenSigner.getEncodedPublicKey(); + return pubKey.orElse(""); + } + + @RequestMapping( + method = {GET, POST}, + value = "/ego-token") + @SneakyThrows + public ResponseEntity user(OAuth2Authentication authentication) { + if (authentication == null) return new ResponseEntity<>("Please login", UNAUTHORIZED); + String token = tokenService.generateUserToken((IDToken) authentication.getPrincipal()); + SecurityContextHolder.getContext().setAuthentication(null); + return new ResponseEntity<>(token, OK); + } + + @ExceptionHandler({InvalidTokenException.class}) + public ResponseEntity handleInvalidTokenException(InvalidTokenException ex) { + log.error(String.format("InvalidTokenException: %s", ex.getMessage())); + log.error("ID ScopedAccessToken not found."); + return new ResponseEntity<>( + "Invalid ID ScopedAccessToken provided.", new HttpHeaders(), BAD_REQUEST); + } + + @ExceptionHandler({InvalidScopeException.class}) + public ResponseEntity handleInvalidScopeException(InvalidTokenException ex) { + log.error(String.format("Invalid ScopeName: %s", ex.getMessage())); + return new ResponseEntity<>(String.format("{\"error\": \"%s\"}", ex.getMessage()), BAD_REQUEST); + } +} diff --git a/src/main/java/bio/overture/ego/controller/GroupController.java b/src/main/java/bio/overture/ego/controller/GroupController.java new file mode 100644 index 000000000..ee9c74c83 --- /dev/null +++ b/src/main/java/bio/overture/ego/controller/GroupController.java @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.controller; + +import static bio.overture.ego.controller.resolver.PageableResolver.LIMIT; +import static bio.overture.ego.controller.resolver.PageableResolver.OFFSET; +import static bio.overture.ego.controller.resolver.PageableResolver.SORT; +import static bio.overture.ego.controller.resolver.PageableResolver.SORTORDER; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.util.StringUtils.isEmpty; +import static org.springframework.web.bind.annotation.RequestMethod.DELETE; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +import bio.overture.ego.model.dto.GroupRequest; +import bio.overture.ego.model.dto.PageDTO; +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.GroupPermission; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.Fields; +import bio.overture.ego.model.search.Filters; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.security.AdminScoped; +import bio.overture.ego.service.ApplicationService; +import bio.overture.ego.service.GroupPermissionService; +import bio.overture.ego.service.GroupService; +import bio.overture.ego.service.UserService; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import java.util.List; +import java.util.UUID; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.annotations.ApiIgnore; + +@Slf4j +@RestController +@RequestMapping("/groups") +public class GroupController { + + /** Dependencies */ + private final GroupService groupService; + + private final UserService userService; + + private final ApplicationService applicationService; + + private final GroupPermissionService groupPermissionService; + + @Autowired + public GroupController( + @NonNull GroupService groupService, + @NonNull UserService userService, + @NonNull GroupPermissionService groupPermissionService, + @NonNull ApplicationService applicationService) { + this.groupService = groupService; + this.userService = userService; + this.groupPermissionService = groupPermissionService; + this.applicationService = applicationService; + } + + @AdminScoped + @RequestMapping(method = GET, value = "") + @ApiImplicitParams({ + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page Groups")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO findGroups( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @RequestParam(value = "query", required = false) String query, + @ApiIgnore @Filters List filters, + Pageable pageable) { + if (isEmpty(query)) { + return new PageDTO<>(groupService.listGroups(filters, pageable)); + } else { + return new PageDTO<>(groupService.findGroups(query, filters, pageable)); + } + } + + @AdminScoped + @RequestMapping(method = POST, value = "") + @ApiResponses( + value = { + @ApiResponse(code = 200, message = "New Group", response = Group.class), + }) + public @ResponseBody Group createGroup( + @RequestHeader(value = AUTHORIZATION) final String accessToken, + @RequestBody GroupRequest createRequest) { + return groupService.create(createRequest); + } + + @AdminScoped + @RequestMapping(method = GET, value = "/{id}") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Group Details", response = Group.class)}) + @JsonView(Views.REST.class) + public @ResponseBody Group getGroup( + @RequestHeader(value = AUTHORIZATION) final String accessToken, + @PathVariable(value = "id") UUID id) { + return groupService.getById(id); + } + + @AdminScoped + @RequestMapping(method = RequestMethod.PUT, value = "/{id}") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Updated group info", response = Group.class)}) + public @ResponseBody Group updateGroup( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id") UUID id, + @RequestBody(required = true) GroupRequest updateRequest) { + return groupService.partialUpdate(id, updateRequest); + } + + @AdminScoped + @RequestMapping(method = DELETE, value = "/{id}") + @ResponseStatus(value = OK) + public void deleteGroup( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id) { + groupService.delete(id); + } + + /* + Permissions related endpoints + */ + @AdminScoped + @RequestMapping(method = GET, value = "/{id}/permissions") + @ApiImplicitParams({ + @ApiImplicitParam( + name = Fields.ID, + required = false, + dataType = "string", + paramType = "query", + value = "Search for ids containing this text"), + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses( + value = { + @ApiResponse(code = 200, message = "Page GroupPermissions for a Group"), + }) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO getGroupPermissionsForGroup( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + Pageable pageable) { + return new PageDTO<>(groupPermissionService.getPermissions(id, pageable)); + } + + @AdminScoped + @RequestMapping(method = POST, value = "/{id}/permissions") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Add group permissions", response = Group.class)}) + public @ResponseBody Group addPermissions( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestBody(required = true) List permissions) { + return groupPermissionService.addPermissions(id, permissions); + } + + @AdminScoped + @RequestMapping(method = DELETE, value = "/{id}/permissions/{permissionIds}") + @ApiResponses(value = {@ApiResponse(code = 200, message = "Delete group permissions")}) + @ResponseStatus(value = OK) + public void deletePermissions( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @PathVariable(value = "permissionIds", required = true) List permissionIds) { + groupPermissionService.deletePermissions(id, permissionIds); + } + + /* + Application related endpoints + */ + @AdminScoped + @RequestMapping(method = GET, value = "/{id}/applications") + @ApiImplicitParams({ + @ApiImplicitParam( + name = Fields.ID, + required = false, + dataType = "string", + paramType = "query", + value = "Search for ids containing this text"), + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page Applications for a Group")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO getApplicationsForGroup( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestParam(value = "query", required = false) String query, + @ApiIgnore @Filters List filters, + Pageable pageable) { + if (StringUtils.isEmpty(query)) { + return new PageDTO<>(applicationService.findApplicationsForGroup(id, filters, pageable)); + } else { + return new PageDTO<>( + applicationService.findApplicationsForGroup(id, query, filters, pageable)); + } + } + + @AdminScoped + @RequestMapping(method = POST, value = "/{id}/applications") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Add Apps to Group", response = Group.class)}) + public @ResponseBody Group addAppsToGroups( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestBody(required = true) List appIds) { + return groupService.associateApplicationsWithGroup(id, appIds); + } + + @AdminScoped + @RequestMapping(method = DELETE, value = "/{id}/applications/{appIds}") + @ApiResponses(value = {@ApiResponse(code = 200, message = "Delete Apps from Group")}) + @ResponseStatus(value = OK) + public void deleteAppsFromGroup( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @PathVariable(value = "appIds", required = true) List appIds) { + groupService.disassociateApplicationsFromGroup(id, appIds); + } + + /* + User related endpoints + */ + @AdminScoped + @RequestMapping(method = GET, value = "/{id}/users") + @ApiImplicitParams({ + @ApiImplicitParam( + name = Fields.ID, + required = false, + dataType = "string", + paramType = "query", + value = "Search for ids containing this text"), + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page Users for a Group")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO getUsersForGroup( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestParam(value = "query", required = false) String query, + @ApiIgnore @Filters List filters, + Pageable pageable) { + if (StringUtils.isEmpty(query)) { + return new PageDTO<>(userService.findUsersForGroup(id, filters, pageable)); + } else { + return new PageDTO<>(userService.findUsersForGroup(id, query, filters, pageable)); + } + } + + @AdminScoped + @RequestMapping(method = POST, value = "/{id}/users") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Add Users to Group", response = Group.class)}) + public @ResponseBody Group addUsersToGroup( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestBody(required = true) List userIds) { + return groupService.associateUsersWithGroup(id, userIds); + } + + @AdminScoped + @RequestMapping(method = DELETE, value = "/{id}/users/{userIds}") + @ApiResponses(value = {@ApiResponse(code = 200, message = "Delete Users from Group")}) + @ResponseStatus(value = OK) + public void deleteUsersFromGroup( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @PathVariable(value = "userIds", required = true) List userIds) { + groupService.disassociateUsersFromGroup(id, userIds); + } +} diff --git a/src/main/java/bio/overture/ego/controller/PolicyController.java b/src/main/java/bio/overture/ego/controller/PolicyController.java new file mode 100644 index 000000000..9b3fa0f76 --- /dev/null +++ b/src/main/java/bio/overture/ego/controller/PolicyController.java @@ -0,0 +1,251 @@ +package bio.overture.ego.controller; + +import static bio.overture.ego.controller.resolver.PageableResolver.LIMIT; +import static bio.overture.ego.controller.resolver.PageableResolver.OFFSET; +import static bio.overture.ego.controller.resolver.PageableResolver.SORT; +import static bio.overture.ego.controller.resolver.PageableResolver.SORTORDER; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.web.bind.annotation.RequestMethod.DELETE; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +import bio.overture.ego.model.dto.GenericResponse; +import bio.overture.ego.model.dto.MaskDTO; +import bio.overture.ego.model.dto.PageDTO; +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.dto.PolicyRequest; +import bio.overture.ego.model.dto.PolicyResponse; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.enums.Fields; +import bio.overture.ego.model.search.Filters; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.security.AdminScoped; +import bio.overture.ego.service.GroupPermissionService; +import bio.overture.ego.service.PolicyService; +import bio.overture.ego.service.UserPermissionService; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonView; +import com.google.common.collect.ImmutableList; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import java.util.List; +import java.util.UUID; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.annotations.ApiIgnore; + +@Slf4j +@RestController +@RequestMapping("/policies") +public class PolicyController { + + /** Dependencies */ + private final PolicyService policyService; + + private final UserPermissionService userPermissionService; + private final GroupPermissionService groupPermissionService; + + @Autowired + public PolicyController( + @NonNull PolicyService policyService, + @NonNull UserPermissionService userPermissionService, + @NonNull GroupPermissionService groupPermissionService) { + this.policyService = policyService; + this.groupPermissionService = groupPermissionService; + this.userPermissionService = userPermissionService; + } + + @AdminScoped + @RequestMapping(method = GET, value = "/{id}") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Get policy by id", response = Policy.class)}) + @JsonView(Views.REST.class) + public @ResponseBody Policy get( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id) { + return policyService.getById(id); + } + + @AdminScoped + @RequestMapping(method = GET, value = "") + @ApiImplicitParams({ + @ApiImplicitParam( + name = Fields.ID, + required = false, + dataType = "string", + paramType = "query", + value = "Search for ids containing this text"), + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page Policies")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO getPolicies( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @ApiIgnore @Filters List filters, + Pageable pageable) { + return new PageDTO<>(policyService.listPolicies(filters, pageable)); + } + + @AdminScoped + @RequestMapping(method = POST, value = "") + @ApiResponses( + value = { + @ApiResponse(code = 200, message = "New Policy", response = Policy.class), + }) + public @ResponseBody Policy create( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @RequestBody(required = true) PolicyRequest createRequest) { + return policyService.create(createRequest); + } + + @AdminScoped + @RequestMapping(method = PUT, value = "/{id}") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Updated Policy", response = Policy.class)}) + public @ResponseBody Policy update( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id") UUID id, + @RequestBody(required = true) PolicyRequest updatedRequst) { + return policyService.partialUpdate(id, updatedRequst); + } + + @AdminScoped + @RequestMapping(method = DELETE, value = "/{id}") + @ResponseStatus(value = HttpStatus.OK) + public void delete( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id) { + policyService.delete(id); + } + + @AdminScoped + @RequestMapping(method = POST, value = "/{id}/permission/group/{group_id}") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Add group permission", response = String.class)}) + @JsonView(Views.REST.class) + public @ResponseBody Group createGroupPermission( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @PathVariable(value = "group_id", required = true) UUID groupId, + @RequestBody(required = true) MaskDTO maskDTO) { + return groupPermissionService.addPermissions( + groupId, ImmutableList.of(new PermissionRequest(id, maskDTO.getMask()))); + } + + @AdminScoped + @RequestMapping(method = DELETE, value = "/{id}/permission/group/{group_id}") + @ApiResponses( + value = { + @ApiResponse( + code = 200, + message = "Delete group permission", + response = GenericResponse.class) + }) + public @ResponseBody GenericResponse deleteGroupPermission( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @PathVariable(value = "group_id", required = true) UUID groupId) { + groupPermissionService.deleteByPolicyAndOwner(id, groupId); + return new GenericResponse("Deleted permission for group '%s' on policy '%s'"); + } + + @AdminScoped + @RequestMapping(method = POST, value = "/{id}/permission/user/{user_id}") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Add user permission", response = String.class)}) + public @ResponseBody String createUserPermission( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @PathVariable(value = "user_id", required = true) UUID userId, + @RequestBody(required = true) MaskDTO maskDTO) { + userPermissionService.addPermissions( + userId, ImmutableList.of(new PermissionRequest(id, maskDTO.getMask()))); + // TODO [rtisma]: change this to actually return proper response + return "1 user permission successfully added to ACL '" + id + "'"; + } + + @AdminScoped + @RequestMapping(method = DELETE, value = "/{id}/permission/user/{user_id}") + @ApiResponses( + value = { + @ApiResponse( + code = 200, + message = "Delete group permission", + response = GenericResponse.class) + }) + public @ResponseBody GenericResponse deleteUserPermission( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @PathVariable(value = "user_id", required = true) UUID userId) { + + userPermissionService.deleteByPolicyAndOwner(id, userId); + return new GenericResponse("Deleted permission for user %s on policy %s"); + } + + @AdminScoped + @RequestMapping(method = GET, value = "/{id}/users") + @ApiResponses( + value = { + @ApiResponse( + code = 200, + message = "Get list of user ids with given policy id", + response = String.class) + }) + public @ResponseBody List findUserIds( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id) { + return userPermissionService.findByPolicy(id); + } + + @AdminScoped + @RequestMapping(method = GET, value = "/{id}/groups") + @ApiResponses( + value = { + @ApiResponse( + code = 200, + message = "Get list of group ids with given policy id", + response = String.class) + }) + public @ResponseBody List findGroupIds( + @RequestHeader(value = AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id) { + return groupPermissionService.findByPolicy(id); + } +} diff --git a/src/main/java/bio/overture/ego/controller/TokenController.java b/src/main/java/bio/overture/ego/controller/TokenController.java new file mode 100644 index 000000000..a0993cf8c --- /dev/null +++ b/src/main/java/bio/overture/ego/controller/TokenController.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.controller; + +import static bio.overture.ego.utils.CollectionUtils.mapToList; +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static java.lang.String.format; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.MULTI_STATUS; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.bind.annotation.RequestMethod.DELETE; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +import bio.overture.ego.model.dto.Scope; +import bio.overture.ego.model.dto.TokenResponse; +import bio.overture.ego.model.dto.TokenScopeResponse; +import bio.overture.ego.model.dto.UserScopesResponse; +import bio.overture.ego.model.params.ScopeName; +import bio.overture.ego.security.AdminScoped; +import bio.overture.ego.security.ApplicationScoped; +import bio.overture.ego.service.TokenService; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import javax.servlet.http.HttpServletRequest; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.common.exceptions.InvalidRequestException; +import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping("/o") +public class TokenController { + + /** Dependencies */ + private final TokenService tokenService; + + @Autowired + public TokenController(@NonNull TokenService tokenService) { + this.tokenService = tokenService; + } + + @ApplicationScoped() + @RequestMapping(method = POST, value = "/check_token") + @ResponseStatus(value = MULTI_STATUS) + @SneakyThrows + public @ResponseBody TokenScopeResponse checkToken( + @RequestHeader(value = "Authorization") final String authToken, + @RequestParam(value = "token") final String token) { + + return tokenService.checkToken(authToken, token); + } + + @RequestMapping(method = GET, value = "/scopes") + @ResponseStatus(value = OK) + @SneakyThrows + public @ResponseBody UserScopesResponse userScope( + @RequestHeader(value = "Authorization") final String auth, + @RequestParam(value = "userName") final String userName) { + return tokenService.userScopes(userName); + } + + @RequestMapping(method = POST, value = "/token") + @ResponseStatus(value = OK) + public @ResponseBody TokenResponse issueToken( + @RequestHeader(value = "Authorization") final String authorization, + @RequestParam(value = "user_id") UUID user_id, + @RequestParam(value = "scopes") ArrayList scopes, + @RequestParam(value = "description", required = false) String description) { + val scopeNames = mapToList(scopes, ScopeName::new); + val t = tokenService.issueToken(user_id, scopeNames, description); + Set issuedScopes = mapToSet(t.scopes(), Scope::toString); + return TokenResponse.builder() + .accessToken(t.getName()) + .scope(issuedScopes) + .exp(t.getSecondsUntilExpiry()) + .description(t.getDescription()) + .build(); + } + + @RequestMapping(method = DELETE, value = "/token") + @ResponseStatus(value = OK) + public @ResponseBody String revokeToken( + @RequestHeader(value = "Authorization") final String authorization, + @RequestParam(value = "token") final String token) { + tokenService.revokeToken(token); + return format("Token '%s' is successfully revoked!", token); + } + + @AdminScoped + @RequestMapping(method = GET, value = "/token") + @ResponseStatus(value = OK) + public @ResponseBody List listToken( + @RequestHeader(value = "Authorization") final String authorization, + @RequestParam(value = "user_id") UUID user_id) { + return tokenService.listToken(user_id); + } + + @ExceptionHandler({InvalidTokenException.class}) + public ResponseEntity handleInvalidTokenException( + HttpServletRequest req, InvalidTokenException ex) { + log.error(format("ID ScopedAccessToken not found.:%s", ex.toString())); + return errorResponse(UNAUTHORIZED, "Invalid token: %s", ex); + } + + @ExceptionHandler({InvalidScopeException.class}) + public ResponseEntity handleInvalidScopeException( + HttpServletRequest req, InvalidTokenException ex) { + log.error(format("Invalid PolicyIdStringWithMaskName: %s", ex.getMessage())); + return new ResponseEntity<>("{\"error\": \"Invalid Scope\"}", new HttpHeaders(), UNAUTHORIZED); + } + + @ExceptionHandler({InvalidRequestException.class}) + public ResponseEntity handleInvalidRequestException( + HttpServletRequest req, InvalidRequestException ex) { + log.error(format("Invalid request: %s", ex.getMessage())); + return new ResponseEntity<>("{\"error\": \"%s\"}".format(ex.getMessage()), BAD_REQUEST); + } + + @ExceptionHandler({UsernameNotFoundException.class}) + public ResponseEntity handleUserNotFoundException( + HttpServletRequest req, InvalidTokenException ex) { + log.error(format("User not found: %s", ex.getMessage())); + return new ResponseEntity<>("{\"error\": \"User not found\"}", UNAUTHORIZED); + } + + private String jsonEscape(String text) { + return text.replace("\"", "\\\""); + } + + private ResponseEntity errorResponse(HttpStatus status, String fmt, Exception ex) { + log.error(format(fmt, ex.getMessage())); + val headers = new HttpHeaders(); + headers.setContentType(APPLICATION_JSON); + val msg = format("{\"error\": \"%s\"}", jsonEscape(ex.getMessage())); + return new ResponseEntity<>(msg, status); + } +} diff --git a/src/main/java/bio/overture/ego/controller/UserController.java b/src/main/java/bio/overture/ego/controller/UserController.java new file mode 100644 index 000000000..28e7d5d7b --- /dev/null +++ b/src/main/java/bio/overture/ego/controller/UserController.java @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.controller; + +import static bio.overture.ego.controller.resolver.PageableResolver.LIMIT; +import static bio.overture.ego.controller.resolver.PageableResolver.OFFSET; +import static bio.overture.ego.controller.resolver.PageableResolver.SORT; +import static bio.overture.ego.controller.resolver.PageableResolver.SORTORDER; +import static org.springframework.util.StringUtils.isEmpty; + +import bio.overture.ego.model.dto.CreateUserRequest; +import bio.overture.ego.model.dto.PageDTO; +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.dto.UpdateUserRequest; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.entity.UserPermission; +import bio.overture.ego.model.enums.Fields; +import bio.overture.ego.model.search.Filters; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.security.AdminScoped; +import bio.overture.ego.service.ApplicationService; +import bio.overture.ego.service.GroupService; +import bio.overture.ego.service.UserPermissionService; +import bio.overture.ego.service.UserService; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import java.util.List; +import java.util.UUID; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.annotations.ApiIgnore; + +@Slf4j +@RestController +@RequestMapping("/users") +public class UserController { + + /** Dependencies */ + private final UserService userService; + + private final GroupService groupService; + private final ApplicationService applicationService; + private final UserPermissionService userPermissionService; + + @Autowired + public UserController( + @NonNull UserService userService, + @NonNull GroupService groupService, + @NonNull UserPermissionService userPermissionService, + @NonNull ApplicationService applicationService) { + this.userService = userService; + this.groupService = groupService; + this.applicationService = applicationService; + this.userPermissionService = userPermissionService; + } + + @AdminScoped + @RequestMapping(method = RequestMethod.GET, value = "") + @ApiImplicitParams({ + @ApiImplicitParam( + name = Fields.ID, + required = false, + dataType = "string", + paramType = "query", + value = "Search for ids containing this text"), + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page Users")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO findUsers( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @ApiParam( + value = + "Query string compares to Users Name, Email, First Name, and Last Name fields.", + required = false) + @RequestParam(value = "query", required = false) + String query, + @ApiIgnore @Filters List filters, + Pageable pageable) { + if (isEmpty(query)) { + return new PageDTO<>(userService.listUsers(filters, pageable)); + } else { + return new PageDTO<>(userService.findUsers(query, filters, pageable)); + } + } + + @AdminScoped + @RequestMapping(method = RequestMethod.POST, value = "") + @ApiResponses( + value = { + @ApiResponse(code = 200, message = "Create new user", response = User.class), + }) + public @ResponseBody User createUser( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @RequestBody(required = true) CreateUserRequest request) { + return userService.create(request); + } + + @AdminScoped + @RequestMapping(method = RequestMethod.GET, value = "/{id}") + @ApiResponses(value = {@ApiResponse(code = 200, message = "User Details", response = User.class)}) + @JsonView(Views.REST.class) + public @ResponseBody User getUser( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id) { + return userService.getById(id); + } + + @AdminScoped + @RequestMapping(method = RequestMethod.PUT, value = "/{id}") + @ApiResponses( + value = { + @ApiResponse( + code = 200, + message = "Partially update using non-null user info", + response = User.class) + }) + public @ResponseBody User updateUser( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestBody(required = true) UpdateUserRequest updateUserRequest) { + return userService.partialUpdate(id, updateUserRequest); + } + + @AdminScoped + @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") + @ResponseStatus(value = HttpStatus.OK) + public void deleteUser( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id) { + userService.delete(id); + } + + /* + Permissions related endpoints + */ + @AdminScoped + @RequestMapping(method = RequestMethod.GET, value = "/{id}/permissions") + @ApiImplicitParams({ + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page User Permissions for a User")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO getPermissions( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + Pageable pageable) { + return new PageDTO<>(userPermissionService.getPermissions(id, pageable)); + } + + @AdminScoped + @RequestMapping(method = RequestMethod.POST, value = "/{id}/permissions") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Add user permissions", response = User.class)}) + public @ResponseBody User addPermissions( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestBody(required = true) List permissions) { + return userPermissionService.addPermissions(id, permissions); + } + + @AdminScoped + @RequestMapping(method = RequestMethod.DELETE, value = "/{id}/permissions/{permissionIds}") + @ApiResponses(value = {@ApiResponse(code = 200, message = "Delete User permissions")}) + @ResponseStatus(value = HttpStatus.OK) + public void deletePermissions( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @PathVariable(value = "permissionIds", required = true) List permissionIds) { + userPermissionService.deletePermissions(id, permissionIds); + } + + /* + Groups related endpoints + */ + @AdminScoped + @RequestMapping(method = RequestMethod.GET, value = "/{id}/groups") + @ApiImplicitParams({ + @ApiImplicitParam( + name = Fields.ID, + required = false, + dataType = "string", + paramType = "query", + value = "Search for ids containing this text"), + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page Groups for a User")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO getGroupsFromUser( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestParam(value = "query", required = false) String query, + @ApiIgnore @Filters List filters, + Pageable pageable) { + if (isEmpty(query)) { + return new PageDTO<>(groupService.findGroupsForUser(id, filters, pageable)); + } else { + return new PageDTO<>(groupService.findGroupsForUser(id, query, filters, pageable)); + } + } + + @AdminScoped + @RequestMapping(method = RequestMethod.POST, value = "/{id}/groups") + @ApiResponses( + value = {@ApiResponse(code = 200, message = "Add Groups to user", response = User.class)}) + public @ResponseBody User addGroupsToUser( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestBody(required = true) List groupIds) { + return userService.associateGroupsWithUser(id, groupIds); + } + + @AdminScoped + @RequestMapping(method = RequestMethod.DELETE, value = "/{id}/groups/{groupIDs}") + @ApiResponses(value = {@ApiResponse(code = 200, message = "Delete Groups from User")}) + @ResponseStatus(value = HttpStatus.OK) + public void deleteGroupsFromUser( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @PathVariable(value = "groupIDs", required = true) List groupIds) { + userService.disassociateGroupsFromUser(id, groupIds); + } + + /* + Applications related endpoints + */ + @AdminScoped + @RequestMapping(method = RequestMethod.GET, value = "/{id}/applications") + @ApiImplicitParams({ + @ApiImplicitParam( + name = Fields.ID, + required = false, + dataType = "string", + paramType = "query", + value = "Search for ids containing this text"), + @ApiImplicitParam( + name = LIMIT, + required = false, + dataType = "string", + paramType = "query", + value = "Number of results to retrieve"), + @ApiImplicitParam( + name = OFFSET, + required = false, + dataType = "string", + paramType = "query", + value = "Index of first result to retrieve"), + @ApiImplicitParam( + name = SORT, + required = false, + dataType = "string", + paramType = "query", + value = "Field to sort on"), + @ApiImplicitParam( + name = SORTORDER, + required = false, + dataType = "string", + paramType = "query", + value = "Sorting order: ASC|DESC. Default order: DESC"), + }) + @ApiResponses(value = {@ApiResponse(code = 200, message = "Page Applications for a User")}) + @JsonView(Views.REST.class) + public @ResponseBody PageDTO getApplicationsFromUser( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestParam(value = "query", required = false) String query, + @ApiIgnore @Filters List filters, + Pageable pageable) { + if (isEmpty(query)) { + return new PageDTO<>(applicationService.findApplicationsForUser(id, filters, pageable)); + } else { + return new PageDTO<>( + applicationService.findApplicationsForUser(id, query, filters, pageable)); + } + } + + @AdminScoped + @RequestMapping(method = RequestMethod.POST, value = "/{id}/applications") + @ApiResponses( + value = { + @ApiResponse(code = 200, message = "Add Applications to User", response = User.class) + }) + public @ResponseBody User addApplicationsToUser( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @RequestBody(required = true) List appIds) { + return userService.addUserToApps(id, appIds); + } + + @AdminScoped + @RequestMapping(method = RequestMethod.DELETE, value = "/{id}/applications/{appIds}") + @ApiResponses(value = {@ApiResponse(code = 200, message = "Delete Applications from User")}) + @ResponseStatus(value = HttpStatus.OK) + public void deleteApplicationsFromUser( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, + @PathVariable(value = "id", required = true) UUID id, + @PathVariable(value = "appIds", required = true) List appIds) { + userService.deleteUserFromApps(id, appIds); + } +} diff --git a/src/main/java/org/overture/ego/controller/resolver/FilterResolver.java b/src/main/java/bio/overture/ego/controller/resolver/FilterResolver.java similarity index 61% rename from src/main/java/org/overture/ego/controller/resolver/FilterResolver.java rename to src/main/java/bio/overture/ego/controller/resolver/FilterResolver.java index 394dabbf2..509706f26 100644 --- a/src/main/java/org/overture/ego/controller/resolver/FilterResolver.java +++ b/src/main/java/bio/overture/ego/controller/resolver/FilterResolver.java @@ -14,29 +14,27 @@ * limitations under the License. */ -package org.overture.ego.controller.resolver; +package bio.overture.ego.controller.resolver; +import bio.overture.ego.model.search.Filters; +import bio.overture.ego.model.search.SearchFilter; +import java.util.ArrayList; +import java.util.List; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import lombok.val; -import org.overture.ego.model.search.Filters; -import org.overture.ego.model.search.SearchFilter; import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; -import java.util.ArrayList; -import java.util.List; - @Slf4j public class FilterResolver implements HandlerMethodArgumentResolver { - @NonNull - private List fieldValues; + @NonNull private List fieldValues; - public FilterResolver(@NonNull List fieldValues){ + public FilterResolver(@NonNull List fieldValues) { this.fieldValues = fieldValues; } @@ -46,18 +44,25 @@ public boolean supportsParameter(MethodParameter methodParameter) { } @Override - public Object resolveArgument(MethodParameter methodParameter, - ModelAndViewContainer modelAndViewContainer, - NativeWebRequest nativeWebRequest, - WebDataBinderFactory webDataBinderFactory) throws Exception { + public Object resolveArgument( + MethodParameter methodParameter, + ModelAndViewContainer modelAndViewContainer, + NativeWebRequest nativeWebRequest, + WebDataBinderFactory webDataBinderFactory) + throws Exception { val filters = new ArrayList(); - nativeWebRequest.getParameterNames().forEachRemaining(p -> { - val matchingField = fieldValues.stream().filter(f -> f.equalsIgnoreCase(p)).findFirst(); - if(matchingField.isPresent()){ - filters.add(new SearchFilter(matchingField.get(),nativeWebRequest.getParameter(p))); - } - }); + nativeWebRequest + .getParameterNames() + .forEachRemaining( + p -> { + val matchingField = + fieldValues.stream().filter(f -> f.equalsIgnoreCase(p)).findFirst(); + if (matchingField.isPresent()) { + filters.add( + new SearchFilter(matchingField.get(), nativeWebRequest.getParameter(p))); + } + }); return filters; } } diff --git a/src/main/java/org/overture/ego/controller/resolver/PageableResolver.java b/src/main/java/bio/overture/ego/controller/resolver/PageableResolver.java similarity index 74% rename from src/main/java/org/overture/ego/controller/resolver/PageableResolver.java rename to src/main/java/bio/overture/ego/controller/resolver/PageableResolver.java index acbc038b4..9e521b2b1 100644 --- a/src/main/java/org/overture/ego/controller/resolver/PageableResolver.java +++ b/src/main/java/bio/overture/ego/controller/resolver/PageableResolver.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.overture.ego.controller.resolver; +package bio.overture.ego.controller.resolver; import org.springframework.core.MethodParameter; import org.springframework.data.domain.Pageable; @@ -26,21 +26,29 @@ import org.springframework.web.method.support.ModelAndViewContainer; public class PageableResolver implements HandlerMethodArgumentResolver { + + public static final String SORT = "sort"; + public static final String SORTORDER = "sortOrder"; + public static final String OFFSET = "offset"; + public static final String LIMIT = "limit"; + @Override public boolean supportsParameter(MethodParameter methodParameter) { return methodParameter.getParameterType().equals(Pageable.class); } @Override - public Object resolveArgument(MethodParameter methodParameter, - ModelAndViewContainer modelAndViewContainer, - NativeWebRequest nativeWebRequest, - WebDataBinderFactory webDataBinderFactory) throws Exception { + public Object resolveArgument( + MethodParameter methodParameter, + ModelAndViewContainer modelAndViewContainer, + NativeWebRequest nativeWebRequest, + WebDataBinderFactory webDataBinderFactory) + throws Exception { // get paging parameters - String limit = nativeWebRequest.getParameter("limit"); - String offset = nativeWebRequest.getParameter("offset"); - String sort = nativeWebRequest.getParameter("sort"); - String sortOrder = nativeWebRequest.getParameter("sortOrder"); + String limit = nativeWebRequest.getParameter(LIMIT); + String offset = nativeWebRequest.getParameter(OFFSET); + String sort = nativeWebRequest.getParameter(SORT); + String sortOrder = nativeWebRequest.getParameter(SORTORDER); return getPageable(limit, offset, sort, sortOrder); } @@ -61,7 +69,7 @@ public int getPageNumber() { @Override public int getPageSize() { - if(StringUtils.isEmpty(limit)){ + if (StringUtils.isEmpty(limit)) { return DEFAULT_LIMIT; } else { return Integer.parseInt(limit); @@ -70,8 +78,8 @@ public int getPageSize() { @Override public long getOffset() { - if(StringUtils.isEmpty(offset)){ - return DEFAULT_PAGE_NUM; + if (StringUtils.isEmpty(offset)) { + return DEFAULT_PAGE_NUM; } else { return Integer.parseInt(offset); } @@ -82,11 +90,12 @@ public Sort getSort() { // set default sort direction Sort.Direction direction = Sort.Direction.DESC; - if( (! StringUtils.isEmpty(sortOrder)) && "asc".equals(sortOrder.toLowerCase())){ + if ((!StringUtils.isEmpty(sortOrder)) && "asc".equals(sortOrder.toLowerCase())) { direction = Sort.Direction.ASC; } // TODO: this is a hack for now to provide default sort on id field - // ideally we should not be making assumption about field name as "id" - it will break if field doesn't exist + // ideally we should not be making assumption about field name as "id" - it will break if + // field doesn't exist return new Sort(direction, StringUtils.isEmpty(sort) ? "id" : sort); } diff --git a/src/main/java/bio/overture/ego/event/token/CleanupTokenListener.java b/src/main/java/bio/overture/ego/event/token/CleanupTokenListener.java new file mode 100644 index 000000000..994b3e909 --- /dev/null +++ b/src/main/java/bio/overture/ego/event/token/CleanupTokenListener.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.event.token; + +import static bio.overture.ego.utils.Collectors.toImmutableSet; + +import bio.overture.ego.model.dto.Scope; +import bio.overture.ego.model.dto.TokenResponse; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.params.ScopeName; +import bio.overture.ego.service.TokenService; +import java.util.Set; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class CleanupTokenListener implements ApplicationListener { + + /** Dependencies */ + private final TokenService tokenService; + + @Autowired + public CleanupTokenListener(@NonNull TokenService tokenService) { + this.tokenService = tokenService; + } + + @Override + public void onApplicationEvent(@NonNull CleanupUserTokensEvent event) { + cleanupTokens(event.getUsers()); + } + + private void cleanupTokens(@NonNull Set users) { + users.forEach(this::cleanupTokensForUser); + } + + private void cleanupTokensForUser(@NonNull User user) { + val scopes = tokenService.userScopes(user.getName()).getScopes(); + val tokens = tokenService.listToken(user.getId()); + + tokens.forEach(t -> verifyToken(t, scopes)); + } + + private void verifyToken(@NonNull TokenResponse token, @NonNull Set scopes) { + // Expand effective scopes to include READ if WRITE is present and convert to Scope type. + val expandedUserScopes = + Scope.explicitScopes( + scopes.stream().map(this::convertStringToScope).collect(toImmutableSet())); + + // Convert token scopes from String to Scope + val tokenScopes = + token.getScope().stream().map(this::convertStringToScope).collect(toImmutableSet()); + + // Compare + if (!expandedUserScopes.containsAll(tokenScopes)) { + log.info( + "Token scopes not contained in user scopes, revoking. {} not in {}", + tokenScopes.toString(), + expandedUserScopes.toString()); + tokenService.revoke(token.getAccessToken()); + } + } + + private Scope convertStringToScope(@NonNull String stringScope) { + val s = new ScopeName(stringScope); + + val policy = new Policy(); + policy.setName(s.getName()); + return new Scope(policy, s.getAccessLevel()); + } +} diff --git a/src/main/java/bio/overture/ego/event/token/CleanupUserTokensEvent.java b/src/main/java/bio/overture/ego/event/token/CleanupUserTokensEvent.java new file mode 100644 index 000000000..c0449c4ca --- /dev/null +++ b/src/main/java/bio/overture/ego/event/token/CleanupUserTokensEvent.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.event.token; + +import bio.overture.ego.model.entity.User; +import java.util.Set; +import lombok.Getter; +import lombok.NonNull; +import org.springframework.context.ApplicationEvent; + +public class CleanupUserTokensEvent extends ApplicationEvent { + + @Getter private Set users; + + public CleanupUserTokensEvent(@NonNull Object source, Set users) { + super(source); + this.users = users; + } +} diff --git a/src/main/java/bio/overture/ego/event/token/RevokeTokenListener.java b/src/main/java/bio/overture/ego/event/token/RevokeTokenListener.java new file mode 100644 index 000000000..888faade7 --- /dev/null +++ b/src/main/java/bio/overture/ego/event/token/RevokeTokenListener.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.event.token; + +import bio.overture.ego.model.entity.Token; +import bio.overture.ego.service.TokenService; +import lombok.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Component +public class RevokeTokenListener implements ApplicationListener { + + /** Dependencies */ + private final TokenService tokenService; + + @Autowired + public RevokeTokenListener(@NonNull TokenService tokenService) { + this.tokenService = tokenService; + } + + @Override + public void onApplicationEvent(@NonNull RevokeTokensEvent event) { + event.getTokens().stream().map(Token::getName).forEach(tokenService::revoke); + } +} diff --git a/src/main/java/bio/overture/ego/event/token/RevokeTokensEvent.java b/src/main/java/bio/overture/ego/event/token/RevokeTokensEvent.java new file mode 100644 index 000000000..20642777e --- /dev/null +++ b/src/main/java/bio/overture/ego/event/token/RevokeTokensEvent.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.event.token; + +import bio.overture.ego.model.entity.Token; +import java.util.Set; +import lombok.Getter; +import lombok.NonNull; +import org.springframework.context.ApplicationEvent; + +public class RevokeTokensEvent extends ApplicationEvent { + + @Getter private Set tokens; + + public RevokeTokensEvent(@NonNull Object source, @NonNull Set tokens) { + super(source); + this.tokens = tokens; + } +} diff --git a/src/main/java/bio/overture/ego/event/token/TokenEventsPublisher.java b/src/main/java/bio/overture/ego/event/token/TokenEventsPublisher.java new file mode 100644 index 000000000..4ac8a9f1a --- /dev/null +++ b/src/main/java/bio/overture/ego/event/token/TokenEventsPublisher.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.event.token; + +import bio.overture.ego.model.entity.Token; +import bio.overture.ego.model.entity.User; +import java.util.Set; +import lombok.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +public class TokenEventsPublisher { + + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + public TokenEventsPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + public void requestTokenCleanupByUsers(@NonNull final Set users) { + applicationEventPublisher.publishEvent(new CleanupUserTokensEvent(this, users)); + } + + public void requestTokenCleanup(@NonNull final Set tokens) { + applicationEventPublisher.publishEvent(new RevokeTokensEvent(this, tokens)); + } +} diff --git a/src/main/java/bio/overture/ego/model/dto/CreateApplicationRequest.java b/src/main/java/bio/overture/ego/model/dto/CreateApplicationRequest.java new file mode 100644 index 000000000..25783592a --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/CreateApplicationRequest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.dto; + +import bio.overture.ego.model.enums.ApplicationType; +import bio.overture.ego.model.enums.StatusType; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateApplicationRequest { + + @NotNull private String name; + + @NotNull private ApplicationType type; + + @NotNull private String clientId; + + @NotNull private String clientSecret; + + private String redirectUri; + + private String description; + + @NotNull private StatusType status; +} diff --git a/src/main/java/bio/overture/ego/model/dto/CreateTokenRequest.java b/src/main/java/bio/overture/ego/model/dto/CreateTokenRequest.java new file mode 100644 index 000000000..e9e19c360 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/CreateTokenRequest.java @@ -0,0 +1,22 @@ +package bio.overture.ego.model.dto; + +import java.util.Date; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateTokenRequest { + + @NotNull private String token; + + @NonNull private Date issueDate; + + @NotNull private boolean isRevoked; +} diff --git a/src/main/java/bio/overture/ego/model/dto/CreateUserRequest.java b/src/main/java/bio/overture/ego/model/dto/CreateUserRequest.java new file mode 100644 index 000000000..68e79f282 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/CreateUserRequest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.dto; + +import bio.overture.ego.model.enums.LanguageType; +import bio.overture.ego.model.enums.StatusType; +import bio.overture.ego.model.enums.UserType; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CreateUserRequest { + + @NotNull private String email; + + @NotNull private UserType type; + + @NotNull private StatusType status; + + private String firstName; + + private String lastName; + + @NotNull private LanguageType preferredLanguage; +} diff --git a/src/main/java/bio/overture/ego/model/dto/GenericResponse.java b/src/main/java/bio/overture/ego/model/dto/GenericResponse.java new file mode 100644 index 000000000..b836b033b --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/GenericResponse.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.model.dto; + +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonView; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +@JsonView(Views.REST.class) +public class GenericResponse { + private String message; +} diff --git a/src/main/java/bio/overture/ego/model/dto/GroupRequest.java b/src/main/java/bio/overture/ego/model/dto/GroupRequest.java new file mode 100644 index 000000000..25124e102 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/GroupRequest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.dto; + +import bio.overture.ego.model.enums.StatusType; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GroupRequest { + + @NotNull private String name; + + private String description; + + @NotNull private StatusType status; +} diff --git a/src/main/java/bio/overture/ego/model/dto/MaskDTO.java b/src/main/java/bio/overture/ego/model/dto/MaskDTO.java new file mode 100644 index 000000000..92c6dcdac --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/MaskDTO.java @@ -0,0 +1,22 @@ +package bio.overture.ego.model.dto; + +import bio.overture.ego.model.enums.AccessLevel; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MaskDTO { + + @NotNull @NonNull private AccessLevel mask; + + public static MaskDTO createMaskDTO(AccessLevel mask) { + return new MaskDTO(mask); + } +} diff --git a/src/main/java/org/overture/ego/model/dto/PageDTO.java b/src/main/java/bio/overture/ego/model/dto/PageDTO.java similarity index 81% rename from src/main/java/org/overture/ego/model/dto/PageDTO.java rename to src/main/java/bio/overture/ego/model/dto/PageDTO.java index 1b10b0bc2..48af61473 100644 --- a/src/main/java/org/overture/ego/model/dto/PageDTO.java +++ b/src/main/java/bio/overture/ego/model/dto/PageDTO.java @@ -14,17 +14,15 @@ * limitations under the License. */ -package org.overture.ego.model.dto; - +package bio.overture.ego.model.dto; +import bio.overture.ego.view.Views; import com.fasterxml.jackson.annotation.JsonView; +import java.util.List; import lombok.Getter; import lombok.NonNull; -import org.overture.ego.view.Views; import org.springframework.data.domain.Page; -import java.util.List; - @Getter @JsonView(Views.REST.class) public class PageDTO { @@ -35,10 +33,9 @@ public class PageDTO { private final List resultSet; public PageDTO(@NonNull final Page page) { - this.limit = page.getSize(); - this.offset = page.getNumber(); - this.count = page.getTotalElements(); - this.resultSet = page.getContent(); + this.limit = page.getSize(); + this.offset = page.getNumber(); + this.count = page.getTotalElements(); + this.resultSet = page.getContent(); } - } diff --git a/src/main/java/bio/overture/ego/model/dto/PermissionRequest.java b/src/main/java/bio/overture/ego/model/dto/PermissionRequest.java new file mode 100644 index 000000000..2baf41782 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/PermissionRequest.java @@ -0,0 +1,26 @@ +package bio.overture.ego.model.dto; + +import bio.overture.ego.model.enums.AccessLevel; +import java.util.UUID; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PermissionRequest { + + @NotNull @NonNull private UUID policyId; + + @NotNull @NonNull private AccessLevel mask; + + @Override + public String toString() { + return policyId + "." + mask; + } +} diff --git a/src/main/java/bio/overture/ego/model/dto/PolicyRequest.java b/src/main/java/bio/overture/ego/model/dto/PolicyRequest.java new file mode 100644 index 000000000..88d3691ec --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/PolicyRequest.java @@ -0,0 +1,16 @@ +package bio.overture.ego.model.dto; + +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PolicyRequest { + + @NotNull private String name; +} diff --git a/src/main/java/bio/overture/ego/model/dto/PolicyResponse.java b/src/main/java/bio/overture/ego/model/dto/PolicyResponse.java new file mode 100644 index 000000000..eb4eede5c --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/PolicyResponse.java @@ -0,0 +1,23 @@ +package bio.overture.ego.model.dto; + +import bio.overture.ego.model.enums.AccessLevel; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonView; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@JsonInclude +@JsonView(Views.REST.class) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PolicyResponse { + + public String id; + public String name; + public AccessLevel mask; +} diff --git a/src/main/java/bio/overture/ego/model/dto/Scope.java b/src/main/java/bio/overture/ego/model/dto/Scope.java new file mode 100644 index 000000000..8d3b526f7 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/Scope.java @@ -0,0 +1,117 @@ +package bio.overture.ego.model.dto; + +import static java.util.Objects.isNull; + +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.enums.AccessLevel; +import bio.overture.ego.model.params.ScopeName; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NonNull; +import lombok.val; + +@Data +@AllArgsConstructor +public class Scope { + + private Policy policy; + private AccessLevel accessLevel; + + @Override + public String toString() { + return getPolicyName() + "." + getAccessLevelName(); + } + + public String getPolicyName() { + if (isNull(policy)) { + return "Null policy"; + } + if (isNull(policy.getName())) { + return "Nameless policy"; + } + return policy.getName(); + } + + public String getAccessLevelName() { + if (isNull(accessLevel)) { + return "Null accessLevel"; + } + return accessLevel.toString(); + } + + public ScopeName toScopeName() { + return new ScopeName(this.toString()); + } + + public static Set missingScopes(Set have, Set want) { + val map = new HashMap(); + val missing = new HashSet(); + for (Scope scope : have) { + map.put(scope.getPolicy(), scope.getAccessLevel()); + } + + for (val s : want) { + val need = s.getAccessLevel(); + AccessLevel got = map.get(s.getPolicy()); + + if (got == null || !AccessLevel.allows(got, need)) { + missing.add(s); + } + } + return missing; + } + + public static Set effectiveScopes(Set have, Set want) { + // In general, our effective scope is the lesser of the scope we have, + // or the scope we want. This lets us have tokens that don't give away all of + // the authority that we do by creating a list of scopes we want to authorize. + val map = new HashMap(); + val effectiveScope = new HashSet(); + for (val scope : have) { + map.put(scope.getPolicy(), scope.getAccessLevel()); + } + + for (val s : want) { + val policy = s.getPolicy(); + val need = s.getAccessLevel(); + val got = map.getOrDefault(policy, AccessLevel.DENY); + // if we can do what we want, then add just what we need + if (AccessLevel.allows(got, need)) { + effectiveScope.add(new Scope(policy, need)); + } else { + // If we can't do what we want, we can do what we have, + // unless our permission is DENY, in which case we can't + // do anything + if (got != AccessLevel.DENY) { + effectiveScope.add(new Scope(policy, got)); + } + } + } + return effectiveScope; + } + + /** + * Return a set of explicit scopes, which always include a scope with READ access for each scope + * with WRITE access. + * + * @param scopes + * @return The explicit version of the set of scopes passed in. + */ + public static Set explicitScopes(Set scopes) { + val explicit = new HashSet(); + for (val s : scopes) { + explicit.add(s); + if (s.getAccessLevel().equals(AccessLevel.WRITE)) { + explicit.add(new Scope(s.getPolicy(), AccessLevel.READ)); + } + } + return explicit; + } + + public static Scope createScope(@NonNull Policy policy, @NonNull AccessLevel accessLevel) { + return new Scope(policy, accessLevel); + } +} diff --git a/src/main/java/bio/overture/ego/model/dto/TokenResponse.java b/src/main/java/bio/overture/ego/model/dto/TokenResponse.java new file mode 100644 index 000000000..d0e6b8905 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/TokenResponse.java @@ -0,0 +1,18 @@ +package bio.overture.ego.model.dto; + +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonView; +import java.util.Set; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +@Value +@Builder +@JsonView(Views.REST.class) +public class TokenResponse { + @NonNull private final String accessToken; + @NonNull private final Set scope; + @NonNull private final Long exp; + private String description; +} diff --git a/src/main/java/bio/overture/ego/model/dto/TokenScopeResponse.java b/src/main/java/bio/overture/ego/model/dto/TokenScopeResponse.java new file mode 100644 index 000000000..f64fbaef1 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/TokenScopeResponse.java @@ -0,0 +1,17 @@ +package bio.overture.ego.model.dto; + +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonView; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +@JsonView(Views.REST.class) +public class TokenScopeResponse { + private String user_name; + private String client_id; + private Long exp; + private Set scope; +} diff --git a/src/main/java/bio/overture/ego/model/dto/UpdateApplicationRequest.java b/src/main/java/bio/overture/ego/model/dto/UpdateApplicationRequest.java new file mode 100644 index 000000000..b55eb45fc --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/UpdateApplicationRequest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.dto; + +import bio.overture.ego.model.enums.ApplicationType; +import bio.overture.ego.model.enums.StatusType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UpdateApplicationRequest { + + private String name; + private String clientId; + private ApplicationType type; + private String clientSecret; + private String redirectUri; + private String description; + private StatusType status; +} diff --git a/src/main/java/bio/overture/ego/model/dto/UpdateUserRequest.java b/src/main/java/bio/overture/ego/model/dto/UpdateUserRequest.java new file mode 100644 index 000000000..89c39cf5f --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/UpdateUserRequest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.dto; + +import bio.overture.ego.model.enums.LanguageType; +import bio.overture.ego.model.enums.StatusType; +import bio.overture.ego.model.enums.UserType; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UpdateUserRequest { + + private String email; + private UserType type; + private StatusType status; + private String firstName; + private String lastName; + private LanguageType preferredLanguage; + private Date lastLogin; +} diff --git a/src/main/java/bio/overture/ego/model/dto/UserScopesResponse.java b/src/main/java/bio/overture/ego/model/dto/UserScopesResponse.java new file mode 100644 index 000000000..cd0475655 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/dto/UserScopesResponse.java @@ -0,0 +1,15 @@ +package bio.overture.ego.model.dto; + +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonView; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +@JsonView(Views.REST.class) +public class UserScopesResponse { + + private Set scopes; +} diff --git a/src/main/java/bio/overture/ego/model/entity/AbstractPermission.java b/src/main/java/bio/overture/ego/model/entity/AbstractPermission.java new file mode 100644 index 000000000..1e6e189e2 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/AbstractPermission.java @@ -0,0 +1,64 @@ +package bio.overture.ego.model.entity; + +import static bio.overture.ego.model.enums.AccessLevel.EGO_ACCESS_LEVEL_ENUM; + +import bio.overture.ego.model.enums.AccessLevel; +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.enums.LombokFields; +import bio.overture.ego.model.enums.SqlFields; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType; +import java.util.UUID; +import javax.persistence.Column; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MappedSuperclass; +import javax.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; + +@Data +@MappedSuperclass +@EqualsAndHashCode(of = {LombokFields.id}) +@ToString(exclude = {LombokFields.policy}) +@TypeDef(name = EGO_ACCESS_LEVEL_ENUM, typeClass = PostgreSQLEnumType.class) +@JsonPropertyOrder({JavaFields.ID, JavaFields.POLICY, JavaFields.OWNER, JavaFields.ACCESS_LEVEL}) +@JsonInclude(JsonInclude.Include.ALWAYS) +@JsonSubTypes({ + @JsonSubTypes.Type(value = UserPermission.class, name = JavaFields.USERPERMISSIONS), + @JsonSubTypes.Type(value = GroupPermission.class, name = JavaFields.GROUPPERMISSION) +}) +public abstract class AbstractPermission> + implements Identifiable { + + @Id + @Column(name = SqlFields.ID, updatable = false, nullable = false) + @GenericGenerator(name = "permission_uuid", strategy = "org.hibernate.id.UUIDGenerator") + @GeneratedValue(generator = "permission_uuid") + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = SqlFields.POLICYID_JOIN, nullable = false) + private Policy policy; + + @NotNull + @Column(name = SqlFields.ACCESS_LEVEL, nullable = false) + @Enumerated(EnumType.STRING) + @Type(type = EGO_ACCESS_LEVEL_ENUM) + private AccessLevel accessLevel; + + public abstract O getOwner(); + + public abstract void setOwner(O owner); +} diff --git a/src/main/java/bio/overture/ego/model/entity/Application.java b/src/main/java/bio/overture/ego/model/entity/Application.java new file mode 100644 index 000000000..dc181a8b0 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/Application.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.entity; + +import static bio.overture.ego.model.enums.AccessLevel.EGO_ENUM; +import static com.google.common.collect.Sets.newHashSet; + +import bio.overture.ego.model.enums.ApplicationType; +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.enums.LombokFields; +import bio.overture.ego.model.enums.SqlFields; +import bio.overture.ego.model.enums.StatusType; +import bio.overture.ego.model.enums.Tables; +import bio.overture.ego.model.join.GroupApplication; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonView; +import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType; +import java.util.Set; +import java.util.UUID; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; + +@Entity +@Table(name = Tables.APPLICATION) +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@JsonView(Views.REST.class) +@ToString(exclude = {LombokFields.groupApplications, LombokFields.users}) +@EqualsAndHashCode(of = {LombokFields.id}) +@JsonPropertyOrder({ + JavaFields.ID, + JavaFields.NAME, + JavaFields.APPLICATIONTYPE, + JavaFields.CLIENTID, + JavaFields.CLIENTSECRET, + JavaFields.REDIRECTURI, + JavaFields.DESCRIPTION, + JavaFields.STATUS +}) +@TypeDef(name = "application_type_enum", typeClass = PostgreSQLEnumType.class) +@TypeDef(name = EGO_ENUM, typeClass = PostgreSQLEnumType.class) +@JsonInclude(JsonInclude.Include.CUSTOM) +public class Application implements Identifiable { + + @Id + @Column(name = SqlFields.ID, updatable = false, nullable = false) + @GenericGenerator(name = "application_uuid", strategy = "org.hibernate.id.UUIDGenerator") + @GeneratedValue(generator = "application_uuid") + private UUID id; + + @NotNull + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.NAME, nullable = false, unique = true) + private String name; + + @NotNull + @Type(type = EGO_ENUM) + @Enumerated(EnumType.STRING) + @Column(name = SqlFields.TYPE, nullable = false) + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + private ApplicationType type; + + @NotNull + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.CLIENTID, nullable = false, unique = true) + private String clientId; + + @NotNull + @Column(name = SqlFields.CLIENTSECRET, nullable = false) + private String clientSecret; + + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.REDIRECTURI) + private String redirectUri; + + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.DESCRIPTION) + private String description; + + @NotNull + @Type(type = EGO_ENUM) + @Enumerated(EnumType.STRING) + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.STATUS, nullable = false) + private StatusType status; + + @JsonIgnore + @Builder.Default + @OneToMany( + mappedBy = JavaFields.APPLICATION, + cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + orphanRemoval = true) + private Set groupApplications = newHashSet(); + + @JsonIgnore + @Builder.Default + @ManyToMany( + mappedBy = JavaFields.APPLICATIONS, + fetch = FetchType.LAZY, + cascade = {CascadeType.MERGE, CascadeType.PERSIST}) + private Set users = newHashSet(); +} diff --git a/src/main/java/bio/overture/ego/model/entity/Group.java b/src/main/java/bio/overture/ego/model/entity/Group.java new file mode 100644 index 000000000..4f10e0f2d --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/Group.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2018. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.entity; + +import static bio.overture.ego.model.enums.AccessLevel.EGO_ENUM; +import static com.google.common.collect.Sets.newHashSet; + +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.enums.LombokFields; +import bio.overture.ego.model.enums.SqlFields; +import bio.overture.ego.model.enums.StatusType; +import bio.overture.ego.model.enums.Tables; +import bio.overture.ego.model.join.GroupApplication; +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonView; +import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType; +import java.util.Set; +import java.util.UUID; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; + +@Data +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = Tables.GROUP) +@JsonView(Views.REST.class) +@EqualsAndHashCode(of = {LombokFields.id}) +@TypeDef(name = EGO_ENUM, typeClass = PostgreSQLEnumType.class) +@ToString( + exclude = {LombokFields.userGroups, LombokFields.groupApplications, LombokFields.permissions}) +@JsonPropertyOrder({ + JavaFields.ID, + JavaFields.NAME, + JavaFields.DESCRIPTION, + JavaFields.STATUS, + JavaFields.GROUPAPPLICATIONS, + JavaFields.GROUPPERMISSIONS +}) +public class Group implements PolicyOwner, NameableEntity { + + @Id + @GeneratedValue(generator = "group_uuid") + @Column(name = SqlFields.ID, updatable = false, nullable = false) + @GenericGenerator(name = "group_uuid", strategy = "org.hibernate.id.UUIDGenerator") + private UUID id; + + @NotNull + @Column(name = SqlFields.NAME, nullable = false, unique = true) + private String name; + + @Column(name = SqlFields.DESCRIPTION) + private String description; + + @NotNull + @Type(type = EGO_ENUM) + @Enumerated(EnumType.STRING) + @Column(name = SqlFields.STATUS, nullable = false) + private StatusType status; + + // TODO: [rtisma] rename this to groupPermissions. + // Ensure anything using JavaFields.PERMISSIONS is also replaced with JavaFields.GROUPPERMISSIONS + @JsonIgnore + @Builder.Default + @OneToMany( + mappedBy = JavaFields.OWNER, + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY) + private Set permissions = newHashSet(); + + @JsonIgnore + @Builder.Default + @OneToMany( + mappedBy = JavaFields.GROUP, + cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + orphanRemoval = true) + private Set groupApplications = newHashSet(); + + @JsonIgnore + @Builder.Default + @OneToMany( + mappedBy = JavaFields.GROUP, + cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + orphanRemoval = true) + private Set userGroups = newHashSet(); +} diff --git a/src/main/java/bio/overture/ego/model/entity/GroupPermission.java b/src/main/java/bio/overture/ego/model/entity/GroupPermission.java new file mode 100644 index 000000000..3de712e39 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/GroupPermission.java @@ -0,0 +1,48 @@ +package bio.overture.ego.model.entity; + +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.enums.LombokFields; +import bio.overture.ego.model.enums.SqlFields; +import bio.overture.ego.model.enums.Tables; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonView; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedAttributeNode; +import javax.persistence.NamedEntityGraph; +import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@Table(name = Tables.GROUP_PERMISSION) +@Data +@JsonInclude() +@AllArgsConstructor +@NoArgsConstructor +@JsonView(Views.REST.class) +@ToString( + callSuper = true, + exclude = {LombokFields.owner}) +@EqualsAndHashCode( + callSuper = true, + of = {LombokFields.id}) +@NamedEntityGraph( + name = "group-permission-entity-with-relationships", + attributeNodes = { + @NamedAttributeNode(value = JavaFields.POLICY), + @NamedAttributeNode(value = JavaFields.OWNER) + }) +public class GroupPermission extends AbstractPermission { + + // Owning side + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = SqlFields.GROUPID_JOIN, nullable = false) + private Group owner; +} diff --git a/src/main/java/bio/overture/ego/model/entity/Identifiable.java b/src/main/java/bio/overture/ego/model/entity/Identifiable.java new file mode 100644 index 000000000..6d0af9aa6 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/Identifiable.java @@ -0,0 +1,6 @@ +package bio.overture.ego.model.entity; + +public interface Identifiable { + + ID getId(); +} diff --git a/src/main/java/bio/overture/ego/model/entity/NameableEntity.java b/src/main/java/bio/overture/ego/model/entity/NameableEntity.java new file mode 100644 index 000000000..2d9393ac8 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/NameableEntity.java @@ -0,0 +1,6 @@ +package bio.overture.ego.model.entity; + +public interface NameableEntity extends Identifiable { + + String getName(); +} diff --git a/src/main/java/bio/overture/ego/model/entity/Policy.java b/src/main/java/bio/overture/ego/model/entity/Policy.java new file mode 100644 index 000000000..b9754e5c1 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/Policy.java @@ -0,0 +1,88 @@ +package bio.overture.ego.model.entity; + +import static com.google.common.collect.Sets.newHashSet; + +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.enums.LombokFields; +import bio.overture.ego.model.enums.SqlFields; +import bio.overture.ego.model.enums.Tables; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonView; +import java.util.Set; +import java.util.UUID; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.NamedAttributeNode; +import javax.persistence.NamedEntityGraph; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +@Entity +@Table(name = Tables.POLICY) +@JsonInclude() +@JsonPropertyOrder({JavaFields.ID, JavaFields.OWNER, JavaFields.NAME}) +@JsonView(Views.REST.class) +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = {LombokFields.id}) +@NamedEntityGraph( + name = "policy-entity-with-relationships", + attributeNodes = { + @NamedAttributeNode(value = JavaFields.USERPERMISSIONS), + @NamedAttributeNode(value = JavaFields.GROUPPERMISSIONS), + }) +public class Policy implements Identifiable { + + @Id + @Column(name = SqlFields.ID, updatable = false, nullable = false) + @GenericGenerator(name = "policy_uuid", strategy = "org.hibernate.id.UUIDGenerator") + @GeneratedValue(generator = "policy_uuid") + private UUID id; + + @NotNull + @Column(name = SqlFields.NAME, unique = true, nullable = false) + private String name; + + @JsonIgnore + @Builder.Default + @OneToMany( + mappedBy = JavaFields.POLICY, + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY) + private Set groupPermissions = newHashSet(); + + @JsonIgnore + @Builder.Default + @OneToMany( + mappedBy = JavaFields.POLICY, + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY) + private Set userPermissions = newHashSet(); + + @JsonIgnore + @Builder.Default + @OneToMany( + mappedBy = JavaFields.POLICY, + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY) + private Set tokenScopes = newHashSet(); +} diff --git a/src/main/java/bio/overture/ego/model/entity/PolicyOwner.java b/src/main/java/bio/overture/ego/model/entity/PolicyOwner.java new file mode 100644 index 000000000..71a7b0cad --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/PolicyOwner.java @@ -0,0 +1,3 @@ +package bio.overture.ego.model.entity; + +public interface PolicyOwner {} diff --git a/src/main/java/bio/overture/ego/model/entity/Token.java b/src/main/java/bio/overture/ego/model/entity/Token.java new file mode 100644 index 000000000..c802a8012 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/Token.java @@ -0,0 +1,103 @@ +package bio.overture.ego.model.entity; + +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static com.google.common.collect.Sets.newHashSet; + +import bio.overture.ego.model.dto.Scope; +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.enums.LombokFields; +import bio.overture.ego.model.enums.SqlFields; +import bio.overture.ego.model.enums.Tables; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.*; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.val; +import org.hibernate.annotations.GenericGenerator; + +@Entity +@Table(name = Tables.TOKEN) +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString(exclude = {LombokFields.owner, LombokFields.scopes}) +@EqualsAndHashCode(of = {LombokFields.id}) +public class Token implements Identifiable { + + @Id + @Column(name = SqlFields.ID, updatable = false, nullable = false) + @GenericGenerator(name = "token_uuid", strategy = "org.hibernate.id.UUIDGenerator") + @GeneratedValue(generator = "token_uuid") + private UUID id; + + @NotNull + @Column(name = SqlFields.NAME, unique = true, nullable = false) + private String name; + + @NotNull + @Column(name = SqlFields.ISSUEDATE, updatable = false, nullable = false) + private Date issueDate; + + @NotNull + @Column(name = SqlFields.EXPIRYDATE, updatable = false, nullable = false) + private Date expiryDate; + + @NotNull + @Column(name = SqlFields.ISREVOKED, nullable = false) + private boolean isRevoked; + + @Column(name = SqlFields.DESCRIPTION) + private String description; + + @NotNull + @JsonIgnore + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = SqlFields.OWNER, nullable = false) + private User owner; + + @JsonIgnore + @OneToMany( + mappedBy = JavaFields.TOKEN, + orphanRemoval = true, + cascade = CascadeType.ALL, + fetch = FetchType.LAZY) + @Builder.Default + private Set scopes = newHashSet(); + + public Long getSecondsUntilExpiry() { + val seconds = expiryDate.getTime() / 1000L - Calendar.getInstance().getTime().getTime() / 1000L; + return seconds > 0 ? seconds : 0; + } + + public void addScope(Scope scope) { + if (scopes == null) { + scopes = new HashSet<>(); + } + scopes.add(new TokenScope(this, scope.getPolicy(), scope.getAccessLevel())); + } + + @JsonIgnore + public Set scopes() { + return mapToSet(scopes, s -> new Scope(s.getPolicy(), s.getAccessLevel())); + } + + public void setScopes(Set scopes) { + this.scopes = mapToSet(scopes, s -> new TokenScope(this, s.getPolicy(), s.getAccessLevel())); + } +} diff --git a/src/main/java/bio/overture/ego/model/entity/TokenScope.java b/src/main/java/bio/overture/ego/model/entity/TokenScope.java new file mode 100644 index 000000000..118adb858 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/TokenScope.java @@ -0,0 +1,61 @@ +package bio.overture.ego.model.entity; + +import static bio.overture.ego.model.enums.AccessLevel.EGO_ACCESS_LEVEL_ENUM; + +import bio.overture.ego.model.enums.AccessLevel; +import bio.overture.ego.model.enums.SqlFields; +import bio.overture.ego.model.enums.Tables; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType; +import java.io.Serializable; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; + +@NoArgsConstructor +@AllArgsConstructor +@Data +@Entity +@TypeDef(name = EGO_ACCESS_LEVEL_ENUM, typeClass = PostgreSQLEnumType.class) +@Table(name = Tables.TOKENSCOPE) +public class TokenScope implements Serializable { + + // TODO; [rtisma] correct the Id stuff. There is a way to define a 2-tuple primary key. refer to + // song Info entity (@EmbeddedId) + // TODO: [rtisma] update sql to use FOREIGNKEY. + @Id + @JsonIgnore + @NotNull + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = SqlFields.TOKENID_JOIN, nullable = false) + private Token token; + + @Id + @NotNull + @ManyToOne + @JoinColumn(name = SqlFields.POLICYID_JOIN, nullable = false) + private Policy policy; + + @NotNull + @Enumerated(EnumType.STRING) + @Type(type = EGO_ACCESS_LEVEL_ENUM) + @Column(name = SqlFields.ACCESS_LEVEL, nullable = false) + private AccessLevel accessLevel; + + @Override + public String toString() { + return policy.getName() + "." + accessLevel.toString(); + } +} diff --git a/src/main/java/bio/overture/ego/model/entity/User.java b/src/main/java/bio/overture/ego/model/entity/User.java new file mode 100644 index 000000000..7c2cf361d --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/User.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.entity; + +import static bio.overture.ego.model.enums.AccessLevel.EGO_ENUM; +import static bio.overture.ego.service.UserService.resolveUsersPermissions; +import static bio.overture.ego.utils.PolicyPermissionUtils.extractPermissionStrings; +import static com.google.common.collect.Sets.newHashSet; + +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.enums.LanguageType; +import bio.overture.ego.model.enums.LombokFields; +import bio.overture.ego.model.enums.SqlFields; +import bio.overture.ego.model.enums.StatusType; +import bio.overture.ego.model.enums.Tables; +import bio.overture.ego.model.enums.UserType; +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonView; +import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; + +@Slf4j +@Entity +@Table(name = Tables.EGOUSER) +@Data +@ToString( + exclude = { + LombokFields.userGroups, + LombokFields.applications, + LombokFields.userPermissions, + LombokFields.tokens + }) +@JsonPropertyOrder({ + JavaFields.ID, + JavaFields.NAME, + JavaFields.EMAIL, + JavaFields.USERTYPE, + JavaFields.STATUS, + JavaFields.GROUPS, + JavaFields.APPLICATIONS, + JavaFields.USERPERMISSIONS, + JavaFields.FIRSTNAME, + JavaFields.LASTNAME, + JavaFields.CREATEDAT, + JavaFields.LASTLOGIN, + JavaFields.PREFERREDLANGUAGE +}) +@JsonInclude() +@EqualsAndHashCode(of = {LombokFields.id}) +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonView(Views.REST.class) +@TypeDef(name = EGO_ENUM, typeClass = PostgreSQLEnumType.class) +public class User implements PolicyOwner, NameableEntity { + + // TODO: find JPA equivalent for GenericGenerator + @Id + @Column(name = SqlFields.ID, updatable = false, nullable = false) + @GenericGenerator(name = "user_uuid", strategy = "org.hibernate.id.UUIDGenerator") + @GeneratedValue(generator = "user_uuid") + private UUID id; + + @NotNull + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.NAME, unique = true, nullable = false) + private String name; + + @NotNull + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.EMAIL, unique = true, nullable = false) + private String email; + + @NotNull + @Type(type = EGO_ENUM) + @Enumerated(EnumType.STRING) + @Column(name = SqlFields.TYPE, nullable = false) + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + private UserType type; + + @NotNull + @Type(type = EGO_ENUM) + @Enumerated(EnumType.STRING) + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.STATUS, nullable = false) + private StatusType status; + + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.FIRSTNAME) + private String firstName; + + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.LASTNAME) + private String lastName; + + @NotNull + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.CREATEDAT) + private Date createdAt; + + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + @Column(name = SqlFields.LASTLOGIN) + private Date lastLogin; + + @Type(type = EGO_ENUM) + @Enumerated(EnumType.STRING) + @Column(name = SqlFields.PREFERREDLANGUAGE) + @JsonView({Views.JWTAccessToken.class, Views.REST.class}) + private LanguageType preferredLanguage; + + @JsonIgnore + @OneToMany( + mappedBy = JavaFields.OWNER, + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY) + @Builder.Default + private Set userPermissions = newHashSet(); + + @JsonIgnore + @Builder.Default + @OneToMany( + mappedBy = JavaFields.OWNER, + orphanRemoval = true, + cascade = CascadeType.ALL, + fetch = FetchType.LAZY) + private Set tokens = newHashSet(); + + @JsonIgnore + @Builder.Default + @OneToMany( + mappedBy = JavaFields.USER, + cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + orphanRemoval = true) + private Set userGroups = newHashSet(); + + @JsonIgnore + @ManyToMany( + fetch = FetchType.LAZY, + cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable( + name = Tables.USER_APPLICATION, + joinColumns = {@JoinColumn(name = SqlFields.USERID_JOIN)}, + inverseJoinColumns = {@JoinColumn(name = SqlFields.APPID_JOIN)}) + @Builder.Default + private Set applications = newHashSet(); + + // TODO: [rtisma] move getPermissions to UserService once DTO task is complete. JsonViews creates + // a dependency for this method. For now, using a UserService static method. + // Creates permissions in JWTAccessToken::context::user + @JsonView(Views.JWTAccessToken.class) + public List getPermissions() { + return extractPermissionStrings(resolveUsersPermissions(this)); + } +} diff --git a/src/main/java/bio/overture/ego/model/entity/UserPermission.java b/src/main/java/bio/overture/ego/model/entity/UserPermission.java new file mode 100644 index 000000000..806b3e600 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/entity/UserPermission.java @@ -0,0 +1,45 @@ +package bio.overture.ego.model.entity; + +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.enums.LombokFields; +import bio.overture.ego.model.enums.SqlFields; +import bio.overture.ego.model.enums.Tables; +import bio.overture.ego.view.Views; +import com.fasterxml.jackson.annotation.JsonView; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedAttributeNode; +import javax.persistence.NamedEntityGraph; +import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@Table(name = Tables.USER_PERMISSION) +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonView(Views.REST.class) +@ToString(callSuper = true) +@EqualsAndHashCode( + callSuper = true, + of = {LombokFields.id}) +@NamedEntityGraph( + name = "user-permission-entity-with-relationships", + attributeNodes = { + @NamedAttributeNode(value = JavaFields.POLICY), + @NamedAttributeNode(value = JavaFields.OWNER) + }) +public class UserPermission extends AbstractPermission { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = SqlFields.USERID_JOIN, nullable = false) + private User owner; +} diff --git a/src/main/java/bio/overture/ego/model/enums/AccessLevel.java b/src/main/java/bio/overture/ego/model/enums/AccessLevel.java new file mode 100644 index 000000000..1de501e23 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/enums/AccessLevel.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2018. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.enums; + +import java.util.Arrays; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.val; + +@RequiredArgsConstructor +public enum AccessLevel { + READ("READ"), + WRITE("WRITE"), + DENY("DENY"); + + public static final String EGO_ENUM = "ego_enum"; + public static final String EGO_ACCESS_LEVEL_ENUM = "ego_access_level_enum"; + + @NonNull private final String value; + + public static AccessLevel fromValue(String value) { + for (val policyMask : values()) { + if (policyMask.value.equalsIgnoreCase(value)) { + return policyMask; + } + } + throw new IllegalArgumentException( + "Invalid enum value '" + value + "', Allowed values are " + Arrays.toString(values())); + } + + /** + * Determine if we are allowed access to what we want, based upon what we have. + * + * @param have The PolicyMask we have. + * @param want The PolicyMask we want. + * @return true if we have access, false if not. + */ + public static boolean allows(AccessLevel have, AccessLevel want) { + // 1) If we're to be denied everything, or the permission is deny everyone, we're denied. + if (have.equals(DENY) || want.equals(DENY)) { + return false; + } + // 2) Otherwise, if we have exactly what we need, we're allowed access. + if (have.equals(want)) { + return true; + } + // 3) We're allowed access to READ if we have WRITE + if (have.equals(WRITE) && want.equals(READ)) { + return true; + } + // 4) Otherwise, we're denied access. + return false; + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/org/overture/ego/model/enums/ApplicationStatus.java b/src/main/java/bio/overture/ego/model/enums/ApplicationStatus.java similarity index 89% rename from src/main/java/org/overture/ego/model/enums/ApplicationStatus.java rename to src/main/java/bio/overture/ego/model/enums/ApplicationStatus.java index 95b40313f..ee6d29547 100644 --- a/src/main/java/org/overture/ego/model/enums/ApplicationStatus.java +++ b/src/main/java/bio/overture/ego/model/enums/ApplicationStatus.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.overture.ego.model.enums; +package bio.overture.ego.model.enums; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -24,10 +24,10 @@ public enum ApplicationStatus { APPROVED("Approved"), DISABLED("Disabled"), PENDING("Pending"), - REJECTED("Rejected"),; + REJECTED("Rejected"), + ; - @NonNull - private final String value; + @NonNull private final String value; @Override public String toString() { diff --git a/src/main/java/org/overture/ego/repository/queryspecification/AclPermissionSpecification.java b/src/main/java/bio/overture/ego/model/enums/ApplicationType.java similarity index 77% rename from src/main/java/org/overture/ego/repository/queryspecification/AclPermissionSpecification.java rename to src/main/java/bio/overture/ego/model/enums/ApplicationType.java index c012527b5..c664248d3 100644 --- a/src/main/java/org/overture/ego/repository/queryspecification/AclPermissionSpecification.java +++ b/src/main/java/bio/overture/ego/model/enums/ApplicationType.java @@ -14,10 +14,14 @@ * limitations under the License. */ -package org.overture.ego.repository.queryspecification; +package bio.overture.ego.model.enums; -import org.overture.ego.model.entity.Permission; - -public class AclPermissionSpecification extends SpecificationBase { +public enum ApplicationType { + CLIENT, + ADMIN; + @Override + public String toString() { + return this.name(); + } } diff --git a/src/main/java/bio/overture/ego/model/enums/Fields.java b/src/main/java/bio/overture/ego/model/enums/Fields.java new file mode 100644 index 000000000..ba1930c7c --- /dev/null +++ b/src/main/java/bio/overture/ego/model/enums/Fields.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.enums; + +import static lombok.AccessLevel.PRIVATE; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class Fields { + + public static final String ID = "id"; +} diff --git a/src/main/java/bio/overture/ego/model/enums/JavaFields.java b/src/main/java/bio/overture/ego/model/enums/JavaFields.java new file mode 100644 index 000000000..ceb8b5222 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/enums/JavaFields.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.enums; + +import static lombok.AccessLevel.PRIVATE; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class JavaFields { + + public static final String ID = "id"; + public static final String NAME = "name"; + public static final String EMAIL = "email"; + public static final String STATUS = "status"; + public static final String POLICY = "policy"; + public static final String ACCESS_LEVEL = "accessLevel"; + public static final String FIRSTNAME = "firstName"; + public static final String LASTNAME = "lastName"; + public static final String CREATEDAT = "createdAt"; + public static final String LASTLOGIN = "lastLogin"; + public static final String PREFERREDLANGUAGE = "preferredLanguage"; + public static final String APPLICATIONS = "applications"; + public static final String APPLICATION = "application"; + public static final String OWNER = "owner"; + public static final String SCOPES = "scopes"; + public static final String GROUPS = "groups"; + public static final String GROUP = "group"; + public static final String USERS = "users"; + public static final String USER = "user"; + public static final String USERTYPE = "usertype"; + public static final String APPLICATIONTYPE = "applicationtype"; + public static final String TOKENS = "tokens"; + public static final String TOKEN = "token"; + public static final String USERPERMISSIONS = "userPermissions"; + public static final String PERMISSIONS = "permissions"; + public static final String USERPERMISSION = "userPermission"; + public static final String GROUPPERMISSION = "groupPermission"; + public static final String GROUPPERMISSIONS = "groupPermissions"; + public static final String DESCRIPTION = "description"; + public static final String CLIENTID = "clientId"; + public static final String CLIENTSECRET = "clientSecret"; + public static final String REDIRECTURI = "redirectUri"; + public static final String USER_ID = "userId"; + public static final String GROUP_ID = "groupId"; + public static final String USERGROUPS = "userGroups"; + public static final String APPLICATION_ID = "applicationId"; + public static final String GROUPAPPLICATIONS = "groupApplications"; +} diff --git a/src/main/java/bio/overture/ego/model/enums/LanguageType.java b/src/main/java/bio/overture/ego/model/enums/LanguageType.java new file mode 100644 index 000000000..b9a53412c --- /dev/null +++ b/src/main/java/bio/overture/ego/model/enums/LanguageType.java @@ -0,0 +1,15 @@ +package bio.overture.ego.model.enums; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum LanguageType { + ENGLISH, + FRENCH, + SPANISH; + + @Override + public String toString() { + return this.name(); + } +} diff --git a/src/main/java/bio/overture/ego/model/enums/LombokFields.java b/src/main/java/bio/overture/ego/model/enums/LombokFields.java new file mode 100644 index 000000000..730200030 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/enums/LombokFields.java @@ -0,0 +1,27 @@ +package bio.overture.ego.model.enums; + +import static lombok.AccessLevel.PRIVATE; + +import lombok.NoArgsConstructor; + +/** + * Note: When using a Lombok annotation with field names (for example @EqualsAndHashCode(ids = + * {LombokFields.id}) lombok does not look at the variable's value, but instead takes the variables + * name as the value. https://github.com/rzwitserloot/lombok/issues/1094 + */ +@NoArgsConstructor(access = PRIVATE) +public class LombokFields { + + public static final String id = "doesn't matter, lombok doesnt use this string"; + public static final String groups = "doesn't matter, lombok doesnt use this string"; + public static final String applications = "doesn't matter, lombok doesnt use this string"; + public static final String policy = "doesn't matter, lombok doesnt use this string"; + public static final String userPermissions = "doesn't matter, lombok doesnt use this string"; + public static final String owner = "doesn't matter, lombok doesnt use this string"; + public static final String scopes = "doesn't matter, lombok doesnt use this string"; + public static final String users = "doesn't matter, lombok doesnt use this string"; + public static final String permissions = "doesn't matter, lombok doesnt use this string"; + public static final String tokens = "doesn't matter, lombok doesnt use this string"; + public static final String userGroups = "doesn't matter, lombok doesnt use this string"; + public static final String groupApplications = "doesn't matter, lombok doesnt use this string"; +} diff --git a/src/main/java/bio/overture/ego/model/enums/SqlFields.java b/src/main/java/bio/overture/ego/model/enums/SqlFields.java new file mode 100644 index 000000000..cc668f7b3 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/enums/SqlFields.java @@ -0,0 +1,34 @@ +package bio.overture.ego.model.enums; + +import static lombok.AccessLevel.PRIVATE; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class SqlFields { + + public static final String ID = "id"; + public static final String NAME = "name"; + public static final String EMAIL = "email"; + public static final String TYPE = "type"; + public static final String STATUS = "status"; + public static final String FIRSTNAME = "firstname"; + public static final String LASTNAME = "lastname"; + public static final String CREATEDAT = "createdat"; + public static final String LASTLOGIN = "lastlogin"; + public static final String PREFERREDLANGUAGE = "preferredlanguage"; + public static final String USERID_JOIN = "user_id"; + public static final String GROUPID_JOIN = "group_id"; + public static final String TOKENID_JOIN = "token_id"; + public static final String APPID_JOIN = "application_id"; + public static final String ACCESS_LEVEL = "access_level"; + public static final String OWNER = "owner"; + public static final String DESCRIPTION = "description"; + public static final String POLICYID_JOIN = "policy_id"; + public static final String CLIENTID = "clientid"; + public static final String CLIENTSECRET = "clientsecret"; + public static final String REDIRECTURI = "redirecturi"; + public static final String ISSUEDATE = "issuedate"; + public static final String EXPIRYDATE = "expirydate"; + public static final String ISREVOKED = "isrevoked"; +} diff --git a/src/main/java/bio/overture/ego/model/enums/StatusType.java b/src/main/java/bio/overture/ego/model/enums/StatusType.java new file mode 100644 index 000000000..ef837eee7 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/enums/StatusType.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.enums; + +import static bio.overture.ego.utils.Joiners.COMMA; +import static bio.overture.ego.utils.Streams.stream; +import static java.lang.String.format; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum StatusType { + APPROVED, + DISABLED, + PENDING, + REJECTED; + + public static StatusType resolveStatusType(@NonNull String statusType) { + return stream(values()) + .filter(x -> x.toString().equals(statusType)) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + format( + "The status type '%s' cannot be resolved. Must be one of: [%s]", + statusType, COMMA.join(values())))); + } + + @Override + public String toString() { + return this.name(); + } +} diff --git a/src/main/java/bio/overture/ego/model/enums/Tables.java b/src/main/java/bio/overture/ego/model/enums/Tables.java new file mode 100644 index 000000000..f73906335 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/enums/Tables.java @@ -0,0 +1,21 @@ +package bio.overture.ego.model.enums; + +import static lombok.AccessLevel.PRIVATE; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class Tables { + + public static final String APPLICATION = "egoapplication"; + public static final String GROUP = "egogroup"; + public static final String TOKEN = "token"; + public static final String GROUP_APPLICATION = "groupapplication"; + public static final String USER_GROUP = "usergroup"; + public static final String EGOUSER = "egouser"; + public static final String USER_APPLICATION = "userapplication"; + public static final String USER_PERMISSION = "userpermission"; + public static final String GROUP_PERMISSION = "grouppermission"; + public static final String POLICY = "policy"; + public static final String TOKENSCOPE = "tokenscope"; +} diff --git a/src/main/java/bio/overture/ego/model/enums/UserType.java b/src/main/java/bio/overture/ego/model/enums/UserType.java new file mode 100644 index 000000000..749ae7807 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/enums/UserType.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.model.enums; + +import static bio.overture.ego.utils.Joiners.COMMA; +import static bio.overture.ego.utils.Streams.stream; +import static java.lang.String.format; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum UserType { + USER, + ADMIN; + + public static UserType resolveUserType(@NonNull String userType) { + return stream(values()) + .filter(x -> x.toString().equals(userType)) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + format( + "The user type '%s' cannot be resolved. Must be one of: [%s]", + userType, COMMA.join(values())))); + } + + @Override + public String toString() { + return this.name(); + } +} diff --git a/src/main/java/bio/overture/ego/model/exceptions/ExceptionHandlers.java b/src/main/java/bio/overture/ego/model/exceptions/ExceptionHandlers.java new file mode 100644 index 000000000..fd4cdb42f --- /dev/null +++ b/src/main/java/bio/overture/ego/model/exceptions/ExceptionHandlers.java @@ -0,0 +1,33 @@ +package bio.overture.ego.model.exceptions; + +import static java.lang.String.format; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +import bio.overture.ego.utils.Joiners; +import javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Slf4j +@ControllerAdvice +public class ExceptionHandlers { + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolationException( + HttpServletRequest req, ConstraintViolationException ex) { + val message = buildConstraintViolationMessage(ex); + log.error(message); + return new ResponseEntity(message, new HttpHeaders(), BAD_REQUEST); + } + + private static String buildConstraintViolationMessage(ConstraintViolationException ex) { + return format( + "Constraint violation: [message] : %s ------- [violations] : %s", + ex.getMessage(), Joiners.COMMA.join(ex.getConstraintViolations())); + } +} diff --git a/src/main/java/bio/overture/ego/model/exceptions/ForbiddenException.java b/src/main/java/bio/overture/ego/model/exceptions/ForbiddenException.java new file mode 100644 index 000000000..29bbc1f48 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/exceptions/ForbiddenException.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.model.exceptions; + +import static org.springframework.http.HttpStatus.FORBIDDEN; + +import lombok.NonNull; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(FORBIDDEN) +public class ForbiddenException extends RuntimeException { + + public ForbiddenException(@NonNull String message) { + super(message); + } +} diff --git a/src/main/java/bio/overture/ego/model/exceptions/MalformedRequestException.java b/src/main/java/bio/overture/ego/model/exceptions/MalformedRequestException.java new file mode 100644 index 000000000..e61078e2c --- /dev/null +++ b/src/main/java/bio/overture/ego/model/exceptions/MalformedRequestException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015 The Ontario Institute for Cancer Research. All rights reserved. + * + * This program and the accompanying materials are made available under the terms of the GNU Public License v3.0. + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package bio.overture.ego.model.exceptions; + +import static java.lang.String.format; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +import lombok.NonNull; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(BAD_REQUEST) +public class MalformedRequestException extends RuntimeException { + public MalformedRequestException(@NonNull String message) { + super(message); + } + + public static void checkMalformedRequest( + boolean expression, @NonNull String formattedMessage, @NonNull Object... args) { + if (!expression) { + throw new MalformedRequestException(format(formattedMessage, args)); + } + } + + public static MalformedRequestException buildMalformedRequest( + @NonNull String formattedMessage, Object... args) { + return new MalformedRequestException(format(formattedMessage, args)); + } +} diff --git a/src/main/java/bio/overture/ego/model/exceptions/NotFoundException.java b/src/main/java/bio/overture/ego/model/exceptions/NotFoundException.java new file mode 100644 index 000000000..4c3438051 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/exceptions/NotFoundException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015 The Ontario Institute for Cancer Research. All rights reserved. + * + * This program and the accompanying materials are made available under the terms of the GNU Public License v3.0. + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package bio.overture.ego.model.exceptions; + +import static java.lang.String.format; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import lombok.NonNull; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(NOT_FOUND) +public class NotFoundException extends RuntimeException { + public NotFoundException(@NonNull String message) { + super(message); + } + + public static void checkNotFound( + boolean expression, @NonNull String formattedMessage, @NonNull Object... args) { + if (!expression) { + throw new NotFoundException(format(formattedMessage, args)); + } + } + + public static NotFoundException buildNotFoundException( + @NonNull String formattedMessage, Object... args) { + return new NotFoundException(format(formattedMessage, args)); + } +} diff --git a/src/main/java/bio/overture/ego/model/exceptions/RequestValidationException.java b/src/main/java/bio/overture/ego/model/exceptions/RequestValidationException.java new file mode 100644 index 000000000..0a86301f2 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/exceptions/RequestValidationException.java @@ -0,0 +1,43 @@ +package bio.overture.ego.model.exceptions; + +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static bio.overture.ego.utils.Joiners.PRETTY_COMMA; +import static java.lang.String.format; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +import javax.validation.Validation; +import javax.validation.Validator; +import lombok.NonNull; +import lombok.val; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(BAD_REQUEST) +public class RequestValidationException extends RuntimeException { + + /** + * Validator is thread-safe so can be a constant + * https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#_validating_constraints + */ + private static final Validator VALIDATOR = + Validation.buildDefaultValidatorFactory().getValidator(); + + public RequestValidationException(String message) { + super(message); + } + + public static void checkRequestValid(@NonNull T objectToValidate) { + val errors = VALIDATOR.validate(objectToValidate); + if (!errors.isEmpty()) { + val requestViolations = + errors.stream().map(RequestViolation::createRequestViolation).collect(toImmutableSet()); + val formattedMessage = + "The object of type '%s' with value '%s' has the following constraint violations: [%s]"; + throw new RequestValidationException( + format( + formattedMessage, + objectToValidate.getClass().getSimpleName(), + objectToValidate, + PRETTY_COMMA.join(requestViolations))); + } + } +} diff --git a/src/main/java/bio/overture/ego/model/exceptions/RequestViolation.java b/src/main/java/bio/overture/ego/model/exceptions/RequestViolation.java new file mode 100644 index 000000000..c9eb2f850 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/exceptions/RequestViolation.java @@ -0,0 +1,22 @@ +package bio.overture.ego.model.exceptions; + +import javax.validation.ConstraintViolation; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +@Value +@Builder +public class RequestViolation { + @NonNull private final String fieldName; + private final Object fieldValue; + @NonNull private final String error; + + public static RequestViolation createRequestViolation(ConstraintViolation v) { + return RequestViolation.builder() + .error(v.getMessage()) + .fieldName(v.getPropertyPath().toString()) + .fieldValue(v.getInvalidValue()) + .build(); + } +} diff --git a/src/main/java/bio/overture/ego/model/exceptions/UniqueViolationException.java b/src/main/java/bio/overture/ego/model/exceptions/UniqueViolationException.java new file mode 100644 index 000000000..e450668a5 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/exceptions/UniqueViolationException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2015 The Ontario Institute for Cancer Research. All rights reserved. + * + * This program and the accompanying materials are made available under the terms of the GNU Public License v3.0. + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package bio.overture.ego.model.exceptions; + +import static java.lang.String.format; +import static org.springframework.http.HttpStatus.CONFLICT; + +import lombok.NonNull; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(CONFLICT) +public class UniqueViolationException extends RuntimeException { + public UniqueViolationException(@NonNull String message) { + super(message); + } + + public static void checkUnique( + boolean expression, @NonNull String formattedMessage, @NonNull Object... args) { + if (!expression) { + throw new UniqueViolationException(format(formattedMessage, args)); + } + } +} diff --git a/src/main/java/bio/overture/ego/model/join/GroupApplication.java b/src/main/java/bio/overture/ego/model/join/GroupApplication.java new file mode 100644 index 000000000..978a8fd88 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/join/GroupApplication.java @@ -0,0 +1,50 @@ +package bio.overture.ego.model.join; + +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.Identifiable; +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.enums.LombokFields; +import bio.overture.ego.model.enums.SqlFields; +import bio.overture.ego.model.enums.Tables; +import javax.persistence.CascadeType; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; +import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@Entity +@Table(name = Tables.GROUP_APPLICATION) +@Builder +@EqualsAndHashCode(of = {LombokFields.id}) +@NoArgsConstructor +@AllArgsConstructor +@ToString(exclude = {JavaFields.GROUP, JavaFields.APPLICATION}) +public class GroupApplication implements Identifiable { + + @EmbeddedId private GroupApplicationId id; + + @MapsId(value = JavaFields.GROUP_ID) + @JoinColumn(name = SqlFields.GROUPID_JOIN, nullable = false, updatable = false) + @ManyToOne( + cascade = {CascadeType.PERSIST, CascadeType.MERGE}, + fetch = FetchType.LAZY) + private Group group; + + @MapsId(value = JavaFields.APPLICATION_ID) + @JoinColumn(name = SqlFields.APPID_JOIN, nullable = false, updatable = false) + @ManyToOne( + cascade = {CascadeType.PERSIST, CascadeType.MERGE}, + fetch = FetchType.LAZY) + private Application application; +} diff --git a/src/main/java/bio/overture/ego/model/join/GroupApplicationId.java b/src/main/java/bio/overture/ego/model/join/GroupApplicationId.java new file mode 100644 index 000000000..c7e5ab2bc --- /dev/null +++ b/src/main/java/bio/overture/ego/model/join/GroupApplicationId.java @@ -0,0 +1,25 @@ +package bio.overture.ego.model.join; + +import bio.overture.ego.model.enums.SqlFields; +import java.io.Serializable; +import java.util.UUID; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@Embeddable +@NoArgsConstructor +@AllArgsConstructor +public class GroupApplicationId implements Serializable { + + @Column(name = SqlFields.GROUPID_JOIN) + private UUID groupId; + + @Column(name = SqlFields.APPID_JOIN) + private UUID applicationId; +} diff --git a/src/main/java/bio/overture/ego/model/join/UserGroup.java b/src/main/java/bio/overture/ego/model/join/UserGroup.java new file mode 100644 index 000000000..6742a71e6 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/join/UserGroup.java @@ -0,0 +1,50 @@ +package bio.overture.ego.model.join; + +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.Identifiable; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.enums.LombokFields; +import bio.overture.ego.model.enums.SqlFields; +import bio.overture.ego.model.enums.Tables; +import javax.persistence.CascadeType; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; +import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@Entity +@Table(name = Tables.USER_GROUP) +@Builder +@EqualsAndHashCode(of = {LombokFields.id}) +@NoArgsConstructor +@AllArgsConstructor +@ToString(exclude = {JavaFields.USER, JavaFields.GROUP}) +public class UserGroup implements Identifiable { + + @EmbeddedId private UserGroupId id; + + @MapsId(value = JavaFields.USER_ID) + @JoinColumn(name = SqlFields.USERID_JOIN, nullable = false, updatable = false) + @ManyToOne( + cascade = {CascadeType.PERSIST, CascadeType.MERGE}, + fetch = FetchType.LAZY) + private User user; + + @MapsId(value = JavaFields.GROUP_ID) + @JoinColumn(name = SqlFields.GROUPID_JOIN, nullable = false, updatable = false) + @ManyToOne( + cascade = {CascadeType.PERSIST, CascadeType.MERGE}, + fetch = FetchType.LAZY) + private Group group; +} diff --git a/src/main/java/bio/overture/ego/model/join/UserGroupId.java b/src/main/java/bio/overture/ego/model/join/UserGroupId.java new file mode 100644 index 000000000..5d74805e4 --- /dev/null +++ b/src/main/java/bio/overture/ego/model/join/UserGroupId.java @@ -0,0 +1,25 @@ +package bio.overture.ego.model.join; + +import bio.overture.ego.model.enums.SqlFields; +import java.io.Serializable; +import java.util.UUID; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@Embeddable +@NoArgsConstructor +@AllArgsConstructor +public class UserGroupId implements Serializable { + + @Column(name = SqlFields.USERID_JOIN) + private UUID userId; + + @Column(name = SqlFields.GROUPID_JOIN) + private UUID groupId; +} diff --git a/src/main/java/bio/overture/ego/model/params/ScopeName.java b/src/main/java/bio/overture/ego/model/params/ScopeName.java new file mode 100644 index 000000000..462693bbe --- /dev/null +++ b/src/main/java/bio/overture/ego/model/params/ScopeName.java @@ -0,0 +1,33 @@ +package bio.overture.ego.model.params; + +import static java.lang.String.format; + +import bio.overture.ego.model.enums.AccessLevel; +import lombok.Data; +import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; + +@Data +public class ScopeName { + private String scopeName; + + public ScopeName(String name) { + if (name.indexOf(".") == -1) { + throw new InvalidScopeException( + format("Bad scope name '%s'. Must be of the form \".\"", name)); + } + scopeName = name; + } + + public AccessLevel getAccessLevel() { + return AccessLevel.fromValue(scopeName.substring(scopeName.lastIndexOf(".") + 1)); + } + + public String getName() { + return scopeName.substring(0, scopeName.lastIndexOf(".")); + } + + @Override + public String toString() { + return scopeName; + } +} diff --git a/src/main/java/org/overture/ego/model/search/Filters.java b/src/main/java/bio/overture/ego/model/search/Filters.java similarity index 91% rename from src/main/java/org/overture/ego/model/search/Filters.java rename to src/main/java/bio/overture/ego/model/search/Filters.java index 6b8761b61..6ad351d3c 100644 --- a/src/main/java/org/overture/ego/model/search/Filters.java +++ b/src/main/java/bio/overture/ego/model/search/Filters.java @@ -14,12 +14,11 @@ * limitations under the License. */ -package org.overture.ego.model.search; +package bio.overture.ego.model.search; import java.lang.annotation.*; @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented -public @interface Filters { -} +public @interface Filters {} diff --git a/src/main/java/org/overture/ego/model/search/SearchFilter.java b/src/main/java/bio/overture/ego/model/search/SearchFilter.java similarity index 86% rename from src/main/java/org/overture/ego/model/search/SearchFilter.java rename to src/main/java/bio/overture/ego/model/search/SearchFilter.java index 4a6d9508b..edee06dbb 100644 --- a/src/main/java/org/overture/ego/model/search/SearchFilter.java +++ b/src/main/java/bio/overture/ego/model/search/SearchFilter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.overture.ego.model.search; +package bio.overture.ego.model.search; import lombok.Data; import lombok.NonNull; @@ -24,9 +24,6 @@ @RequiredArgsConstructor public class SearchFilter { - @NonNull - private String filterField; - @NonNull - private String filterValue; - + @NonNull private String filterField; + @NonNull private String filterValue; } diff --git a/src/main/java/org/overture/ego/provider/facebook/FacebookTokenService.java b/src/main/java/bio/overture/ego/provider/facebook/FacebookTokenService.java similarity index 56% rename from src/main/java/org/overture/ego/provider/facebook/FacebookTokenService.java rename to src/main/java/bio/overture/ego/provider/facebook/FacebookTokenService.java index 2de8737c4..4319ada3b 100644 --- a/src/main/java/org/overture/ego/provider/facebook/FacebookTokenService.java +++ b/src/main/java/bio/overture/ego/provider/facebook/FacebookTokenService.java @@ -14,127 +14,136 @@ * limitations under the License. */ -package org.overture.ego.provider.facebook; - +package bio.overture.ego.provider.facebook; +import bio.overture.ego.token.IDToken; +import bio.overture.ego.utils.TypeUtils; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import javax.annotation.PostConstruct; import lombok.NoArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import lombok.val; -import org.overture.ego.token.IDToken; -import org.overture.ego.utils.TypeUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpMethod; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; -import javax.annotation.PostConstruct; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - - @Slf4j @Component @NoArgsConstructor public class FacebookTokenService { /* - Variables - */ - @Value("${facebook.client.id}") + * Constants + */ + private static final String USER_EMAIL = "email"; + private static final String USER_NAME = "name"; + private static final String USER_GIVEN_NAME = "given_name"; + private static final String USER_LAST_NAME = "family_name"; + private static final String IS_VALID = "is_valid"; + private static final String DATA = "data"; + /* + * Dependencies + */ + protected RestTemplate fbConnector; + /* + * Variables + */ + @Value("${facebook.client.clientId}") private String clientId; - @Value("${facebook.client.secret}") + + @Value("${facebook.client.clientSecret}") private String clientSecret; + @Value("${facebook.client.accessTokenUri}") private String accessTokenUri; + @Value("${facebook.client.tokenValidateUri}") private String tokenValidateUri; + @Value("${facebook.client.timeout.connect}") private int connectTimeout; + @Value("${facebook.client.timeout.read}") private int readTimeout; + @Value("${facebook.resource.userInfoUri}") private String userInfoUri; - /* - Dependencies - */ - protected RestTemplate fbConnector; - - /* - Constants - */ - private final static String USER_EMAIL = "email"; - private final static String USER_NAME = "name"; - private final static String USER_GIVEN_NAME = "given_name"; - private final static String USER_LAST_NAME = "family_name"; - private final static String IS_VALID = "is_valid"; - private final static String DATA = "data"; - @PostConstruct - private void init(){ + private void init() { fbConnector = new RestTemplate(httpRequestFactory()); } - public boolean validToken(String fbToken){ + public boolean validToken(String fbToken) { log.debug("Validating Facebook token: {}", fbToken); val tokenCheckUri = getValidationUri(fbToken); try { - return fbConnector.execute(new URI(tokenCheckUri), HttpMethod.GET, null, - response -> { - val jsonObj = getJsonResponseAsMap(response.getBody()); - if(jsonObj.isPresent()) { - val output = ((HashMap) jsonObj.get().get(DATA)); - if (output.containsKey(IS_VALID)) { - return (Boolean)output.get(IS_VALID); - } else { - log.error("Error while validating Facebook token: {}", output); - return false; - } - } else return false; - }); - } catch (URISyntaxException uex){ + return fbConnector.execute( + new URI(tokenCheckUri), + HttpMethod.GET, + null, + response -> { + val jsonObj = getJsonResponseAsMap(response.getBody()); + if (jsonObj.isPresent()) { + val output = ((HashMap) jsonObj.get().get(DATA)); + if (output.containsKey(IS_VALID)) { + return (Boolean) output.get(IS_VALID); + } else { + log.error("Error while validating Facebook token: {}", output); + return false; + } + } else return false; + }); + } catch (URISyntaxException uex) { log.error("Invalid URI syntax: {}, {}", tokenCheckUri, uex.getMessage()); return false; } } - public Optional getAuthInfo(String fbToken){ + public Optional getAuthInfo(String fbToken) { log.debug("Getting details for Facebook token: {}", fbToken); val userDetailsUri = getUserDetailsUri(fbToken); try { - return fbConnector.execute(new URI(userDetailsUri), HttpMethod.GET, null, - response -> { - val jsonObj = getJsonResponseAsMap(response.getBody()); - if(jsonObj.isPresent()) { - val output = new HashMap(); - output.put(USER_EMAIL, jsonObj.get().get(USER_EMAIL).toString()); - val name = jsonObj.get().get(USER_NAME).toString().split(" "); - output.put(USER_GIVEN_NAME, name[0]); - output.put(USER_LAST_NAME, name[1]); - return Optional.of(TypeUtils.convertToAnotherType(output, IDToken.class)); - } else return Optional.empty(); - }); - } catch (URISyntaxException uex){ + return fbConnector.execute( + new URI(userDetailsUri), + HttpMethod.GET, + null, + response -> { + val jsonObj = getJsonResponseAsMap(response.getBody()); + if (jsonObj.isPresent()) { + val output = new HashMap(); + output.put(USER_EMAIL, jsonObj.get().get(USER_EMAIL).toString()); + val name = jsonObj.get().get(USER_NAME).toString().split(" "); + output.put(USER_GIVEN_NAME, name[0]); + output.put(USER_LAST_NAME, name[1]); + return Optional.of(TypeUtils.convertToAnotherType(output, IDToken.class)); + } else return Optional.empty(); + }); + } catch (URISyntaxException uex) { log.error("Invalid URI syntax: {}, {}", userDetailsUri, uex.getMessage()); return Optional.empty(); - } - catch (Exception ex){ + } catch (Exception ex) { log.error("Error getting email response from Facebook: {}", ex.getMessage()); - log.debug("Error getting email response from Facebook for uri: {}, {}", userDetailsUri,ex.getMessage()); + log.debug( + "Error getting email response from Facebook for uri: {}, {}", + userDetailsUri, + ex.getMessage()); return Optional.empty(); } } - private Optional getJsonResponseAsMap(InputStream jsonResponse){ + private Optional> getJsonResponseAsMap(InputStream jsonResponse) { val objectMapper = new ObjectMapper(); Map jsonObj = null; @@ -152,9 +161,12 @@ private String getUserDetailsUri(String fbToken) { } @SneakyThrows - private String getValidationUri(String fbToken){ - return tokenValidateUri+"?input_token=" + fbToken + "&access_token="+ - URLEncoder.encode(clientId + "|" + clientSecret, "UTF-8"); + private String getValidationUri(String fbToken) { + return tokenValidateUri + + "?input_token=" + + fbToken + + "&access_token=" + + URLEncoder.encode(clientId + "|" + clientSecret, "UTF-8"); } private HttpComponentsClientHttpRequestFactory httpRequestFactory() { @@ -163,5 +175,4 @@ private HttpComponentsClientHttpRequestFactory httpRequestFactory() { factory.setReadTimeout(readTimeout); return factory; } - } diff --git a/src/main/java/org/overture/ego/provider/google/GoogleTokenService.java b/src/main/java/bio/overture/ego/provider/google/GoogleTokenService.java similarity index 52% rename from src/main/java/org/overture/ego/provider/google/GoogleTokenService.java rename to src/main/java/bio/overture/ego/provider/google/GoogleTokenService.java index e27f7c15a..7991a4dde 100644 --- a/src/main/java/org/overture/ego/provider/google/GoogleTokenService.java +++ b/src/main/java/bio/overture/ego/provider/google/GoogleTokenService.java @@ -14,83 +14,75 @@ * limitations under the License. */ -package org.overture.ego.provider.google; +package bio.overture.ego.provider.google; +import static bio.overture.ego.utils.TypeUtils.convertToAnotherType; +import static java.util.Arrays.asList; + +import bio.overture.ego.token.IDToken; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; -import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Map; +import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.SneakyThrows; -import lombok.Synchronized; import lombok.extern.slf4j.Slf4j; import lombok.val; -import org.overture.ego.token.IDToken; -import org.overture.ego.utils.TypeUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.jwt.JwtHelper; import org.springframework.stereotype.Component; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - @Slf4j @Component +@NoArgsConstructor public class GoogleTokenService { - @Value("${google.client.Ids}") + /* + * Dependencies + */ + @Value("${google.client.clientId}") private String clientIDs; - private HttpTransport transport; - private JsonFactory jsonFactory; - private GoogleIdTokenVerifier verifier; - public GoogleTokenService() { - transport = new NetHttpTransport(); - jsonFactory = new JacksonFactory(); - } + /* + * State + */ + @Getter(lazy = true) + private final GoogleIdTokenVerifier verifier = initVerifier(); public boolean validToken(String token) { - if (verifier == null) - initVerifier(); + val verifier = this.getVerifier(); GoogleIdToken idToken = null; try { idToken = verifier.verify(token); - } catch (GeneralSecurityException gEX) { + } catch (GeneralSecurityException | IOException gEX) { log.error("Error while verifying google token: {}", gEX); - } catch (IOException ioEX) { - log.error("Error while verifying google token: {}", ioEX); - } catch (Exception ex) { - log.error("Error while verifying google token: {}", ex); } - return (idToken != null); } - @Synchronized - private void initVerifier() { - List targetAudience; - if (clientIDs.contains(",")) - targetAudience = Arrays.asList(clientIDs.split(",")); - else { - targetAudience = new ArrayList(); - targetAudience.add(clientIDs); - } - verifier = - new GoogleIdTokenVerifier.Builder(transport, jsonFactory) - .setAudience(targetAudience) - .build(); + private GoogleIdTokenVerifier initVerifier() { + checkState(); + val targetAudience = asList(clientIDs.split(",")); + return new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory()) + .setAudience(targetAudience) + .build(); } @SneakyThrows - public IDToken decode(String token){ - val tokenDecoded = JwtHelper.decode(token); - val authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class); - return TypeUtils.convertToAnotherType(authInfo, IDToken.class); + public IDToken decode(String token) { + val claims = JwtHelper.decode(token).getClaims(); + val authInfo = new ObjectMapper().readValue(claims, Map.class); + return convertToAnotherType(authInfo, IDToken.class); + } + + private void checkState() { + if (clientIDs == null) { + throw new IllegalStateException("No client Ids are configured for google. "); + } } } diff --git a/src/main/java/bio/overture/ego/repository/ApplicationRepository.java b/src/main/java/bio/overture/ego/repository/ApplicationRepository.java new file mode 100644 index 000000000..7d4e8fe5b --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/ApplicationRepository.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.repository; + +import bio.overture.ego.model.entity.Application; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +public interface ApplicationRepository extends NamedRepository { + + Optional getApplicationByNameIgnoreCase(String name); + + boolean existsByClientIdIgnoreCase(String clientId); + + boolean existsByNameIgnoreCase(String name); + + Set findAllByIdIn(List ids); + + /** Refer to NamedRepository.findByName Deprecation note */ + @Override + @Deprecated + default Optional findByName(String name) { + return getApplicationByNameIgnoreCase(name); + } +} diff --git a/src/main/java/bio/overture/ego/repository/BaseRepository.java b/src/main/java/bio/overture/ego/repository/BaseRepository.java new file mode 100644 index 000000000..e60030aa0 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/BaseRepository.java @@ -0,0 +1,14 @@ +package bio.overture.ego.repository; + +import java.util.Collection; +import java.util.Set; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.PagingAndSortingRepository; + +@NoRepositoryBean +public interface BaseRepository + extends PagingAndSortingRepository, JpaSpecificationExecutor { + + Set findAllByIdIn(Collection ids); +} diff --git a/src/main/java/bio/overture/ego/repository/GroupPermissionRepository.java b/src/main/java/bio/overture/ego/repository/GroupPermissionRepository.java new file mode 100644 index 000000000..401b80446 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/GroupPermissionRepository.java @@ -0,0 +1,15 @@ +package bio.overture.ego.repository; + +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH; + +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.GroupPermission; +import java.util.Set; +import java.util.UUID; +import org.springframework.data.jpa.repository.EntityGraph; + +public interface GroupPermissionRepository extends PermissionRepository { + + @EntityGraph(value = "group-permission-entity-with-relationships", type = FETCH) + Set findAllByOwner_Id(UUID id); +} diff --git a/src/main/java/org/overture/ego/repository/UserRepository.java b/src/main/java/bio/overture/ego/repository/GroupRepository.java similarity index 55% rename from src/main/java/org/overture/ego/repository/UserRepository.java rename to src/main/java/bio/overture/ego/repository/GroupRepository.java index 8e8b57ec7..aa57ccc3c 100644 --- a/src/main/java/org/overture/ego/repository/UserRepository.java +++ b/src/main/java/bio/overture/ego/repository/GroupRepository.java @@ -14,21 +14,22 @@ * limitations under the License. */ -package org.overture.ego.repository; - -import org.overture.ego.model.entity.User; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.repository.PagingAndSortingRepository; +package bio.overture.ego.repository; +import bio.overture.ego.model.entity.Group; +import java.util.Optional; import java.util.UUID; +public interface GroupRepository extends NamedRepository { -public interface UserRepository extends - PagingAndSortingRepository, JpaSpecificationExecutor { + Optional getGroupByNameIgnoreCase(String name); - Page findAllByStatusIgnoreCase(String status, Pageable pageable); - User findOneByNameIgnoreCase(String name); + boolean existsByNameIgnoreCase(String name); + /** Refer to NamedRepository.findByName Deprecation note */ + @Override + @Deprecated + default Optional findByName(String name) { + return getGroupByNameIgnoreCase(name); + } } diff --git a/src/main/java/bio/overture/ego/repository/NamedRepository.java b/src/main/java/bio/overture/ego/repository/NamedRepository.java new file mode 100644 index 000000000..a3e10bf23 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/NamedRepository.java @@ -0,0 +1,16 @@ +package bio.overture.ego.repository; + +import java.util.Optional; +import org.springframework.data.repository.NoRepositoryBean; + +@NoRepositoryBean +public interface NamedRepository extends BaseRepository { + + /** + * TODO: [rtisma] Deprecated because this should be implemented at the service layer using dynamic + * fetching and not the entity graph. Leaving this for now. Once all services are implementing \ + * findByName, this can be removed from the NameRepository interface, and anything extending it + */ + @Deprecated + Optional findByName(String name); +} diff --git a/src/main/java/bio/overture/ego/repository/PermissionRepository.java b/src/main/java/bio/overture/ego/repository/PermissionRepository.java new file mode 100644 index 000000000..902288ce6 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/PermissionRepository.java @@ -0,0 +1,23 @@ +package bio.overture.ego.repository; + +import bio.overture.ego.model.entity.AbstractPermission; +import bio.overture.ego.model.entity.NameableEntity; +import bio.overture.ego.model.enums.AccessLevel; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import org.springframework.data.repository.NoRepositoryBean; + +@NoRepositoryBean +public interface PermissionRepository< + O extends NameableEntity, T extends AbstractPermission> + extends BaseRepository { + + Set findAllByPolicy_Id(UUID id); + + Set findAllByOwner_Id(UUID id); + + Optional findByPolicy_IdAndOwner_id(UUID policyId, UUID ownerId); + + Set findAllByPolicy_IdAndAccessLevel(UUID policyId, AccessLevel accessLevel); +} diff --git a/src/main/java/bio/overture/ego/repository/PolicyRepository.java b/src/main/java/bio/overture/ego/repository/PolicyRepository.java new file mode 100644 index 000000000..5a4b14072 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/PolicyRepository.java @@ -0,0 +1,23 @@ +package bio.overture.ego.repository; + +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH; + +import bio.overture.ego.model.entity.Policy; +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.EntityGraph; + +public interface PolicyRepository extends NamedRepository { + + @EntityGraph(value = "policy-entity-with-relationships", type = FETCH) + Optional getPolicyByNameIgnoreCase(String name); + + boolean existsByNameIgnoreCase(String name); + + /** Refer to NamedRepository.findByName Deprecation note */ + @Override + @Deprecated + default Optional findByName(String name) { + return getPolicyByNameIgnoreCase(name); + } +} diff --git a/src/main/java/bio/overture/ego/repository/TokenStoreRepository.java b/src/main/java/bio/overture/ego/repository/TokenStoreRepository.java new file mode 100644 index 000000000..87b7b93ba --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/TokenStoreRepository.java @@ -0,0 +1,33 @@ +package bio.overture.ego.repository; + +import bio.overture.ego.model.entity.Token; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface TokenStoreRepository extends NamedRepository { + + Optional getTokenByNameIgnoreCase(String name); + + Token findOneByNameIgnoreCase(String token); + + Set findAllByIdIn(List ids); + + @Modifying + @Query( + value = + "update token set isrevoked=true where token.id in (select revokes.id from ((select token.id, string_agg(concat(cast (tokenscope.policy_id as text), '.', tokenscope.access_level), ',' order by tokenscope.policy_id, tokenscope.access_level) as policies from token left join tokenscope on token.id = tokenscope.token_id where token.owner=:userId group by token.id order by policies, token.issuedate desc) EXCEPT (select distinct on (policies) token.id, string_agg(concat(cast (tokenscope.policy_id as text), '.', tokenscope.access_level), ',' order by tokenscope.policy_id, tokenscope.access_level) as policies from token left join tokenscope on token.id = tokenscope.token_id where token.owner=:userId group by token.id order by policies, token.issuedate desc)) as revokes)", + nativeQuery = true) + int revokeRedundantTokens(@Param("userId") UUID userId); + + // Set findAllByOwnerAndScopes(List ids); + + @Override + default Optional findByName(String name) { + return getTokenByNameIgnoreCase(name); + } +} diff --git a/src/main/java/bio/overture/ego/repository/UserPermissionRepository.java b/src/main/java/bio/overture/ego/repository/UserPermissionRepository.java new file mode 100644 index 000000000..8f29e7209 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/UserPermissionRepository.java @@ -0,0 +1,15 @@ +package bio.overture.ego.repository; + +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH; + +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.entity.UserPermission; +import java.util.Set; +import java.util.UUID; +import org.springframework.data.jpa.repository.EntityGraph; + +public interface UserPermissionRepository extends PermissionRepository { + + @EntityGraph(value = "user-permission-entity-with-relationships", type = FETCH) + Set findAllByOwner_Id(UUID id); +} diff --git a/src/main/java/org/overture/ego/repository/GroupRepository.java b/src/main/java/bio/overture/ego/repository/UserRepository.java similarity index 54% rename from src/main/java/org/overture/ego/repository/GroupRepository.java rename to src/main/java/bio/overture/ego/repository/UserRepository.java index 75ffa7405..954b83973 100644 --- a/src/main/java/org/overture/ego/repository/GroupRepository.java +++ b/src/main/java/bio/overture/ego/repository/UserRepository.java @@ -14,22 +14,26 @@ * limitations under the License. */ -package org.overture.ego.repository; - - -import org.overture.ego.model.entity.Group; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.repository.PagingAndSortingRepository; +package bio.overture.ego.repository; +import bio.overture.ego.model.entity.User; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; import java.util.UUID; +public interface UserRepository extends NamedRepository { + + Optional getUserByNameIgnoreCase(String name); -public interface GroupRepository extends - PagingAndSortingRepository, JpaSpecificationExecutor { + boolean existsByEmailIgnoreCase(String email); - Group findOneByNameIgnoreCase(String name); - Page findAllByStatusIgnoreCase(String status, Pageable pageable); + Set findAllByIdIn(Collection userIds); + /** Refer to NamedRepository.findByName Deprecation note */ + @Override + @Deprecated + default Optional findByName(String name) { + return getUserByNameIgnoreCase(name); + } } diff --git a/src/main/java/bio/overture/ego/repository/join/UserGroupRepository.java b/src/main/java/bio/overture/ego/repository/join/UserGroupRepository.java new file mode 100644 index 000000000..7cadbb28d --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/join/UserGroupRepository.java @@ -0,0 +1,7 @@ +package bio.overture.ego.repository.join; + +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.model.join.UserGroupId; +import bio.overture.ego.repository.BaseRepository; + +public interface UserGroupRepository extends BaseRepository {} diff --git a/src/main/java/bio/overture/ego/repository/queryspecification/ApplicationSpecification.java b/src/main/java/bio/overture/ego/repository/queryspecification/ApplicationSpecification.java new file mode 100644 index 000000000..a93e6956a --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/queryspecification/ApplicationSpecification.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.repository.queryspecification; + +import static bio.overture.ego.model.enums.JavaFields.CLIENTID; +import static bio.overture.ego.model.enums.JavaFields.CLIENTSECRET; +import static bio.overture.ego.model.enums.JavaFields.DESCRIPTION; +import static bio.overture.ego.model.enums.JavaFields.GROUP; +import static bio.overture.ego.model.enums.JavaFields.GROUPAPPLICATIONS; +import static bio.overture.ego.model.enums.JavaFields.ID; +import static bio.overture.ego.model.enums.JavaFields.NAME; +import static bio.overture.ego.model.enums.JavaFields.STATUS; +import static bio.overture.ego.model.enums.JavaFields.USERS; + +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.join.GroupApplication; +import bio.overture.ego.utils.QueryUtils; +import java.util.UUID; +import javax.persistence.criteria.Join; +import lombok.NonNull; +import lombok.val; +import org.springframework.data.jpa.domain.Specification; + +public class ApplicationSpecification extends SpecificationBase { + public static Specification containsText(@NonNull String text) { + val finalText = QueryUtils.prepareForQuery(text); + return (root, query, builder) -> { + query.distinct(true); + return builder.or( + getQueryPredicates( + builder, root, finalText, NAME, CLIENTID, CLIENTSECRET, DESCRIPTION, STATUS)); + }; + } + + public static Specification inGroup(@NonNull UUID groupId) { + return (root, query, builder) -> { + query.distinct(true); + Join applicationJoin = root.join(GROUPAPPLICATIONS); + Join groupJoin = applicationJoin.join(GROUP); + return builder.equal(groupJoin.get(ID), groupId); + }; + } + + public static Specification usedBy(@NonNull UUID userId) { + return (root, query, builder) -> { + query.distinct(true); + Join applicationUserJoin = root.join(USERS); + return builder.equal(applicationUserJoin.get(ID), userId); + }; + } +} diff --git a/src/main/java/bio/overture/ego/repository/queryspecification/GroupSpecification.java b/src/main/java/bio/overture/ego/repository/queryspecification/GroupSpecification.java new file mode 100644 index 000000000..80cdfa11d --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/queryspecification/GroupSpecification.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.repository.queryspecification; + +import static bio.overture.ego.model.enums.JavaFields.APPLICATION; +import static bio.overture.ego.model.enums.JavaFields.DESCRIPTION; +import static bio.overture.ego.model.enums.JavaFields.GROUPAPPLICATIONS; +import static bio.overture.ego.model.enums.JavaFields.ID; +import static bio.overture.ego.model.enums.JavaFields.NAME; +import static bio.overture.ego.model.enums.JavaFields.STATUS; +import static bio.overture.ego.model.enums.JavaFields.USER; +import static bio.overture.ego.model.enums.JavaFields.USERGROUPS; + +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.join.GroupApplication; +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.utils.QueryUtils; +import java.util.UUID; +import javax.persistence.criteria.Join; +import lombok.NonNull; +import lombok.val; +import org.springframework.data.jpa.domain.Specification; + +public class GroupSpecification extends SpecificationBase { + public static Specification containsText(@NonNull String text) { + val finalText = QueryUtils.prepareForQuery(text); + return (root, query, builder) -> + builder.or(getQueryPredicates(builder, root, finalText, NAME, DESCRIPTION, STATUS)); + } + + public static Specification containsApplication(@NonNull UUID appId) { + return (root, query, builder) -> { + query.distinct(true); + Join groupJoin = root.join(GROUPAPPLICATIONS); + Join applicationJoin = groupJoin.join(APPLICATION); + return builder.equal(applicationJoin.get(ID), appId); + }; + } + + public static Specification containsUser(@NonNull UUID userId) { + return (root, query, builder) -> { + query.distinct(true); + Join groupJoin = root.join(USERGROUPS); + Join userJoin = groupJoin.join(USER); + return builder.equal(userJoin.get(ID), userId); + }; + } +} diff --git a/src/main/java/org/overture/ego/repository/queryspecification/AclEntitySpecification.java b/src/main/java/bio/overture/ego/repository/queryspecification/PolicySpecification.java similarity index 59% rename from src/main/java/org/overture/ego/repository/queryspecification/AclEntitySpecification.java rename to src/main/java/bio/overture/ego/repository/queryspecification/PolicySpecification.java index a98821961..1a7069c28 100644 --- a/src/main/java/org/overture/ego/repository/queryspecification/AclEntitySpecification.java +++ b/src/main/java/bio/overture/ego/repository/queryspecification/PolicySpecification.java @@ -14,24 +14,24 @@ * limitations under the License. */ -package org.overture.ego.repository.queryspecification; +package bio.overture.ego.repository.queryspecification; +import static bio.overture.ego.model.enums.JavaFields.NAME; + +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.utils.QueryUtils; +import lombok.NonNull; import lombok.val; -import org.overture.ego.model.entity.Policy; -import org.overture.ego.model.entity.User; -import org.overture.ego.utils.QueryUtils; import org.springframework.data.jpa.domain.Specification; -import javax.annotation.Nonnull; - +public class PolicySpecification extends SpecificationBase { -public class AclEntitySpecification extends SpecificationBase { - - public static Specification containsText(@Nonnull String text) { + public static Specification containsText(@NonNull String text) { val finalText = QueryUtils.prepareForQuery(text); - return (root, query, builder) -> builder.or(getQueryPredicates(builder,root,finalText, - "name") - ); + return (root, query, builder) -> { + query.distinct(true); + return builder.or(getQueryPredicates(builder, root, finalText, NAME)); + }; } - } diff --git a/src/main/java/bio/overture/ego/repository/queryspecification/SpecificationBase.java b/src/main/java/bio/overture/ego/repository/queryspecification/SpecificationBase.java new file mode 100644 index 000000000..f7e1a8004 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/queryspecification/SpecificationBase.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.repository.queryspecification; + +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.utils.QueryUtils; +import java.util.Arrays; +import java.util.List; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import lombok.NonNull; +import lombok.val; +import org.springframework.data.jpa.domain.Specification; + +public class SpecificationBase { + protected static Predicate[] getQueryPredicates( + @NonNull CriteriaBuilder builder, + @NonNull Root root, + String queryText, + @NonNull String... params) { + return Arrays.stream(params) + .map(p -> filterByField(builder, root, p, queryText)) + .toArray(Predicate[]::new); + } + + public static Predicate filterByField( + @NonNull CriteriaBuilder builder, + @NonNull Root root, + @NonNull String fieldName, + String fieldValue) { + val finalText = QueryUtils.prepareForQuery(fieldValue); + + // Cast "as" String so that we can search ENUM types + return builder.like(builder.lower(root.get(fieldName).as(String.class)), finalText); + } + + public static Specification filterBy(@NonNull List filters) { + return (root, query, builder) -> { + query.distinct(true); + return builder.and( + filters.stream() + .map(f -> filterByField(builder, root, f.getFilterField(), f.getFilterValue())) + .toArray(Predicate[]::new)); + }; + } +} diff --git a/src/main/java/org/overture/ego/model/enums/UserRole.java b/src/main/java/bio/overture/ego/repository/queryspecification/TokenStoreSpecification.java similarity index 69% rename from src/main/java/org/overture/ego/model/enums/UserRole.java rename to src/main/java/bio/overture/ego/repository/queryspecification/TokenStoreSpecification.java index 8bc409609..346134d79 100644 --- a/src/main/java/org/overture/ego/model/enums/UserRole.java +++ b/src/main/java/bio/overture/ego/repository/queryspecification/TokenStoreSpecification.java @@ -14,21 +14,8 @@ * limitations under the License. */ -package org.overture.ego.model.enums; +package bio.overture.ego.repository.queryspecification; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; +import bio.overture.ego.model.entity.Token; -@RequiredArgsConstructor -public enum UserRole { - USER("USER"), - ADMIN("ADMIN"); - - @NonNull private final String value; - - @Override - public String toString() { - return value; - } - -} \ No newline at end of file +public class TokenStoreSpecification extends SpecificationBase {} diff --git a/src/main/java/bio/overture/ego/repository/queryspecification/UserPermissionSpecification.java b/src/main/java/bio/overture/ego/repository/queryspecification/UserPermissionSpecification.java new file mode 100644 index 000000000..cabf253b1 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/queryspecification/UserPermissionSpecification.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.repository.queryspecification; + +import static bio.overture.ego.model.enums.JavaFields.ID; +import static bio.overture.ego.model.enums.JavaFields.OWNER; +import static bio.overture.ego.model.enums.JavaFields.POLICY; + +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.UserPermission; +import java.util.UUID; +import javax.persistence.criteria.Join; +import lombok.NonNull; +import org.springframework.data.jpa.domain.Specification; + +public class UserPermissionSpecification extends SpecificationBase { + + public static Specification withPolicy(@NonNull UUID policyId) { + return (root, query, builder) -> { + query.distinct(true); + Join applicationJoin = root.join(POLICY); + return builder.equal(applicationJoin.get(ID), policyId); + }; + } + + public static Specification withUser(@NonNull UUID userId) { + return (root, query, builder) -> { + query.distinct(true); + Join applicationJoin = root.join(OWNER); + return builder.equal(applicationJoin.get(ID), userId); + }; + } +} diff --git a/src/main/java/bio/overture/ego/repository/queryspecification/UserSpecification.java b/src/main/java/bio/overture/ego/repository/queryspecification/UserSpecification.java new file mode 100644 index 000000000..d9be46ae7 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/queryspecification/UserSpecification.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.repository.queryspecification; + +import static bio.overture.ego.model.enums.JavaFields.APPLICATIONS; +import static bio.overture.ego.model.enums.JavaFields.EMAIL; +import static bio.overture.ego.model.enums.JavaFields.FIRSTNAME; +import static bio.overture.ego.model.enums.JavaFields.GROUP; +import static bio.overture.ego.model.enums.JavaFields.ID; +import static bio.overture.ego.model.enums.JavaFields.LASTNAME; +import static bio.overture.ego.model.enums.JavaFields.NAME; +import static bio.overture.ego.model.enums.JavaFields.STATUS; +import static bio.overture.ego.model.enums.JavaFields.USERGROUPS; + +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.JavaFields; +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.utils.QueryUtils; +import java.util.UUID; +import javax.persistence.criteria.Join; +import lombok.NonNull; +import lombok.val; +import org.springframework.data.jpa.domain.Specification; + +public class UserSpecification extends SpecificationBase { + + public static Specification containsText(@NonNull String text) { + val finalText = QueryUtils.prepareForQuery(text); + return (root, query, builder) -> { + query.distinct(true); + return builder.or( + getQueryPredicates(builder, root, finalText, NAME, EMAIL, FIRSTNAME, LASTNAME, STATUS)); + }; + } + + public static Specification inGroup(@NonNull UUID groupId) { + return (root, query, builder) -> { + query.distinct(true); + Join userJoin = root.join(USERGROUPS); + Join groupJoin = userJoin.join(GROUP); + return builder.equal(groupJoin.get(JavaFields.ID), groupId); + }; + } + + public static Specification ofApplication(@NonNull UUID appId) { + return (root, query, builder) -> { + query.distinct(true); + Join applicationJoin = root.join(APPLICATIONS); + return builder.equal(applicationJoin.get(ID), appId); + }; + } +} diff --git a/src/main/java/bio/overture/ego/repository/queryspecification/builder/AbstractSpecificationBuilder.java b/src/main/java/bio/overture/ego/repository/queryspecification/builder/AbstractSpecificationBuilder.java new file mode 100644 index 000000000..b5483b396 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/queryspecification/builder/AbstractSpecificationBuilder.java @@ -0,0 +1,40 @@ +package bio.overture.ego.repository.queryspecification.builder; + +import static bio.overture.ego.model.enums.JavaFields.NAME; + +import bio.overture.ego.model.entity.Identifiable; +import bio.overture.ego.model.enums.JavaFields; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import lombok.NonNull; +import lombok.val; +import org.springframework.data.jpa.domain.Specification; + +public abstract class AbstractSpecificationBuilder, ID> { + + protected abstract Root setupFetchStrategy(Root root); + + public Specification buildByNameIgnoreCase(@NonNull String name) { + return (fromUser, query, builder) -> { + val root = setupFetchStrategy(fromUser); + return equalsNameIgnoreCasePredicate(root, builder, name); + }; + } + + public Specification buildById(@NonNull ID id) { + return (fromUser, query, builder) -> { + val root = setupFetchStrategy(fromUser); + return equalsIdPredicate(root, builder, id); + }; + } + + private Predicate equalsIdPredicate(Root root, CriteriaBuilder builder, ID id) { + return builder.equal(root.get(JavaFields.ID), id); + } + + private Predicate equalsNameIgnoreCasePredicate( + Root root, CriteriaBuilder builder, String name) { + return builder.equal(builder.upper(root.get(NAME)), builder.upper(builder.literal(name))); + } +} diff --git a/src/main/java/bio/overture/ego/repository/queryspecification/builder/ApplicationSpecificationBuilder.java b/src/main/java/bio/overture/ego/repository/queryspecification/builder/ApplicationSpecificationBuilder.java new file mode 100644 index 000000000..d72dafa38 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/queryspecification/builder/ApplicationSpecificationBuilder.java @@ -0,0 +1,52 @@ +package bio.overture.ego.repository.queryspecification.builder; + +import static bio.overture.ego.model.enums.JavaFields.CLIENTID; +import static bio.overture.ego.model.enums.JavaFields.GROUP; +import static bio.overture.ego.model.enums.JavaFields.GROUPAPPLICATIONS; +import static bio.overture.ego.model.enums.JavaFields.USERS; +import static javax.persistence.criteria.JoinType.LEFT; + +import bio.overture.ego.model.entity.Application; +import java.util.UUID; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import lombok.NonNull; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.val; +import org.springframework.data.jpa.domain.Specification; + +@Setter +@Accessors(fluent = true, chain = true) +public class ApplicationSpecificationBuilder + extends AbstractSpecificationBuilder { + + private boolean fetchGroups; + private boolean fetchUsers; + + public Specification buildByClientIdIgnoreCase(@NonNull String clientId) { + return (fromApplication, query, builder) -> { + val root = setupFetchStrategy(fromApplication); + return equalsNameIgnoreCasePredicate(root, builder, clientId); + }; + } + + private Predicate equalsNameIgnoreCasePredicate( + Root root, CriteriaBuilder builder, String clientId) { + return builder.equal( + builder.upper(root.get(CLIENTID)), builder.upper(builder.literal(clientId))); + } + + @Override + protected Root setupFetchStrategy(Root root) { + if (fetchGroups) { + val fromGroupApplications = root.fetch(GROUPAPPLICATIONS, LEFT); + fromGroupApplications.fetch(GROUP, LEFT); + } + if (fetchUsers) { + root.fetch(USERS, LEFT); + } + return root; + } +} diff --git a/src/main/java/bio/overture/ego/repository/queryspecification/builder/GroupSpecificationBuilder.java b/src/main/java/bio/overture/ego/repository/queryspecification/builder/GroupSpecificationBuilder.java new file mode 100644 index 000000000..092a33593 --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/queryspecification/builder/GroupSpecificationBuilder.java @@ -0,0 +1,40 @@ +package bio.overture.ego.repository.queryspecification.builder; + +import static bio.overture.ego.model.enums.JavaFields.APPLICATION; +import static bio.overture.ego.model.enums.JavaFields.GROUPAPPLICATIONS; +import static bio.overture.ego.model.enums.JavaFields.PERMISSIONS; +import static bio.overture.ego.model.enums.JavaFields.USER; +import static bio.overture.ego.model.enums.JavaFields.USERGROUPS; +import static javax.persistence.criteria.JoinType.LEFT; + +import bio.overture.ego.model.entity.Group; +import java.util.UUID; +import javax.persistence.criteria.Root; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.val; + +@Setter +@Accessors(fluent = true, chain = true) +public class GroupSpecificationBuilder extends AbstractSpecificationBuilder { + + private boolean fetchApplications; + private boolean fetchUserGroups; + private boolean fetchGroupPermissions; + + @Override + protected Root setupFetchStrategy(Root root) { + if (fetchApplications) { + val fromGroupApplications = root.fetch(GROUPAPPLICATIONS, LEFT); + fromGroupApplications.fetch(APPLICATION, LEFT); + } + if (fetchUserGroups) { + val fromUserGroup = root.fetch(USERGROUPS, LEFT); + fromUserGroup.fetch(USER, LEFT); + } + if (fetchGroupPermissions) { + root.fetch(PERMISSIONS, LEFT); + } + return root; + } +} diff --git a/src/main/java/bio/overture/ego/repository/queryspecification/builder/UserSpecificationBuilder.java b/src/main/java/bio/overture/ego/repository/queryspecification/builder/UserSpecificationBuilder.java new file mode 100644 index 000000000..85b7b7c9d --- /dev/null +++ b/src/main/java/bio/overture/ego/repository/queryspecification/builder/UserSpecificationBuilder.java @@ -0,0 +1,38 @@ +package bio.overture.ego.repository.queryspecification.builder; + +import static bio.overture.ego.model.enums.JavaFields.APPLICATIONS; +import static bio.overture.ego.model.enums.JavaFields.GROUP; +import static bio.overture.ego.model.enums.JavaFields.USERGROUPS; +import static bio.overture.ego.model.enums.JavaFields.USERPERMISSIONS; +import static javax.persistence.criteria.JoinType.LEFT; + +import bio.overture.ego.model.entity.User; +import java.util.UUID; +import javax.persistence.criteria.Root; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.val; + +@Setter +@Accessors(fluent = true, chain = true) +public class UserSpecificationBuilder extends AbstractSpecificationBuilder { + + private boolean fetchUserPermissions; + private boolean fetchUserGroups; + private boolean fetchApplications; + + @Override + protected Root setupFetchStrategy(Root root) { + if (fetchApplications) { + root.fetch(APPLICATIONS, LEFT); + } + if (fetchUserGroups) { + val fromUserGroup = root.fetch(USERGROUPS, LEFT); + fromUserGroup.fetch(GROUP, LEFT); + } + if (fetchUserPermissions) { + root.fetch(USERPERMISSIONS, LEFT); + } + return root; + } +} diff --git a/src/main/java/org/overture/ego/security/AdminScoped.java b/src/main/java/bio/overture/ego/security/AdminScoped.java similarity index 89% rename from src/main/java/org/overture/ego/security/AdminScoped.java rename to src/main/java/bio/overture/ego/security/AdminScoped.java index f2cd694ee..7339acfbe 100644 --- a/src/main/java/org/overture/ego/security/AdminScoped.java +++ b/src/main/java/bio/overture/ego/security/AdminScoped.java @@ -14,17 +14,13 @@ * limitations under the License. */ -package org.overture.ego.security; - -import org.springframework.security.access.prepost.PreAuthorize; +package bio.overture.ego.security; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.springframework.security.access.prepost.PreAuthorize; -/** - * Method Security Meta Annotation - */ +/** Method Security Meta Annotation */ @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("@authorizationManager.authorizeWithAdminRole(authentication)") -public @interface AdminScoped { -} +public @interface AdminScoped {} diff --git a/src/main/java/org/overture/ego/model/enums/UserStatus.java b/src/main/java/bio/overture/ego/security/ApplicationScoped.java similarity index 63% rename from src/main/java/org/overture/ego/model/enums/UserStatus.java rename to src/main/java/bio/overture/ego/security/ApplicationScoped.java index 973f4ab6f..503849286 100644 --- a/src/main/java/org/overture/ego/model/enums/UserStatus.java +++ b/src/main/java/bio/overture/ego/security/ApplicationScoped.java @@ -14,23 +14,13 @@ * limitations under the License. */ -package org.overture.ego.model.enums; +package bio.overture.ego.security; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.security.access.prepost.PreAuthorize; -@RequiredArgsConstructor -public enum UserStatus { - APPROVED("Approved"), - DISABLED("Disabled"), - PENDING("Pending"), - REJECTED("Rejected"),; - - @NonNull private final String value; - - @Override - public String toString() { - return value; - } - -} \ No newline at end of file +/** Method Security Meta Annotation */ +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("@authorizationManager.authorizeWithApplication(authentication)") +public @interface ApplicationScoped {} diff --git a/src/main/java/org/overture/ego/security/AuthorizationManager.java b/src/main/java/bio/overture/ego/security/AuthorizationManager.java similarity index 75% rename from src/main/java/org/overture/ego/security/AuthorizationManager.java rename to src/main/java/bio/overture/ego/security/AuthorizationManager.java index 6da7b5be6..8e73cc981 100644 --- a/src/main/java/org/overture/ego/security/AuthorizationManager.java +++ b/src/main/java/bio/overture/ego/security/AuthorizationManager.java @@ -14,14 +14,15 @@ * limitations under the License. */ -package org.overture.ego.security; +package bio.overture.ego.security; +import lombok.NonNull; import org.springframework.security.core.Authentication; public interface AuthorizationManager { + boolean authorize(Authentication authentication); - boolean authorize(Authentication authentication); - boolean authorizeWithAdminRole(Authentication authentication); + boolean authorizeWithAdminRole(Authentication authentication); + boolean authorizeWithApplication(@NonNull Authentication authentication); } - diff --git a/src/main/java/org/overture/ego/security/AuthorizationStrategyConfig.java b/src/main/java/bio/overture/ego/security/AuthorizationStrategyConfig.java similarity index 95% rename from src/main/java/org/overture/ego/security/AuthorizationStrategyConfig.java rename to src/main/java/bio/overture/ego/security/AuthorizationStrategyConfig.java index 2fe5cd1a4..cc608e07c 100644 --- a/src/main/java/org/overture/ego/security/AuthorizationStrategyConfig.java +++ b/src/main/java/bio/overture/ego/security/AuthorizationStrategyConfig.java @@ -14,8 +14,7 @@ * limitations under the License. */ -package org.overture.ego.security; - +package bio.overture.ego.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityProperties; @@ -34,9 +33,7 @@ @Profile("auth") public class AuthorizationStrategyConfig extends GlobalMethodSecurityConfiguration { - @Autowired - private ApplicationContext context; - + @Autowired private ApplicationContext context; @Override protected MethodSecurityExpressionHandler createExpressionHandler() { @@ -44,5 +41,4 @@ protected MethodSecurityExpressionHandler createExpressionHandler() { handler.setApplicationContext(context); return handler; } - } diff --git a/src/main/java/org/overture/ego/security/CorsFilter.java b/src/main/java/bio/overture/ego/security/CorsFilter.java similarity index 51% rename from src/main/java/org/overture/ego/security/CorsFilter.java rename to src/main/java/bio/overture/ego/security/CorsFilter.java index e6dbc2cd7..37977204a 100644 --- a/src/main/java/org/overture/ego/security/CorsFilter.java +++ b/src/main/java/bio/overture/ego/security/CorsFilter.java @@ -14,32 +14,64 @@ * limitations under the License. */ -package org.overture.ego.security; +package bio.overture.ego.security; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.service.ApplicationService; +import java.net.URI; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - +// The filter it's better to add to security chain, reference +// https://spring.io/guides/topicals/spring-security-architecture/ @Component +@Slf4j @Order(Ordered.HIGHEST_PRECEDENCE) public class CorsFilter implements Filter { + @Autowired private ApplicationService applicationService; @Override @SneakyThrows public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) { final HttpServletResponse response = (HttpServletResponse) res; final HttpServletRequest request = (HttpServletRequest) req; - response.addHeader("Access-Control-Allow-Origin", "*"); - response.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH, HEAD, OPTIONS"); - response.addHeader("Access-Control-Allow-Headers", - "Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, " + - "Access-Control-Request-Headers, token, AUTHORIZATION"); - response.addHeader("Access-Control-Expose-Headers", "Access-Control-Allow-Origin, Access-Control-Allow-Credentials"); + + String clientId = request.getParameter("client_id"); + + // allow ego app access token at /oauth/ego-token + if (clientId != null) { + try { + Application app = applicationService.getByClientId(clientId); + URI uri = new URI(app.getRedirectUri()); + response.setHeader( + "Access-Control-Allow-Origin", + uri.getScheme() + + "://" + + uri.getHost() + + (uri.getPort() == -1 ? "" : ":" + uri.getPort())); + } catch (NullPointerException ex) { + log.warn(ex.getMessage()); + } + } else { + response.addHeader("Access-Control-Allow-Origin", "*"); + } + + response.addHeader( + "Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH, HEAD, OPTIONS"); + response.addHeader( + "Access-Control-Allow-Headers", + "Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, " + + "Access-Control-Request-Headers, token, AUTHORIZATION"); + response.addHeader( + "Access-Control-Expose-Headers", + "Access-Control-Allow-Origin, Access-Control-Allow-Credentials"); response.addHeader("Access-Control-Allow-Credentials", "true"); response.addIntHeader("Access-Control-Max-Age", 10); if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { @@ -50,13 +82,8 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain filter } @Override - public void init(FilterConfig filterConfig) throws ServletException { - - } + public void init(FilterConfig filterConfig) {} @Override - public void destroy() { - - } - -} \ No newline at end of file + public void destroy() {} +} diff --git a/src/main/java/org/overture/ego/security/DefaultAuthorizationManager.java b/src/main/java/bio/overture/ego/security/DefaultAuthorizationManager.java similarity index 79% rename from src/main/java/org/overture/ego/security/DefaultAuthorizationManager.java rename to src/main/java/bio/overture/ego/security/DefaultAuthorizationManager.java index c003ee82a..9e28b7536 100644 --- a/src/main/java/org/overture/ego/security/DefaultAuthorizationManager.java +++ b/src/main/java/bio/overture/ego/security/DefaultAuthorizationManager.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.overture.ego.security; +package bio.overture.ego.security; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; /* - Default Authorization Manager allows working without actual auth headers. - Meant to be used for development environment. - */ + Default Authorization Manager allows working without actual auth headers. + Meant to be used for development environment. +*/ @Slf4j public class DefaultAuthorizationManager implements AuthorizationManager { @@ -36,4 +36,8 @@ public boolean authorizeWithAdminRole(Authentication authentication) { return true; } + @Override + public boolean authorizeWithApplication(Authentication authentication) { + return true; + } } diff --git a/src/main/java/bio/overture/ego/security/JWTAuthorizationFilter.java b/src/main/java/bio/overture/ego/security/JWTAuthorizationFilter.java new file mode 100644 index 000000000..258b01829 --- /dev/null +++ b/src/main/java/bio/overture/ego/security/JWTAuthorizationFilter.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.security; + +import static bio.overture.ego.utils.TypeUtils.convertToAnotherType; +import static org.springframework.util.DigestUtils.md5Digest; + +import bio.overture.ego.model.exceptions.ForbiddenException; +import bio.overture.ego.service.ApplicationService; +import bio.overture.ego.service.TokenService; +import bio.overture.ego.token.app.AppTokenClaims; +import bio.overture.ego.token.user.UserTokenClaims; +import bio.overture.ego.view.Views; +import java.util.ArrayList; +import java.util.Arrays; +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.util.StringUtils; + +@Slf4j +public class JWTAuthorizationFilter extends BasicAuthenticationFilter { + + private String[] publicEndpoints = null; + + @Value("${auth.token.prefix}") + private String TOKEN_PREFIX; + + @Autowired private TokenService tokenService; + @Autowired private ApplicationService applicationService; + + public JWTAuthorizationFilter(AuthenticationManager authManager, String[] publicEndpoints) { + super(authManager); + this.publicEndpoints = publicEndpoints; + } + + @Override + @SneakyThrows + public void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain chain) { + + if (isPublicEndpoint(request.getServletPath())) { + chain.doFilter(request, response); + return; + } + val tokenPayload = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (tokenPayload != null && tokenPayload.startsWith(applicationService.APP_TOKEN_PREFIX)) { + authenticateApplication(tokenPayload); + } else { + authenticateUserOrApplication(tokenPayload); + } + chain.doFilter(request, response); + } + + /** + * Responsible for authenticating a Bearer JWT into a user or application. + * + * @param tokenPayload The string representation of the Authorization Header with the token prefix + * included + */ + private void authenticateUserOrApplication(String tokenPayload) { + if (!isValidToken(tokenPayload)) { + log.warn( + "Invalid token (MD5sum): {}", + tokenPayload == null ? "null token" : new String(md5Digest(tokenPayload.getBytes()))); + SecurityContextHolder.clearContext(); + return; + } + + UsernamePasswordAuthenticationToken authentication = null; + val body = tokenService.getTokenClaims(removeTokenPrefix(tokenPayload)); + try { + // Test Conversion + convertToAnotherType(body, UserTokenClaims.class, Views.JWTAccessToken.class); + authentication = + new UsernamePasswordAuthenticationToken( + tokenService.getTokenUserInfo(removeTokenPrefix(tokenPayload)), + null, + new ArrayList<>()); + SecurityContextHolder.getContext().setAuthentication(authentication); + return; // Escape + } catch (Exception e) { + log.debug(e.getMessage()); + log.warn("Token is valid but not a User JWT"); + } + + try { + // Test Conversion + convertToAnotherType(body, AppTokenClaims.class, Views.JWTAccessToken.class); + authentication = + new UsernamePasswordAuthenticationToken( + tokenService.getTokenAppInfo(removeTokenPrefix(tokenPayload)), + null, + new ArrayList<>()); + SecurityContextHolder.getContext().setAuthentication(authentication); + return; // Escape + } catch (Exception e) { + log.debug(e.getMessage()); + log.warn("Token is valid but not an Application JWT"); + } + + throw new ForbiddenException("Bad Token"); + } + + private void authenticateApplication(String token) { + val application = applicationService.findByBasicToken(token); + + // Deny access if they don't have a valid app token for + // one of our applications + if (application == null) { + SecurityContextHolder.clearContext(); + return; + } + + val authentication = + new UsernamePasswordAuthenticationToken(application, null, new ArrayList<>()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private boolean isValidToken(String token) { + return !StringUtils.isEmpty(token) + && token.contains(TOKEN_PREFIX) + && tokenService.isValidToken(removeTokenPrefix(token)); + } + + private String removeTokenPrefix(String token) { + return token.replace(TOKEN_PREFIX, "").trim(); + } + + private boolean isPublicEndpoint(String endpointPath) { + if (this.publicEndpoints != null) { + return Arrays.stream(this.publicEndpoints).anyMatch(item -> item.equals(endpointPath)); + } else return false; + } +} diff --git a/src/main/java/bio/overture/ego/security/OAuth2ClientResources.java b/src/main/java/bio/overture/ego/security/OAuth2ClientResources.java new file mode 100644 index 000000000..488efef71 --- /dev/null +++ b/src/main/java/bio/overture/ego/security/OAuth2ClientResources.java @@ -0,0 +1,56 @@ +package bio.overture.ego.security; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.regex.Pattern; +import lombok.val; +import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.security.oauth2.client.token.AccessTokenRequest; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +public class OAuth2ClientResources { + + @NestedConfigurationProperty + private AuthorizationCodeResourceDetails client = + new AuthorizationCodeResourceDetails() { + // Do not send url parameter (including the application id of ego) to authorization server + // because some authorization server like google does not support parameter in redirect url + @Override + public String getRedirectUri(AccessTokenRequest request) { + try { + val uri = new URI(request.getCurrentUri()); + val attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + val session = attr.getRequest().getSession(true); + val pattern = Pattern.compile("client_id=([^&]+)?"); + val matcher = pattern.matcher(uri.getQuery()); + if (matcher.find()) { + session.setAttribute("ego_client_id", matcher.group(1)); + } + + if (getPreEstablishedRedirectUri() != null) { + return getPreEstablishedRedirectUri(); + } + + return new URI( + uri.getScheme(), uri.getAuthority(), uri.getPath(), null, uri.getFragment()) + .toString(); + } catch (URISyntaxException e) { + return request.getCurrentUri(); + } + } + }; + + @NestedConfigurationProperty + private ResourceServerProperties resource = new ResourceServerProperties(); + + public AuthorizationCodeResourceDetails getClient() { + return client; + } + + public ResourceServerProperties getResource() { + return resource; + } +} diff --git a/src/main/java/bio/overture/ego/security/OAuth2SsoFilter.java b/src/main/java/bio/overture/ego/security/OAuth2SsoFilter.java new file mode 100644 index 000000000..a40345de9 --- /dev/null +++ b/src/main/java/bio/overture/ego/security/OAuth2SsoFilter.java @@ -0,0 +1,183 @@ +package bio.overture.ego.security; + +import bio.overture.ego.service.ApplicationService; +import java.io.IOException; +import java.util.*; +import javax.servlet.Filter; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Profile; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RestOperations; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.filter.CompositeFilter; + +@Component +@Profile("auth") +public class OAuth2SsoFilter extends CompositeFilter { + + private OAuth2ClientContext oauth2ClientContext; + private ApplicationService applicationService; + private SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler = + new SimpleUrlAuthenticationSuccessHandler() { + public void onAuthenticationSuccess( + HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException { + val application = + applicationService.getByClientId( + (String) request.getSession().getAttribute("ego_client_id")); + this.setDefaultTargetUrl(application.getRedirectUri()); + super.onAuthenticationSuccess(request, response, authentication); + } + }; + + @Autowired + public OAuth2SsoFilter( + @Qualifier("oauth2ClientContext") OAuth2ClientContext oauth2ClientContext, + ApplicationService applicationService, + OAuth2ClientResources google, + OAuth2ClientResources facebook, + OAuth2ClientResources github, + OAuth2ClientResources linkedin) { + this.oauth2ClientContext = oauth2ClientContext; + this.applicationService = applicationService; + val filters = new ArrayList(); + + filters.add(new GoogleFilter(google)); + filters.add(new FacebookFilter(facebook)); + filters.add(new GithubFilter(github)); + filters.add(new LinkedInFilter(linkedin)); + setFilters(filters); + } + + class OAuth2SsoChildFilter extends OAuth2ClientAuthenticationProcessingFilter { + public OAuth2SsoChildFilter(String path, OAuth2ClientResources client) { + super(path); + OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext); + super.setRestTemplate(template); + super.setAuthenticationSuccessHandler(simpleUrlAuthenticationSuccessHandler); + } + + @Override + public Authentication attemptAuthentication( + HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + // Don't use the existing access token, otherwise, it would attempt to get github user info + // with linkedin access token + this.restTemplate.getOAuth2ClientContext().setAccessToken(null); + return super.attemptAuthentication(request, response); + } + } + + class GithubFilter extends OAuth2SsoChildFilter { + public GithubFilter(OAuth2ClientResources client) { + super("/oauth/login/github", client); + super.setTokenServices( + new OAuth2UserInfoTokenServices( + client.getResource().getUserInfoUri(), + client.getClient().getClientId(), + super.restTemplate) { + @Override + protected Map transformMap(Map map, String accessToken) + throws NoSuchElementException { + OAuth2RestOperations restTemplate = getRestTemplate(accessToken); + String email; + + try { + // [{email, primary, verified}] + email = + (String) + restTemplate + .exchange( + "https://api.github.com/user/emails", + HttpMethod.GET, + null, + new ParameterizedTypeReference>>() {}) + .getBody().stream() + .filter( + x -> + x.get("verified").equals(true) && x.get("primary").equals(true)) + .findAny() + .orElse(Collections.emptyMap()) + .get("email"); + } catch (RestClientException | ClassCastException ex) { + return Collections.singletonMap("error", "Could not fetch user details"); + } + + if (email != null) { + map.put("email", email); + + String name = (String) map.get("name"); + String[] names = name.split(" "); + if (names.length == 2) { + map.put("given_name", names[0]); + map.put("family_name", names[1]); + } + return map; + } else { + return Collections.singletonMap("error", "Could not fetch user details"); + } + } + }); + } + } + + class LinkedInFilter extends OAuth2SsoChildFilter { + public LinkedInFilter(OAuth2ClientResources client) { + super("/oauth/login/linkedin", client); + super.setTokenServices( + new OAuth2UserInfoTokenServices( + client.getResource().getUserInfoUri(), + client.getClient().getClientId(), + super.restTemplate) { + @Override + protected Map transformMap( + Map map, String accessToken) { + String email = (String) map.get("emailAddress"); + + if (email != null) { + map.put("email", email); + map.put("given_name", map.get("firstName")); + map.put("family_name", map.get("lastName")); + return map; + } else { + return Collections.singletonMap("error", "Could not fetch user details"); + } + } + }); + } + } + + class GoogleFilter extends OAuth2SsoChildFilter { + public GoogleFilter(OAuth2ClientResources client) { + super("/oauth/login/google", client); + super.setTokenServices( + new OAuth2UserInfoTokenServices( + client.getResource().getUserInfoUri(), + client.getClient().getClientId(), + super.restTemplate)); + } + } + + class FacebookFilter extends OAuth2SsoChildFilter { + public FacebookFilter(OAuth2ClientResources client) { + super("/oauth/login/facebook", client); + super.setTokenServices( + new OAuth2UserInfoTokenServices( + client.getResource().getUserInfoUri(), + client.getClient().getClientId(), + super.restTemplate)); + } + } +} diff --git a/src/main/java/bio/overture/ego/security/OAuth2UserInfoTokenServices.java b/src/main/java/bio/overture/ego/security/OAuth2UserInfoTokenServices.java new file mode 100644 index 000000000..b66e8276d --- /dev/null +++ b/src/main/java/bio/overture/ego/security/OAuth2UserInfoTokenServices.java @@ -0,0 +1,130 @@ +package bio.overture.ego.security; + +import bio.overture.ego.token.IDToken; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor; +import org.springframework.boot.autoconfigure.security.oauth2.resource.FixedAuthoritiesExtractor; +import org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.OAuth2RestOperations; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; + +// This class make sure email is in the user info. User info endpoint of Github does not contain +// private email. +@Slf4j +public class OAuth2UserInfoTokenServices + implements ResourceServerTokenServices, PrincipalExtractor { + + /** Dependencies */ + private final String userInfoEndpointUrl; + + private final String clientId; + private final OAuth2RestOperations restTemplate; + + private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor(); + + public OAuth2UserInfoTokenServices( + String userInfoEndpointUrl, String clientId, OAuth2RestOperations restTemplate) { + this.userInfoEndpointUrl = userInfoEndpointUrl; + this.clientId = clientId; + this.restTemplate = restTemplate; + } + + public IDToken extractPrincipal(Map map) { + String email; + + if (map.get("email") instanceof String) { + email = (String) map.get("email"); + } else { + return null; + } + + val givenName = (String) map.getOrDefault("given_name", map.getOrDefault("first_name", "")); + val familyName = (String) map.getOrDefault("family_name", map.getOrDefault("last_name", "")); + + return new IDToken(email, givenName, familyName); + } + + @Override + public OAuth2Authentication loadAuthentication(String accessToken) + throws AuthenticationException, InvalidTokenException { + Map map = getMap(this.userInfoEndpointUrl, accessToken); + map = transformMap(map, accessToken); + if (map.containsKey("error")) { + if (log.isDebugEnabled()) { + log.debug("userinfo returned error: " + map.get("error")); + } + throw new InvalidTokenException(accessToken); + } + return extractAuthentication(map); + } + + // Guarantee that email will be fetched + protected Map transformMap(Map map, String accessToken) + throws NoSuchElementException { + if (map.get("email") == null) { + return Collections.singletonMap("error", "Could not fetch user details"); + } + return map; + } + + private OAuth2Authentication extractAuthentication(Map map) { + Object principal = getPrincipal(map); + List authorities = this.authoritiesExtractor.extractAuthorities(map); + val request = new OAuth2Request(null, this.clientId, null, true, null, null, null, null, null); + val token = new UsernamePasswordAuthenticationToken(principal, "N/A", authorities); + token.setDetails(map); + return new OAuth2Authentication(request, token); + } + + /** + * Return the principal that should be used for the token. The default implementation delegates to + * the {@link PrincipalExtractor}. + * + * @param map the source map + * @return the principal or {@literal "unknown"} + */ + protected Object getPrincipal(Map map) { + Object principal = this.extractPrincipal(map); + return (principal == null ? "unknown" : principal); + } + + @Override + public OAuth2AccessToken readAccessToken(String accessToken) { + throw new UnsupportedOperationException("Not supported: read access token"); + } + + protected OAuth2RestOperations getRestTemplate(String accessToken) { + val existingToken = restTemplate.getOAuth2ClientContext().getAccessToken(); + if (existingToken == null || !accessToken.equals(existingToken.getValue())) { + val token = new DefaultOAuth2AccessToken(accessToken); + val tokenType = DefaultOAuth2AccessToken.BEARER_TYPE; + token.setTokenType(tokenType); + restTemplate.getOAuth2ClientContext().setAccessToken(token); + } + return restTemplate; + } + + @SuppressWarnings("unchecked") + protected Map getMap(String path, String accessToken) { + try { + val restTemplate = getRestTemplate(accessToken); + return restTemplate.getForEntity(path, Map.class).getBody(); + } catch (Exception ex) { + log.warn("Could not fetch user details: " + ex.getClass() + ", " + ex.getMessage()); + return Collections.singletonMap("error", "Could not fetch user details"); + } + } +} diff --git a/src/main/java/bio/overture/ego/security/SecureAuthorizationManager.java b/src/main/java/bio/overture/ego/security/SecureAuthorizationManager.java new file mode 100644 index 000000000..ebaf4f43b --- /dev/null +++ b/src/main/java/bio/overture/ego/security/SecureAuthorizationManager.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.security; + +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static bio.overture.ego.model.enums.UserType.ADMIN; +import static bio.overture.ego.model.enums.UserType.USER; + +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.ApplicationType; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.security.core.Authentication; + +@Slf4j +@Profile("auth") +public class SecureAuthorizationManager implements AuthorizationManager { + public boolean authorize(@NonNull Authentication authentication) { + log.info("Trying to authorize as user"); + User user = (User) authentication.getPrincipal(); + return user.getType() == USER && isActiveUser(user); + } + + public boolean authorizeWithAdminRole(@NonNull Authentication authentication) { + boolean status = false; + + if (authentication.getPrincipal() instanceof User) { + User user = (User) authentication.getPrincipal(); + log.info("Trying to authorize user '" + user.getName() + "' as admin"); + status = user.getType() == ADMIN && isActiveUser(user); + } else if (authentication.getPrincipal() instanceof Application) { + Application application = (Application) authentication.getPrincipal(); + log.info("Trying to authorize application '" + application.getName() + "' as admin"); + status = application.getType() == ApplicationType.ADMIN; + } else { + log.info("Unknown applicationType of authentication passed to authorizeWithAdminRole"); + } + log.info("Authorization " + (status ? "succeeded" : "failed")); + return status; + } + + public boolean authorizeWithApplication(@NonNull Authentication authentication) { + // User user = (User)authentication.getPrincipal(); + // return authorize(authentication) && user.getApplications().contains(appName); + log.info("Trying to authorize as application"); + return true; + } + + public boolean isActiveUser(User user) { + return user.getStatus() == APPROVED; + } +} diff --git a/src/main/java/org/overture/ego/security/UserAuthenticationManager.java b/src/main/java/bio/overture/ego/security/UserAuthenticationManager.java similarity index 77% rename from src/main/java/org/overture/ego/security/UserAuthenticationManager.java rename to src/main/java/bio/overture/ego/security/UserAuthenticationManager.java index 219832a7f..b8462512c 100644 --- a/src/main/java/org/overture/ego/security/UserAuthenticationManager.java +++ b/src/main/java/bio/overture/ego/security/UserAuthenticationManager.java @@ -14,14 +14,16 @@ * limitations under the License. */ -package org.overture.ego.security; +package bio.overture.ego.security; +import bio.overture.ego.provider.facebook.FacebookTokenService; +import bio.overture.ego.provider.google.GoogleTokenService; +import bio.overture.ego.service.TokenService; +import java.util.ArrayList; +import javax.servlet.http.HttpServletRequest; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import lombok.val; -import org.overture.ego.provider.facebook.FacebookTokenService; -import org.overture.ego.provider.google.GoogleTokenService; -import org.overture.ego.token.TokenService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; import org.springframework.security.authentication.AuthenticationManager; @@ -32,37 +34,30 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import javax.servlet.http.HttpServletRequest; -import java.util.ArrayList; - @Slf4j @Component @Primary public class UserAuthenticationManager implements AuthenticationManager { - @Autowired - private GoogleTokenService googleTokenService; - @Autowired - private FacebookTokenService facebookTokenService; - @Autowired - private TokenService tokenService; + @Autowired private GoogleTokenService googleTokenService; + @Autowired private FacebookTokenService facebookTokenService; + @Autowired private TokenService tokenService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + HttpServletRequest request = + ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); val provider = request.getParameter("provider"); val idToken = request.getParameter("id_token"); String username = ""; - if("google".equals(provider.toLowerCase())){ + if ("google".equals(provider.toLowerCase())) { username = exchangeGoogleTokenForAuth(idToken); - } else if ("facebook".equals(provider.toLowerCase())){ + } else if ("facebook".equals(provider.toLowerCase())) { username = exchangeFacebookTokenForAuth(idToken); } else return null; - return new UsernamePasswordAuthenticationToken( - username, - null, new ArrayList<>()); + return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>()); } @SneakyThrows @@ -71,7 +66,6 @@ private String exchangeGoogleTokenForAuth(final String idToken) { throw new Exception("Invalid user token:" + idToken); val authInfo = googleTokenService.decode(idToken); return tokenService.generateUserToken(authInfo); - } @SneakyThrows @@ -79,11 +73,10 @@ private String exchangeFacebookTokenForAuth(final String idToken) { if (!facebookTokenService.validToken(idToken)) throw new Exception("Invalid user token:" + idToken); val authInfo = facebookTokenService.getAuthInfo(idToken); - if(authInfo.isPresent()) { + if (authInfo.isPresent()) { return tokenService.generateUserToken(authInfo.get()); } else { throw new Exception("Unable to generate auth token for this user"); } } - } diff --git a/src/main/java/bio/overture/ego/service/AbstractBaseService.java b/src/main/java/bio/overture/ego/service/AbstractBaseService.java new file mode 100644 index 000000000..981880ff1 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/AbstractBaseService.java @@ -0,0 +1,70 @@ +package bio.overture.ego.service; + +import static bio.overture.ego.utils.EntityServices.checkEntityExistence; +import static bio.overture.ego.utils.EntityServices.getManyEntities; + +import bio.overture.ego.model.entity.Identifiable; +import bio.overture.ego.repository.BaseRepository; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; + +/** + * Base implementation + * + * @param + */ +@RequiredArgsConstructor +public abstract class AbstractBaseService, ID> + implements BaseService { + + @Getter @NonNull private final Class entityType; + @Getter @NonNull private final BaseRepository repository; + + @Override + public String getEntityTypeName() { + return entityType.getSimpleName(); + } + + @Override + public Optional findById(@NonNull ID id) { + return getRepository().findById(id); + } + + @Override + public boolean isExist(@NonNull ID id) { + return getRepository().existsById(id); + } + + @Override + public void delete(@NonNull ID id) { + checkExistence(id); + getRepository().deleteById(id); + } + + @Override + public Page findAll(Specification specification, Pageable pageable) { + return getRepository().findAll(specification, pageable); + } + + @Override + public Set getMany(@NonNull Collection ids) { + return getManyEntities(entityType, repository, ids); + } + + @Override + public void checkExistence(Collection ids) { + checkEntityExistence(getEntityType(), getRepository(), ids); + } + + @Override + public void checkExistence(ID id) { + checkEntityExistence(getEntityType(), getRepository(), id); + } +} diff --git a/src/main/java/bio/overture/ego/service/AbstractNamedService.java b/src/main/java/bio/overture/ego/service/AbstractNamedService.java new file mode 100644 index 000000000..8486ffc91 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/AbstractNamedService.java @@ -0,0 +1,37 @@ +package bio.overture.ego.service; + +import static bio.overture.ego.model.exceptions.NotFoundException.checkNotFound; + +import bio.overture.ego.model.entity.Identifiable; +import bio.overture.ego.repository.NamedRepository; +import java.util.Optional; +import lombok.NonNull; +import lombok.val; + +public abstract class AbstractNamedService, ID> + extends AbstractBaseService implements NamedService { + + private final NamedRepository namedRepository; + + public AbstractNamedService( + @NonNull Class entityType, @NonNull NamedRepository repository) { + super(entityType, repository); + this.namedRepository = repository; + } + + @Override + public Optional findByName(@NonNull String name) { + return namedRepository.findByName(name); + } + + @Override + public T getByName(@NonNull String name) { + val result = findByName(name); + checkNotFound( + result.isPresent(), + "The '%s' entity with name '%s' was not found", + getEntityTypeName(), + name); + return result.get(); + } +} diff --git a/src/main/java/bio/overture/ego/service/AbstractPermissionService.java b/src/main/java/bio/overture/ego/service/AbstractPermissionService.java new file mode 100644 index 000000000..7bba286a5 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/AbstractPermissionService.java @@ -0,0 +1,353 @@ +package bio.overture.ego.service; + +import static bio.overture.ego.model.dto.Scope.createScope; +import static bio.overture.ego.model.enums.JavaFields.ID; +import static bio.overture.ego.model.enums.JavaFields.POLICY; +import static bio.overture.ego.model.exceptions.MalformedRequestException.checkMalformedRequest; +import static bio.overture.ego.model.exceptions.NotFoundException.buildNotFoundException; +import static bio.overture.ego.model.exceptions.NotFoundException.checkNotFound; +import static bio.overture.ego.model.exceptions.UniqueViolationException.checkUnique; +import static bio.overture.ego.utils.CollectionUtils.difference; +import static bio.overture.ego.utils.CollectionUtils.mapToList; +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static bio.overture.ego.utils.Joiners.COMMA; +import static bio.overture.ego.utils.PermissionRequestAnalyzer.analyze; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.util.Arrays.stream; +import static java.util.Collections.reverse; +import static java.util.Comparator.comparing; +import static java.util.Objects.isNull; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; +import static javax.persistence.criteria.JoinType.LEFT; + +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.dto.PolicyResponse; +import bio.overture.ego.model.dto.Scope; +import bio.overture.ego.model.entity.AbstractPermission; +import bio.overture.ego.model.entity.NameableEntity; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.repository.PermissionRepository; +import bio.overture.ego.utils.PermissionRequestAnalyzer.PermissionAnalysis; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Transactional +public abstract class AbstractPermissionService< + O extends NameableEntity, P extends AbstractPermission> + extends AbstractBaseService { + + /** Dependencies */ + private final BaseService policyBaseService; + + private final BaseService ownerBaseService; + private final PermissionRepository permissionRepository; + private final Class ownerType; + + public AbstractPermissionService( + @NonNull Class ownerType, + @NonNull Class

entityType, + @NonNull BaseService ownerBaseService, + @NonNull BaseService policyBaseService, + @NonNull PermissionRepository repository) { + super(entityType, repository); + this.permissionRepository = repository; + this.ownerType = ownerType; + this.policyBaseService = policyBaseService; + this.ownerBaseService = ownerBaseService; + } + + protected abstract Collection

getPermissionsFromOwner(O owner); + + protected abstract Collection

getPermissionsFromPolicy(Policy policy); + + // TODO: [rtisma] not correctly implemented. Should implement dynamic fetching + @Override + public P getWithRelationships(@NonNull UUID id) { + val result = (Optional

) permissionRepository.findOne(fetchSpecification(id, true)); + checkNotFound(result.isPresent(), "The groupPermissionId '%s' does not exist", id); + return result.get(); + } + + public List findByPolicy(UUID policyId) { + val permissions = ImmutableList.copyOf(permissionRepository.findAllByPolicy_Id(policyId)); + return mapToList(permissions, this::convertToPolicyResponse); + } + + public Page

getPermissions(@NonNull UUID ownerId, @NonNull Pageable pageable) { + ownerBaseService.checkExistence(ownerId); + val permissions = ImmutableList.copyOf(permissionRepository.findAllByOwner_Id(ownerId)); + return new PageImpl<>(permissions, pageable, permissions.size()); + } + + public void deleteByPolicyAndOwner(@NonNull UUID policyId, @NonNull UUID ownerId) { + val perm = getByPolicyAndOwner(policyId, ownerId); + getRepository().delete(perm); + } + + public void deletePermissions(@NonNull UUID ownerId, @NonNull Collection idsToDelete) { + checkMalformedRequest( + !idsToDelete.isEmpty(), + "Must add at least 1 permission for %s '%s'", + getOwnerTypeName(), + ownerId); + val owner = ownerBaseService.getWithRelationships(ownerId); + + val permissions = getPermissionsFromOwner(owner); + val filteredPermissionMap = + permissions.stream() + .filter(x -> idsToDelete.contains(x.getId())) + .collect(toMap(AbstractPermission::getId, identity())); + + val existingPermissionIds = filteredPermissionMap.keySet(); + val nonExistingPermissionIds = difference(idsToDelete, existingPermissionIds); + checkNotFound( + nonExistingPermissionIds.isEmpty(), + "The following %s ids for the %s '%s' were not found", + getEntityTypeName(), + getOwnerTypeName(), + COMMA.join(nonExistingPermissionIds)); + val permissionsToRemove = filteredPermissionMap.values(); + + disassociatePermissions(permissionsToRemove); + getRepository().deleteAll(permissionsToRemove); + } + + /** + * Adds permissions for the supplied owner. The input permissionRequests are sanitized and then + * used to create new permissions and update existing ones. + * + * @param ownerId permissionRequests will be applied to the owner with this ownerId + * @param permissionRequests permission to be created or updated + * @return owner with new and updated permissions + */ + public O addPermissions( + @NonNull UUID ownerId, @NonNull List permissionRequests) { + checkMalformedRequest( + !permissionRequests.isEmpty(), + "Must add at least 1 permission for %s '%s'", + getOwnerTypeName(), + ownerId); + + // Check policies all exist + policyBaseService.checkExistence(mapToSet(permissionRequests, PermissionRequest::getPolicyId)); + + val owner = ownerBaseService.getWithRelationships(ownerId); + + // Convert the GroupPermission to PermissionRequests since all permission requests apply to the + // same owner (the group) + val existingPermissions = getPermissionsFromOwner(owner); + val existingPermissionRequests = + mapToSet(existingPermissions, AbstractPermissionService::convertToPermissionRequest); + val permissionAnalysis = analyze(existingPermissionRequests, permissionRequests); + + // Check there are no unresolvable permission requests + checkUnique( + permissionAnalysis.getUnresolvableMap().isEmpty(), + "Found multiple (%s) PermissionRequests with policyIds that have multiple masks: %s", + permissionAnalysis.getUnresolvableMap().keySet().size(), + permissionAnalysis.summarizeUnresolvables()); + + // Check that are no permission requests that effectively exist + checkUnique( + permissionAnalysis.getDuplicates().isEmpty(), + "The following permissions already exist for %s '%s': ", + getOwnerTypeName(), + ownerId, + COMMA.join(permissionAnalysis.getDuplicates())); + + return createOrUpdatePermissions(owner, permissionAnalysis); + } + + private P getByPolicyAndOwner(@NonNull UUID policyId, @NonNull UUID ownerId) { + return permissionRepository + .findByPolicy_IdAndOwner_id(policyId, ownerId) + .orElseThrow( + () -> + buildNotFoundException( + "%s for policy '%s' and owner '%s' cannot be cannot be found", + getEntityTypeName(), policyId, ownerId)); + } + + private String getOwnerTypeName() { + return ownerType.getSimpleName(); + } + + /** Specification that allows for dynamic loading of relationships */ + private Specification fetchSpecification(UUID id, boolean fetchPolicy) { + return (fromOwner, query, builder) -> { + if (fetchPolicy) { + fromOwner.fetch(POLICY, LEFT); + } + return builder.equal(fromOwner.get(ID), id); + }; + } + + /** + * Create or Update the permission for the group based on the supplied analysis + * + * @param owner with all its relationships loaded + * @param permissionAnalysis containing pre-sanitized lists of createable and updateable requests + */ + private O createOrUpdatePermissions(O owner, PermissionAnalysis permissionAnalysis) { + val updatedGroup = updateGroupPermissions(owner, permissionAnalysis.getUpdateables()); + return createGroupPermissions(updatedGroup, permissionAnalysis.getCreateables()); + } + + /** + * Update existing Permissions for an owner with different data while maintaining the same + * relationships + * + * @param owner with all its relationships loaded + */ + private O updateGroupPermissions( + O owner, Collection updatePermissionRequests) { + val existingPermissions = getPermissionsFromOwner(owner); + val existingPermissionIndex = uniqueIndex(existingPermissions, x -> x.getPolicy().getId()); + + updatePermissionRequests.forEach( + p -> { + val policyId = p.getPolicyId(); + val mask = p.getMask(); + checkNotFound( + existingPermissionIndex.containsKey(policyId), + "Could not find existing %s with policyId '%s' for %s '%s'", + getEntityTypeName(), + policyId, + getOwnerTypeName(), + owner.getId()); + val gp = existingPermissionIndex.get(policyId); + gp.setAccessLevel(mask); + }); + return owner; + } + + /** + * Create new Permissions for the owner + * + * @param owner with all its relationships loaded + */ + private O createGroupPermissions( + O owner, Collection createablePermissionRequests) { + val existingPermissions = getPermissionsFromOwner(owner); + val existingPermissionIndex = uniqueIndex(existingPermissions, x -> x.getPolicy().getId()); + val requestedPolicyIds = mapToSet(createablePermissionRequests, PermissionRequest::getPolicyId); + + // Double check the permissions you are creating dont conflict with whats existing + val redundantPolicyIds = + Sets.intersection(requestedPolicyIds, existingPermissionIndex.keySet()); + checkUnique( + redundantPolicyIds.isEmpty(), + "%ss with the following policyIds could not be created because " + + "%ss with those policyIds already exist: %s", + getEntityTypeName(), + getEntityTypeName(), + COMMA.join(redundantPolicyIds)); + + val requestedPolicyMap = + uniqueIndex(policyBaseService.getMany(requestedPolicyIds), Policy::getId); + createablePermissionRequests.forEach(x -> createGroupPermission(requestedPolicyMap, owner, x)); + return owner; + } + + @SneakyThrows + private void createGroupPermission( + Map policyMap, O owner, PermissionRequest request) { + val gp = getEntityType().newInstance(); + val policy = policyMap.get(request.getPolicyId()); + gp.setAccessLevel(request.getMask()); + associatePermission(owner, gp); + associatePermission(policy, gp); + } + + public static Scope buildScope(@NonNull AbstractPermission permission) { + return createScope(permission.getPolicy(), permission.getAccessLevel()); + } + + private static PermissionRequest convertToPermissionRequest(AbstractPermission p) { + return PermissionRequest.builder() + .mask(p.getAccessLevel()) + .policyId(p.getPolicy().getId()) + .build(); + } + + /** + * Stateless member methods If these stateless member methods were static, their signature would + * look ugly with all the generic type bounding. In the interest of more readable code, using + * member methods is a cleaner approach. + */ + public static Set resolveFinalPermissions( + Collection... collections) { + val combinedPermissionAgg = + stream(collections) + .flatMap(Collection::stream) + .filter(x -> !isNull(x.getPolicy())) + .collect(groupingBy(AbstractPermission::getPolicy)); + return combinedPermissionAgg.values().stream() + .map(AbstractPermissionService::resolvePermissions) + .collect(toImmutableSet()); + } + + private static AbstractPermission resolvePermissions( + List permissions) { + checkState(!permissions.isEmpty(), "Input permission list cannot be empty"); + permissions.sort(comparing(AbstractPermission::getAccessLevel)); + reverse(permissions); + return permissions.get(0); + } + + private PolicyResponse convertToPolicyResponse(@NonNull P p) { + val name = p.getOwner().getName(); + val id = p.getOwner().getId().toString(); + val mask = p.getAccessLevel(); + return PolicyResponse.builder().name(name).id(id).mask(mask).build(); + } + + /** + * Disassociates group permissions from its parents + * + * @param permissions assumed to be loaded with parents + */ + public void disassociatePermissions(Collection

permissions) { + permissions.forEach( + x -> { + val ownerPermissions = getPermissionsFromOwner(x.getOwner()); + ownerPermissions.remove(x); + val policyPermissions = getPermissionsFromPolicy(x.getPolicy()); + policyPermissions.remove(x); + x.setPolicy(null); + x.setOwner(null); + }); + } + + public void associatePermission(@NonNull Policy policy, @NonNull P permission) { + val policyPermissions = getPermissionsFromPolicy(policy); + policyPermissions.add(permission); + permission.setPolicy(policy); + } + + public void associatePermission(@NonNull O owner, @NonNull P permission) { + val ownerPermissions = getPermissionsFromOwner(owner); + ownerPermissions.add(permission); + permission.setOwner(owner); + } +} diff --git a/src/main/java/bio/overture/ego/service/ApplicationService.java b/src/main/java/bio/overture/ego/service/ApplicationService.java new file mode 100644 index 000000000..1161d111d --- /dev/null +++ b/src/main/java/bio/overture/ego/service/ApplicationService.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.service; + +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static bio.overture.ego.model.exceptions.NotFoundException.checkNotFound; +import static bio.overture.ego.model.exceptions.RequestValidationException.checkRequestValid; +import static bio.overture.ego.model.exceptions.UniqueViolationException.checkUnique; +import static bio.overture.ego.token.app.AppTokenClaims.AUTHORIZED_GRANTS; +import static bio.overture.ego.token.app.AppTokenClaims.ROLE; +import static bio.overture.ego.token.app.AppTokenClaims.SCOPES; +import static bio.overture.ego.utils.CollectionUtils.setOf; +import static bio.overture.ego.utils.EntityServices.checkEntityExistence; +import static bio.overture.ego.utils.FieldUtils.onUpdateDetected; +import static bio.overture.ego.utils.Splitters.COLON_SPLITTER; +import static java.lang.String.format; +import static org.mapstruct.factory.Mappers.getMapper; +import static org.springframework.data.jpa.domain.Specification.where; + +import bio.overture.ego.model.dto.CreateApplicationRequest; +import bio.overture.ego.model.dto.UpdateApplicationRequest; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.join.GroupApplication; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.repository.ApplicationRepository; +import bio.overture.ego.repository.GroupRepository; +import bio.overture.ego.repository.UserRepository; +import bio.overture.ego.repository.queryspecification.ApplicationSpecification; +import bio.overture.ego.repository.queryspecification.builder.ApplicationSpecificationBuilder; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.TargetType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.ClientRegistrationException; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class ApplicationService extends AbstractNamedService + implements ClientDetailsService { + + /** Constants */ + public static final ApplicationConverter APPLICATION_CONVERTER = + getMapper(ApplicationConverter.class); + + public static final String APP_TOKEN_PREFIX = "Basic "; + + /* + Dependencies + */ + private final ApplicationRepository applicationRepository; + private final PasswordEncoder passwordEncoder; + private final GroupRepository groupRepository; + private final UserRepository userRepository; + + @Autowired + public ApplicationService( + @NonNull ApplicationRepository applicationRepository, + @NonNull GroupRepository groupRepository, + @NonNull UserRepository userRepository, + @NonNull PasswordEncoder passwordEncoder) { + super(Application.class, applicationRepository); + this.applicationRepository = applicationRepository; + this.passwordEncoder = passwordEncoder; + this.groupRepository = groupRepository; + this.userRepository = userRepository; + } + + @Override + public void delete(@NonNull UUID groupId) { + val application = getWithRelationships(groupId); + disassociateAllGroupsFromApplication(application); + disassociateAllUsersFromApplication(application); + getRepository().delete(application); + } + + @SuppressWarnings("unchecked") + @Override + public Optional findByName(@NonNull String name) { + return (Optional) + getRepository() + .findOne( + new ApplicationSpecificationBuilder() + .fetchGroups(true) + .fetchUsers(true) + .buildByNameIgnoreCase(name)); + } + + public Application create(@NonNull CreateApplicationRequest request) { + validateCreateRequest(request); + val application = APPLICATION_CONVERTER.convertToApplication(request); + return getRepository().save(application); + } + + public Application partialUpdate(@NonNull UUID id, @NonNull UpdateApplicationRequest request) { + val app = getById(id); + validateUpdateRequest(app, request); + APPLICATION_CONVERTER.updateApplication(request, app); + return getRepository().save(app); + } + + @Override + public Application getWithRelationships(@NonNull UUID id) { + return get(id, true, true); + } + + @SuppressWarnings("unchecked") + public Page listApps( + @NonNull List filters, @NonNull Pageable pageable) { + return getRepository().findAll(ApplicationSpecification.filterBy(filters), pageable); + } + + @SuppressWarnings("unchecked") + public Page findApps( + @NonNull String query, @NonNull List filters, @NonNull Pageable pageable) { + return getRepository() + .findAll( + where(ApplicationSpecification.containsText(query)) + .and(ApplicationSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findApplicationsForUser( + @NonNull UUID userId, @NonNull List filters, @NonNull Pageable pageable) { + checkEntityExistence(User.class, userRepository, userId); + return getRepository() + .findAll( + where(ApplicationSpecification.usedBy(userId)) + .and(ApplicationSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findApplicationsForUser( + @NonNull UUID userId, + @NonNull String query, + @NonNull List filters, + @NonNull Pageable pageable) { + checkEntityExistence(User.class, userRepository, userId); + return getRepository() + .findAll( + where(ApplicationSpecification.usedBy(userId)) + .and(ApplicationSpecification.containsText(query)) + .and(ApplicationSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findApplicationsForGroup( + @NonNull UUID groupId, @NonNull List filters, @NonNull Pageable pageable) { + checkEntityExistence(Group.class, groupRepository, groupId); + return getRepository() + .findAll( + where(ApplicationSpecification.inGroup(groupId)) + .and(ApplicationSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findApplicationsForGroup( + @NonNull UUID groupId, + @NonNull String query, + @NonNull List filters, + @NonNull Pageable pageable) { + checkEntityExistence(Group.class, groupRepository, groupId); + return getRepository() + .findAll( + where(ApplicationSpecification.inGroup(groupId)) + .and(ApplicationSpecification.containsText(query)) + .and(ApplicationSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Optional findByClientId(@NonNull String clientId) { + return (Optional) + getRepository() + .findOne( + new ApplicationSpecificationBuilder() + .fetchGroups(true) + .fetchUsers(true) + .buildByClientIdIgnoreCase(clientId)); + } + + public Application getByClientId(@NonNull String clientId) { + val result = findByClientId(clientId); + checkNotFound( + result.isPresent(), + "The '%s' entity with clientId '%s' was not found", + Application.class.getSimpleName(), + clientId); + return result.get(); + } + + public Application findByBasicToken(@NonNull String token) { + log.info(format("Looking for token '%s'", token)); + val base64encoding = removeAppTokenPrefix(token); + log.info(format("Decoding '%s'", base64encoding)); + + val contents = new String(Base64.getDecoder().decode(base64encoding)); + log.info(format("Decoded to '%s'", contents)); + + val parts = COLON_SPLITTER.splitToList(contents); + val clientId = parts.get(0); + log.info(format("Extracted client id '%s'", clientId)); + return getByClientId(clientId); + } + + @Override + public ClientDetails loadClientByClientId(@NonNull String clientId) + throws ClientRegistrationException { + // find client using clientid + + val application = getByClientId(clientId); + + if (application.getStatus() != APPROVED) { + throw new ClientRegistrationException("Client Access is not approved."); + } + + // transform application to client details + val approvedScopes = Arrays.asList(SCOPES); + val clientDetails = new BaseClientDetails(); + clientDetails.setClientId(clientId); + clientDetails.setClientSecret(passwordEncoder.encode(application.getClientSecret())); + clientDetails.setAuthorizedGrantTypes(Arrays.asList(AUTHORIZED_GRANTS)); + clientDetails.setScope(approvedScopes); + clientDetails.setRegisteredRedirectUri(setOf(application.getRedirectUri())); + clientDetails.setAutoApproveScopes(approvedScopes); + val authorities = new HashSet(); + authorities.add(new SimpleGrantedAuthority(ROLE)); + clientDetails.setAuthorities(authorities); + return clientDetails; + } + + private void validateUpdateRequest(Application originalApplication, UpdateApplicationRequest r) { + onUpdateDetected( + originalApplication.getClientId(), + r.getClientId(), + () -> checkClientIdUnique(r.getClientId())); + onUpdateDetected( + originalApplication.getName(), r.getName(), () -> checkNameUnique(r.getName())); + } + + private void validateCreateRequest(CreateApplicationRequest r) { + checkRequestValid(r); + checkNameUnique(r.getName()); + checkClientIdUnique(r.getClientId()); + } + + private void checkClientIdUnique(String clientId) { + checkUnique( + !applicationRepository.existsByClientIdIgnoreCase(clientId), + "An application with the same clientId already exists"); + } + + private void checkNameUnique(String name) { + checkUnique( + !applicationRepository.existsByNameIgnoreCase(name), + "An application with the same name already exists"); + } + + @SuppressWarnings("unchecked") + private Application get(UUID id, boolean fetchUsers, boolean fetchGroups) { + val result = + (Optional) + getRepository() + .findOne( + new ApplicationSpecificationBuilder() + .fetchUsers(fetchUsers) + .fetchGroups(fetchGroups) + .buildById(id)); + checkNotFound(result.isPresent(), "The applicationId '%s' does not exist", id); + return result.get(); + } + + public static void disassociateAllGroupsFromApplication(@NonNull Application a) { + val groupApplications = a.getGroupApplications(); + disassociateGroupApplicationsFromApplication(a, groupApplications); + } + + public static void disassociateAllUsersFromApplication(@NonNull Application a) { + val users = a.getUsers(); + disassociateUsersFromApplication(a, users); + } + + public static void disassociateUsersFromApplication( + @NonNull Application application, @NonNull Collection users) { + users.forEach( + u -> { + u.getApplications().remove(application); + application.getUsers().remove(u); + }); + } + + public static void disassociateGroupApplicationsFromApplication( + @NonNull Application application, @NonNull Collection groupApplications) { + groupApplications.forEach( + ga -> { + ga.getGroup().getGroupApplications().remove(ga); + ga.setGroup(null); + ga.setApplication(null); + }); + application.getGroupApplications().removeAll(groupApplications); + } + + private static String removeAppTokenPrefix(String token) { + return token.replace(APP_TOKEN_PREFIX, "").trim(); + } + + @Mapper( + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + unmappedTargetPolicy = ReportingPolicy.WARN) + public abstract static class ApplicationConverter { + + public abstract Application convertToApplication(CreateApplicationRequest request); + + public abstract void updateApplication( + Application updatingApplication, @MappingTarget Application applicationToUpdate); + + public abstract void updateApplication( + UpdateApplicationRequest updateRequest, @MappingTarget Application applicationToUpdate); + + protected Application initApplicationEntity(@TargetType Class appClass) { + return Application.builder().build(); + } + } +} diff --git a/src/main/java/bio/overture/ego/service/BaseService.java b/src/main/java/bio/overture/ego/service/BaseService.java new file mode 100644 index 000000000..91b68acf7 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/BaseService.java @@ -0,0 +1,44 @@ +package bio.overture.ego.service; + +import static java.lang.String.format; + +import bio.overture.ego.model.exceptions.NotFoundException; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import lombok.NonNull; +import lombok.val; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; + +public interface BaseService { + + String getEntityTypeName(); + + default T getById(@NonNull ID id) { + val entity = findById(id); + return entity.orElseThrow( + () -> + new NotFoundException( + format( + "The '%s' entity with id '%s' does not exist", + getEntityTypeName(), id.toString()))); + } + + Optional findById(ID id); + + boolean isExist(ID id); + + void delete(ID id); + + Page findAll(Specification specification, Pageable pageable); + + Set getMany(Collection ids); + + T getWithRelationships(ID id); + + void checkExistence(Collection ids); + + void checkExistence(ID id); +} diff --git a/src/main/java/bio/overture/ego/service/GroupPermissionService.java b/src/main/java/bio/overture/ego/service/GroupPermissionService.java new file mode 100644 index 000000000..306a6bee4 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/GroupPermissionService.java @@ -0,0 +1,80 @@ +package bio.overture.ego.service; + +import static bio.overture.ego.utils.CollectionUtils.mapToImmutableSet; + +import bio.overture.ego.event.token.TokenEventsPublisher; +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.GroupPermission; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.repository.GroupPermissionRepository; +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class GroupPermissionService extends AbstractPermissionService { + + /** Dependencies */ + private final GroupService groupService; + + private final TokenEventsPublisher tokenEventsPublisher; + + @Autowired + public GroupPermissionService( + @NonNull GroupPermissionRepository repository, + @NonNull GroupService groupService, + @NonNull TokenEventsPublisher tokenEventsPublisher, + @NonNull PolicyService policyService) { + super(Group.class, GroupPermission.class, groupService, policyService, repository); + this.groupService = groupService; + this.tokenEventsPublisher = tokenEventsPublisher; + } + + /** + * Decorates the call to addPermissions with the functionality to also cleanup user tokens in the + * event that the permission added downgrades the available scopes to the users of this group. + * + * @param groupId Id of the group who's permissions are being added or updated + * @param permissionRequests A list of permission changes + */ + @Override + public Group addPermissions( + @NonNull UUID groupId, @NonNull List permissionRequests) { + val group = super.addPermissions(groupId, permissionRequests); + val users = mapToImmutableSet(group.getUserGroups(), UserGroup::getUser); + tokenEventsPublisher.requestTokenCleanupByUsers(users); + return group; + } + + /** + * Decorates the call to deletePermissions with the functionality to also cleanup user tokens + * + * @param groupId Id of the group who's permissions are being deleted + * @param idsToDelete Ids of the permission to delete + */ + @Override + public void deletePermissions(@NonNull UUID groupId, @NonNull Collection idsToDelete) { + super.deletePermissions(groupId, idsToDelete); + val group = groupService.getWithRelationships(groupId); + val users = mapToImmutableSet(group.getUserGroups(), UserGroup::getUser); + tokenEventsPublisher.requestTokenCleanupByUsers(users); + } + + @Override + protected Collection getPermissionsFromOwner(@NonNull Group owner) { + return owner.getPermissions(); + } + + @Override + protected Collection getPermissionsFromPolicy(@NonNull Policy policy) { + return policy.getGroupPermissions(); + } +} diff --git a/src/main/java/bio/overture/ego/service/GroupService.java b/src/main/java/bio/overture/ego/service/GroupService.java new file mode 100644 index 000000000..c2c16f7b9 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/GroupService.java @@ -0,0 +1,439 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.service; + +import static bio.overture.ego.model.exceptions.NotFoundException.buildNotFoundException; +import static bio.overture.ego.model.exceptions.NotFoundException.checkNotFound; +import static bio.overture.ego.model.exceptions.RequestValidationException.checkRequestValid; +import static bio.overture.ego.model.exceptions.UniqueViolationException.checkUnique; +import static bio.overture.ego.utils.CollectionUtils.difference; +import static bio.overture.ego.utils.CollectionUtils.intersection; +import static bio.overture.ego.utils.CollectionUtils.mapToImmutableSet; +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static bio.overture.ego.utils.Converters.convertToGroupApplication; +import static bio.overture.ego.utils.Converters.convertToIds; +import static bio.overture.ego.utils.Converters.convertToUserGroup; +import static bio.overture.ego.utils.EntityServices.checkEntityExistence; +import static bio.overture.ego.utils.EntityServices.getManyEntities; +import static bio.overture.ego.utils.FieldUtils.onUpdateDetected; +import static bio.overture.ego.utils.Ids.checkDuplicates; +import static bio.overture.ego.utils.Joiners.PRETTY_COMMA; +import static org.mapstruct.factory.Mappers.getMapper; +import static org.springframework.data.jpa.domain.Specification.where; + +import bio.overture.ego.event.token.TokenEventsPublisher; +import bio.overture.ego.model.dto.GroupRequest; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.join.GroupApplication; +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.repository.GroupRepository; +import bio.overture.ego.repository.UserRepository; +import bio.overture.ego.repository.queryspecification.GroupSpecification; +import bio.overture.ego.repository.queryspecification.builder.GroupSpecificationBuilder; +import bio.overture.ego.utils.EntityServices; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import javax.transaction.Transactional; +import lombok.NonNull; +import lombok.val; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.TargetType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@Transactional +public class GroupService extends AbstractNamedService { + + /** Constants */ + private static final GroupConverter GROUP_CONVERTER = getMapper(GroupConverter.class); + + /** Dependencies */ + private final GroupRepository groupRepository; + + private final UserRepository userRepository; + private final ApplicationService applicationService; + private final TokenEventsPublisher tokenEventsPublisher; + + @Autowired + public GroupService( + @NonNull GroupRepository groupRepository, + @NonNull UserRepository userRepository, + @NonNull ApplicationService applicationService, + @NonNull TokenEventsPublisher tokenEventsPublisher) { + super(Group.class, groupRepository); + this.groupRepository = groupRepository; + this.applicationService = applicationService; + this.tokenEventsPublisher = tokenEventsPublisher; + this.userRepository = userRepository; + } + + @SuppressWarnings("unchecked") + @Override + public Optional findByName(@NonNull String name) { + return (Optional) + getRepository() + .findOne( + new GroupSpecificationBuilder() + .fetchApplications(true) + .fetchUserGroups(true) + .fetchGroupPermissions(true) + .buildByNameIgnoreCase(name)); + } + + public Group create(@NonNull GroupRequest request) { + validateCreateRequest(request); + val group = GROUP_CONVERTER.convertToGroup(request); + return getRepository().save(group); + } + + /** + * Decorate the delete method for group's users to also trigger a token check after group delete. + * + * @param groupId The ID of the group to be deleted. + */ + @Override + public void delete(@NonNull UUID groupId) { + val group = getWithRelationships(groupId); + val users = mapToSet(group.getUserGroups(), UserGroup::getUser); + disassociateAllUsersFromGroup(group); + disassociateAllApplicationsFromGroup(group); + tokenEventsPublisher.requestTokenCleanupByUsers(users); + getRepository().delete(group); + } + + public Group getWithRelationships(@NonNull UUID id) { + return get(id, true, true, true); + } + + public Group getWithUserGroups(@NonNull UUID id) { + return get(id, false, true, false); + } + + public Group getWithApplications(@NonNull UUID id) { + return get(id, true, false, false); + } + + public void disassociateUsersFromGroup(@NonNull UUID id, @NonNull Collection userIds) { + // check duplicate userIds + checkDuplicates(User.class, userIds); + + // Get existing associated child ids with the parent + val groupWithUserGroups = getWithUserGroups(id); + val users = mapToImmutableSet(groupWithUserGroups.getUserGroups(), UserGroup::getUser); + val existingAssociatedUserIds = convertToIds(users); + + // Get existing and non-existing non-associated user ids. Error out if there are existing and + // non-existing non-associated user ids + val nonAssociatedUserIds = difference(userIds, existingAssociatedUserIds); + if (!nonAssociatedUserIds.isEmpty()) { + EntityServices.checkEntityExistence(User.class, userRepository, nonAssociatedUserIds); + throw buildNotFoundException( + "The following existing %s ids cannot be disassociated from %s '%s' " + + "because they are not associated with it", + User.class.getSimpleName(), getEntityTypeName(), id); + } + + // Since all user ids exist and are associated with the group, disassociate them from + // eachother + val userIdsToDisassociate = ImmutableSet.copyOf(userIds); + val userGroupsToDisassociate = + groupWithUserGroups.getUserGroups().stream() + .filter(ug -> userIdsToDisassociate.contains(ug.getId().getUserId())) + .collect(toImmutableSet()); + + disassociateUserGroupsFromGroup(groupWithUserGroups, userGroupsToDisassociate); + tokenEventsPublisher.requestTokenCleanupByUsers(users); + } + + public Group associateUsersWithGroup(@NonNull UUID id, @NonNull Collection userIds) { + // check duplicate userIds + checkDuplicates(User.class, userIds); + + // Get existing associated user ids with the group + val groupWithUserGroups = getWithUserGroups(id); + val users = mapToImmutableSet(groupWithUserGroups.getUserGroups(), UserGroup::getUser); + val existingAssociatedUserIds = convertToIds(users); + + // Check there are no user ids that are already associated with the group + val existingAlreadyAssociatedUserIds = intersection(existingAssociatedUserIds, userIds); + checkUnique( + existingAlreadyAssociatedUserIds.isEmpty(), + "The following %s ids are already associated with %s '%s': [%s]", + User.class.getSimpleName(), + getEntityTypeName(), + id, + PRETTY_COMMA.join(existingAlreadyAssociatedUserIds)); + + // Get all unassociated user ids. If they do not exist, an error is thrown + val nonAssociatedUserIds = difference(userIds, existingAssociatedUserIds); + val nonAssociatedUsers = getManyEntities(User.class, userRepository, nonAssociatedUserIds); + + // Associate the existing users with the group + nonAssociatedUsers.stream() + .map(u -> convertToUserGroup(u, groupWithUserGroups)) + .forEach(UserGroupService::associateSelf); + tokenEventsPublisher.requestTokenCleanupByUsers(users); + return groupWithUserGroups; + } + + public Group partialUpdate(@NonNull UUID id, @NonNull GroupRequest r) { + val group = getById(id); + validateUpdateRequest(group, r); + GROUP_CONVERTER.updateGroup(r, group); + return getRepository().save(group); + } + + @SuppressWarnings("unchecked") + public Page listGroups(@NonNull List filters, @NonNull Pageable pageable) { + return getRepository().findAll(GroupSpecification.filterBy(filters), pageable); + } + + @SuppressWarnings("unchecked") + public Page findGroups( + @NonNull String query, @NonNull List filters, @NonNull Pageable pageable) { + return getRepository() + .findAll( + where(GroupSpecification.containsText(query)).and(GroupSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findGroupsForUser( + @NonNull UUID userId, @NonNull List filters, @NonNull Pageable pageable) { + checkEntityExistence(User.class, userRepository, userId); + return getRepository() + .findAll( + where(GroupSpecification.containsUser(userId)) + .and(GroupSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findGroupsForUser( + @NonNull UUID userId, + @NonNull String query, + @NonNull List filters, + @NonNull Pageable pageable) { + checkEntityExistence(User.class, userRepository, userId); + return getRepository() + .findAll( + where(GroupSpecification.containsUser(userId)) + .and(GroupSpecification.containsText(query)) + .and(GroupSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findGroupsForApplication( + @NonNull UUID appId, @NonNull List filters, @NonNull Pageable pageable) { + applicationService.checkExistence(appId); + return getRepository() + .findAll( + where(GroupSpecification.containsApplication(appId)) + .and(GroupSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findGroupsForApplication( + @NonNull UUID appId, + @NonNull String query, + @NonNull List filters, + @NonNull Pageable pageable) { + applicationService.checkExistence(appId); + return getRepository() + .findAll( + where(GroupSpecification.containsApplication(appId)) + .and(GroupSpecification.containsText(query)) + .and(GroupSpecification.filterBy(filters)), + pageable); + } + + public Group associateApplicationsWithGroup( + @NonNull UUID id, @NonNull Collection applicationIds) { + // check duplicate applicationIds + checkDuplicates(Application.class, applicationIds); + + // Get existing associated application ids with the group + val groupWithApplications = getWithApplications(id); + val applications = + mapToImmutableSet( + groupWithApplications.getGroupApplications(), GroupApplication::getApplication); + val existingAssociatedApplicationIds = convertToIds(applications); + + // Check there are no application ids that are already associated with the group + val existingAlreadyAssociatedApplicationIds = + intersection(existingAssociatedApplicationIds, applicationIds); + checkUnique( + existingAlreadyAssociatedApplicationIds.isEmpty(), + "The following %s ids are already associated with %s '%s': [%s]", + Application.class.getSimpleName(), + getEntityTypeName(), + id, + PRETTY_COMMA.join(existingAlreadyAssociatedApplicationIds)); + + // Get all unassociated application ids. If they do not exist, an error is thrown + val nonAssociatedApplicationIds = difference(applicationIds, existingAssociatedApplicationIds); + val nonAssociatedApplications = applicationService.getMany(nonAssociatedApplicationIds); + + // Associate the existing applications with the group + nonAssociatedApplications.stream() + .map(a -> convertToGroupApplication(groupWithApplications, a)) + .forEach(GroupService::associateSelf); + return groupWithApplications; + } + + public void disassociateApplicationsFromGroup( + @NonNull UUID id, @NonNull Collection applicationIds) { + // check duplicate applicationIds + checkDuplicates(Application.class, applicationIds); + + // Get existing associated child ids with the parent + val groupWithApplications = getWithApplications(id); + val applications = + mapToImmutableSet( + groupWithApplications.getGroupApplications(), GroupApplication::getApplication); + val existingAssociatedApplicationIds = convertToIds(applications); + + // Get existing and non-existing non-associated application ids. Error out if there are existing + // and + // non-existing non-associated application ids + val nonAssociatedApplicationIds = difference(applicationIds, existingAssociatedApplicationIds); + if (!nonAssociatedApplicationIds.isEmpty()) { + applicationService.checkExistence(nonAssociatedApplicationIds); + throw buildNotFoundException( + "The following existing %s ids cannot be disassociated from %s '%s' " + + "because they are not associated with it", + Application.class.getSimpleName(), getEntityTypeName(), id); + } + + // Since all applicaiton ids exist and are associated with the group, disassociate them from + // eachother + val applicationIdsToDisassociate = ImmutableSet.copyOf(applicationIds); + val groupApplicationsToDisassociate = + groupWithApplications.getGroupApplications().stream() + .filter(ga -> applicationIdsToDisassociate.contains(ga.getId().getApplicationId())) + .collect(toImmutableSet()); + + disassociateGroupApplicationsFromGroup(groupWithApplications, groupApplicationsToDisassociate); + } + + @SuppressWarnings("unchecked") + private Group get( + UUID id, boolean fetchApplications, boolean fetchUserGroups, boolean fetchGroupPermissions) { + val result = + (Optional) + getRepository() + .findOne( + new GroupSpecificationBuilder() + .fetchGroupPermissions(fetchGroupPermissions) + .fetchUserGroups(fetchUserGroups) + .fetchApplications(fetchApplications) + .buildById(id)); + checkNotFound(result.isPresent(), "The groupId '%s' does not exist", id); + return result.get(); + } + + private void validateCreateRequest(GroupRequest createRequest) { + checkRequestValid(createRequest); + checkNameUnique(createRequest.getName()); + } + + private void validateUpdateRequest(Group originalGroup, GroupRequest updateRequest) { + onUpdateDetected( + originalGroup.getName(), + updateRequest.getName(), + () -> checkNameUnique(updateRequest.getName())); + } + + private void checkNameUnique(String name) { + checkUnique( + !groupRepository.existsByNameIgnoreCase(name), "A group with same name already exists"); + } + + public static void disassociateGroupApplicationsFromGroup( + @NonNull Group g, @NonNull Collection groupApplications) { + groupApplications.forEach( + ga -> { + ga.getApplication().getGroupApplications().remove(ga); + ga.setApplication(null); + ga.setGroup(null); + }); + g.getGroupApplications().removeAll(groupApplications); + } + + public static void disassociateUserGroupsFromGroup( + @NonNull Group g, @NonNull Collection userGroups) { + userGroups.forEach( + ug -> { + ug.getUser().getUserGroups().remove(ug); + ug.setUser(null); + ug.setGroup(null); + }); + g.getUserGroups().removeAll(userGroups); + } + + public static void disassociateAllUsersFromGroup(@NonNull Group g) { + val userGroups = g.getUserGroups(); + disassociateUserGroupsFromGroup(g, userGroups); + } + + public static void disassociateAllApplicationsFromGroup(@NonNull Group g) { + val groupApplications = g.getGroupApplications(); + disassociateGroupApplicationsFromGroup(g, groupApplications); + } + + private static void associateSelf(@NonNull GroupApplication ga) { + ga.getGroup().getGroupApplications().add(ga); + ga.getApplication().getGroupApplications().add(ga); + } + + @Mapper( + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + unmappedTargetPolicy = ReportingPolicy.WARN) + public abstract static class GroupConverter { + + public abstract Group convertToGroup(GroupRequest request); + + public abstract void updateGroup(Group updatingGroup, @MappingTarget Group groupToUpdate); + + public Group copy(Group groupToCopy) { + val newGroup = initGroupEntity(Group.class); + updateGroup(groupToCopy, newGroup); + return newGroup; + } + + public abstract void updateGroup(GroupRequest request, @MappingTarget Group groupToUpdate); + + protected Group initGroupEntity(@TargetType Class groupClass) { + return Group.builder().build(); + } + } +} diff --git a/src/main/java/bio/overture/ego/service/NamedService.java b/src/main/java/bio/overture/ego/service/NamedService.java new file mode 100644 index 000000000..776ec8296 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/NamedService.java @@ -0,0 +1,10 @@ +package bio.overture.ego.service; + +import java.util.Optional; + +public interface NamedService extends BaseService { + + Optional findByName(String name); + + T getByName(String name); +} diff --git a/src/main/java/bio/overture/ego/service/PolicyService.java b/src/main/java/bio/overture/ego/service/PolicyService.java new file mode 100644 index 000000000..3668559a3 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/PolicyService.java @@ -0,0 +1,150 @@ +package bio.overture.ego.service; + +import static bio.overture.ego.model.enums.JavaFields.ID; +import static bio.overture.ego.model.enums.JavaFields.PERMISSIONS; +import static bio.overture.ego.model.enums.JavaFields.USERPERMISSIONS; +import static bio.overture.ego.model.exceptions.NotFoundException.checkNotFound; +import static bio.overture.ego.model.exceptions.RequestValidationException.checkRequestValid; +import static bio.overture.ego.model.exceptions.UniqueViolationException.checkUnique; +import static bio.overture.ego.utils.FieldUtils.onUpdateDetected; +import static javax.persistence.criteria.JoinType.LEFT; +import static org.mapstruct.factory.Mappers.getMapper; + +import bio.overture.ego.event.token.TokenEventsPublisher; +import bio.overture.ego.model.dto.PolicyRequest; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.TokenScope; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.repository.PolicyRepository; +import bio.overture.ego.repository.queryspecification.PolicySpecification; +import bio.overture.ego.utils.Collectors; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.TargetType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +public class PolicyService extends AbstractNamedService { + + /** Constants */ + private static final PolicyConverter POLICY_CONVERTER = getMapper(PolicyConverter.class); + + /** Dependencies */ + private final PolicyRepository policyRepository; + + private final TokenEventsPublisher tokenEventsPublisher; + + @Autowired + public PolicyService( + @NonNull PolicyRepository policyRepository, + @NonNull TokenEventsPublisher tokenEventsPublisher) { + super(Policy.class, policyRepository); + this.policyRepository = policyRepository; + this.tokenEventsPublisher = tokenEventsPublisher; + } + + public Policy create(@NonNull PolicyRequest createRequest) { + validateCreateRequest(createRequest); + val policy = POLICY_CONVERTER.convertToPolicy(createRequest); + return getRepository().save(policy); + } + + @Override + public Policy getWithRelationships(@NonNull UUID id) { + val result = (Optional) getRepository().findOne(fetchSpecification(id, true, true)); + checkNotFound(result.isPresent(), "The policyId '%s' does not exist", id); + return result.get(); + } + + public void delete(@NonNull UUID id) { + checkExistence(id); + val policy = this.getById(id); + + // For semantic/readability reasons, revoke tokens AFTER policy is deleted. + val tokensToRevoke = + policy.getTokenScopes().stream() + .map(TokenScope::getToken) + .collect(Collectors.toImmutableSet()); + super.delete(id); + tokenEventsPublisher.requestTokenCleanup(tokensToRevoke); + } + + public Page listPolicies( + @NonNull List filters, @NonNull Pageable pageable) { + return policyRepository.findAll(PolicySpecification.filterBy(filters), pageable); + } + + public Policy partialUpdate(@NonNull UUID id, @NonNull PolicyRequest updateRequest) { + val policy = getById(id); + validateUpdateRequest(policy, updateRequest); + POLICY_CONVERTER.updatePolicy(updateRequest, policy); + return getRepository().save(policy); + } + + private void validateCreateRequest(PolicyRequest createRequest) { + checkRequestValid(createRequest); + checkNameUnique(createRequest.getName()); + } + + private void validateUpdateRequest(Policy originalPolicy, PolicyRequest updateRequest) { + onUpdateDetected( + originalPolicy.getName(), + updateRequest.getName(), + () -> checkNameUnique(updateRequest.getName())); + } + + private void checkNameUnique(String name) { + checkUnique( + !policyRepository.existsByNameIgnoreCase(name), "A policy with same name already exists"); + } + + private static Specification fetchSpecification( + UUID id, boolean fetchGroupPermissions, boolean fetchUserPermissions) { + return (fromPolicy, query, builder) -> { + if (fetchGroupPermissions) { + fromPolicy.fetch(PERMISSIONS, LEFT); + } + if (fetchUserPermissions) { + fromPolicy.fetch(USERPERMISSIONS, LEFT); + } + return builder.equal(fromPolicy.get(ID), id); + }; + } + + @Mapper( + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + unmappedTargetPolicy = ReportingPolicy.WARN) + public abstract static class PolicyConverter { + + public abstract Policy convertToPolicy(PolicyRequest request); + + public abstract void updatePolicy(Policy updatingPolicy, @MappingTarget Policy policyToUpdate); + + public Policy copy(Policy policyToCopy) { + val newPolicy = initPolicyEntity(Policy.class); + updatePolicy(policyToCopy, newPolicy); + return newPolicy; + } + + public abstract void updatePolicy(PolicyRequest request, @MappingTarget Policy policyToUpdate); + + protected Policy initPolicyEntity(@TargetType Class policyClass) { + return Policy.builder().build(); + } + } +} diff --git a/src/main/java/bio/overture/ego/service/TokenService.java b/src/main/java/bio/overture/ego/service/TokenService.java new file mode 100644 index 000000000..4159f0c8f --- /dev/null +++ b/src/main/java/bio/overture/ego/service/TokenService.java @@ -0,0 +1,488 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.service; + +import static bio.overture.ego.model.dto.Scope.effectiveScopes; +import static bio.overture.ego.model.dto.Scope.explicitScopes; +import static bio.overture.ego.model.enums.ApplicationType.ADMIN; +import static bio.overture.ego.model.enums.JavaFields.APPLICATIONS; +import static bio.overture.ego.model.enums.JavaFields.ID; +import static bio.overture.ego.model.enums.JavaFields.SCOPES; +import static bio.overture.ego.model.enums.JavaFields.USERS; +import static bio.overture.ego.model.exceptions.NotFoundException.checkNotFound; +import static bio.overture.ego.service.UserService.extractScopes; +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static bio.overture.ego.utils.TypeUtils.convertToAnotherType; +import static java.lang.String.format; +import static java.util.UUID.fromString; +import static javax.persistence.criteria.JoinType.LEFT; +import static org.springframework.util.DigestUtils.md5Digest; + +import bio.overture.ego.model.dto.Scope; +import bio.overture.ego.model.dto.TokenResponse; +import bio.overture.ego.model.dto.TokenScopeResponse; +import bio.overture.ego.model.dto.UpdateUserRequest; +import bio.overture.ego.model.dto.UserScopesResponse; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Token; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.params.ScopeName; +import bio.overture.ego.repository.TokenStoreRepository; +import bio.overture.ego.token.IDToken; +import bio.overture.ego.token.TokenClaims; +import bio.overture.ego.token.app.AppJWTAccessToken; +import bio.overture.ego.token.app.AppTokenClaims; +import bio.overture.ego.token.app.AppTokenContext; +import bio.overture.ego.token.signer.TokenSigner; +import bio.overture.ego.token.user.UserJWTAccessToken; +import bio.overture.ego.token.user.UserTokenClaims; +import bio.overture.ego.token.user.UserTokenContext; +import bio.overture.ego.view.Views; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.security.InvalidKeyException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.common.exceptions.InvalidRequestException; +import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class TokenService extends AbstractNamedService { + + /* + * Constant + */ + private static final String ISSUER_NAME = "ego"; + + /* + * Dependencies + */ + private TokenSigner tokenSigner; + private UserService userService; + private ApplicationService applicationService; + private TokenStoreService tokenStoreService; + private PolicyService policyService; + + /** Configuration */ + @Value("${jwt.duration:86400000}") + private int DURATION; + + @Value("${apitoken.duration:365}") + private int API_TOKEN_DURATION; + + public TokenService( + @NonNull TokenSigner tokenSigner, + @NonNull UserService userService, + @NonNull ApplicationService applicationService, + @NonNull TokenStoreService tokenStoreService, + @NonNull PolicyService policyService, + @NonNull TokenStoreRepository tokenStoreRepository) { + super(Token.class, tokenStoreRepository); + this.tokenSigner = tokenSigner; + this.userService = userService; + this.applicationService = applicationService; + this.tokenStoreService = tokenStoreService; + this.policyService = policyService; + } + + @Override + public Token getWithRelationships(@NonNull UUID id) { + val result = + (Optional) getRepository().findOne(fetchSpecification(id, true, true, true)); + checkNotFound(result.isPresent(), "The tokenId '%s' does not exist", id); + return result.get(); + } + + public String generateUserToken(IDToken idToken) { + val userName = idToken.getEmail(); + val user = + userService + .findByName(userName) + .orElseGet( + () -> { + log.info("User not found, creating."); + return userService.createFromIDToken(idToken); + }); + + val u = UpdateUserRequest.builder().lastLogin(new Date()).build(); + userService.partialUpdate(user.getId(), u); + return generateUserToken(user); + } + + @SneakyThrows + public String generateUserToken(User u) { + Set permissionNames = mapToSet(extractScopes(u), p -> p.toString()); + return generateUserToken(u, permissionNames); + } + + public Set getScopes(Set scopeNames) { + return mapToSet(scopeNames, this::getScope); + } + + public Scope getScope(ScopeName name) { + val policy = policyService.getByName(name.getName()); + + return new Scope(policy, name.getAccessLevel()); + } + + public Set missingScopes(String userName, Set scopeNames) { + val user = userService.getByName(userName); + val userScopes = extractScopes(user); + val requestedScopes = getScopes(scopeNames); + return Scope.missingScopes(userScopes, requestedScopes); + } + + public String str(Object o) { + if (o == null) { + return "null"; + } else { + return "'" + o.toString() + "'"; + } + } + + public String strList(Collection collection) { + if (collection == null) { + return "null"; + } + val l = new ArrayList(collection); + return l.toString(); + } + + @SneakyThrows + public Token issueToken(UUID user_id, List scopeNames, String description) { + log.info(format("Looking for user '%s'", str(user_id))); + log.info(format("Scopes are '%s'", strList(scopeNames))); + log.info(format("Token description is '%s'", description)); + + val u = + userService + .findById(user_id) + .orElseThrow( + () -> new UsernameNotFoundException(format("Can't find user '%s'", str(user_id)))); + + log.info(format("Got user with id '%s'", str(u.getId()))); + val userScopes = extractScopes(u); + + log.info(format("User's scopes are '%s'", str(userScopes))); + + val requestedScopes = getScopes(new HashSet<>(scopeNames)); + + val missingScopes = Scope.missingScopes(userScopes, requestedScopes); + if (!missingScopes.isEmpty()) { + val msg = format("User %s has no access to scopes [%s]", str(user_id), str(missingScopes)); + log.info(msg); + throw new InvalidScopeException(msg); + } + + val tokenString = generateTokenString(); + log.info(format("Generated token string '%s'", str(tokenString))); + + val cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_YEAR, API_TOKEN_DURATION); + val expiryDate = cal.getTime(); + + val today = Calendar.getInstance(); + + val token = new Token(); + token.setExpiryDate(expiryDate); + token.setIssueDate(today.getTime()); + token.setRevoked(false); + token.setName(tokenString); + token.setOwner(u); + token.setDescription(description); + + for (Scope requestedScope : requestedScopes) { + token.addScope(requestedScope); + } + + log.info("Creating token in token store"); + tokenStoreService.create(token); + + log.info(format("Returning '%s'", str(token))); + + return token; + } + + public Optional findByTokenString(String token) { + return tokenStoreService.findByTokenName(token); + } + + public String generateTokenString() { + return UUID.randomUUID().toString(); + } + + public String generateUserToken(User u, Set scope) { + val tokenContext = new UserTokenContext(u); + tokenContext.setScope(scope); + val tokenClaims = new UserTokenClaims(); + tokenClaims.setIss(ISSUER_NAME); + tokenClaims.setValidDuration(DURATION); + tokenClaims.setContext(tokenContext); + + return getSignedToken(tokenClaims); + } + + @SneakyThrows + public String generateAppToken(Application application) { + val tokenContext = new AppTokenContext(application); + val tokenClaims = new AppTokenClaims(); + tokenClaims.setIss(ISSUER_NAME); + tokenClaims.setValidDuration(DURATION); + tokenClaims.setContext(tokenContext); + return getSignedToken(tokenClaims); + } + + public boolean isValidToken(String token) { + Jws decodedToken = null; + try { + decodedToken = Jwts.parser().setSigningKey(tokenSigner.getKey().get()).parseClaimsJws(token); + } catch (JwtException e) { + log.error("JWT token is invalid", e); + } + return (decodedToken != null); + } + + public User getTokenUserInfo(String token) { + try { + val body = getTokenClaims(token); + val tokenClaims = + convertToAnotherType(body, UserTokenClaims.class, Views.JWTAccessToken.class); + return userService.getById(fromString(tokenClaims.getSub())); + } catch (JwtException | ClassCastException e) { + log.error("Issue handling user token (MD5sum) {}", new String(md5Digest(token.getBytes()))); + return null; + } + } + + public Application getTokenAppInfo(String token) { + try { + val body = getTokenClaims(token); + val tokenClaims = + convertToAnotherType(body, AppTokenClaims.class, Views.JWTAccessToken.class); + return applicationService.getById(fromString(tokenClaims.getSub())); + } catch (JwtException | ClassCastException e) { + log.error( + "Issue handling application token (MD5sum) {}", new String(md5Digest(token.getBytes()))); + return null; + } + } + + @SneakyThrows + public Claims getTokenClaims(String token) { + if (tokenSigner.getKey().isPresent()) { + return Jwts.parser() + .setSigningKey(tokenSigner.getKey().get()) + .parseClaimsJws(token) + .getBody(); + } else { + throw new InvalidKeyException("Invalid signing key for the token."); + } + } + + public UserJWTAccessToken getUserAccessToken(String token) { + return new UserJWTAccessToken(token, this); + } + + public AppJWTAccessToken getAppAccessToken(String token) { + return new AppJWTAccessToken(token, this); + } + + @SneakyThrows + private String getSignedToken(TokenClaims claims) { + if (tokenSigner.getKey().isPresent()) { + return Jwts.builder() + .setClaims(convertToAnotherType(claims, Map.class, Views.JWTAccessToken.class)) + .signWith(SignatureAlgorithm.RS256, tokenSigner.getKey().get()) + .compact(); + } else { + throw new InvalidKeyException("Invalid signing key for the token."); + } + } + + @SneakyThrows + public TokenScopeResponse checkToken(String authToken, String token) { + if (token == null) { + log.debug("Null token"); + throw new InvalidTokenException("No token field found in POST request"); + } + + log.debug(format("token ='%s'", token)); + val application = applicationService.findByBasicToken(authToken); + + val t = + findByTokenString(token).orElseThrow(() -> new InvalidTokenException("Token not found")); + + if (t.isRevoked()) + throw new InvalidTokenException( + format("Token \"%s\" has expired or is no longer valid. ", token)); + + // We want to limit the scopes listed in the token to those scopes that the user + // is allowed to access at the time the token is checked -- we don't assume that + // they have not changed since the token was issued. + val clientId = application.getClientId(); + val owner = t.getOwner(); + val scopes = explicitScopes(effectiveScopes(extractScopes(owner), t.scopes())); + val names = mapToSet(scopes, Scope::toString); + + return new TokenScopeResponse(owner.getName(), clientId, t.getSecondsUntilExpiry(), names); + } + + public UserScopesResponse userScopes(@NonNull String userName) { + val user = userService.getByName(userName); + val scopes = extractScopes(user); + val names = mapToSet(scopes, Scope::toString); + + return new UserScopesResponse(names); + } + + public void revokeToken(@NonNull String tokenName) { + validateTokenName(tokenName); + val principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + if (principal instanceof User) { + revokeTokenAsUser(tokenName, (User) principal); + } else if (principal instanceof Application) { + revokeTokenAsApplication(tokenName, (Application) principal); + } else { + log.info("Unknown type of authentication, token is not allowed to be revoked."); + throw new InvalidRequestException("Unknown type of authentication."); + } + } + + private void revokeTokenAsUser(String tokenName, User user) { + if (userService.isAdmin(user) && userService.isActiveUser(user)) { + revoke(tokenName); + } else { + // if it's a regular user, check if the token belongs to the user + verifyToken(tokenName, user.getId()); + revoke(tokenName); + } + } + + private void revokeTokenAsApplication(String tokenName, Application application) { + if (application.getType() == ADMIN) { + revoke(tokenName); + } else { + throw new InvalidRequestException( + format("The application does not have permission to revoke token '%s'", tokenName)); + } + } + + private void verifyToken(String token, UUID userId) { + val currentToken = + findByTokenString(token).orElseThrow(() -> new InvalidTokenException("Token not found.")); + + if (!currentToken.getOwner().getId().equals(userId)) { + throw new InvalidTokenException("Users can only revoke tokens that belong to them."); + } + } + + private void validateTokenName(@NonNull String tokenName) { + log.info(format("Validating token: '%s'.", tokenName)); + + if (tokenName.isEmpty()) { + throw new InvalidTokenException("Token cannot be empty."); + } + + if (tokenName.length() > 2048) { + throw new InvalidRequestException("Invalid token, the maximum length for a token is 2048."); + } + } + + public void revoke(String token) { + val currentToken = + findByTokenString(token).orElseThrow(() -> new InvalidTokenException("Token not found.")); + if (currentToken.isRevoked()) { + throw new InvalidTokenException(format("Token '%s' is already revoked.", token)); + } + currentToken.setRevoked(true); + getRepository().save(currentToken); + } + + public List listToken(@NonNull UUID userId) { + val user = + userService + .findById(userId) + .orElseThrow( + () -> new UsernameNotFoundException(format("Can't find user '%s'", str(userId)))); + + val tokens = user.getTokens(); + if (tokens.isEmpty()) { + return new ArrayList<>(); + } + + val unrevokedTokens = + tokens.stream().filter((token -> !token.isRevoked())).collect(Collectors.toSet()); + List response = new ArrayList<>(); + unrevokedTokens.forEach( + token -> { + createTokenResponse(token, response); + }); + + return response; + } + + private void createTokenResponse(@NonNull Token token, @NonNull List responses) { + val scopes = mapToSet(token.scopes(), Scope::toString); + responses.add( + TokenResponse.builder() + .accessToken(token.getName()) + .scope(scopes) + .exp(token.getSecondsUntilExpiry()) + .description(token.getDescription()) + .build()); + } + + public static Specification fetchSpecification( + UUID id, boolean fetchUser, boolean fetchApplications, boolean fetchTokenScopes) { + return (fromToken, query, builder) -> { + if (fetchUser) { + fromToken.fetch(USERS, LEFT); + } + if (fetchApplications) { + fromToken.fetch(APPLICATIONS, LEFT); + } + if (fetchTokenScopes) { + fromToken.fetch(SCOPES, LEFT); + } + return builder.equal(fromToken.get(ID), id); + }; + } +} diff --git a/src/main/java/bio/overture/ego/service/TokenStoreService.java b/src/main/java/bio/overture/ego/service/TokenStoreService.java new file mode 100644 index 000000000..8440cbe71 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/TokenStoreService.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.service; + +import static bio.overture.ego.model.exceptions.NotFoundException.checkNotFound; +import static bio.overture.ego.service.TokenService.fetchSpecification; + +import bio.overture.ego.model.dto.CreateTokenRequest; +import bio.overture.ego.model.entity.Token; +import bio.overture.ego.repository.TokenStoreRepository; +import java.util.Optional; +import java.util.UUID; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.commons.lang.NotImplementedException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +public class TokenStoreService extends AbstractNamedService { + + /** Dependencies */ + private final TokenStoreRepository tokenRepository; + + @Autowired + public TokenStoreService(@NonNull TokenStoreRepository repository) { + super(Token.class, repository); + this.tokenRepository = repository; + } + + @Override + public Token getWithRelationships(@NonNull UUID id) { + val result = + (Optional) getRepository().findOne(fetchSpecification(id, true, true, true)); + checkNotFound(result.isPresent(), "The tokenId '%s' does not exist", id); + return result.get(); + } + + public Token create(@NonNull CreateTokenRequest createTokenRequest) { + throw new NotImplementedException(); + } + + @Deprecated + public Token create(@NonNull Token scopedAccessToken) { + Token res = tokenRepository.save(scopedAccessToken); + tokenRepository.revokeRedundantTokens(scopedAccessToken.getOwner().getId()); + return res; + } + + public Optional findByTokenName(String tokenName) { + return tokenRepository.findByName(tokenName); + } +} diff --git a/src/main/java/bio/overture/ego/service/UserGroupService.java b/src/main/java/bio/overture/ego/service/UserGroupService.java new file mode 100644 index 000000000..3ccb0a2c2 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/UserGroupService.java @@ -0,0 +1,12 @@ +package bio.overture.ego.service; + +import bio.overture.ego.model.join.UserGroup; +import lombok.NonNull; + +public class UserGroupService { + + public static void associateSelf(@NonNull UserGroup ug) { + ug.getGroup().getUserGroups().add(ug); + ug.getUser().getUserGroups().add(ug); + } +} diff --git a/src/main/java/bio/overture/ego/service/UserPermissionService.java b/src/main/java/bio/overture/ego/service/UserPermissionService.java new file mode 100644 index 000000000..1408a260f --- /dev/null +++ b/src/main/java/bio/overture/ego/service/UserPermissionService.java @@ -0,0 +1,77 @@ +package bio.overture.ego.service; + +import bio.overture.ego.event.token.TokenEventsPublisher; +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.entity.UserPermission; +import bio.overture.ego.repository.UserPermissionRepository; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +public class UserPermissionService extends AbstractPermissionService { + + /** Dependencies */ + private final UserService userService; + + private final TokenEventsPublisher tokenEventsPublisher; + + @Autowired + public UserPermissionService( + @NonNull UserPermissionRepository repository, + @NonNull UserService userService, + @NonNull TokenEventsPublisher tokenEventsPublisher, + @NonNull PolicyService policyService) { + super(User.class, UserPermission.class, userService, policyService, repository); + this.userService = userService; + this.tokenEventsPublisher = tokenEventsPublisher; + } + + /** + * Decorates the call to addPermissions with the functionality to also cleanup user tokens in the + * event that the permission added downgrades the available scopes to the user. + * + * @param userId Id of the user who's permissions are being added or updated + * @param permissionRequests A list of permission changes + */ + @Override + public User addPermissions( + @NonNull UUID userId, @NonNull List permissionRequests) { + val user = super.addPermissions(userId, permissionRequests); + tokenEventsPublisher.requestTokenCleanupByUsers(ImmutableSet.of(userService.getById(userId))); + return user; + } + + /** + * Decorates the call to deletePermissions with the functionality to also cleanup user tokens + * + * @param userId Id of the user who's permissions are being deleted + * @param idsToDelete Ids of the permission to delete + */ + @Override + public void deletePermissions(@NonNull UUID userId, @NonNull Collection idsToDelete) { + super.deletePermissions(userId, idsToDelete); + tokenEventsPublisher.requestTokenCleanupByUsers(ImmutableSet.of(userService.getById(userId))); + } + + @Override + protected Collection getPermissionsFromOwner(@NonNull User owner) { + return owner.getUserPermissions(); + } + + @Override + protected Collection getPermissionsFromPolicy(@NonNull Policy policy) { + return policy.getUserPermissions(); + } +} diff --git a/src/main/java/bio/overture/ego/service/UserService.java b/src/main/java/bio/overture/ego/service/UserService.java new file mode 100644 index 000000000..b53f56603 --- /dev/null +++ b/src/main/java/bio/overture/ego/service/UserService.java @@ -0,0 +1,459 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.service; + +import static bio.overture.ego.model.enums.UserType.ADMIN; +import static bio.overture.ego.model.exceptions.NotFoundException.checkNotFound; +import static bio.overture.ego.model.exceptions.RequestValidationException.checkRequestValid; +import static bio.overture.ego.model.exceptions.UniqueViolationException.checkUnique; +import static bio.overture.ego.service.AbstractPermissionService.resolveFinalPermissions; +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static bio.overture.ego.utils.Converters.convertToUserGroup; +import static bio.overture.ego.utils.EntityServices.checkEntityExistence; +import static bio.overture.ego.utils.FieldUtils.onUpdateDetected; +import static bio.overture.ego.utils.Joiners.COMMA; +import static java.lang.String.format; +import static java.util.Collections.reverse; +import static java.util.Comparator.comparing; +import static java.util.Objects.isNull; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Stream.concat; +import static org.springframework.data.jpa.domain.Specification.where; + +import bio.overture.ego.config.UserDefaultsConfig; +import bio.overture.ego.event.token.TokenEventsPublisher; +import bio.overture.ego.model.dto.CreateUserRequest; +import bio.overture.ego.model.dto.Scope; +import bio.overture.ego.model.dto.UpdateUserRequest; +import bio.overture.ego.model.entity.AbstractPermission; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.GroupPermission; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.entity.UserPermission; +import bio.overture.ego.model.exceptions.NotFoundException; +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.repository.GroupRepository; +import bio.overture.ego.repository.UserRepository; +import bio.overture.ego.repository.queryspecification.UserSpecification; +import bio.overture.ego.repository.queryspecification.builder.UserSpecificationBuilder; +import bio.overture.ego.token.IDToken; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import javax.transaction.Transactional; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.TargetType; +import org.mapstruct.factory.Mappers; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@Transactional +public class UserService extends AbstractNamedService { + + /** Constants */ + public static final UserConverter USER_CONVERTER = Mappers.getMapper(UserConverter.class); + + /** Dependencies */ + private final GroupRepository groupRepository; + + private final TokenEventsPublisher tokenEventsPublisher; + private final ApplicationService applicationService; + private final UserRepository userRepository; + + /** Configuration */ + private final UserDefaultsConfig userDefaultsConfig; + + @Autowired + public UserService( + @NonNull UserRepository userRepository, + @NonNull GroupRepository groupRepository, + @NonNull ApplicationService applicationService, + @NonNull UserDefaultsConfig userDefaultsConfig, + @NonNull TokenEventsPublisher tokenEventsPublisher) { + super(User.class, userRepository); + this.userRepository = userRepository; + this.groupRepository = groupRepository; + this.applicationService = applicationService; + this.userDefaultsConfig = userDefaultsConfig; + this.tokenEventsPublisher = tokenEventsPublisher; + } + + @Override + public void delete(@NonNull UUID id) { + val user = getWithRelationships(id); + disassociateAllGroupsFromUser(user); + disassociateAllApplicationsFromUser(user); + tokenEventsPublisher.requestTokenCleanupByUsers(ImmutableSet.of(user)); + super.delete(id); + } + + public User create(@NonNull CreateUserRequest request) { + validateCreateRequest(request); + val user = USER_CONVERTER.convertToUser(request); + return getRepository().save(user); + } + + @SuppressWarnings("unchecked") + @Override + public Optional findByName(String name) { + return (Optional) + getRepository() + .findOne( + new UserSpecificationBuilder() + .fetchApplications(true) + .fetchUserGroups(true) + .fetchUserPermissions(true) + .buildByNameIgnoreCase(name)); + } + + @SuppressWarnings("unchecked") + public User get( + @NonNull UUID id, + boolean fetchUserPermissions, + boolean fetchUserGroups, + boolean fetchApplications) { + val result = + (Optional) + getRepository() + .findOne( + new UserSpecificationBuilder() + .fetchUserPermissions(fetchUserPermissions) + .fetchUserGroups(fetchUserGroups) + .fetchApplications(fetchApplications) + .buildById(id)); + checkNotFound(result.isPresent(), "The userId '%s' does not exist", id); + return result.get(); + } + + public User createFromIDToken(IDToken idToken) { + return create( + CreateUserRequest.builder() + .email(idToken.getEmail()) + .firstName(idToken.getGiven_name()) + .lastName(idToken.getFamily_name()) + .status(userDefaultsConfig.getDefaultUserStatus()) + .type(userDefaultsConfig.getDefaultUserType()) + .build()); + } + + public User addUserToApps(@NonNull UUID id, @NonNull List appIds) { + val user = getById(id); + val apps = applicationService.getMany(appIds); + associateUserWithApplications(user, apps); + // TODO: @rtisma test setting apps even if there were existing apps before does not delete the + // existing ones. Becuase the PERSIST and MERGE cascade applicationType is used, this should + // work correctly + return getRepository().save(user); + } + + @Override + public User getWithRelationships(@NonNull UUID id) { + return get(id, true, true, true); + } + + /** + * Partially updates a user using only non-null {@code UpdateUserRequest} {@param r} object + * + * @param r updater + * @param id updatee + */ + public User partialUpdate(@NonNull UUID id, @NonNull UpdateUserRequest r) { + val user = getById(id); + validateUpdateRequest(user, r); + USER_CONVERTER.updateUser(r, user); + return getRepository().save(user); + } + + @SuppressWarnings("unchecked") + public Page listUsers(@NonNull List filters, @NonNull Pageable pageable) { + return getRepository().findAll(UserSpecification.filterBy(filters), pageable); + } + + @SuppressWarnings("unchecked") + public Page findUsers( + @NonNull String query, @NonNull List filters, @NonNull Pageable pageable) { + return getRepository() + .findAll( + where(UserSpecification.containsText(query)).and(UserSpecification.filterBy(filters)), + pageable); + } + + public void disassociateGroupsFromUser(@NonNull UUID id, @NonNull Collection groupIds) { + val userWithRelationships = get(id, false, true, false); + val userGroupsToDisassociate = + userWithRelationships.getUserGroups().stream() + .filter(x -> groupIds.contains(x.getId().getGroupId())) + .collect(toImmutableSet()); + disassociateUserGroupsFromUser(userWithRelationships, userGroupsToDisassociate); + tokenEventsPublisher.requestTokenCleanupByUsers(ImmutableSet.of(userWithRelationships)); + } + + public User associateGroupsWithUser(@NonNull UUID id, @NonNull Collection groupIds) { + val user = getWithRelationships(id); + val groups = groupRepository.findAllByIdIn(groupIds); + groups.stream().map(g -> convertToUserGroup(user, g)).forEach(UserGroupService::associateSelf); + tokenEventsPublisher.requestTokenCleanupByUsers(ImmutableSet.of(user)); + return user; + } + + // TODO @rtisma: add test for all entities to ensure they implement .equals() using only the id + // field + // TODO @rtisma: add test for checking user exists + // TODO @rtisma: add test for checking application exists for a user + public void deleteUserFromApps(@NonNull UUID id, @NonNull Collection appIds) { + val user = getWithRelationships(id); + checkApplicationsExistForUser(user, appIds); + val appsToDisassociate = + user.getApplications().stream() + .filter(a -> appIds.contains(a.getId())) + .collect(toImmutableSet()); + disassociateUserFromApplications(user, appsToDisassociate); + getRepository().save(user); + } + + @SuppressWarnings("unchecked") + public Page findUsersForGroup( + @NonNull UUID groupId, @NonNull List filters, @NonNull Pageable pageable) { + checkEntityExistence(Group.class, groupRepository, groupId); + return userRepository.findAll( + where(UserSpecification.inGroup(groupId)).and(UserSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findUsersForGroup( + @NonNull UUID groupId, + @NonNull String query, + @NonNull List filters, + @NonNull Pageable pageable) { + checkEntityExistence(Group.class, groupRepository, groupId); + return userRepository.findAll( + where(UserSpecification.inGroup(groupId)) + .and(UserSpecification.containsText(query)) + .and(UserSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findUsersForApplication( + @NonNull UUID appId, @NonNull List filters, @NonNull Pageable pageable) { + applicationService.checkExistence(appId); + return getRepository() + .findAll( + where(UserSpecification.ofApplication(appId)).and(UserSpecification.filterBy(filters)), + pageable); + } + + @SuppressWarnings("unchecked") + public Page findUsersForApplication( + @NonNull UUID appId, + @NonNull String query, + @NonNull List filters, + @NonNull Pageable pageable) { + applicationService.checkExistence(appId); + return getRepository() + .findAll( + where(UserSpecification.ofApplication(appId)) + .and(UserSpecification.containsText(query)) + .and(UserSpecification.filterBy(filters)), + pageable); + } + + private void validateCreateRequest(CreateUserRequest r) { + checkRequestValid(r); + checkEmailUnique(r.getEmail()); + } + + private void validateUpdateRequest(User originalUser, UpdateUserRequest r) { + onUpdateDetected(originalUser.getEmail(), r.getEmail(), () -> checkEmailUnique(r.getEmail())); + } + + private void checkEmailUnique(String email) { + checkUnique( + !userRepository.existsByEmailIgnoreCase(email), "A user with same email already exists"); + } + + @SuppressWarnings("unchecked") + public static Set resolveUsersPermissions(User user) { + val up = user.getUserPermissions(); + Collection userPermissions = isNull(up) ? ImmutableList.of() : up; + + val userGroups = user.getUserGroups(); + + Collection groupPermissions = + isNull(userGroups) + ? ImmutableList.of() + : userGroups.stream() + .map(UserGroup::getGroup) + .map(Group::getPermissions) + .flatMap(Collection::stream) + .collect(toImmutableSet()); + return resolveFinalPermissions(userPermissions, groupPermissions); + } + + // TODO: [rtisma] this is the old implementation. Ensure there is a test for this, and if there + // isnt, + // create one, and ensure the Old and new refactored method are correct + @Deprecated + public static Set getPermissionsListOld(User user) { + // Get user's individual permission (stream) + val userPermissions = + Optional.ofNullable(user.getUserPermissions()).orElse(new HashSet<>()).stream(); + + // Get permissions from the user's groups (stream) + val userGroupsPermissions = + Optional.ofNullable(user.getUserGroups()).orElse(new HashSet<>()).stream() + .map(UserGroup::getGroup) + .map(Group::getPermissions) + .flatMap(Collection::stream); + + // Combine individual user permissions and the user's + // groups (if they have any) permissions + val combinedPermissions = + concat(userPermissions, userGroupsPermissions) + .collect(groupingBy(AbstractPermission::getPolicy)); + // If we have no permissions at all return an empty list + if (combinedPermissions.values().size() == 0) { + return new HashSet<>(); + } + + // If we do have permissions ... sort the grouped permissions (by PolicyIdStringWithMaskName) + // on PolicyMask, extracting the first value of the sorted list into the final + // permissions list + HashSet finalPermissionsList = new HashSet<>(); + + combinedPermissions.forEach( + (entity, permissions) -> { + permissions.sort(comparing(AbstractPermission::getAccessLevel)); + reverse(permissions); + finalPermissionsList.add(permissions.get(0)); + }); + return finalPermissionsList; + } + + public static Set extractScopes(@NonNull User user) { + return mapToSet(resolveUsersPermissions(user), AbstractPermissionService::buildScope); + } + + public static void disassociateUserFromApplications( + @NonNull User user, @NonNull Collection applications) { + user.getApplications().removeAll(applications); + applications.forEach(x -> x.getUsers().remove(user)); + } + + public static void associateUserWithApplications( + @NonNull User user, @NonNull Collection apps) { + apps.forEach(a -> associateUserWithApplication(user, a)); + } + + public static void associateUserWithApplication(@NonNull User user, @NonNull Application app) { + user.getApplications().add(app); + app.getUsers().add(user); + } + + public static void disassociateAllApplicationsFromUser(@NonNull User user) { + user.getApplications().forEach(x -> x.getUsers().remove(user)); + user.getApplications().clear(); + } + + public static void disassociateAllGroupsFromUser(@NonNull User userWithRelationships) { + disassociateUserGroupsFromUser(userWithRelationships, userWithRelationships.getUserGroups()); + } + + public static void disassociateUserGroupsFromUser( + @NonNull User user, @NonNull Collection userGroups) { + userGroups.forEach( + ug -> { + ug.getGroup().getUserGroups().remove(ug); + ug.setUser(null); + ug.setGroup(null); + }); + user.getUserGroups().removeAll(userGroups); + } + + public static void checkApplicationsExistForUser( + @NonNull User user, @NonNull Collection appIds) { + val existingAppIds = + user.getApplications().stream().map(Application::getId).collect(toImmutableSet()); + val nonExistentAppIds = + appIds.stream().filter(x -> !existingAppIds.contains(x)).collect(toImmutableSet()); + if (!nonExistentAppIds.isEmpty()) { + throw new NotFoundException( + format( + "The following applications do not exist for user '%s': %s", + user.getId(), COMMA.join(nonExistentAppIds))); + } + } + + @Mapper( + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + unmappedTargetPolicy = ReportingPolicy.WARN) + public abstract static class UserConverter { + + public abstract User convertToUser(CreateUserRequest request); + + public abstract void updateUser(User updatingUser, @MappingTarget User userToUpdate); + + public abstract UpdateUserRequest convertToUpdateRequest(User user); + + public abstract void updateUser( + UpdateUserRequest updateRequest, @MappingTarget User userToUpdate); + + protected User initUserEntity(@TargetType Class userClass) { + return User.builder().build(); + } + + @AfterMapping + protected void correctUserData(@MappingTarget User userToUpdate) { + // Set UserName to equal the email. + userToUpdate.setName(userToUpdate.getEmail()); + + // Set Created At date to Now if not defined + if (isNull(userToUpdate.getCreatedAt())) { + userToUpdate.setCreatedAt(new Date()); + } + } + } + + public boolean isActiveUser(User user) { + return isAdmin(user); + } + + public boolean isAdmin(User user) { + return user.getType() == ADMIN; + } +} diff --git a/src/main/java/org/overture/ego/token/CustomTokenEnhancer.java b/src/main/java/bio/overture/ego/token/CustomTokenEnhancer.java similarity index 60% rename from src/main/java/org/overture/ego/token/CustomTokenEnhancer.java rename to src/main/java/bio/overture/ego/token/CustomTokenEnhancer.java index da146d97c..1159d77d4 100644 --- a/src/main/java/org/overture/ego/token/CustomTokenEnhancer.java +++ b/src/main/java/bio/overture/ego/token/CustomTokenEnhancer.java @@ -14,52 +14,49 @@ * limitations under the License. */ -package org.overture.ego.token; - +package bio.overture.ego.token; + +import bio.overture.ego.service.ApplicationService; +import bio.overture.ego.service.TokenService; +import bio.overture.ego.service.UserService; +import bio.overture.ego.token.app.AppJWTAccessToken; +import bio.overture.ego.token.app.AppTokenClaims; +import bio.overture.ego.token.user.UserJWTAccessToken; import lombok.val; -import org.overture.ego.service.ApplicationService; -import org.overture.ego.service.UserService; -import org.overture.ego.token.app.AppJWTAccessToken; -import org.overture.ego.token.app.AppTokenClaims; -import org.overture.ego.token.user.UserJWTAccessToken; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenEnhancer; - public class CustomTokenEnhancer implements TokenEnhancer { - @Autowired - private TokenService tokenService; - @Autowired - private UserService userService; - @Autowired - private ApplicationService applicationService; + @Autowired private TokenService tokenService; + @Autowired private UserService userService; + @Autowired private ApplicationService applicationService; @Override - public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { + public OAuth2AccessToken enhance( + OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { // get user or application token - return - oAuth2Authentication.getAuthorities() != null - && - oAuth2Authentication - .getAuthorities().stream().anyMatch(authority -> AppTokenClaims.ROLE.equals(authority.getAuthority())) ? - getApplicationAccessToken(oAuth2Authentication.getPrincipal().toString()) : - getUserAccessToken(oAuth2Authentication.getPrincipal().toString()); + return oAuth2Authentication.getAuthorities() != null + && oAuth2Authentication.getAuthorities().stream() + .anyMatch(authority -> AppTokenClaims.ROLE.equals(authority.getAuthority())) + ? getApplicationAccessToken(oAuth2Authentication.getPrincipal().toString()) + : getUserAccessToken(oAuth2Authentication.getPrincipal().toString()); } - private UserJWTAccessToken getUserAccessToken(String userName){ + private UserJWTAccessToken getUserAccessToken(String userName) { val user = userService.getByName(userName); val token = tokenService.generateUserToken(user); + return tokenService.getUserAccessToken(token); } - private AppJWTAccessToken getApplicationAccessToken(String clientId){ + private AppJWTAccessToken getApplicationAccessToken(String clientId) { val app = applicationService.getByClientId(clientId); val token = tokenService.generateAppToken(app); + return tokenService.getAppAccessToken(token); } - } diff --git a/src/main/java/org/overture/ego/token/IDToken.java b/src/main/java/bio/overture/ego/token/IDToken.java similarity index 93% rename from src/main/java/org/overture/ego/token/IDToken.java rename to src/main/java/bio/overture/ego/token/IDToken.java index 6f8519311..70d8c0a01 100644 --- a/src/main/java/org/overture/ego/token/IDToken.java +++ b/src/main/java/bio/overture/ego/token/IDToken.java @@ -14,19 +14,19 @@ * limitations under the License. */ -package org.overture.ego.token; +package bio.overture.ego.token; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.*; @Data +@Builder @NoArgsConstructor @AllArgsConstructor -@Builder @JsonIgnoreProperties(ignoreUnknown = true) public class IDToken { - @NonNull - private String email; + + @NonNull private String email; private String given_name; private String family_name; } diff --git a/src/main/java/org/overture/ego/token/TokenClaims.java b/src/main/java/bio/overture/ego/token/TokenClaims.java similarity index 72% rename from src/main/java/org/overture/ego/token/TokenClaims.java rename to src/main/java/bio/overture/ego/token/TokenClaims.java index 09c559b89..5468bfb7d 100644 --- a/src/main/java/org/overture/ego/token/TokenClaims.java +++ b/src/main/java/bio/overture/ego/token/TokenClaims.java @@ -14,42 +14,34 @@ * limitations under the License. */ -package org.overture.ego.token; +package bio.overture.ego.token; +import bio.overture.ego.view.Views; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonView; -import lombok.*; -import org.overture.ego.view.Views; - import java.util.List; import java.util.UUID; +import lombok.*; @Data @NoArgsConstructor @JsonView(Views.JWTAccessToken.class) public abstract class TokenClaims { - @NonNull - protected Integer iat; + @NonNull protected Integer iat; - @NonNull - protected Integer exp; + @NonNull protected Integer exp; - @NonNull - @JsonIgnore - protected Integer validDuration; + @NonNull @JsonIgnore protected Integer validDuration; - @Getter - protected String sub; + @Getter protected String sub; - @NonNull - protected String iss; + @NonNull protected String iss; - @Getter - protected List aud; + @Getter protected List aud; /* - Defaults - */ + Defaults + */ private String jti = UUID.randomUUID().toString(); @Getter(AccessLevel.NONE) @@ -57,12 +49,11 @@ public abstract class TokenClaims { @JsonIgnore private long initTime = System.currentTimeMillis(); - public int getExp(){ - return ((int) ((this.initTime + validDuration)/ 1000L)); + public int getExp() { + return ((int) ((this.initTime + validDuration) / 1000L)); } - public int getIat(){ + public int getIat() { return (int) (this.initTime / 1000L); } - } diff --git a/src/main/java/org/overture/ego/token/app/AppJWTAccessToken.java b/src/main/java/bio/overture/ego/token/app/AppJWTAccessToken.java similarity index 96% rename from src/main/java/org/overture/ego/token/app/AppJWTAccessToken.java rename to src/main/java/bio/overture/ego/token/app/AppJWTAccessToken.java index 0801f7bd8..65e5b983a 100644 --- a/src/main/java/org/overture/ego/token/app/AppJWTAccessToken.java +++ b/src/main/java/bio/overture/ego/token/app/AppJWTAccessToken.java @@ -14,16 +14,15 @@ * limitations under the License. */ -package org.overture.ego.token.app; +package bio.overture.ego.token.app; +import bio.overture.ego.service.TokenService; import io.jsonwebtoken.Claims; +import java.util.*; import lombok.val; -import org.overture.ego.token.TokenService; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; -import java.util.*; - public class AppJWTAccessToken implements OAuth2AccessToken { private Claims tokenClaims = null; diff --git a/src/main/java/org/overture/ego/token/app/AppTokenClaims.java b/src/main/java/bio/overture/ego/token/app/AppTokenClaims.java similarity index 65% rename from src/main/java/org/overture/ego/token/app/AppTokenClaims.java rename to src/main/java/bio/overture/ego/token/app/AppTokenClaims.java index c9a92879f..a1252afdd 100644 --- a/src/main/java/org/overture/ego/token/app/AppTokenClaims.java +++ b/src/main/java/bio/overture/ego/token/app/AppTokenClaims.java @@ -14,45 +14,43 @@ * limitations under the License. */ -package org.overture.ego.token.app; +package bio.overture.ego.token.app; +import bio.overture.ego.token.TokenClaims; +import bio.overture.ego.view.Views; import com.fasterxml.jackson.annotation.JsonView; +import com.google.common.collect.ImmutableList; +import java.util.List; import lombok.Data; import lombok.NoArgsConstructor; import lombok.NonNull; -import org.overture.ego.token.TokenClaims; -import org.overture.ego.view.Views; import org.springframework.util.StringUtils; -import java.util.Arrays; -import java.util.List; - @Data @NoArgsConstructor @JsonView(Views.JWTAccessToken.class) public class AppTokenClaims extends TokenClaims { /* - Constants - */ - public static final String[] AUTHORIZED_GRANTS= - {"authorization_code","client_credentials", "password", "refresh_token"}; - public static final String[] SCOPES = {"read","write", "delete"}; + Constants + */ + public static final String[] AUTHORIZED_GRANTS = { + "authorization_code", "client_credentials", "password", "refresh_token" + }; + public static final String[] SCOPES = {"read", "write", "delete"}; public static final String ROLE = "ROLE_CLIENT"; - @NonNull - private AppTokenContext context; + @NonNull private AppTokenContext context; - public String getSub(){ - if(StringUtils.isEmpty(sub)) { + public String getSub() { + if (StringUtils.isEmpty(sub)) { return String.valueOf(this.context.getAppInfo().getId()); } else { return sub; } } - public List getAud(){ - return Arrays.asList(this.context.getAppInfo().getName()); + public List getAud() { + return ImmutableList.of(this.context.getAppInfo().getName()); } - } diff --git a/src/main/java/org/overture/ego/token/app/AppTokenContext.java b/src/main/java/bio/overture/ego/token/app/AppTokenContext.java similarity index 90% rename from src/main/java/org/overture/ego/token/app/AppTokenContext.java rename to src/main/java/bio/overture/ego/token/app/AppTokenContext.java index 0e9a52e97..8cd2e7962 100644 --- a/src/main/java/org/overture/ego/token/app/AppTokenContext.java +++ b/src/main/java/bio/overture/ego/token/app/AppTokenContext.java @@ -14,8 +14,10 @@ * limitations under the License. */ -package org.overture.ego.token.app; +package bio.overture.ego.token.app; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.view.Views; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonView; @@ -23,8 +25,6 @@ import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.overture.ego.model.entity.Application; -import org.overture.ego.view.Views; @Data @NoArgsConstructor diff --git a/src/main/java/org/overture/ego/token/signer/DefaultTokenSigner.java b/src/main/java/bio/overture/ego/token/signer/DefaultTokenSigner.java similarity index 81% rename from src/main/java/org/overture/ego/token/signer/DefaultTokenSigner.java rename to src/main/java/bio/overture/ego/token/signer/DefaultTokenSigner.java index e77336293..3f9dc5e89 100644 --- a/src/main/java/org/overture/ego/token/signer/DefaultTokenSigner.java +++ b/src/main/java/bio/overture/ego/token/signer/DefaultTokenSigner.java @@ -14,37 +14,34 @@ * limitations under the License. */ -package org.overture.ego.token.signer; +package bio.overture.ego.token.signer; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import sun.misc.BASE64Encoder; - -import javax.annotation.PostConstruct; import java.security.*; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.Optional; +import javax.annotation.PostConstruct; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; @Slf4j @Service @Profile("!jks") public class DefaultTokenSigner implements TokenSigner { - /* - Constants - */ - private static final String KEYFACTORY_TYPE= "RSA"; + Constants + */ + private static final String KEYFACTORY_TYPE = "RSA"; /* - Dependencies - */ + Dependencies + */ @Value("${token.private-key}") private String encodedPrivKey; @@ -58,22 +55,22 @@ public class DefaultTokenSigner implements TokenSigner { private PrivateKey privateKey; private PublicKey publicKey; - @PostConstruct @SneakyThrows - private void init(){ + private void init() { keyFactory = KeyFactory.getInstance(KEYFACTORY_TYPE); try { val decodedpriv = Base64.getDecoder().decode(encodedPrivKey); - val decodedPub = Base64.getDecoder().decode(encodedPubKey); + val decodedPub = Base64.getDecoder().decode(encodedPubKey); X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(decodedPub); PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(decodedpriv); publicKey = keyFactory.generatePublic(pubKeySpec); privateKey = keyFactory.generatePrivate(privKeySpec); - } catch (InvalidKeySpecException specEx){ + } catch (InvalidKeySpecException specEx) { log.error("Error loading keys:{}", specEx); } } + @Override public Optional getKey() { return Optional.of(privateKey); @@ -86,14 +83,13 @@ public Optional getKeyPair() { @Override public Optional getEncodedPublicKey() { - if(publicKey != null){ - val b64 = new BASE64Encoder(); - String encodedKey = b64.encodeBuffer(publicKey.getEncoded()); - encodedKey= "-----BEGIN PUBLIC KEY-----\r\n" + encodedKey + "-----END PUBLIC KEY-----"; + if (publicKey != null) { + val b64 = Base64.getEncoder(); + String encodedKey = b64.encodeToString(publicKey.getEncoded()); + encodedKey = "-----BEGIN PUBLIC KEY-----\r\n" + encodedKey + "-----END PUBLIC KEY-----"; return Optional.of(encodedKey); } else { return Optional.empty(); } - } } diff --git a/src/main/java/org/overture/ego/token/signer/JKSTokenSigner.java b/src/main/java/bio/overture/ego/token/signer/JKSTokenSigner.java similarity index 65% rename from src/main/java/org/overture/ego/token/signer/JKSTokenSigner.java rename to src/main/java/bio/overture/ego/token/signer/JKSTokenSigner.java index d0869ced6..34cd6604f 100644 --- a/src/main/java/org/overture/ego/token/signer/JKSTokenSigner.java +++ b/src/main/java/bio/overture/ego/token/signer/JKSTokenSigner.java @@ -14,21 +14,20 @@ * limitations under the License. */ -package org.overture.ego.token.signer; +package bio.overture.ego.token.signer; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.*; +import java.util.Base64; +import java.util.Optional; +import javax.annotation.PostConstruct; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import lombok.val; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import sun.misc.BASE64Encoder; - -import javax.annotation.PostConstruct; -import java.io.FileInputStream; -import java.io.IOException; -import java.security.*; -import java.util.Optional; @Slf4j @Service @@ -36,12 +35,12 @@ public class JKSTokenSigner implements TokenSigner { /* - Constants - */ - private static final String KEYSTORE_TYPE= "JKS"; + Constants + */ + private static final String KEYSTORE_TYPE = "JKS"; /* - Dependencies - */ + Dependencies + */ @Value("${token.key-store}") private String keyStorePath; @@ -55,58 +54,57 @@ public class JKSTokenSigner implements TokenSigner { */ private KeyStore keyStore; - @PostConstruct @SneakyThrows - private void init(){ + private void init() { keyStore = KeyStore.getInstance(KEYSTORE_TYPE); - try(val keyStoreFile = new FileInputStream(keyStorePath)) { + try (val keyStoreFile = new FileInputStream(keyStorePath)) { keyStore.load(keyStoreFile, keyStorePwd.toCharArray()); - } catch (IOException ioex){ + } catch (IOException ioex) { log.error("Error loading keystore:{}", ioex); } } - public Optional getKey(){ - try{ - return Optional.of(keyStore.getKey(keyalias,keyStorePwd.toCharArray())); - }catch (Exception ex) { + + public Optional getKey() { + try { + return Optional.of(keyStore.getKey(keyalias, keyStorePwd.toCharArray())); + } catch (Exception ex) { log.error("Error getting the key:{}", ex); return Optional.empty(); } } - public Optional getKeyPair(){ - val key = this.getKey(); - val publicKey = this.getPublicKey(); - if(key.isPresent() && publicKey.isPresent()){ - return Optional.of(new KeyPair(publicKey.get(), (PrivateKey) key.get())); - } else { - return Optional.empty(); - } + public Optional getKeyPair() { + val key = this.getKey(); + val publicKey = this.getPublicKey(); + if (key.isPresent() && publicKey.isPresent()) { + return Optional.of(new KeyPair(publicKey.get(), (PrivateKey) key.get())); + } else { + return Optional.empty(); + } } - public Optional getPublicKey(){ - try{ + public Optional getPublicKey() { + try { val cert = keyStore.getCertificate(keyalias); val publicKey = cert.getPublicKey(); return Optional.of(publicKey); - }catch (Exception ex) { + } catch (Exception ex) { log.error("Error getting the public key:{}", ex); return Optional.empty(); } } @SneakyThrows - public Optional getEncodedPublicKey(){ + public Optional getEncodedPublicKey() { val publicKey = this.getPublicKey(); - if(publicKey.isPresent()){ - val b64 = new BASE64Encoder(); - String encodedKey = b64.encodeBuffer(publicKey.get().getEncoded()); - encodedKey= "-----BEGIN PUBLIC KEY-----\r\n" + encodedKey + "-----END PUBLIC KEY-----"; + if (publicKey.isPresent()) { + val b64 = Base64.getEncoder(); + String encodedKey = b64.encodeToString(publicKey.get().getEncoded()); + encodedKey = "-----BEGIN PUBLIC KEY-----\r\n" + encodedKey + "-----END PUBLIC KEY-----"; return Optional.of(encodedKey); } else { return Optional.empty(); } } - } diff --git a/src/main/java/org/overture/ego/token/signer/TokenSigner.java b/src/main/java/bio/overture/ego/token/signer/TokenSigner.java similarity index 95% rename from src/main/java/org/overture/ego/token/signer/TokenSigner.java rename to src/main/java/bio/overture/ego/token/signer/TokenSigner.java index 0c1aa04e2..de2e50217 100644 --- a/src/main/java/org/overture/ego/token/signer/TokenSigner.java +++ b/src/main/java/bio/overture/ego/token/signer/TokenSigner.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.overture.ego.token.signer; +package bio.overture.ego.token.signer; import java.security.Key; import java.security.KeyPair; @@ -23,6 +23,8 @@ public interface TokenSigner { Optional getKey(); + Optional getKeyPair(); + Optional getEncodedPublicKey(); } diff --git a/src/main/java/org/overture/ego/token/user/UserJWTAccessToken.java b/src/main/java/bio/overture/ego/token/user/UserJWTAccessToken.java similarity index 82% rename from src/main/java/org/overture/ego/token/user/UserJWTAccessToken.java rename to src/main/java/bio/overture/ego/token/user/UserJWTAccessToken.java index 57378ae90..75ac822e3 100644 --- a/src/main/java/org/overture/ego/token/user/UserJWTAccessToken.java +++ b/src/main/java/bio/overture/ego/token/user/UserJWTAccessToken.java @@ -14,25 +14,25 @@ * limitations under the License. */ -package org.overture.ego.token.user; +package bio.overture.ego.token.user; +import bio.overture.ego.service.TokenService; import io.jsonwebtoken.Claims; +import java.util.*; import lombok.Data; import lombok.extern.slf4j.Slf4j; import lombok.val; -import org.overture.ego.token.TokenService; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; -import java.util.*; - @Slf4j @Data public class UserJWTAccessToken implements OAuth2AccessToken { private Claims tokenClaims = null; - private String token= null; - public UserJWTAccessToken(String token, TokenService tokenService){ + private String token = null; + + public UserJWTAccessToken(String token, TokenService tokenService) { this.token = token; this.tokenClaims = tokenService.getTokenClaims(token); } @@ -40,13 +40,14 @@ public UserJWTAccessToken(String token, TokenService tokenService){ @Override public Map getAdditionalInformation() { val output = new HashMap(); - output.put("groups",getUser().get("groups")); + output.put("groups", getUser().get("groups")); return output; } @Override public Set getScope() { - return new HashSet(((ArrayList)getUser().get("roles"))); + val scope = ((UserTokenContext) tokenClaims.get("context")).getScope(); + return new HashSet<>(scope); } @Override @@ -61,7 +62,7 @@ public String getTokenType() { @Override public boolean isExpired() { - return getExpiresIn() <=0; + return getExpiresIn() <= 0; } @Override @@ -79,8 +80,7 @@ public String getValue() { return token; } - private Map getUser(){ - return (Map)((Map)tokenClaims.get("context")).get("user"); + private Map getUser() { + return (Map) ((Map) tokenClaims.get("context")).get("user"); } - } diff --git a/src/main/java/org/overture/ego/token/user/UserTokenClaims.java b/src/main/java/bio/overture/ego/token/user/UserTokenClaims.java similarity index 65% rename from src/main/java/org/overture/ego/token/user/UserTokenClaims.java rename to src/main/java/bio/overture/ego/token/user/UserTokenClaims.java index 2df65167e..388aaf2dd 100644 --- a/src/main/java/org/overture/ego/token/user/UserTokenClaims.java +++ b/src/main/java/bio/overture/ego/token/user/UserTokenClaims.java @@ -14,38 +14,42 @@ * limitations under the License. */ -package org.overture.ego.token.user; - +package bio.overture.ego.token.user; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.token.TokenClaims; +import bio.overture.ego.view.Views; import com.fasterxml.jackson.annotation.JsonView; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import lombok.Data; import lombok.NoArgsConstructor; import lombok.NonNull; -import org.overture.ego.token.TokenClaims; -import org.overture.ego.view.Views; import org.springframework.util.StringUtils; -import java.util.List; - - @Data @NoArgsConstructor @JsonView(Views.JWTAccessToken.class) public class UserTokenClaims extends TokenClaims { - @NonNull - private UserTokenContext context; + @NonNull private UserTokenContext context; - public String getSub(){ - if(StringUtils.isEmpty(sub)) { + public String getSub() { + if (StringUtils.isEmpty(sub)) { return String.valueOf(this.context.getUserInfo().getId()); } else { return sub; } } - public List getAud(){ - return this.context.getUserInfo().getApplications(); + public Set getScope() { + return this.context.getScope(); } + public List getAud() { + return this.context.getUserInfo().getApplications().stream() + .map(Application::getName) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/org/overture/ego/token/user/UserTokenContext.java b/src/main/java/bio/overture/ego/token/user/UserTokenContext.java similarity index 87% rename from src/main/java/org/overture/ego/token/user/UserTokenContext.java rename to src/main/java/bio/overture/ego/token/user/UserTokenContext.java index 0c542e1c3..0d0b69e7b 100644 --- a/src/main/java/org/overture/ego/token/user/UserTokenContext.java +++ b/src/main/java/bio/overture/ego/token/user/UserTokenContext.java @@ -14,17 +14,18 @@ * limitations under the License. */ -package org.overture.ego.token.user; +package bio.overture.ego.token.user; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.view.Views; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonView; +import java.util.Set; import lombok.Data; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.overture.ego.model.entity.User; -import org.overture.ego.view.Views; @Data @NoArgsConstructor @@ -32,8 +33,9 @@ @JsonInclude(JsonInclude.Include.ALWAYS) @JsonView(Views.JWTAccessToken.class) public class UserTokenContext { - @NonNull @JsonProperty("user") private User userInfo; + + private Set Scope; } diff --git a/src/main/java/bio/overture/ego/utils/CollectionUtils.java b/src/main/java/bio/overture/ego/utils/CollectionUtils.java new file mode 100644 index 000000000..d86cda61a --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/CollectionUtils.java @@ -0,0 +1,76 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.utils.Collectors.toImmutableList; +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static java.util.stream.IntStream.range; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import lombok.NonNull; +import lombok.val; + +public class CollectionUtils { + + public static Set mapToSet(Collection collection, Function mapper) { + return collection.stream().map(mapper).collect(toSet()); + } + + public static Set mapToImmutableSet(Collection collection, Function mapper) { + return collection.stream().map(mapper).collect(toImmutableSet()); + } + + public static List mapToList(Collection collection, Function mapper) { + return collection.stream().map(mapper).collect(toList()); + } + + public static Set findDuplicates(Collection collection) { + val exitingSet = Sets.newHashSet(); + val duplicateSet = Sets.newHashSet(); + collection.forEach( + x -> { + if (exitingSet.contains(x)) { + duplicateSet.add(x); + } else { + exitingSet.add(x); + } + }); + return duplicateSet; + } + + public static Set setOf(String... strings) { + return stream(strings).collect(toSet()); + } + + public static List listOf(String... strings) { + return asList(strings); + } + + public static Set difference(Collection left, Collection right) { + return Sets.difference(ImmutableSet.copyOf(left), ImmutableSet.copyOf(right)); + } + + public static Set intersection(Collection left, Collection right) { + return Sets.intersection(ImmutableSet.copyOf(left), ImmutableSet.copyOf(right)); + } + + public static List repeatedCallsOf(@NonNull Supplier callback, int numberOfCalls) { + return range(0, numberOfCalls).boxed().map(x -> callback.get()).collect(toImmutableList()); + } + + public static Set concatToSet(@NonNull Collection... collections) { + return stream(collections).flatMap(Collection::stream).collect(toImmutableSet()); + } + + public static List concatToList(@NonNull Collection... collections) { + return stream(collections).flatMap(Collection::stream).collect(toImmutableList()); + } +} diff --git a/src/main/java/bio/overture/ego/utils/Collectors.java b/src/main/java/bio/overture/ego/utils/Collectors.java new file mode 100644 index 000000000..de7cc49db --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/Collectors.java @@ -0,0 +1,52 @@ +package bio.overture.ego.utils; + +import static lombok.AccessLevel.PRIVATE; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collector; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@NoArgsConstructor(access = PRIVATE) +public class Collectors { + + public static Collector, ImmutableList> toImmutableList() { + return Collector.of( + ImmutableList.Builder::new, + ImmutableList.Builder::add, + (b1, b2) -> b1.addAll(b2.build()), + ImmutableList.Builder::build); + } + + public static Collector, ImmutableSet> toImmutableSet() { + return Collector.of( + ImmutableSet.Builder::new, + ImmutableSet.Builder::add, + (b1, b2) -> b1.addAll(b2.build()), + ImmutableSet.Builder::build); + } + + public static + Collector, ImmutableMap> toImmutableMap( + @NonNull Function keyMapper, + @NonNull Function valueMapper) { + + final BiConsumer, T> accumulator = + (builder, entry) -> builder.put(keyMapper.apply(entry), valueMapper.apply(entry)); + + return Collector.of( + ImmutableMap.Builder::new, + accumulator, + (b1, b2) -> b1.putAll(b2.build()), + ImmutableMap.Builder::build); + } + + public static Collector, ImmutableMap> toImmutableMap( + @NonNull Function keyMapper) { + return toImmutableMap(keyMapper, Function.identity()); + } +} diff --git a/src/main/java/bio/overture/ego/utils/Converters.java b/src/main/java/bio/overture/ego/utils/Converters.java new file mode 100644 index 000000000..3a8e57c41 --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/Converters.java @@ -0,0 +1,86 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.utils.Collectors.toImmutableList; +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Sets.newHashSet; +import static java.util.Objects.isNull; +import static lombok.AccessLevel.PRIVATE; + +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.Identifiable; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.join.GroupApplication; +import bio.overture.ego.model.join.GroupApplicationId; +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.model.join.UserGroupId; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.val; + +@NoArgsConstructor(access = PRIVATE) +public class Converters { + + public static List convertToUUIDList(Collection uuids) { + return uuids.stream().map(UUID::fromString).collect(toImmutableList()); + } + + public static Set convertToUUIDSet(Collection uuids) { + return uuids.stream().map(UUID::fromString).collect(toImmutableSet()); + } + + public static > Set convertToIds(Collection entities) { + return entities.stream().map(Identifiable::getId).collect(toImmutableSet()); + } + + public static List nullToEmptyList(List list) { + if (isNull(list)) { + return newArrayList(); + } else { + return list; + } + } + + public static Set nullToEmptySet(Set set) { + if (isNull(set)) { + return newHashSet(); + } else { + return set; + } + } + + public static Collection nullToEmptyCollection(Collection collection) { + if (isNull(collection)) { + return newHashSet(); + } else { + return collection; + } + } + + /** + * If {@param nullableValue} is non-null, then the {@param consumer} will accept it, otherwise, + * nothing. + */ + public static void nonNullAcceptor(V nullableValue, @NonNull Consumer consumer) { + if (!isNull(nullableValue)) { + consumer.accept(nullableValue); + } + } + + public static UserGroup convertToUserGroup(@NonNull User u, @NonNull Group g) { + val id = UserGroupId.builder().groupId(g.getId()).userId(u.getId()).build(); + return UserGroup.builder().id(id).user(u).group(g).build(); + } + + public static GroupApplication convertToGroupApplication( + @NonNull Group g, @NonNull Application a) { + val id = GroupApplicationId.builder().applicationId(a.getId()).groupId(g.getId()).build(); + return GroupApplication.builder().id(id).group(g).application(a).build(); + } +} diff --git a/src/main/java/bio/overture/ego/utils/Defaults.java b/src/main/java/bio/overture/ego/utils/Defaults.java new file mode 100644 index 000000000..a0e9cb7c9 --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/Defaults.java @@ -0,0 +1,20 @@ +package bio.overture.ego.utils; + +import lombok.SneakyThrows; + +public class Defaults { + T val; + + @SneakyThrows + Defaults(T value) { + val = value; + } + + static Defaults create(X value) { + return new Defaults<>(value); + } + + T def(T value) { + return value == null ? val : value; + } +} diff --git a/src/main/java/bio/overture/ego/utils/EntityServices.java b/src/main/java/bio/overture/ego/utils/EntityServices.java new file mode 100644 index 000000000..9a42b735d --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/EntityServices.java @@ -0,0 +1,64 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.model.exceptions.NotFoundException.checkNotFound; +import static bio.overture.ego.utils.CollectionUtils.difference; +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static bio.overture.ego.utils.Joiners.COMMA; +import static lombok.AccessLevel.PRIVATE; + +import bio.overture.ego.model.entity.Identifiable; +import bio.overture.ego.repository.BaseRepository; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.Set; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.val; + +@NoArgsConstructor(access = PRIVATE) +public class EntityServices { + + public static , ID> Set getManyEntities( + @NonNull Class entityType, + @NonNull BaseRepository repository, + @NonNull Collection ids) { + val entities = repository.findAllByIdIn(ImmutableList.copyOf(ids)); + + val requestedIds = ImmutableSet.copyOf(ids); + val existingIds = entities.stream().map(Identifiable::getId).collect(toImmutableSet()); + val nonExistingIds = difference(requestedIds, existingIds); + + checkNotFound( + nonExistingIds.isEmpty(), + "Entities of entityType '%s' were not found for the following ids: %s", + resolveEntityTypeName(entityType), + COMMA.join(nonExistingIds)); + return entities; + } + + public static , ID> void checkEntityExistence( + @NonNull Class entityType, + @NonNull BaseRepository repository, + @NonNull Collection ids) { + val missingIds = ids.stream().filter(x -> !repository.existsById(x)).collect(toImmutableSet()); + checkNotFound( + missingIds.isEmpty(), + "The following '%s' entity ids do no exist: %s", + resolveEntityTypeName(entityType), + COMMA.join(missingIds)); + } + + public static , ID> void checkEntityExistence( + @NonNull Class entityType, @NonNull BaseRepository repository, @NonNull ID id) { + checkNotFound( + repository.existsById(id), + "The '%s' entity with id '%s' does not exist", + resolveEntityTypeName(entityType), + id); + } + + private static String resolveEntityTypeName(Class entityType) { + return entityType.getSimpleName(); + } +} diff --git a/src/main/java/bio/overture/ego/utils/FieldUtils.java b/src/main/java/bio/overture/ego/utils/FieldUtils.java new file mode 100644 index 000000000..c7c6f009e --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/FieldUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. + */ + +package bio.overture.ego.utils; + +import static java.util.Objects.isNull; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FieldUtils { + + public static List getStaticFieldList(Class c) { + return Arrays.stream(c.getDeclaredFields()).map(f -> f).collect(Collectors.toList()); + } + + public static List getStaticFieldValueList(Class c) { + return Arrays.stream(c.getDeclaredFields()) + .map(f -> getFieldValue(f)) + .collect(Collectors.toList()); + } + + public static String getFieldValue(Field field) { + try { + return field.get(null).toString(); + } catch (IllegalAccessException ex) { + log.warn( + "Illegal access exception. Variable: {} is either private or non-static", + field.getName()); + return ""; + } + } + + /** + * returns true if the updated value is different than the original value, otherwise false. If the + * updated value is null, then it returns false + */ + public static boolean isUpdated(T originalValue, T updatedValue) { + return !isNull(updatedValue) && !updatedValue.equals(originalValue); + } + + public static void onUpdateDetected( + T originalValue, T updatedValue, @NonNull Runnable callback) { + if (isUpdated(originalValue, updatedValue)) { + callback.run(); + } + } +} diff --git a/src/main/java/bio/overture/ego/utils/HibernateSessions.java b/src/main/java/bio/overture/ego/utils/HibernateSessions.java new file mode 100644 index 000000000..fe6d67920 --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/HibernateSessions.java @@ -0,0 +1,28 @@ +package bio.overture.ego.utils; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.hibernate.collection.internal.AbstractPersistentCollection; + +@Slf4j +public class HibernateSessions { + + public static void unsetSession(@NonNull Set property) { + unsetSession((Collection) property); + } + + public static void unsetSession(@NonNull List property) { + unsetSession((Collection) property); + } + + public static void unsetSession(@NonNull Collection property) { + if (property instanceof AbstractPersistentCollection) { + val persistentProperty = (AbstractPersistentCollection) property; + persistentProperty.unsetSession(persistentProperty.getSession()); + } + } +} diff --git a/src/main/java/bio/overture/ego/utils/Ids.java b/src/main/java/bio/overture/ego/utils/Ids.java new file mode 100644 index 000000000..cd14535f4 --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/Ids.java @@ -0,0 +1,27 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.model.exceptions.MalformedRequestException.checkMalformedRequest; +import static bio.overture.ego.utils.CollectionUtils.findDuplicates; +import static bio.overture.ego.utils.Joiners.PRETTY_COMMA; +import static lombok.AccessLevel.PRIVATE; + +import bio.overture.ego.model.entity.Identifiable; +import java.util.Collection; +import java.util.UUID; +import lombok.NoArgsConstructor; +import lombok.val; + +@NoArgsConstructor(access = PRIVATE) +public class Ids { + + public static > void checkDuplicates( + Class entityType, Collection ids) { + // check duplicate ids + val duplicateIds = findDuplicates(ids); + checkMalformedRequest( + duplicateIds.isEmpty(), + "The following %s ids contain duplicates: [%s]", + entityType.getSimpleName(), + PRETTY_COMMA.join(duplicateIds)); + } +} diff --git a/src/main/java/bio/overture/ego/utils/Joiners.java b/src/main/java/bio/overture/ego/utils/Joiners.java new file mode 100644 index 000000000..0d0c7b356 --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/Joiners.java @@ -0,0 +1,16 @@ +package bio.overture.ego.utils; + +import static lombok.AccessLevel.PRIVATE; + +import com.google.common.base.Joiner; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class Joiners { + + public static final Joiner COMMA = Joiner.on(","); + public static final Joiner NEWLINE_COMMA = Joiner.on(",\n"); + public static final Joiner PRETTY_COMMA = Joiner.on(" , "); + public static final Joiner PATH = Joiner.on("/"); + public static final Joiner AMPERSAND = Joiner.on("&"); +} diff --git a/src/main/java/bio/overture/ego/utils/PermissionRequestAnalyzer.java b/src/main/java/bio/overture/ego/utils/PermissionRequestAnalyzer.java new file mode 100644 index 000000000..3643ab601 --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/PermissionRequestAnalyzer.java @@ -0,0 +1,129 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static bio.overture.ego.utils.Joiners.COMMA; +import static bio.overture.ego.utils.PermissionRequestAnalyzer.REQUEST_TYPE.DUPLICATE; +import static bio.overture.ego.utils.PermissionRequestAnalyzer.REQUEST_TYPE.NEW; +import static bio.overture.ego.utils.PermissionRequestAnalyzer.REQUEST_TYPE.UPDATE; +import static com.google.common.collect.Maps.newHashMap; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.lang.String.format; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.joining; +import static lombok.AccessLevel.PRIVATE; + +import bio.overture.ego.model.dto.PermissionRequest; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Value; +import lombok.val; + +@NoArgsConstructor(access = PRIVATE) +public class PermissionRequestAnalyzer { + + /** Constants */ + private static final List EMPTY_PERMISSION_REQUEST_LIST = ImmutableList.of(); + + public enum REQUEST_TYPE { + DUPLICATE, + NEW, + UPDATE; + } + + /** + * Analyzes permission requests by comparing the {@param rawPermissionRequests} to {@param + * existingPermissionRequests} and categorizes them based on their { @code REQUEST_TYPE } and + * packs it all into a { @code PermissionAnalysis }. + * + * @param existingPermissionRequests collection of PermissionRequests that already exist + * @param rawPermissionRequests collection of PermissionRequests to analyze against the existing + * ones + * @return PermissionAnalysis + */ + public static PermissionAnalysis analyze( + @NonNull Collection existingPermissionRequests, + @NonNull Collection rawPermissionRequests) { + val existingPermissionRequestIndex = + uniqueIndex(existingPermissionRequests, PermissionRequest::getPolicyId); + + val unresolvableRequestMap = filterUnresolvableRequests(rawPermissionRequests); + val typeMap = + rawPermissionRequests.stream() + .filter(x -> !unresolvableRequestMap.containsKey(x.getPolicyId())) + .collect(groupingBy(x -> resolvePermType(existingPermissionRequestIndex, x))); + + return PermissionAnalysis.builder() + .unresolvableMap(unresolvableRequestMap) + .duplicates(extractPermissionRequests(typeMap, DUPLICATE)) + .createables(extractPermissionRequests(typeMap, NEW)) + .updateables(extractPermissionRequests(typeMap, UPDATE)) + .build(); + } + + private static Set extractPermissionRequests( + Map> typeMap, REQUEST_TYPE permType) { + return ImmutableSet.copyOf(typeMap.getOrDefault(permType, EMPTY_PERMISSION_REQUEST_LIST)); + } + + private static REQUEST_TYPE resolvePermType( + Map existingPermissionRequestIndex, PermissionRequest r) { + if (existingPermissionRequestIndex.containsValue(r)) { + return DUPLICATE; + } else if (existingPermissionRequestIndex.containsKey(r.getPolicyId())) { + return UPDATE; + } else { + return NEW; + } + } + + private static Map> filterUnresolvableRequests( + @NonNull Collection rawPermissionRequests) { + val grouping = + rawPermissionRequests.stream().collect(groupingBy(PermissionRequest::getPolicyId)); + val unresolvableRequestMap = newHashMap(grouping); + grouping.values().stream() + // filter aggregates that have multiple permissions for the same policyID + .filter(x -> x.size() == 1) + .map(x -> x.get(0)) + .map(PermissionRequest::getPolicyId) + .forEach(unresolvableRequestMap::remove); + return ImmutableMap.copyOf(unresolvableRequestMap); + } + + @Value + @Builder + public static class PermissionAnalysis { + + private static final String SEP = " , "; + + @NonNull private final Map> unresolvableMap; + @NonNull private final Set duplicates; + @NonNull private final Set createables; + @NonNull private final Set updateables; + + public Optional summarizeUnresolvables() { + if (unresolvableMap.isEmpty()) { + return Optional.empty(); + } + return Optional.of( + unresolvableMap.entrySet().stream().map(this::createDescription).collect(joining(SEP))); + } + + private String createDescription(Map.Entry> entry) { + val policyId = entry.getKey(); + val unresolvablePermissionRequests = entry.getValue(); + val masks = mapToSet(unresolvablePermissionRequests, PermissionRequest::getMask); + return format("%s : [%s]", policyId, COMMA.join(masks)); + } + } +} diff --git a/src/main/java/bio/overture/ego/utils/PolicyPermissionUtils.java b/src/main/java/bio/overture/ego/utils/PolicyPermissionUtils.java new file mode 100644 index 000000000..b470b84de --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/PolicyPermissionUtils.java @@ -0,0 +1,21 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.utils.CollectionUtils.mapToList; + +import bio.overture.ego.model.entity.AbstractPermission; +import java.util.Collection; +import java.util.List; +import lombok.NonNull; + +public class PolicyPermissionUtils { + + public static String extractPermissionString(@NonNull AbstractPermission permission) { + return String.format( + "%s.%s", permission.getPolicy().getName(), permission.getAccessLevel().toString()); + } + + public static List extractPermissionStrings( + @NonNull Collection permissions) { + return mapToList(permissions, PolicyPermissionUtils::extractPermissionString); + } +} diff --git a/src/main/java/org/overture/ego/utils/QueryUtils.java b/src/main/java/bio/overture/ego/utils/QueryUtils.java similarity index 86% rename from src/main/java/org/overture/ego/utils/QueryUtils.java rename to src/main/java/bio/overture/ego/utils/QueryUtils.java index b35df8fa1..bec6e136d 100644 --- a/src/main/java/org/overture/ego/utils/QueryUtils.java +++ b/src/main/java/bio/overture/ego/utils/QueryUtils.java @@ -14,24 +14,22 @@ * limitations under the License. */ -package org.overture.ego.utils; +package bio.overture.ego.utils; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; - @Slf4j public class QueryUtils { - public static String prepareForQuery(String text){ + public static String prepareForQuery(String text) { String output = text; - if(StringUtils.isEmpty(output)){ - return ""; + if (StringUtils.isEmpty(output)) { + return ""; } if (!output.contains("%")) { output = "%" + output + "%"; } return output.toLowerCase(); } - } diff --git a/src/main/java/bio/overture/ego/utils/Splitters.java b/src/main/java/bio/overture/ego/utils/Splitters.java new file mode 100644 index 000000000..070609d88 --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/Splitters.java @@ -0,0 +1,13 @@ +package bio.overture.ego.utils; + +import static lombok.AccessLevel.PRIVATE; + +import com.google.common.base.Splitter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class Splitters { + + public static final Splitter COMMA_SPLITTER = Splitter.on(','); + public static final Splitter COLON_SPLITTER = Splitter.on(':'); +} diff --git a/src/main/java/bio/overture/ego/utils/Streams.java b/src/main/java/bio/overture/ego/utils/Streams.java new file mode 100644 index 000000000..7b9326f9d --- /dev/null +++ b/src/main/java/bio/overture/ego/utils/Streams.java @@ -0,0 +1,30 @@ +package bio.overture.ego.utils; + +import com.google.common.collect.ImmutableList; +import java.util.Iterator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import lombok.NonNull; + +public class Streams { + + public static Stream stream(@NonNull Iterator iterator) { + return stream(() -> iterator, false); + } + + public static Stream stream(@NonNull Iterable iterable) { + return stream(iterable, false); + } + + @SafeVarargs + public static Stream stream(@NonNull T... values) { + return ImmutableList.copyOf(values).stream(); + } + + /* + * Helpers + */ + private static Stream stream(Iterable iterable, boolean inParallel) { + return StreamSupport.stream(iterable.spliterator(), inParallel); + } +} diff --git a/src/main/java/org/overture/ego/utils/TypeUtils.java b/src/main/java/bio/overture/ego/utils/TypeUtils.java similarity index 85% rename from src/main/java/org/overture/ego/utils/TypeUtils.java rename to src/main/java/bio/overture/ego/utils/TypeUtils.java index faa852161..6122ae32a 100644 --- a/src/main/java/org/overture/ego/utils/TypeUtils.java +++ b/src/main/java/bio/overture/ego/utils/TypeUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.overture.ego.utils; +package bio.overture.ego.utils; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.MapperFeature; @@ -22,11 +22,10 @@ import lombok.SneakyThrows; import lombok.val; - - public class TypeUtils { @SneakyThrows - public static T convertToAnotherType(Object fromObject, Class tClass, Class serializationView){ + public static T convertToAnotherType( + Object fromObject, Class tClass, Class serializationView) { val mapper = new ObjectMapper(); mapper.configure(JsonGenerator.Feature.IGNORE_UNKNOWN, true); mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false); @@ -34,7 +33,7 @@ public static T convertToAnotherType(Object fromObject, Class tClass, Cl return mapper.readValue(serializedValue, tClass); } - public static T convertToAnotherType(Object fromObject, Class tClass){ + public static T convertToAnotherType(Object fromObject, Class tClass) { val mapper = new ObjectMapper(); mapper.configure(JsonGenerator.Feature.IGNORE_UNKNOWN, true); return mapper.convertValue(fromObject, tClass); diff --git a/src/main/java/org/overture/ego/view/Views.java b/src/main/java/bio/overture/ego/view/Views.java similarity index 89% rename from src/main/java/org/overture/ego/view/Views.java rename to src/main/java/bio/overture/ego/view/Views.java index 8ad5c2fbf..6cd1fe2ac 100644 --- a/src/main/java/org/overture/ego/view/Views.java +++ b/src/main/java/bio/overture/ego/view/Views.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package org.overture.ego.view; +package bio.overture.ego.view; public interface Views { - interface JWTAccessToken{}; - interface REST{}; + interface JWTAccessToken {}; + + interface REST {}; } diff --git a/src/main/java/db/migration/V1_1__complete_uuid_migration.java b/src/main/java/db/migration/V1_1__complete_uuid_migration.java index 3bec102ef..9a7a8d254 100644 --- a/src/main/java/db/migration/V1_1__complete_uuid_migration.java +++ b/src/main/java/db/migration/V1_1__complete_uuid_migration.java @@ -1,22 +1,22 @@ package db.migration; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import lombok.val; import org.flywaydb.core.api.migration.spring.SpringJdbcMigration; -import org.overture.ego.model.entity.Application; -import org.overture.ego.model.entity.Group; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; -import java.util.UUID; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - @Slf4j public class V1_1__complete_uuid_migration implements SpringJdbcMigration { public void migrate(JdbcTemplate jdbcTemplate) throws Exception { - log.info("Flyway java migration: V1_1__complete_uuid_migration running ******************************"); + log.info( + "Flyway java migration: V1_1__complete_uuid_migration running ******************************"); // Development tests for migration left in for future use potentially // This whole class can be refactored into a SQL based migration @@ -42,16 +42,22 @@ public void migrate(JdbcTemplate jdbcTemplate) throws Exception { jdbcTemplate.execute("ALTER TABLE USERAPPLICATION ADD appUuid UUID"); // Drop fk contrainsts - jdbcTemplate.execute("ALTER TABLE GROUPAPPLICATION DROP CONSTRAINT groupapplication_grpid_fkey"); - jdbcTemplate.execute("ALTER TABLE GROUPAPPLICATION DROP CONSTRAINT groupapplication_appid_fkey"); + jdbcTemplate.execute( + "ALTER TABLE GROUPAPPLICATION DROP CONSTRAINT groupapplication_grpid_fkey"); + jdbcTemplate.execute( + "ALTER TABLE GROUPAPPLICATION DROP CONSTRAINT groupapplication_appid_fkey"); jdbcTemplate.execute("ALTER TABLE USERGROUP DROP CONSTRAINT usergroup_grpid_fkey"); jdbcTemplate.execute("ALTER TABLE USERAPPLICATION DROP CONSTRAINT userapplication_appid_fkey"); // Update fk mapping columns for applications and groups - jdbcTemplate.execute("UPDATE GROUPAPPLICATION SET grpUuid = EGOGROUP.uuid FROM EGOGROUP WHERE EGOGROUP.id = GROUPAPPLICATION.grpId"); - jdbcTemplate.execute("UPDATE GROUPAPPLICATION SET appUuid = EGOAPPLICATION.uuid FROM EGOAPPLICATION WHERE EGOAPPLICATION.id = GROUPAPPLICATION.appId"); - jdbcTemplate.execute("UPDATE USERGROUP SET grpUuid = EGOGROUP.uuid FROM EGOGROUP WHERE EGOGROUP.id = USERGROUP.grpId"); - jdbcTemplate.execute("UPDATE USERAPPLICATION SET appUuid = EGOAPPLICATION.uuid FROM EGOAPPLICATION WHERE EGOAPPLICATION.id = USERAPPLICATION.appId"); + jdbcTemplate.execute( + "UPDATE GROUPAPPLICATION SET grpUuid = EGOGROUP.uuid FROM EGOGROUP WHERE EGOGROUP.id = GROUPAPPLICATION.grpId"); + jdbcTemplate.execute( + "UPDATE GROUPAPPLICATION SET appUuid = EGOAPPLICATION.uuid FROM EGOAPPLICATION WHERE EGOAPPLICATION.id = GROUPAPPLICATION.appId"); + jdbcTemplate.execute( + "UPDATE USERGROUP SET grpUuid = EGOGROUP.uuid FROM EGOGROUP WHERE EGOGROUP.id = USERGROUP.grpId"); + jdbcTemplate.execute( + "UPDATE USERAPPLICATION SET appUuid = EGOAPPLICATION.uuid FROM EGOAPPLICATION WHERE EGOAPPLICATION.id = USERAPPLICATION.appId"); // Clean up temporary columns for EGOAPPLICATION and re-add PK contraints jdbcTemplate.execute("ALTER TABLE EGOAPPLICATION DROP CONSTRAINT EGOAPPLICATION_pkey"); @@ -76,29 +82,42 @@ public void migrate(JdbcTemplate jdbcTemplate) throws Exception { jdbcTemplate.execute("ALTER TABLE USERGROUP RENAME COLUMN grpUuid TO grpId"); jdbcTemplate.execute("ALTER TABLE USERAPPLICATION RENAME COLUMN appUuid TO appId"); - jdbcTemplate.execute("ALTER TABLE GROUPAPPLICATION ADD FOREIGN KEY (grpId) REFERENCES EGOGROUP (id)"); - jdbcTemplate.execute("ALTER TABLE GROUPAPPLICATION ADD FOREIGN KEY (appId) REFERENCES EGOAPPLICATION (id)"); + jdbcTemplate.execute( + "ALTER TABLE GROUPAPPLICATION ADD FOREIGN KEY (grpId) REFERENCES EGOGROUP (id)"); + jdbcTemplate.execute( + "ALTER TABLE GROUPAPPLICATION ADD FOREIGN KEY (appId) REFERENCES EGOAPPLICATION (id)"); jdbcTemplate.execute("ALTER TABLE USERGROUP ADD FOREIGN KEY (grpId) REFERENCES EGOGROUP (id)"); - jdbcTemplate.execute("ALTER TABLE USERAPPLICATION ADD FOREIGN KEY (appId) REFERENCES EGOAPPLICATION (id)"); + jdbcTemplate.execute( + "ALTER TABLE USERAPPLICATION ADD FOREIGN KEY (appId) REFERENCES EGOAPPLICATION (id)"); // Test queries to ensure all is good (if flag set to true) if (runWithTest) { testUuidMigration(jdbcTemplate, userOneId, userTwoId); } - log.info("****************************** Flyway java migration: V1_1__complete_uuid_migration complete"); + log.info( + "****************************** Flyway java migration: V1_1__complete_uuid_migration complete"); } private void createTestData(JdbcTemplate jdbcTemplate, UUID userOneId, UUID userTwoId) { - jdbcTemplate.update("INSERT INTO EGOUSER (id, name, email, status) VALUES (?, 'userOne', 'userOne@email.com', 'Pending')", userOneId); - jdbcTemplate.update("INSERT INTO EGOUSER (id, name, email, status) VALUES (?, 'userTwo', 'userTwo@email.com', 'Pending')", userTwoId); - - jdbcTemplate.execute("INSERT INTO EGOAPPLICATION (id, name, clientid, clientsecret, status) VALUES (1, 'appOne', '123', '321', 'Pending')"); - jdbcTemplate.execute("INSERT INTO EGOAPPLICATION (id, name, clientid, clientsecret, status) VALUES (2, 'appTwo', '456', '654', 'Pending')"); - jdbcTemplate.execute("INSERT INTO EGOAPPLICATION (id, name, clientid, clientsecret, status) VALUES (3, 'appThree', '789', '987', 'Pending')"); - - jdbcTemplate.execute("INSERT INTO EGOGROUP (id, name, status) VALUES (1, 'groupOne', 'Pending')"); - jdbcTemplate.execute("INSERT INTO EGOGROUP (id, name, status) VALUES (2, 'groupTwo', 'Pending')"); + jdbcTemplate.update( + "INSERT INTO EGOUSER (id, name, email, status) VALUES (?, 'userOne', 'userOne@email.com', 'Pending')", + userOneId); + jdbcTemplate.update( + "INSERT INTO EGOUSER (id, name, email, status) VALUES (?, 'userTwo', 'userTwo@email.com', 'Pending')", + userTwoId); + + jdbcTemplate.execute( + "INSERT INTO EGOAPPLICATION (id, name, clientid, clientsecret, status) VALUES (1, 'appOne', '123', '321', 'Pending')"); + jdbcTemplate.execute( + "INSERT INTO EGOAPPLICATION (id, name, clientid, clientsecret, status) VALUES (2, 'appTwo', '456', '654', 'Pending')"); + jdbcTemplate.execute( + "INSERT INTO EGOAPPLICATION (id, name, clientid, clientsecret, status) VALUES (3, 'appThree', '789', '987', 'Pending')"); + + jdbcTemplate.execute( + "INSERT INTO EGOGROUP (id, name, status) VALUES (1, 'groupOne', 'Pending')"); + jdbcTemplate.execute( + "INSERT INTO EGOGROUP (id, name, status) VALUES (2, 'groupTwo', 'Pending')"); jdbcTemplate.update("INSERT INTO USERGROUP (userid, grpid) VALUES (?, 1)", userOneId); jdbcTemplate.update("INSERT INTO USERGROUP (userid, grpid) VALUES (?, 2)", userTwoId); @@ -112,8 +131,11 @@ private void createTestData(JdbcTemplate jdbcTemplate, UUID userOneId, UUID user } private void testUuidMigration(JdbcTemplate jdbcTemplate, UUID userOneId, UUID userTwoId) { - val egoGroups = jdbcTemplate.query("SELECT * FROM EGOGROUP", new BeanPropertyRowMapper(Group.class)); - val egoApplications = jdbcTemplate.query("SELECT * FROM EGOAPPLICATION", new BeanPropertyRowMapper(Application.class)); + val egoGroups = + jdbcTemplate.query("SELECT * FROM EGOGROUP", new BeanPropertyRowMapper(Group.class)); + val egoApplications = + jdbcTemplate.query( + "SELECT * FROM EGOAPPLICATION", new BeanPropertyRowMapper(Application.class)); val userGroups = jdbcTemplate.queryForList("SELECT * FROM USERGROUP"); val userApplications = jdbcTemplate.queryForList("SELECT * FROM USERAPPLICATION"); val groupApplications = jdbcTemplate.queryForList("SELECT * FROM GROUPAPPLICATION"); diff --git a/src/main/java/db/migration/V1_3__string_to_date.java b/src/main/java/db/migration/V1_3__string_to_date.java index 218af5e55..0146de4ca 100644 --- a/src/main/java/db/migration/V1_3__string_to_date.java +++ b/src/main/java/db/migration/V1_3__string_to_date.java @@ -1,64 +1,69 @@ package db.migration; +import static org.junit.Assert.assertTrue; + +import bio.overture.ego.model.entity.User; +import java.util.Date; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import lombok.val; import org.flywaydb.core.api.migration.spring.SpringJdbcMigration; -import org.overture.ego.model.entity.User; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; -import java.util.Date; -import java.util.UUID; - -import static org.junit.Assert.assertTrue; - @Slf4j public class V1_3__string_to_date implements SpringJdbcMigration { - @Override - public void migrate(JdbcTemplate jdbcTemplate) throws Exception { - log.info("Flyway java migration: V1_3__string_to_date running ******************************"); - - boolean runWithTest = false; - UUID userOneId = UUID.randomUUID(); - UUID userTwoId = UUID.randomUUID(); + @Override + public void migrate(JdbcTemplate jdbcTemplate) throws Exception { + log.info("Flyway java migration: V1_3__string_to_date running ******************************"); - if (runWithTest) { - createTestData(jdbcTemplate, userOneId, userTwoId); - } + boolean runWithTest = false; + UUID userOneId = UUID.randomUUID(); + UUID userTwoId = UUID.randomUUID(); - jdbcTemplate.execute("ALTER TABLE EGOUSER ALTER CREATEDAT DROP DEFAULT, ALTER CREATEDAT TYPE TIMESTAMP WITHOUT TIME ZONE USING DATE(CREATEDAT);"); + if (runWithTest) { + createTestData(jdbcTemplate, userOneId, userTwoId); + } - jdbcTemplate.execute("ALTER TABLE EGOUSER ALTER LASTLOGIN DROP DEFAULT, ALTER LASTLOGIN TYPE TIMESTAMP WITHOUT TIME ZONE USING DATE(LASTLOGIN);"); + jdbcTemplate.execute( + "ALTER TABLE EGOUSER ALTER CREATEDAT DROP DEFAULT, ALTER CREATEDAT TYPE TIMESTAMP WITHOUT TIME ZONE USING DATE(CREATEDAT);"); - if (runWithTest) { - testDateType(jdbcTemplate); - } + jdbcTemplate.execute( + "ALTER TABLE EGOUSER ALTER LASTLOGIN DROP DEFAULT, ALTER LASTLOGIN TYPE TIMESTAMP WITHOUT TIME ZONE USING DATE(LASTLOGIN);"); - log.info("****************************** Flyway java migration: V1_3__string_to_date complete"); + if (runWithTest) { + testDateType(jdbcTemplate); } - private void createTestData(JdbcTemplate jdbcTemplate, UUID userOneId, UUID userTwoId) { - jdbcTemplate.update("INSERT INTO EGOUSER (id, name, email, role, status, createdAt, lastlogin) " + - "VALUES (?, 'userOne', 'userOne@email.com', 'user', 'Pending', '2017-01-15 04:35:55', '2016-12-15 23:20:51')", userOneId); + log.info("****************************** Flyway java migration: V1_3__string_to_date complete"); + } - jdbcTemplate.update("INSERT INTO EGOUSER (id, name, email, role, status, createdAt, lastlogin) " + - "VALUES (?, 'userTwo', 'userTwo@email.com', 'user', 'Pending', '2017-04-05 05:05:50', '2017-06-16 02:44:19')", userTwoId); - } + private void createTestData(JdbcTemplate jdbcTemplate, UUID userOneId, UUID userTwoId) { + jdbcTemplate.update( + "INSERT INTO EGOUSER (id, name, email, role, status, createdAt, lastlogin) " + + "VALUES (?, 'userOne', 'userOne@email.com', 'user', 'Pending', '2017-01-15 04:35:55', '2016-12-15 23:20:51')", + userOneId); - private void testDateType(JdbcTemplate jdbcTemplate) { - val egoUsers = jdbcTemplate.query("SELECT * FROM EGOUSER", new BeanPropertyRowMapper(User.class)); + jdbcTemplate.update( + "INSERT INTO EGOUSER (id, name, email, role, status, createdAt, lastlogin) " + + "VALUES (?, 'userTwo', 'userTwo@email.com', 'user', 'Pending', '2017-04-05 05:05:50', '2017-06-16 02:44:19')", + userTwoId); + } - val createdAtOne = ((User) egoUsers.get(0)).getCreatedAt(); - val createdAtTwo = ((User) egoUsers.get(1)).getCreatedAt(); + private void testDateType(JdbcTemplate jdbcTemplate) { + val egoUsers = + jdbcTemplate.query("SELECT * FROM EGOUSER", new BeanPropertyRowMapper(User.class)); - val lastloginOne = ((User) egoUsers.get(0)).getLastLogin(); - val lastloginTwo = ((User) egoUsers.get(1)).getLastLogin(); + val createdAtOne = ((User) egoUsers.get(0)).getCreatedAt(); + val createdAtTwo = ((User) egoUsers.get(1)).getCreatedAt(); - assertTrue(createdAtOne instanceof Date); - assertTrue(createdAtTwo instanceof Date); - assertTrue(lastloginOne instanceof Date); - assertTrue(lastloginTwo instanceof Date); + val lastloginOne = ((User) egoUsers.get(0)).getLastLogin(); + val lastloginTwo = ((User) egoUsers.get(1)).getLastLogin(); - } + assertTrue(createdAtOne instanceof Date); + assertTrue(createdAtTwo instanceof Date); + assertTrue(lastloginOne instanceof Date); + assertTrue(lastloginTwo instanceof Date); + } } diff --git a/src/main/java/org/overture/ego/config/ReactorConfig.java b/src/main/java/org/overture/ego/config/ReactorConfig.java deleted file mode 100644 index 7bfee5fc7..000000000 --- a/src/main/java/org/overture/ego/config/ReactorConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.overture.ego.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import reactor.Environment; -import reactor.bus.EventBus; - - -@Configuration -public class ReactorConfig { - - @Bean - public Environment env() { - return Environment.initializeIfEmpty() - .assignErrorJournal(); - } - - @Bean - public EventBus createEventBus(Environment env) { - return EventBus.create(env, Environment.THREAD_POOL); - } - -} diff --git a/src/main/java/org/overture/ego/config/RequestLoggingFilterConfig.java b/src/main/java/org/overture/ego/config/RequestLoggingFilterConfig.java deleted file mode 100644 index 4ab9d7c51..000000000 --- a/src/main/java/org/overture/ego/config/RequestLoggingFilterConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.overture.ego.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.filter.CommonsRequestLoggingFilter; - -@Configuration -public class RequestLoggingFilterConfig { - - @Bean - public CommonsRequestLoggingFilter logFilter() { - CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); - filter.setIncludeQueryString(true); - filter.setIncludePayload(true); - filter.setMaxPayloadLength(10000); - filter.setIncludeHeaders(false); - filter.setIncludeClientInfo(true); - return filter; - } - -} diff --git a/src/main/java/org/overture/ego/config/SecureServerConfig.java b/src/main/java/org/overture/ego/config/SecureServerConfig.java deleted file mode 100644 index 1d2868840..000000000 --- a/src/main/java/org/overture/ego/config/SecureServerConfig.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.config; - -import lombok.SneakyThrows; -import org.overture.ego.security.AuthorizationManager; -import org.overture.ego.security.JWTAuthorizationFilter; -import org.overture.ego.security.SecureAuthorizationManager; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; - - - -@Configuration -@EnableWebSecurity -@Profile("auth") -public class SecureServerConfig extends WebSecurityConfigurerAdapter { - - /* - Constants - */ - private final String[] PUBLIC_ENDPOINTS = - new String[] {"/oauth/token","/oauth/google/token", "/oauth/facebook/token", "/oauth/token/public_key", - "/oauth/token/verify"}; - - @Autowired - private AuthenticationManager authenticationManager; - - @Bean - @SneakyThrows - public JWTAuthorizationFilter authorizationFilter() { - return new JWTAuthorizationFilter(authenticationManager,PUBLIC_ENDPOINTS); - } - - @Bean - public AuthorizationManager authorizationManager() { - return new SecureAuthorizationManager(); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.csrf().disable() - .authorizeRequests() - .antMatchers("/", "/oauth/**","/swagger**","/swagger-resources/**","/configuration/ui","/configuration/**","/v2/api**","/webjars/**").permitAll() - .anyRequest().authenticated().and().authorizeRequests() - .and() - .addFilterAfter(authorizationFilter(), BasicAuthenticationFilter.class) - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - } - -} diff --git a/src/main/java/org/overture/ego/config/SwaggerConfig.java b/src/main/java/org/overture/ego/config/SwaggerConfig.java deleted file mode 100644 index 00c975af4..000000000 --- a/src/main/java/org/overture/ego/config/SwaggerConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; - -@EnableSwagger2 -@Configuration -public class SwaggerConfig { - - @Bean - public Docket productApi() { - return new Docket(DocumentationType.SWAGGER_2) - .select() - .apis(RequestHandlerSelectors.basePackage("org.overture.ego.controller")) - .build() - .apiInfo(metaInfo()); - } - - private ApiInfo metaInfo() { - - return new ApiInfo( - "ego Service API", - "ego API Documentation", - "0.01", - "", - "", - "Apache License Version 2.0", - "" - ); - } - -} diff --git a/src/main/java/org/overture/ego/controller/ApplicationController.java b/src/main/java/org/overture/ego/controller/ApplicationController.java deleted file mode 100644 index d10888652..000000000 --- a/src/main/java/org/overture/ego/controller/ApplicationController.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.controller; - -import com.fasterxml.jackson.annotation.JsonView; -import io.swagger.annotations.ApiImplicitParam; -import io.swagger.annotations.ApiImplicitParams; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import lombok.extern.slf4j.Slf4j; -import org.overture.ego.model.dto.PageDTO; -import org.overture.ego.model.entity.Application; -import org.overture.ego.model.entity.Group; -import org.overture.ego.model.entity.User; -import org.overture.ego.model.exceptions.PostWithIdentifierException; -import org.overture.ego.model.search.Filters; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.security.AdminScoped; -import org.overture.ego.service.ApplicationService; -import org.overture.ego.service.GroupService; -import org.overture.ego.service.UserService; -import org.overture.ego.view.Views; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; -import springfox.documentation.annotations.ApiIgnore; - -import javax.persistence.EntityNotFoundException; -import javax.servlet.http.HttpServletRequest; -import java.util.List; - -@Slf4j -@RestController -@RequestMapping("/applications") -public class ApplicationController { - - @Autowired - private ApplicationService applicationService; - @Autowired - private GroupService groupService; - @Autowired - private UserService userService; - - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Number of results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC"), - @ApiImplicitParam(name = "status", dataType = "string", paramType = "query", - value = "Filter by status. " + - "You could also specify filters on any field of the entity being queried as " + - "query parameters in this format: name=something") - - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of Applications", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getApplicationsList( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @RequestParam(value = "query", required = false) String query, - @ApiIgnore @Filters List filters, - Pageable pageable) { - if(StringUtils.isEmpty(query)){ - return new PageDTO<>(applicationService.listApps(filters, pageable)); - } else { - return new PageDTO<>(applicationService.findApps(query, filters, pageable)); - } - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "New Application", response = Application.class), - @ApiResponse(code = 400, message = PostWithIdentifierException.reason, response = Application.class) - } - ) - public @ResponseBody - Application create( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @RequestBody(required = true) Application applicationInfo) { - if (applicationInfo.getId() != null) { - throw new PostWithIdentifierException(); - } - - return applicationService.create(applicationInfo); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Application Details", response = Application.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - Application get( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String applicationId) { - return applicationService.get(applicationId); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.PUT, value = "/{id}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Updated application info", response = Application.class) - } - ) - public @ResponseBody - Application updateApplication( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @RequestBody(required = true) Application updatedApplicationInfo) { - return applicationService.update(updatedApplicationInfo); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") - @ResponseStatus(value = HttpStatus.OK) - public void deleteApplication( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String applicationId) { - applicationService.delete(applicationId); - } - - /* - Users related endpoints - */ - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}/users") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Number of results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC"), - @ApiImplicitParam(name = "status", dataType = "string", paramType = "query", - value = "Filter by status. " + - "You could also specify filters on any field of the entity being queried as " + - "query parameters in this format: name=something") - - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of Users of group", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getApplicationUsers( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String appId, - @RequestParam(value = "query", required = false) String query, - @ApiIgnore @Filters List filters, - Pageable pageable) - { - if(StringUtils.isEmpty(query)){ - return new PageDTO<>(userService.findAppUsers(appId, filters, pageable)); - } else { - return new PageDTO<>(userService.findAppUsers(appId, query, filters, pageable)); - } - } - - /* - Groups related endpoints - */ - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}/groups") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Number of results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC"), - @ApiImplicitParam(name = "status", dataType = "string", paramType = "query", - value = "Filter by status. " + - "You could also specify filters on any field of the entity being queried as " + - "query parameters in this format: name=something") - - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of Applications of group", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getApplicationsGroups( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String appId, - @RequestParam(value = "query", required = false) String query, - @ApiIgnore @Filters List filters, - Pageable pageable) - { - if(StringUtils.isEmpty(query)) { - return new PageDTO<>(groupService.findApplicationGroups(appId, filters, pageable)); - } else { - return new PageDTO<>(groupService.findApplicationGroups(appId, query, filters, pageable)); - } - } - - @ExceptionHandler({ EntityNotFoundException.class }) - public ResponseEntity handleEntityNotFoundException(HttpServletRequest req, EntityNotFoundException ex) { - log.error("Application ID not found."); - return new ResponseEntity("Invalid Application ID provided.", new HttpHeaders(), - HttpStatus.BAD_REQUEST); - } - -} diff --git a/src/main/java/org/overture/ego/controller/AuthController.java b/src/main/java/org/overture/ego/controller/AuthController.java deleted file mode 100644 index 09120149a..000000000 --- a/src/main/java/org/overture/ego/controller/AuthController.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.controller; - -import lombok.AllArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.overture.ego.provider.facebook.FacebookTokenService; -import org.overture.ego.provider.google.GoogleTokenService; -import org.overture.ego.token.TokenService; -import org.overture.ego.token.signer.TokenSigner; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; - -import javax.servlet.http.HttpServletRequest; - -@Slf4j -@RestController -@RequestMapping("/oauth") -@AllArgsConstructor(onConstructor = @__({@Autowired})) -public class AuthController { - private TokenService tokenService; - private GoogleTokenService googleTokenService; - private FacebookTokenService facebookTokenService; - private TokenSigner tokenSigner; - - @RequestMapping(method = RequestMethod.GET, value = "/google/token") - @ResponseStatus(value = HttpStatus.OK) - @SneakyThrows - public @ResponseBody - String exchangeGoogleTokenForAuth( - @RequestHeader(value = "token", required = true) final String idToken) { - if (!googleTokenService.validToken(idToken)) - throw new InvalidTokenException("Invalid user token:" + idToken); - val authInfo = googleTokenService.decode(idToken); - return tokenService.generateUserToken(authInfo); - } - - @RequestMapping(method = RequestMethod.GET, value = "/facebook/token") - @ResponseStatus(value = HttpStatus.OK) - @SneakyThrows - public @ResponseBody - String exchangeFacebookTokenForAuth( - @RequestHeader(value = "token", required = true) final String idToken) { - if (!facebookTokenService.validToken(idToken)) - throw new InvalidTokenException("Invalid user token:" + idToken); - val authInfo = facebookTokenService.getAuthInfo(idToken); - if(authInfo.isPresent()) { - return tokenService.generateUserToken(authInfo.get()); - } else { - throw new InvalidTokenException("Unable to generate auth token for this user"); - } - } - - @RequestMapping(method = RequestMethod.GET, value = "/token/verify") - @ResponseStatus(value = HttpStatus.OK) - @SneakyThrows - public @ResponseBody - boolean verifyJWToken( - @RequestHeader(value = "token", required = true) final String token) { - if (StringUtils.isEmpty(token)) { - throw new InvalidTokenException("Token is empty"); - } - - if ( ! tokenService.validateToken(token) ) { - throw new InvalidTokenException("Token failed validation"); - } - return true; - } - - @RequestMapping(method = RequestMethod.GET, value = "/token/public_key") - @ResponseStatus(value = HttpStatus.OK) - public @ResponseBody - String getPublicKey() { - val pubKey = tokenSigner.getEncodedPublicKey(); - if(pubKey.isPresent()){ - return pubKey.get(); - } else { - return ""; - } - } - - @ExceptionHandler({ InvalidTokenException.class }) - public ResponseEntity handleInvalidTokenException(HttpServletRequest req, InvalidTokenException ex) { - log.error("ID Token not found."); - return new ResponseEntity("Invalid ID Token provided.", new HttpHeaders(), - HttpStatus.BAD_REQUEST); - } - -} diff --git a/src/main/java/org/overture/ego/controller/GroupController.java b/src/main/java/org/overture/ego/controller/GroupController.java deleted file mode 100644 index b9206c797..000000000 --- a/src/main/java/org/overture/ego/controller/GroupController.java +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.controller; - -import com.fasterxml.jackson.annotation.JsonView; -import io.swagger.annotations.ApiImplicitParam; -import io.swagger.annotations.ApiImplicitParams; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.overture.ego.model.dto.PageDTO; -import org.overture.ego.model.entity.GroupPermission; -import org.overture.ego.model.entity.Application; -import org.overture.ego.model.entity.Group; -import org.overture.ego.model.entity.User; -import org.overture.ego.model.exceptions.PostWithIdentifierException; -import org.overture.ego.model.params.Scope; -import org.overture.ego.model.search.Filters; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.security.AdminScoped; -import org.overture.ego.service.ApplicationService; -import org.overture.ego.service.GroupService; -import org.overture.ego.service.UserService; -import org.overture.ego.view.Views; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; -import springfox.documentation.annotations.ApiIgnore; - -import javax.persistence.EntityNotFoundException; -import javax.servlet.http.HttpServletRequest; -import java.util.List; - -@Slf4j -@RestController -@RequestMapping("/groups") -@AllArgsConstructor(onConstructor = @__({@Autowired})) -public class GroupController { - /** - * Dependencies - */ - private final GroupService groupService; - private final ApplicationService applicationService; - private final UserService userService; - - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Number of results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC"), - @ApiImplicitParam(name = "status", dataType = "string", paramType = "query", - value = "Filter by status. " + - "You could also specify filters on any field of the entity being queried as " + - "query parameters in this format: name=something") - - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of Groups", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getGroupsList( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @RequestParam(value = "query", required = false) String query, - @ApiIgnore @Filters List filters, - Pageable pageable) { - if(StringUtils.isEmpty(query)) { - return new PageDTO<>(groupService.listGroups(filters, pageable)); - } else { - return new PageDTO<>(groupService.findGroups(query, filters, pageable)); - } - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "New Group", response = Group.class), - @ApiResponse(code = 400, message = PostWithIdentifierException.reason, response = Group.class) - } - ) - public @ResponseBody - Group createGroup( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @RequestBody(required = true) Group groupInfo) { - if (groupInfo.getId() != null) { - throw new PostWithIdentifierException(); - } - return groupService.create(groupInfo); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Group Details", response = Group.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - Group getGroup( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String groupId) { - return groupService.get(groupId); - } - - - @AdminScoped - @RequestMapping(method = RequestMethod.PUT, value = "/{id}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Updated group info", response = Group.class) - } - ) - public @ResponseBody - Group updateGroup( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @RequestBody(required = true) Group updatedGroupInfo) { - return groupService.update(updatedGroupInfo); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") - @ResponseStatus(value = HttpStatus.OK) - public void deleteGroup( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String groupId) { - groupService.delete(groupId); - } - - /* - Permissions related endpoints - */ - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}/permissions") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC") - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of group permissions", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getScopes( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String id, - Pageable pageable) - { - return new PageDTO<>(groupService.getGroupPermissions(id, pageable)); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "/{id}/permissions") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Add group permissions", response = Group.class) - } - ) - public @ResponseBody - Group addPermissions( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String id, - @RequestBody(required = true) List permissions - ) { - return groupService.addGroupPermissions(id, permissions); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.DELETE, value = "/{id}/permissions/{permissionIds}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Delete group permissions") - } - ) - @ResponseStatus(value = HttpStatus.OK) - public void deletePermissions( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String id, - @PathVariable(value = "permissionIds", required = true) List permissionIds) { - groupService.deleteGroupPermissions(id,permissionIds); - } - - /* - Application related endpoints - */ - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}/applications") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Number of results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC"), - @ApiImplicitParam(name = "status", dataType = "string", paramType = "query", - value = "Filter by status. " + - "You could also specify filters on any field of the entity being queried as " + - "query parameters in this format: name=something") - - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of Applications of group", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getGroupsApplications( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String groupId, - @RequestParam(value = "query", required = false) String query, - @ApiIgnore @Filters List filters, - Pageable pageable) - { - if(StringUtils.isEmpty(query)) { - return new PageDTO<>(applicationService.findGroupApplications(groupId, filters, pageable)); - } else { - return new PageDTO<>(applicationService.findGroupApplications(groupId, query, filters, pageable)); - } - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "/{id}/applications") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Add Apps to Group", response = Group.class) - } - ) - public @ResponseBody - Group addAppsToGroups( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String grpId, - @RequestBody(required = true) List apps) { - return groupService.addAppsToGroup(grpId,apps); - } - - - @AdminScoped - @RequestMapping(method = RequestMethod.DELETE, value = "/{id}/applications/{appIDs}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Delete Apps from Group") - } - ) - @ResponseStatus(value = HttpStatus.OK) - public void deleteAppsFromGroup( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String grpId, - @PathVariable(value = "appIDs", required = true) List appIDs) { - groupService.deleteAppsFromGroup(grpId,appIDs); - } - - /* - User related endpoints - */ - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}/users") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Number of results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC"), - @ApiImplicitParam(name = "status", dataType = "string", paramType = "query", - value = "Filter by status. " + - "You could also specify filters on any field of the entity being queried as " + - "query parameters in this format: name=something") - - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of Users of group", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getGroupsUsers( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String groupId, - @RequestParam(value = "query", required = false) String query, - @ApiIgnore @Filters List filters, - Pageable pageable) - { - if(StringUtils.isEmpty(query)) { - return new PageDTO<>(userService.findGroupUsers(groupId, filters, pageable)); - } else { - return new PageDTO<>(userService.findGroupUsers(groupId, query, filters, pageable)); - } - } - - @ExceptionHandler({ EntityNotFoundException.class }) - public ResponseEntity handleEntityNotFoundException(HttpServletRequest req, EntityNotFoundException ex) { - log.error("Group ID not found."); - return new ResponseEntity("Invalid Group ID provided.", new HttpHeaders(), - HttpStatus.BAD_REQUEST); - } -} diff --git a/src/main/java/org/overture/ego/controller/PolicyController.java b/src/main/java/org/overture/ego/controller/PolicyController.java deleted file mode 100644 index 3dd65fdf4..000000000 --- a/src/main/java/org/overture/ego/controller/PolicyController.java +++ /dev/null @@ -1,175 +0,0 @@ -package org.overture.ego.controller; - -import com.fasterxml.jackson.annotation.JsonView; -import io.swagger.annotations.ApiImplicitParam; -import io.swagger.annotations.ApiImplicitParams; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.overture.ego.model.dto.PageDTO; -import org.overture.ego.model.entity.Policy; -import org.overture.ego.model.exceptions.PostWithIdentifierException; -import org.overture.ego.model.params.Scope; -import org.overture.ego.model.search.Filters; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.security.AdminScoped; -import org.overture.ego.service.GroupService; -import org.overture.ego.service.PolicyService; -import org.overture.ego.service.UserService; -import org.overture.ego.view.Views; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; -import springfox.documentation.annotations.ApiIgnore; - -import java.util.ArrayList; -import java.util.List; - -@Slf4j -@RestController -@RequestMapping("/policies") -public class PolicyController { - private final PolicyService policyService; - private final GroupService groupService; - private final UserService userService; - - @Autowired - public PolicyController(PolicyService policyService, GroupService groupService, UserService userService) { - this.policyService=policyService; - this.groupService=groupService; - this.userService=userService; - } - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Get policy by id", response = Policy.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - Policy get( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String applicationId) { - return policyService.get(applicationId); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Number of results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC"), - @ApiImplicitParam(name = "status", dataType = "string", paramType = "query", - value = "Filter by status. " + - "You could also specify filters on any field of the entity being queried as " + - "query parameters in this format: name=something") - - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of Policies", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getPolicies( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @ApiIgnore @Filters List filters, - Pageable pageable) { - return new PageDTO<>(policyService.listAclEntities(filters, pageable)); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "New ACL Entity", response = Policy.class), - @ApiResponse(code = 400, message = PostWithIdentifierException.reason, response = Policy.class) - } - ) - public @ResponseBody - Policy create( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @RequestBody(required = true) Policy policy) { - if (policy.getId() != null) { - throw new PostWithIdentifierException(); - } - return policyService.create(policy); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.PUT, value = "/{id}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Updated Scope", response = Policy.class) - } - ) - public @ResponseBody - Policy update( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @RequestBody(required = true) Policy updatedPolicy) { - return policyService.update(updatedPolicy); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") - @ResponseStatus(value = HttpStatus.OK) - public void delete( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String id) { - policyService.delete(id); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "/{id}/permission/group/{group_id}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Add user permission", response = String.class) - } - ) - public @ResponseBody - String createGroupPermission( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String id, - @PathVariable(value = "group_id", required = true) String groupId, - @RequestBody(required = true) String mask - ) { - val permission = new Scope(id, mask); - val list = new ArrayList(); - list.add(permission); - groupService.addGroupPermissions(groupId, list); - return "1 group permission added to ACL successfully"; - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "/{id}/permission/user/{user_id}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Add user permission", response = String.class) - } - ) - public @ResponseBody - String createUserPermission( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String id, - @PathVariable(value = "user_id", required = true) String userId, - @RequestBody(required = true) String mask - ) { - val permission = new Scope(id, mask); - val list = new ArrayList(); - list.add(permission); - userService.addUserPermissions(userId, list); - - return "1 user permission successfully added to ACL '" + id + "'"; - } - -} diff --git a/src/main/java/org/overture/ego/controller/UserController.java b/src/main/java/org/overture/ego/controller/UserController.java deleted file mode 100644 index be32dfeba..000000000 --- a/src/main/java/org/overture/ego/controller/UserController.java +++ /dev/null @@ -1,367 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.controller; - -import com.fasterxml.jackson.annotation.JsonView; -import io.swagger.annotations.*; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.overture.ego.model.dto.PageDTO; -import org.overture.ego.model.entity.Application; -import org.overture.ego.model.entity.Group; -import org.overture.ego.model.entity.User; -import org.overture.ego.model.entity.UserPermission; -import org.overture.ego.model.exceptions.PostWithIdentifierException; -import org.overture.ego.model.params.Scope; -import org.overture.ego.model.search.Filters; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.security.AdminScoped; -import org.overture.ego.service.ApplicationService; -import org.overture.ego.service.GroupService; -import org.overture.ego.service.UserService; -import org.overture.ego.view.Views; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; -import springfox.documentation.annotations.ApiIgnore; - -import javax.persistence.EntityNotFoundException; -import javax.servlet.http.HttpServletRequest; -import java.util.List; - -@Slf4j -@RestController -@RequestMapping("/users") -@AllArgsConstructor(onConstructor = @__({@Autowired})) -public class UserController { - /** - * Dependencies - */ - - private final UserService userService; - private final GroupService groupService; - private final ApplicationService applicationService; - - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC"), - @ApiImplicitParam(name = "status", dataType = "string", paramType = "query", - value = "Filter by status. " + - "You could also specify filters on any field of the entity being queried as " + - "query parameters in this format: name=something") - - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of Users", response = PageDTO.class) - } - ) - - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getUsersList( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @ApiParam(value="Query string compares to Users Name, Email, First Name, and Last Name fields.", required=false ) @RequestParam(value = "query", required = false) String query, - @ApiIgnore @Filters List filters, - Pageable pageable) - { - if(StringUtils.isEmpty(query)) { - return new PageDTO<>(userService.listUsers(filters, pageable)); - } else { - return new PageDTO<>(userService.findUsers(query, filters, pageable)); - } - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Create new user", response = User.class), - @ApiResponse(code = 400, message = PostWithIdentifierException.reason, response = User.class) - } - ) - public @ResponseBody - User create( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @RequestBody(required = true) User userInfo) { - if (userInfo.getId() != null) { - throw new PostWithIdentifierException(); - } - return userService.create(userInfo); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "User Details", response = User.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - User getUser( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String id) { - return userService.get(id); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.PUT, value = "/{id}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Updated user info", response = User.class) - } - ) - public @ResponseBody - User updateUser( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @RequestBody(required = true) User updatedUserInfo) { - return userService.update(updatedUserInfo); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") - @ResponseStatus(value = HttpStatus.OK) - public void deleteUser( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String userId) { - userService.delete(userId); - } - - /* - Permissions related endpoints - */ - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}/permissions") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC") - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of user permissions", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getPermissions( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String id, - Pageable pageable) - { - return new PageDTO<>(userService.getUserPermissions(id, pageable)); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "/{id}/permissions") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Add user permissions", response = User.class) - } - ) - public @ResponseBody - User addPermissions( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String id, - @RequestBody(required = true) List permissions - ) { - return userService.addUserPermissions(id, permissions); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.DELETE, value = "/{id}/permissions/{permissionIds}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Delete user permissions") - } - ) - @ResponseStatus(value = HttpStatus.OK) - public void deletePermissions( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String id, - @PathVariable(value = "permissionIds", required = true) List permissionIds) { - userService.deleteUserPermissions(id,permissionIds); - } - - /* - Groups related endpoints - */ - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}/groups") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC"), - @ApiImplicitParam(name = "status", dataType = "string", paramType = "query", - value = "Filter by status. " + - "You could also specify filters on any field of the entity being queried as " + - "query parameters in this format: name=something") - - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of Groups of user", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getUsersGroups( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String userId, - @RequestParam(value = "query", required = false) String query, - @ApiIgnore @Filters List filters, - Pageable pageable) - { - if(StringUtils.isEmpty(query)) { - return new PageDTO<>(groupService.findUserGroups(userId, filters, pageable)); - } else { - return new PageDTO<>(groupService.findUserGroups(userId, query, filters, pageable)); - } - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "/{id}/groups") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Add Groups to user", response = User.class) - } - ) - public @ResponseBody - User addGroupsToUser( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String userId, - @RequestBody(required = true) List groupIDs) { - - return userService.addUserToGroups(userId,groupIDs); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.DELETE, value = "/{id}/groups/{groupIDs}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Delete Groups from User") - } - ) - @ResponseStatus(value = HttpStatus.OK) - public void deleteGroupFromUser( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String userId, - @PathVariable(value = "groupIDs", required = true) List groupIDs) { - userService.deleteUserFromGroups(userId,groupIDs); - } - - /* - Applications related endpoints - */ - @AdminScoped - @RequestMapping(method = RequestMethod.GET, value = "/{id}/applications") - @ApiImplicitParams({ - @ApiImplicitParam(name = "limit", dataType = "string", paramType = "query", - value = "Results to retrieve"), - @ApiImplicitParam(name = "offset", dataType = "string", paramType = "query", - value = "Index of first result to retrieve"), - @ApiImplicitParam(name = "sort", dataType = "string", paramType = "query", - value = "Field to sort on"), - @ApiImplicitParam(name = "sortOrder", dataType = "string", paramType = "query", - value = "Sorting order: ASC|DESC. Default order: DESC"), - @ApiImplicitParam(name = "status", dataType = "string", paramType = "query", - value = "Filter by status. " + - "You could also specify filters on any field of the entity being queried as " + - "query parameters in this format: name=something") - - }) - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Page of apps of user", response = PageDTO.class) - } - ) - @JsonView(Views.REST.class) - public @ResponseBody - PageDTO getUsersApplications( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String userId, - @RequestParam(value = "query", required = false) String query, - @ApiIgnore @Filters List filters, - Pageable pageable) - { - if(StringUtils.isEmpty(query)) { - return new PageDTO<>(applicationService.findUserApps(userId, filters, pageable)); - } else { - return new PageDTO<>(applicationService.findUserApps(userId, query, filters, pageable)); - } - } - - @AdminScoped - @RequestMapping(method = RequestMethod.POST, value = "/{id}/applications") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Add Applications to user", response = User.class) - } - ) - public @ResponseBody - User addAppsToUser( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String userId, - @RequestBody(required = true) List appIDs) { - return userService.addUserToApps(userId,appIDs); - } - - @AdminScoped - @RequestMapping(method = RequestMethod.DELETE, value = "/{id}/applications/{appIDs}") - @ApiResponses( - value = { - @ApiResponse(code = 200, message = "Delete Applications from User") - } - ) - @ResponseStatus(value = HttpStatus.OK) - public void deleteAppFromUser( - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = true) final String accessToken, - @PathVariable(value = "id", required = true) String userId, - @PathVariable(value = "appIDs", required = true) List appIDs) { - userService.deleteUserFromApps(userId,appIDs); - } - - @ExceptionHandler({ EntityNotFoundException.class }) - public ResponseEntity handleEntityNotFoundException(HttpServletRequest req, EntityNotFoundException ex) { - log.error("User ID not found."); - return new ResponseEntity("Invalid User ID provided.", new HttpHeaders(), - HttpStatus.BAD_REQUEST); - } -} diff --git a/src/main/java/org/overture/ego/model/entity/Application.java b/src/main/java/org/overture/ego/model/entity/Application.java deleted file mode 100644 index 971e6e51e..000000000 --- a/src/main/java/org/overture/ego/model/entity/Application.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.model.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.annotation.JsonView; -import lombok.*; -import org.hibernate.annotations.Cascade; -import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.LazyCollection; -import org.hibernate.annotations.LazyCollectionOption; -import org.overture.ego.model.enums.Fields; -import org.overture.ego.view.Views; - -import javax.persistence.*; -import java.util.*; -import java.util.stream.Collectors; - -@Entity -@Table(name = "egoapplication") -@Data -@ToString(exclude={"wholeGroups","wholeUsers"}) -@JsonPropertyOrder({"id", "name", "clientId", "clientSecret", "redirectUri", "description", "status"}) -@JsonInclude(JsonInclude.Include.CUSTOM) -@EqualsAndHashCode(of={"id"}) -@NoArgsConstructor -@RequiredArgsConstructor -@JsonView(Views.REST.class) -public class Application { - - @Id - @Column(nullable = false, name = Fields.ID, updatable = false) - @GenericGenerator( - name = "application_uuid", - strategy = "org.hibernate.id.UUIDGenerator") - @GeneratedValue(generator = "application_uuid") - UUID id; - - @JsonView({Views.JWTAccessToken.class,Views.REST.class}) - @NonNull - @Column(nullable = false, name = Fields.NAME) - String name; - - @JsonView({Views.JWTAccessToken.class,Views.REST.class}) - @NonNull - @Column(nullable = false, name = Fields.CLIENTID) - String clientId; - - @NonNull - @Column(nullable = false, name = Fields.CLIENTSECRET) - String clientSecret; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @Column(name = Fields.REDIRECTURI) - String redirectUri; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @Column(name = Fields.DESCRIPTION) - String description; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @Column(name = Fields.STATUS) - String status; - - @ManyToMany() - @Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinTable(name = "groupapplication", joinColumns = { @JoinColumn(name = Fields.APPID_JOIN) }, - inverseJoinColumns = { @JoinColumn(name = Fields.GROUPID_JOIN) }) - @JsonIgnore - Set wholeGroups; - - @ManyToMany() - @Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinTable(name = "userapplication", joinColumns = {@JoinColumn(name = Fields.APPID_JOIN)}, - inverseJoinColumns = {@JoinColumn(name = Fields.USERID_JOIN)}) - @JsonIgnore - Set wholeUsers; - - @JsonIgnore - public HashSet getURISet(){ - val output = new HashSet(); - output.add(this.redirectUri); - return output; - } - - @JsonView(Views.JWTAccessToken.class) - public List getGroups(){ - if(this.wholeGroups == null) { - return new ArrayList(); - } - return this.wholeGroups.stream().map(g -> g.getName()).collect(Collectors.toList()); - } - - public void update(Application other) { - this.name = other.name; - this.clientId = other.clientId; - this.clientSecret = other.clientSecret; - this.redirectUri = other.redirectUri; - this.description = other.description; - this.status = other.status; - - // Do not update ID; - - // Update Users and Groups only if provided (not null) - if (other.wholeUsers != null) { - this.wholeUsers = other.wholeUsers; - } - - if (other.wholeGroups != null) { - this.wholeGroups = other.wholeGroups; - } - } - - -} diff --git a/src/main/java/org/overture/ego/model/entity/Group.java b/src/main/java/org/overture/ego/model/entity/Group.java deleted file mode 100644 index 930370e70..000000000 --- a/src/main/java/org/overture/ego/model/entity/Group.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.model.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.annotation.JsonView; -import lombok.*; -import org.hibernate.annotations.Cascade; -import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.LazyCollection; -import org.hibernate.annotations.LazyCollectionOption; -import org.overture.ego.model.enums.PolicyMask; -import org.overture.ego.model.enums.Fields; -import org.overture.ego.view.Views; - -import javax.persistence.*; -import java.util.*; - -@Data -@ToString(exclude={"wholeUsers","wholeApplications", "groupPermissions"}) -@Table(name = "egogroup") -@Entity -@JsonPropertyOrder({"id", "name", "description", "status","wholeApplications", "groupPermissions"}) -@JsonInclude(JsonInclude.Include.ALWAYS) -@EqualsAndHashCode(of={"id"}) -@NoArgsConstructor -@RequiredArgsConstructor -@JsonView(Views.REST.class) -public class Group implements PolicyOwner { - - @Id - @Column(nullable = false, name = Fields.ID, updatable = false) - @GenericGenerator( - name = "group_uuid", - strategy = "org.hibernate.id.UUIDGenerator") - @GeneratedValue(generator = "group_uuid") - UUID id; - - @Column(nullable = false, name = Fields.NAME, updatable = false) - @NonNull - String name; - - @Column(nullable = false, name = Fields.DESCRIPTION, updatable = false) - String description; - - @Column(nullable = false, name = Fields.STATUS, updatable = false) - String status; - - @ManyToMany(targetEntity = Application.class) - @Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinTable(name = "groupapplication", joinColumns = { @JoinColumn(name = Fields.GROUPID_JOIN) }, - inverseJoinColumns = { @JoinColumn(name = Fields.APPID_JOIN) }) - @JsonIgnore - Set wholeApplications; - - @ManyToMany() - @Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinTable(name = "usergroup", joinColumns = {@JoinColumn(name = Fields.GROUPID_JOIN)}, - inverseJoinColumns = {@JoinColumn(name = Fields.USERID_JOIN)}) - @JsonIgnore - Set wholeUsers; - - @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinColumn(name=Fields.OWNER) - @JsonIgnore - protected Set groupOwnedAclEntities; - - @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinColumn(name=Fields.SID) - @JsonIgnore - protected List groupPermissions; - - public void addApplication(@NonNull Application app){ - initApplications(); - this.wholeApplications.add(app); - } - - public void addUser(@NonNull User u){ - initUsers(); - this.wholeUsers.add(u); - } - - public void addNewPermission(@NonNull Policy policy, @NonNull PolicyMask mask) { - initPermissions(); - val permission = GroupPermission.builder() - .entity(policy) - .mask(mask) - .sid(this) - .build(); - this.groupPermissions.add(permission); - } - - public void removeApplication(@NonNull UUID appId){ - this.wholeApplications.removeIf(a -> a.id.equals(appId)); - } - - public void removeUser(@NonNull UUID userId){ - if(this.wholeUsers == null) return; - this.wholeUsers.removeIf(u -> u.id.equals(userId)); - } - - public void removePermission(@NonNull UUID permissionId) { - if (this.groupPermissions == null) return; - this.groupPermissions.removeIf(p -> p.id.equals(permissionId)); - } - - protected void initPermissions() { - if (this.groupPermissions == null) { - this.groupPermissions = new ArrayList(); - } - } - - public void update(Group other) { - this.name = other.name; - this.description = other.description; - this.status = other.status; - - // Do not update ID, that is programmatic. - - // Update Users and Applications only if provided (not null) - if (other.wholeApplications != null) { - this.wholeApplications = other.wholeApplications; - } - - if (other.wholeUsers != null) { - this.wholeUsers = other.wholeUsers; - } - } - - private void initApplications(){ - if(this.wholeApplications == null){ - this.wholeApplications = new HashSet<>(); - } - } - - private void initUsers(){ - if(this.wholeUsers == null) { - this.wholeUsers = new HashSet<>(); - } - } - - -} - diff --git a/src/main/java/org/overture/ego/model/entity/GroupPermission.java b/src/main/java/org/overture/ego/model/entity/GroupPermission.java deleted file mode 100644 index 2b59b5609..000000000 --- a/src/main/java/org/overture/ego/model/entity/GroupPermission.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.overture.ego.model.entity; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.annotation.JsonView; -import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType; -import lombok.*; -import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.Type; -import org.hibernate.annotations.TypeDef; -import org.overture.ego.model.enums.PolicyMask; -import org.overture.ego.model.enums.Fields; -import org.overture.ego.view.Views; - -import javax.persistence.*; -import java.util.UUID; - -@Entity -@Table(name = "aclgrouppermission") -@Data -@JsonPropertyOrder({"id","entity","sid", "mask"}) -@JsonInclude(JsonInclude.Include.ALWAYS) -@EqualsAndHashCode(of={"id"}) -@TypeDef( - name = "ego_acl_enum", - typeClass = PostgreSQLEnumType.class -) -@Builder -@AllArgsConstructor -@NoArgsConstructor -@JsonView(Views.REST.class) -public class GroupPermission extends Permission { - - @Id - @Column(nullable = false, name = Fields.ID, updatable = false) - @GenericGenerator( - name = "acl_user_group_uuid", - strategy = "org.hibernate.id.UUIDGenerator") - @GeneratedValue(generator = "acl_user_group_uuid") - UUID id; - - @NonNull - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = Fields.ENTITY) - Policy entity; - - @NonNull - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = Fields.SID) - Group sid; - - @NonNull - @Column(nullable = false, name = Fields.MASK) - @Enumerated(EnumType.STRING) - @Type( type = "ego_acl_enum" ) - PolicyMask mask; -} diff --git a/src/main/java/org/overture/ego/model/entity/Permission.java b/src/main/java/org/overture/ego/model/entity/Permission.java deleted file mode 100644 index d0a132be0..000000000 --- a/src/main/java/org/overture/ego/model/entity/Permission.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.overture.ego.model.entity; - -import lombok.Data; -import org.overture.ego.model.enums.PolicyMask; - -import java.util.UUID; - -@Data -public abstract class Permission { - UUID id; - Policy entity; - PolicyOwner sid; - PolicyMask mask; - - public void update(Permission other) { - this.entity = other.entity; - this.sid = other.sid; - this.mask = other.mask; - // Don't merge the ID - that is procedural. - } -} diff --git a/src/main/java/org/overture/ego/model/entity/Policy.java b/src/main/java/org/overture/ego/model/entity/Policy.java deleted file mode 100644 index 687693496..000000000 --- a/src/main/java/org/overture/ego/model/entity/Policy.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.overture.ego.model.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.annotation.JsonView; -import lombok.*; -import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.LazyCollection; -import org.hibernate.annotations.LazyCollectionOption; -import org.overture.ego.model.enums.Fields; -import org.overture.ego.view.Views; - -import javax.persistence.*; -import java.util.Set; -import java.util.UUID; - -@Entity -@Table(name = "aclentity") -@Data -@JsonPropertyOrder({"id","owner","name"}) -@JsonInclude(JsonInclude.Include.ALWAYS) -@EqualsAndHashCode(of={"id"}) -@Builder -@AllArgsConstructor -@NoArgsConstructor -@JsonView(Views.REST.class) -public class Policy { - - @Id - @Column(nullable = false, name = Fields.ID, updatable = false) - @GenericGenerator( - name = "acl_entity_uuid", - strategy = "org.hibernate.id.UUIDGenerator") - @GeneratedValue(generator = "acl_entity_uuid") - UUID id; - - @NonNull - @Column(nullable = false, name = Fields.OWNER) - UUID owner; - - @NonNull - @Column(nullable = false, name = Fields.NAME, unique = true) - String name; - - @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinColumn(name=Fields.ENTITY) - @JsonIgnore - protected Set groupPermissions; - - @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinColumn(name=Fields.ENTITY) - @JsonIgnore - protected Set userPermissions; - - public void update(Policy other) { - this.owner = other.owner; - this.name = other.name; - - // Don't merge the ID - that is procedural. - - // Don't merge groupPermissions or userPermissions if not present in other. - // This is because the PUT action for update usually does not include these fields - // as a consequence of the GET option to retrieve a aclEntity not including these fields - // To clear groupPermissions and userPermissions, use the dedicated services for deleting - // associations or pass in an empty Set. - if (other.groupPermissions != null) { - this.groupPermissions = other.groupPermissions; - } - - if (other.userPermissions != null) { - this.userPermissions = other.userPermissions; - } - } - -} diff --git a/src/main/java/org/overture/ego/model/entity/PolicyOwner.java b/src/main/java/org/overture/ego/model/entity/PolicyOwner.java deleted file mode 100644 index 627c080db..000000000 --- a/src/main/java/org/overture/ego/model/entity/PolicyOwner.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.overture.ego.model.entity; - -public interface PolicyOwner { -} diff --git a/src/main/java/org/overture/ego/model/entity/User.java b/src/main/java/org/overture/ego/model/entity/User.java deleted file mode 100644 index 9a645e57b..000000000 --- a/src/main/java/org/overture/ego/model/entity/User.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.model.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.annotation.JsonView; -import lombok.*; -import org.hibernate.annotations.Cascade; -import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.LazyCollection; -import org.hibernate.annotations.LazyCollectionOption; -import org.overture.ego.model.enums.PolicyMask; -import org.overture.ego.model.enums.Fields; -import org.overture.ego.view.Views; - -import javax.persistence.*; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.overture.ego.utils.AclPermissionUtils.extractPermissionStrings; - -@Entity -@Table(name = "egouser") -@Data -@ToString(exclude = {"wholeGroups", "wholeApplications", "userPermissions"}) -@JsonPropertyOrder({"id", "name", "email", "role", "status", "wholeGroups", - "wholeApplications", "userPermissions", "firstName", "lastName", "createdAt", "lastLogin", "preferredLanguage"}) -@JsonInclude(JsonInclude.Include.ALWAYS) -@EqualsAndHashCode(of = {"id"}) -@Builder -@AllArgsConstructor -@NoArgsConstructor -@JsonView(Views.REST.class) -public class User implements PolicyOwner { - - @Id - @Column(nullable = false, name = Fields.ID, updatable = false) - @GenericGenerator( - name = "user_uuid", - strategy = "org.hibernate.id.UUIDGenerator") - @GeneratedValue(generator = "user_uuid") - UUID id; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @NonNull - @Column(nullable = false, name = Fields.NAME, unique = true) - String name; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @NonNull - @Column(nullable = false, name = Fields.EMAIL, unique = true) - String email; - - @NonNull - @Column(nullable = false, name = Fields.ROLE) - String role; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @Column(name = Fields.STATUS) - String status; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @Column(name = Fields.FIRSTNAME) - String firstName; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @Column(name = Fields.LASTNAME) - String lastName; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @Column(name = Fields.CREATEDAT) - Date createdAt; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @Column(name = Fields.LASTLOGIN) - Date lastLogin; - - @JsonView({Views.JWTAccessToken.class, Views.REST.class}) - @Column(name = Fields.PREFERREDLANGUAGE) - String preferredLanguage; - - @ManyToMany(targetEntity = Group.class) - @Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinTable(name = "usergroup", joinColumns = {@JoinColumn(name = Fields.USERID_JOIN)}, - inverseJoinColumns = {@JoinColumn(name = Fields.GROUPID_JOIN)}) - @JsonIgnore - protected Set wholeGroups; - - @ManyToMany(targetEntity = Application.class) - @Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinTable(name = "userapplication", joinColumns = {@JoinColumn(name = Fields.USERID_JOIN)}, - inverseJoinColumns = {@JoinColumn(name = Fields.APPID_JOIN)}) - @JsonIgnore - protected Set wholeApplications; - - @OneToMany(cascade = CascadeType.ALL) - @LazyCollection(LazyCollectionOption.FALSE) - @JoinColumn(name = Fields.SID) - @JsonIgnore - protected List userPermissions; - - // Creates groups in JWTAccessToken::context::user - @JsonView(Views.JWTAccessToken.class) - public List getGroups() { - if (this.wholeGroups == null) { - return new ArrayList(); - } - return this.wholeGroups.stream().map(g -> g.getName()).collect(Collectors.toList()); - } - - // Creates permissions in JWTAccessToken::context::user - @JsonView(Views.JWTAccessToken.class) - public List getPermissions() { - - // Get user's individual permission (stream) - val userPermissions = Optional.ofNullable(this.getUserPermissions()) - .orElse(new ArrayList<>()) - .stream(); - - // Get permissions from the user's groups (stream) - val userGroupsPermissions = Optional.ofNullable(this.getWholeGroups()) - .orElse(new HashSet<>()) - .stream() - .map(Group::getGroupPermissions) - .flatMap(List::stream); - - // Combine individual user permissions and the user's - // groups (if they have any) permissions - val combinedPermissions = Stream.concat(userPermissions, userGroupsPermissions) - .collect(Collectors.groupingBy(Permission::getEntity)); - - // If we have no permissions at all return an empty list - if (combinedPermissions.values().size() == 0) { - return new ArrayList<>(); - } - - // If we do have permissions ... sort the grouped permissions (by Scope) - // on PolicyMask, extracting the first value of the sorted list into the final - // permissions list - List finalPermissionsList = new ArrayList<>(); - - combinedPermissions.forEach((entity, permissions) -> { - permissions.sort(Comparator.comparing(Permission::getMask).reversed()); - finalPermissionsList.add(permissions.get(0)); - }); - - // Convert final permissions list for JSON output - return extractPermissionStrings(finalPermissionsList); - } - - @JsonIgnore - public List getApplications() { - if (this.wholeApplications == null) { - return new ArrayList(); - } - return this.wholeApplications.stream().map(a -> a.getName()).collect(Collectors.toList()); - } - - @JsonView(Views.JWTAccessToken.class) - public List getRoles() { - return Arrays.asList(this.getRole()); - } - - /* - Roles is an array only in JWT but a String in Database. - This is done for future compatibility - at the moment ego only needs one Role but this may change - as project progresses. - Currently, using the only role by extracting first role in the array - */ - public void setRoles(@NonNull List roles) { - if (roles.size() > 0) - this.role = roles.get(0); - } - - public void addNewApplication(@NonNull Application app) { - initApplications(); - this.wholeApplications.add(app); - } - - public void addNewGroup(@NonNull Group g) { - initGroups(); - this.wholeGroups.add(g); - } - - public void addNewPermission(@NonNull Policy policy, @NonNull PolicyMask mask) { - initPermissions(); - val permission = UserPermission.builder() - .entity(policy) - .mask(mask) - .sid(this) - .build(); - this.userPermissions.add(permission); - } - - public void removeApplication(@NonNull UUID appId) { - if (this.wholeApplications == null) return; - this.wholeApplications.removeIf(a -> a.id.equals(appId)); - } - - public void removeGroup(@NonNull UUID grpId) { - if (this.wholeGroups == null) return; - this.wholeGroups.removeIf(g -> g.id.equals(grpId)); - } - - public void removePermission(@NonNull UUID permissionId) { - if (this.userPermissions == null) return; - this.userPermissions.removeIf(p -> p.id.equals(permissionId)); - } - - protected void initApplications() { - if (this.wholeApplications == null) { - this.wholeApplications = new HashSet(); - } - } - - protected void initGroups() { - if (this.wholeGroups == null) { - this.wholeGroups = new HashSet(); - } - } - - protected void initPermissions() { - if (this.userPermissions == null) { - this.userPermissions = new ArrayList(); - } - } - - public void update(User other) { - this.name = other.name; - this.firstName = other.firstName; - this.lastName = other.lastName; - this.role = other.role; - this.status = other.status; - this.preferredLanguage = other.preferredLanguage; - - // Don't merge the ID, CreatedAt, or LastLogin date - those are procedural. - - // Don't merge wholeGroups, wholeApplications or userPermissions if not present in other - // This is because the PUT action for update usually does not include these fields - // as a consequence of the GET option to retrieve a user not including these fields - // To clear wholeApplications, wholeGroups or userPermissions, use the dedicated services - // for deleting associations or pass in an empty Set. - if (other.wholeApplications != null) { - this.wholeApplications = other.wholeApplications; - } - - if (other.wholeGroups != null) { - this.wholeGroups = other.wholeGroups; - } - - if (other.userPermissions != null) { - this.userPermissions = other.userPermissions; - } - } - -} diff --git a/src/main/java/org/overture/ego/model/entity/UserPermission.java b/src/main/java/org/overture/ego/model/entity/UserPermission.java deleted file mode 100644 index 4d980861e..000000000 --- a/src/main/java/org/overture/ego/model/entity/UserPermission.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.overture.ego.model.entity; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.annotation.JsonView; -import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType; -import lombok.*; -import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.Type; -import org.hibernate.annotations.TypeDef; -import org.overture.ego.model.enums.PolicyMask; -import org.overture.ego.model.enums.Fields; -import org.overture.ego.view.Views; - -import javax.persistence.*; -import java.util.UUID; - -@Entity -@Table(name = "acluserpermission") -@Data -@JsonPropertyOrder({"id","entity","sid","mask"}) -@JsonInclude(JsonInclude.Include.ALWAYS) -@EqualsAndHashCode(of={"id"}) -@TypeDef( - name = "ego_acl_enum", - typeClass = PostgreSQLEnumType.class -) -@Builder -@AllArgsConstructor -@NoArgsConstructor -@JsonView(Views.REST.class) -public class UserPermission extends Permission { - - @Id - @Column(nullable = false, name = Fields.ID, updatable = false) - @GenericGenerator( - name = "acl_user_permission_uuid", - strategy = "org.hibernate.id.UUIDGenerator") - @GeneratedValue(generator = "acl_user_permission_uuid") - UUID id; - - @NonNull - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = Fields.ENTITY) - Policy entity; - - @NonNull - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = Fields.SID) - User sid; - - @NonNull - @Column(nullable = false, name = Fields.MASK) - @Enumerated(EnumType.STRING) - @Type( type = "ego_acl_enum" ) - PolicyMask mask; -} diff --git a/src/main/java/org/overture/ego/model/enums/Fields.java b/src/main/java/org/overture/ego/model/enums/Fields.java deleted file mode 100644 index 4ca48e521..000000000 --- a/src/main/java/org/overture/ego/model/enums/Fields.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.model.enums; - -public class Fields { - - public static final String ID = "id"; - public static final String NAME = "name"; - public static final String EMAIL = "email"; - public static final String ROLE = "role"; - public static final String STATUS = "status"; - public static final String FIRSTNAME = "firstname"; - public static final String LASTNAME = "lastname"; - public static final String CREATEDAT = "createdat"; - public static final String LASTLOGIN = "lastlogin"; - public static final String PREFERREDLANGUAGE = "preferredlanguage"; - public static final String DESCRIPTION = "description"; - public static final String CLIENTID = "clientid"; - public static final String CLIENTSECRET = "clientsecret"; - public static final String REDIRECTURI = "redirecturi"; - public static final String USERID_JOIN = "userid"; - public static final String GROUPID_JOIN = "grpid"; - public static final String APPID_JOIN = "appid"; - public static final String OWNER = "owner"; - public static final String ENTITY = "entity"; - public static final String SID = "sid"; - public static final String MASK = "mask"; - -} diff --git a/src/main/java/org/overture/ego/model/enums/PolicyMask.java b/src/main/java/org/overture/ego/model/enums/PolicyMask.java deleted file mode 100644 index d55cbdfc1..000000000 --- a/src/main/java/org/overture/ego/model/enums/PolicyMask.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2018. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.model.enums; - -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.val; - -import java.util.Arrays; - -@RequiredArgsConstructor -public enum PolicyMask { - READ("READ"), - WRITE("WRITE"), - DENY("DENY"); - - @NonNull - private final String value; - - @Override - public String toString() { - return value; - } - - public static PolicyMask fromValue(String value) { - for (val aclMask : values()) { - if (aclMask.value.equalsIgnoreCase(value)) { - return aclMask; - } - } - throw new IllegalArgumentException( - "Unknown enum type " + value + ", Allowed values are " + Arrays.toString(values())); - } -} diff --git a/src/main/java/org/overture/ego/model/exceptions/PostWithIdentifierException.java b/src/main/java/org/overture/ego/model/exceptions/PostWithIdentifierException.java deleted file mode 100644 index 6816d7b16..000000000 --- a/src/main/java/org/overture/ego/model/exceptions/PostWithIdentifierException.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.overture.ego.model.exceptions; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value= HttpStatus.BAD_REQUEST, reason= PostWithIdentifierException.reason) -public class PostWithIdentifierException extends RuntimeException { - public static final String reason="Create requests must not include the 'id' field."; -} diff --git a/src/main/java/org/overture/ego/model/params/Scope.java b/src/main/java/org/overture/ego/model/params/Scope.java deleted file mode 100644 index 6bfde204a..000000000 --- a/src/main/java/org/overture/ego/model/params/Scope.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.overture.ego.model.params; - -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; - -@Data -@NoArgsConstructor -@RequiredArgsConstructor -public class Scope { - @NonNull - private String aclEntityId; - @NonNull - private String mask; -} diff --git a/src/main/java/org/overture/ego/reactor/events/UserEvents.java b/src/main/java/org/overture/ego/reactor/events/UserEvents.java deleted file mode 100644 index 1dbcc6c2b..000000000 --- a/src/main/java/org/overture/ego/reactor/events/UserEvents.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.overture.ego.reactor.events; - -import org.overture.ego.model.entity.User; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import reactor.bus.Event; -import reactor.bus.EventBus; - -@Service -public class UserEvents { - - // EVENT NAMES - public static String UPDATE = UserEvents.class.getName() + ".UPDATE"; - - @Autowired - private EventBus eventBus; - - public void update(User user) { - eventBus.notify( - UserEvents.UPDATE, - Event.wrap(user) - ); - } - -} diff --git a/src/main/java/org/overture/ego/reactor/receiver/UserReceiver.java b/src/main/java/org/overture/ego/reactor/receiver/UserReceiver.java deleted file mode 100644 index f5224d540..000000000 --- a/src/main/java/org/overture/ego/reactor/receiver/UserReceiver.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.overture.ego.reactor.receiver; - -import lombok.extern.slf4j.Slf4j; -import org.overture.ego.model.entity.User; -import org.overture.ego.reactor.events.UserEvents; -import org.overture.ego.service.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import reactor.bus.Event; -import reactor.bus.EventBus; -import reactor.bus.selector.Selectors; -import reactor.fn.Consumer; - -import javax.annotation.PostConstruct; - - -@Component -@Slf4j -public class UserReceiver { - - @Autowired - private EventBus eventBus; - @Autowired - private UserService userService; - - @PostConstruct - public void onStartUp() { - // Initialize Reactor Listeners - // ============================ - - // UPDATE - eventBus.on( - Selectors.R(UserEvents.UPDATE), - update() - ); - } - - private Consumer> update() { - return (updateEvent) -> { - log.debug("Update event received: " + updateEvent.getData()); - try { - User data = (User) updateEvent.getData(); - userService.update(data); - } catch (ClassCastException e) { - log.error("Update event received incompatible data type.", e); - } - }; - } - -} diff --git a/src/main/java/org/overture/ego/repository/AclEntityRepository.java b/src/main/java/org/overture/ego/repository/AclEntityRepository.java deleted file mode 100644 index 20f8b7597..000000000 --- a/src/main/java/org/overture/ego/repository/AclEntityRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.overture.ego.repository; - -import org.overture.ego.model.entity.Policy; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.repository.PagingAndSortingRepository; - -import java.util.UUID; - -public interface AclEntityRepository - extends PagingAndSortingRepository, JpaSpecificationExecutor { - - Policy findOneByNameIgnoreCase(String name); -} diff --git a/src/main/java/org/overture/ego/repository/AclGroupPermissionRepository.java b/src/main/java/org/overture/ego/repository/AclGroupPermissionRepository.java deleted file mode 100644 index 946bb84d2..000000000 --- a/src/main/java/org/overture/ego/repository/AclGroupPermissionRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.overture.ego.repository; - -import org.overture.ego.model.entity.GroupPermission; - -public interface AclGroupPermissionRepository - extends PermissionRepository { -} diff --git a/src/main/java/org/overture/ego/repository/AclUserPermissionRepository.java b/src/main/java/org/overture/ego/repository/AclUserPermissionRepository.java deleted file mode 100644 index 9acb79f31..000000000 --- a/src/main/java/org/overture/ego/repository/AclUserPermissionRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.overture.ego.repository; - -import org.overture.ego.model.entity.UserPermission; - -public interface AclUserPermissionRepository - extends PermissionRepository { -} diff --git a/src/main/java/org/overture/ego/repository/ApplicationRepository.java b/src/main/java/org/overture/ego/repository/ApplicationRepository.java deleted file mode 100644 index 8c540ee4c..000000000 --- a/src/main/java/org/overture/ego/repository/ApplicationRepository.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.repository; - -import org.overture.ego.model.entity.Application; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.repository.PagingAndSortingRepository; - -import java.util.UUID; - - -public interface ApplicationRepository - extends PagingAndSortingRepository, JpaSpecificationExecutor { - - Application findOneByClientIdIgnoreCase(String clientId); - Application findOneByNameIgnoreCase(String name); - Application findOneByName(String name); - Page findAllByStatusIgnoreCase(String status, Pageable pageable); - -} diff --git a/src/main/java/org/overture/ego/repository/PermissionRepository.java b/src/main/java/org/overture/ego/repository/PermissionRepository.java deleted file mode 100644 index 5769290c2..000000000 --- a/src/main/java/org/overture/ego/repository/PermissionRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.overture.ego.repository; - -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.repository.NoRepositoryBean; -import org.springframework.data.repository.PagingAndSortingRepository; - -import java.util.UUID; - -@NoRepositoryBean -public interface PermissionRepository extends PagingAndSortingRepository, JpaSpecificationExecutor { -} diff --git a/src/main/java/org/overture/ego/repository/queryspecification/ApplicationSpecification.java b/src/main/java/org/overture/ego/repository/queryspecification/ApplicationSpecification.java deleted file mode 100644 index 13dd648b9..000000000 --- a/src/main/java/org/overture/ego/repository/queryspecification/ApplicationSpecification.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.repository.queryspecification; - -import lombok.val; -import org.overture.ego.model.entity.Application; -import org.overture.ego.model.entity.Group; -import org.overture.ego.model.entity.User; -import org.overture.ego.utils.QueryUtils; -import org.springframework.data.jpa.domain.Specification; - -import javax.annotation.Nonnull; -import javax.persistence.criteria.Join; -import java.util.UUID; - -public class ApplicationSpecification extends SpecificationBase { - public static Specification containsText(@Nonnull String text) { - val finalText = QueryUtils.prepareForQuery(text); - return (root, query, builder) -> - builder.or(getQueryPredicates(builder,root,finalText, - "name","clientId","clientSecret","description","status") - ); - } - - public static Specification inGroup(@Nonnull UUID groupId) { - return (root, query, builder) -> - { - Join groupJoin = root.join("wholeGroups"); - return builder.equal(groupJoin. get("id"), groupId); - }; - - } - - public static Specification usedBy(@Nonnull UUID userId) { - return (root, query, builder) -> - { - Join applicationUserJoin = root.join("wholeUsers"); - return builder.equal(applicationUserJoin. get("id"), userId); - }; - } - -} diff --git a/src/main/java/org/overture/ego/repository/queryspecification/GroupSpecification.java b/src/main/java/org/overture/ego/repository/queryspecification/GroupSpecification.java deleted file mode 100644 index 1305e304b..000000000 --- a/src/main/java/org/overture/ego/repository/queryspecification/GroupSpecification.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.repository.queryspecification; - -import lombok.val; -import org.overture.ego.model.entity.Application; -import org.overture.ego.model.entity.Group; -import org.overture.ego.model.entity.User; -import org.overture.ego.utils.QueryUtils; -import org.springframework.data.jpa.domain.Specification; - -import javax.annotation.Nonnull; -import javax.persistence.criteria.Join; -import java.util.UUID; - -public class GroupSpecification extends SpecificationBase { - public static Specification containsText(@Nonnull String text) { - val finalText = QueryUtils.prepareForQuery(text); - return (root, query, builder) -> builder.or(getQueryPredicates(builder,root,finalText, - "name","description","status") - ); - } - - public static Specification containsApplication(@Nonnull UUID appId) { - return (root, query, builder) -> - { - Join groupJoin = root.join("wholeApplications"); - return builder.equal(groupJoin. get("id"), appId); - }; - } - - public static Specification containsUser(@Nonnull UUID userId) { - return (root, query, builder) -> - { - Join groupJoin = root.join("wholeUsers"); - return builder.equal(groupJoin. get("id"), userId); - }; - } - -} diff --git a/src/main/java/org/overture/ego/repository/queryspecification/SpecificationBase.java b/src/main/java/org/overture/ego/repository/queryspecification/SpecificationBase.java deleted file mode 100644 index 37229f1ce..000000000 --- a/src/main/java/org/overture/ego/repository/queryspecification/SpecificationBase.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.repository.queryspecification; - - -import lombok.NonNull; -import lombok.val; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.utils.QueryUtils; -import org.springframework.data.jpa.domain.Specification; - -import javax.annotation.Nonnull; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; -import java.util.Arrays; -import java.util.List; - -public class SpecificationBase { - protected static Predicate[] getQueryPredicates(@NonNull CriteriaBuilder builder, - @NonNull Root root, - String queryText, - @NonNull String... params){ - return Arrays.stream(params).map(p -> - filterByField(builder, root, p, queryText)).toArray(Predicate[]::new); - } - - public static Predicate filterByField(@NonNull CriteriaBuilder builder, @NonNull Root root, - @NonNull String fieldName,String fieldValue) { - val finalText = QueryUtils.prepareForQuery(fieldValue); - return builder.like(builder.lower(root.get(fieldName)), finalText); - } - - public static Specification filterBy(@Nonnull List filters) { - return (root, query, builder) -> builder.and( - filters.stream().map(f -> filterByField(builder,root, - f.getFilterField(),f.getFilterValue())).toArray(Predicate[]::new) - ); - } -} diff --git a/src/main/java/org/overture/ego/repository/queryspecification/UserSpecification.java b/src/main/java/org/overture/ego/repository/queryspecification/UserSpecification.java deleted file mode 100644 index f044d2cfb..000000000 --- a/src/main/java/org/overture/ego/repository/queryspecification/UserSpecification.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.repository.queryspecification; - -import lombok.val; -import org.overture.ego.model.entity.Application; -import org.overture.ego.model.entity.Group; -import org.overture.ego.model.entity.User; -import org.overture.ego.utils.QueryUtils; -import org.springframework.data.jpa.domain.Specification; - -import javax.annotation.Nonnull; -import javax.persistence.criteria.Join; -import java.util.UUID; - - -public class UserSpecification extends SpecificationBase { - - public static Specification containsText(@Nonnull String text) { - val finalText = QueryUtils.prepareForQuery(text); - return (root, query, builder) -> builder.or(getQueryPredicates(builder,root,finalText, - "name","email","firstName","lastName","status") - ); - } - - public static Specification inGroup(@Nonnull UUID groupId) { - return (root, query, builder) -> - { - Join groupJoin = root.join("wholeGroups"); - return builder.equal(groupJoin. get("id"), groupId); - }; - - } - - public static Specification ofApplication(@Nonnull UUID appId) { - return (root, query, builder) -> - { - Join applicationJoin = root.join("wholeApplications"); - return builder.equal(applicationJoin. get("id"), appId); - }; - - } -} diff --git a/src/main/java/org/overture/ego/security/JWTAuthorizationFilter.java b/src/main/java/org/overture/ego/security/JWTAuthorizationFilter.java deleted file mode 100644 index 7c09d54fc..000000000 --- a/src/main/java/org/overture/ego/security/JWTAuthorizationFilter.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.security; - -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.overture.ego.token.TokenService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.util.StringUtils; - -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.Arrays; - -@Slf4j -public class JWTAuthorizationFilter extends BasicAuthenticationFilter { - - private String[] publicEndpoints = null; - - @Value("${auth.token.prefix}") - private String TOKEN_PREFIX; - - @Autowired - private TokenService tokenService; - - - public JWTAuthorizationFilter(AuthenticationManager authManager, String[] publicEndpoints) { - super(authManager); - this.publicEndpoints = publicEndpoints; - } - - @Override - @SneakyThrows - public void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain chain) { - String tokenPayload = ""; - - // No need to validate a token even if one is passed for public endpoints - if(isPublicEndpoint(request.getServletPath())){ - chain.doFilter(request,response); - return; - } else{ - tokenPayload = request.getHeader(HttpHeaders.AUTHORIZATION); - } - if (!isValidToken(tokenPayload)) { - SecurityContextHolder.clearContext(); - chain.doFilter(request,response); - return; - } - val authentication = - new UsernamePasswordAuthenticationToken( - tokenService.getTokenUserInfo(removeTokenPrefix(tokenPayload)), - null, new ArrayList<>()); - SecurityContextHolder.getContext().setAuthentication(authentication); - chain.doFilter(request,response); - } - - private boolean isValidToken(String token){ - return !StringUtils.isEmpty(token) && - token.contains(TOKEN_PREFIX) && - tokenService.validateToken(removeTokenPrefix(token)); - } - - private String removeTokenPrefix(String token){ - return token.replace(TOKEN_PREFIX,"").trim(); - } - - private boolean isPublicEndpoint(String endpointPath){ - if(this.publicEndpoints != null){ - return Arrays.stream(this.publicEndpoints).anyMatch(item -> item.equals(endpointPath)); - } else return false; - } - -} diff --git a/src/main/java/org/overture/ego/security/SecureAuthorizationManager.java b/src/main/java/org/overture/ego/security/SecureAuthorizationManager.java deleted file mode 100644 index 8f00f014a..000000000 --- a/src/main/java/org/overture/ego/security/SecureAuthorizationManager.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.security; - -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import org.overture.ego.model.entity.User; -import org.springframework.context.annotation.Profile; -import org.springframework.security.core.Authentication; - - -@Slf4j -@Profile("auth") -public class SecureAuthorizationManager implements AuthorizationManager { - - - public boolean authorize(@NonNull Authentication authentication) { - User user = (User)authentication.getPrincipal(); - return "user".equals(user.getRole().toLowerCase()) && isActiveUser(user); - } - - public boolean authorizeWithAdminRole(@NonNull Authentication authentication) { - User user = (User)authentication.getPrincipal(); - return "admin".equals(user.getRole().toLowerCase()) && isActiveUser(user); - } - - public boolean authorizeWithGroup(@NonNull Authentication authentication, String groupName) { - User user = (User)authentication.getPrincipal(); - return authorize(authentication) && user.getGroups().contains(groupName); - } - - public boolean authorizeWithApplication(@NonNull Authentication authentication, String appName) { - User user = (User)authentication.getPrincipal(); - return authorize(authentication) && user.getApplications().contains(appName); - } - - public boolean isActiveUser(User user){ - return "approved".equals(user.getStatus().toLowerCase()); - } - -} diff --git a/src/main/java/org/overture/ego/service/ApplicationService.java b/src/main/java/org/overture/ego/service/ApplicationService.java deleted file mode 100644 index a668b455a..000000000 --- a/src/main/java/org/overture/ego/service/ApplicationService.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.service; - -import lombok.NonNull; -import lombok.val; -import org.overture.ego.model.entity.Application; -import org.overture.ego.model.enums.ApplicationStatus; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.repository.ApplicationRepository; -import org.overture.ego.repository.queryspecification.ApplicationSpecification; -import org.overture.ego.token.app.AppTokenClaims; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.security.oauth2.provider.ClientDetailsService; -import org.springframework.security.oauth2.provider.ClientRegistrationException; -import org.springframework.security.oauth2.provider.client.BaseClientDetails; -import org.springframework.stereotype.Service; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.UUID; - -import static java.util.UUID.fromString; -import static org.springframework.data.jpa.domain.Specifications.where; - - -@Service -public class ApplicationService extends BaseService implements ClientDetailsService { - - /* - Dependencies - */ - @Autowired - private ApplicationRepository applicationRepository; - - @Autowired - private PasswordEncoder passwordEncoder; - - public Application create(@NonNull Application applicationInfo) { - return applicationRepository.save(applicationInfo); - } - - public Application get(@NonNull String applicationId) { - return getById(applicationRepository, fromString(applicationId)); - } - - public Application update(@NonNull Application updatedApplicationInfo) { - Application app = getById(applicationRepository, updatedApplicationInfo.getId()); - app.update(updatedApplicationInfo); - applicationRepository.save(app); - return updatedApplicationInfo; - } - - public void delete(@NonNull String applicationId) { - applicationRepository.deleteById(fromString(applicationId)); - } - - public Page listApps(@NonNull List filters, @NonNull Pageable pageable) { - return applicationRepository.findAll(ApplicationSpecification.filterBy(filters), pageable); - } - - public Page findApps(@NonNull String query, @NonNull List filters, - @NonNull Pageable pageable) { - return applicationRepository.findAll(where(ApplicationSpecification.containsText(query)) - .and(ApplicationSpecification.filterBy(filters)), pageable); - } - - public Page findUserApps(@NonNull String userId, @NonNull List filters, - @NonNull Pageable pageable){ - return applicationRepository.findAll( - where(ApplicationSpecification.usedBy(fromString(userId))) - .and(ApplicationSpecification.filterBy(filters)), - pageable); - } - - public Page findUserApps(@NonNull String userId, @NonNull String query, - @NonNull List filters, @NonNull Pageable pageable){ - return applicationRepository.findAll( - where(ApplicationSpecification.usedBy(fromString(userId))) - .and(ApplicationSpecification.containsText(query)) - .and(ApplicationSpecification.filterBy(filters)), - pageable); - } - - public Page findGroupApplications(@NonNull String groupId, @NonNull List filters, - @NonNull Pageable pageable){ - return applicationRepository.findAll( - where(ApplicationSpecification.inGroup(fromString(groupId))) - .and(ApplicationSpecification.filterBy(filters)), - pageable); - } - - public Page findGroupApplications(@NonNull String groupId, @NonNull String query, - @NonNull List filters, - @NonNull Pageable pageable){ - return applicationRepository.findAll( - where(ApplicationSpecification.inGroup(fromString(groupId))) - .and(ApplicationSpecification.containsText(query)) - .and(ApplicationSpecification.filterBy(filters)), - pageable); - } - - public Application getByName(@NonNull String appName) { - return applicationRepository.findOneByNameIgnoreCase(appName); - } - - public Application getByClientId(@NonNull String clientId) { - return applicationRepository.findOneByClientIdIgnoreCase(clientId); - } - - @Override - public ClientDetails loadClientByClientId(@NonNull String clientId) throws ClientRegistrationException { - // find client using clientid - - val application = getByClientId(clientId); - - if(application == null) { - throw new ClientRegistrationException("Client ID not found."); - } - - if(!application.getStatus().equals(ApplicationStatus.APPROVED.toString())) { - throw new ClientRegistrationException - ("Client Access is not approved."); - } - - // transform application to client details - val approvedScopes = Arrays.asList(AppTokenClaims.SCOPES); - val clientDetails = new BaseClientDetails(); - clientDetails.setClientId(clientId); - clientDetails.setClientSecret(passwordEncoder.encode(application.getClientSecret())); - clientDetails.setAuthorizedGrantTypes(Arrays.asList(AppTokenClaims.AUTHORIZED_GRANTS)); - clientDetails.setScope(approvedScopes); - clientDetails.setRegisteredRedirectUri(application.getURISet()); - clientDetails.setAutoApproveScopes(approvedScopes); - val authorities = new HashSet(); - authorities.add(new SimpleGrantedAuthority(AppTokenClaims.ROLE)); - clientDetails.setAuthorities(authorities); - return clientDetails; - } - -} diff --git a/src/main/java/org/overture/ego/service/BaseService.java b/src/main/java/org/overture/ego/service/BaseService.java deleted file mode 100644 index 4b94022db..000000000 --- a/src/main/java/org/overture/ego/service/BaseService.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.overture.ego.service; - -import org.springframework.data.repository.PagingAndSortingRepository; - -import javax.persistence.EntityNotFoundException; -import java.util.Optional; - -public abstract class BaseService { - - protected T getById(PagingAndSortingRepository repository, E id){ - Optional entity = repository.findById(id); - // TODO @AlexLepsa - replace with return entity.orElseThrow... - entity.orElseThrow(EntityNotFoundException::new); - return entity.get(); - } -} diff --git a/src/main/java/org/overture/ego/service/GroupService.java b/src/main/java/org/overture/ego/service/GroupService.java deleted file mode 100644 index 519233232..000000000 --- a/src/main/java/org/overture/ego/service/GroupService.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.service; - -import lombok.AllArgsConstructor; -import lombok.NonNull; -import lombok.val; -import org.overture.ego.model.entity.GroupPermission; -import org.overture.ego.model.entity.Group; -import org.overture.ego.model.enums.PolicyMask; -import org.overture.ego.model.params.Scope; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.repository.GroupRepository; -import org.overture.ego.repository.queryspecification.GroupSpecification; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.UUID; - -import static java.util.UUID.fromString; -import static org.springframework.data.jpa.domain.Specifications.where; - -@Service -@AllArgsConstructor(onConstructor = @__({@Autowired})) -public class GroupService extends BaseService { - private final GroupRepository groupRepository; - private final ApplicationService applicationService; - private final PolicyService policyService; - - public Group create(@NonNull Group groupInfo) { - return groupRepository.save(groupInfo); - } - - public Group addAppsToGroup(@NonNull String grpId, @NonNull List appIDs){ - val group = getById(groupRepository, fromString(grpId)); - appIDs.forEach(appId -> { - val app = applicationService.get(appId); - group.addApplication(app); - }); - return groupRepository.save(group); - } - - public Group addGroupPermissions(@NonNull String groupId, @NonNull List permissions) { - val group = getById(groupRepository, fromString(groupId)); - permissions.forEach(permission -> { - group.addNewPermission(policyService.get(permission.getAclEntityId()), PolicyMask.fromValue(permission.getMask())); - }); - return groupRepository.save(group); - } - - public Group get(@NonNull String groupId) { - return getById(groupRepository, fromString(groupId)); - } - - public Group getByName(@NonNull String groupName) { - return groupRepository.findOneByNameIgnoreCase(groupName); - } - - public Group update(@NonNull Group updatedGroupInfo) { - Group group = getById(groupRepository,updatedGroupInfo.getId()); - group.update(updatedGroupInfo); - return groupRepository.save(group); - } - - public void delete(@NonNull String groupId) { - groupRepository.deleteById(fromString(groupId)); - } - - public Page listGroups(@NonNull List filters, @NonNull Pageable pageable) { - return groupRepository.findAll(GroupSpecification.filterBy(filters), pageable); - } - - public Page getGroupPermissions(@NonNull String groupId, @NonNull Pageable pageable) { - val groupPermissions = getById(groupRepository,fromString(groupId)).getGroupPermissions(); - return new PageImpl<>(groupPermissions, pageable, groupPermissions.size()); - } - - public Page findGroups(@NonNull String query, @NonNull List filters, @NonNull Pageable pageable) { - return groupRepository.findAll(where(GroupSpecification.containsText(query)) - .and(GroupSpecification.filterBy(filters)), pageable); - } - - public Page findUserGroups(@NonNull String userId, @NonNull List filters, @NonNull Pageable pageable){ - return groupRepository.findAll( - where(GroupSpecification.containsUser(fromString(userId))) - .and(GroupSpecification.filterBy(filters)), - pageable); - } - - public Page findUserGroups(@NonNull String userId, @NonNull String query, @NonNull List filters, - @NonNull Pageable pageable){ - return groupRepository.findAll( - where(GroupSpecification.containsUser(fromString(userId))) - .and(GroupSpecification.containsText(query)) - .and(GroupSpecification.filterBy(filters)), - pageable); - } - - public Page findApplicationGroups(@NonNull String appId, @NonNull List filters, - @NonNull Pageable pageable){ - return groupRepository.findAll( - where(GroupSpecification.containsApplication(fromString(appId))) - .and(GroupSpecification.filterBy(filters)), - pageable); - } - - public Page findApplicationGroups(@NonNull String appId, @NonNull String query, - @NonNull List filters, @NonNull Pageable pageable){ - return groupRepository.findAll( - where(GroupSpecification.containsApplication(fromString(appId))) - .and(GroupSpecification.containsText(query)) - .and(GroupSpecification.filterBy(filters)), - pageable); - } - - public void deleteAppsFromGroup(@NonNull String grpId, @NonNull List appIDs) { - val group = getById(groupRepository,fromString(grpId)); - appIDs.forEach(appId -> { - // TODO if app id not valid (does not exist) we need to throw EntityNotFoundException - group.removeApplication(fromString(appId)); - }); - groupRepository.save(group); - } - - public void deleteGroupPermissions(@NonNull String userId, @NonNull List permissionsIds) { - val group = getById(groupRepository, fromString(userId)); - permissionsIds.forEach(permissionsId -> { - group.removePermission(fromString(permissionsId)); - }); - groupRepository.save(group); - } -} diff --git a/src/main/java/org/overture/ego/service/PermissionService.java b/src/main/java/org/overture/ego/service/PermissionService.java deleted file mode 100644 index f9e546b6d..000000000 --- a/src/main/java/org/overture/ego/service/PermissionService.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.overture.ego.service; - -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import org.overture.ego.model.entity.Permission; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.repository.PermissionRepository; -import org.overture.ego.repository.queryspecification.AclPermissionSpecification; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.UUID; - -import static java.util.UUID.fromString; - -@Slf4j -@Transactional -public abstract class PermissionService extends BaseService { - - private PermissionRepository repository; - - // Create - public Permission create(@NonNull Permission entity) { - return repository.save(entity); - } - - // Read - public Permission get(@NonNull String entityId) { - return getById(repository, fromString(entityId)); - } - - public Page listAclEntities(@NonNull List filters, @NonNull Pageable pageable) { - return repository.findAll(AclPermissionSpecification.filterBy(filters), pageable); - } - - // Update - public Permission update(@NonNull Permission updatedEntity) { - Permission entity = getById(repository, updatedEntity.getId()); - entity.update(updatedEntity); - repository.save(entity); - return updatedEntity; - } - - // Delete - public void delete(@NonNull String entityId) { - repository.deleteById(fromString(entityId)); - } - -} diff --git a/src/main/java/org/overture/ego/service/PolicyService.java b/src/main/java/org/overture/ego/service/PolicyService.java deleted file mode 100644 index 829b5a00f..000000000 --- a/src/main/java/org/overture/ego/service/PolicyService.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.overture.ego.service; - -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import org.overture.ego.model.entity.Policy; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.repository.AclEntityRepository; -import org.overture.ego.repository.queryspecification.AclEntitySpecification; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.UUID; - -import static java.util.UUID.fromString; - -@Slf4j -@Service -@Transactional -public class PolicyService extends BaseService { - - /* - Dependencies - */ - @Autowired - private AclEntityRepository aclEntityRepository; - - // Create - public Policy create(@NonNull Policy policy) { - return aclEntityRepository.save(policy); - } - - - // Read - public Policy get(@NonNull String aclEntityId) { - return getById(aclEntityRepository, fromString(aclEntityId)); - } - - public Policy getByName(@NonNull String aclEntityName) { - return aclEntityRepository.findOneByNameIgnoreCase(aclEntityName); - } - - public Page listAclEntities(@NonNull List filters, @NonNull Pageable pageable) { - return aclEntityRepository.findAll(AclEntitySpecification.filterBy(filters), pageable); - } - - - // Update - public Policy update(@NonNull Policy updatedPolicy) { - Policy policy = getById(aclEntityRepository, updatedPolicy.getId()); - policy.update(updatedPolicy); - aclEntityRepository.save(policy); - return updatedPolicy; - } - - // Delete - public void delete(@NonNull String aclEntityId) { - aclEntityRepository.deleteById(fromString(aclEntityId)); - } - -} diff --git a/src/main/java/org/overture/ego/service/UserService.java b/src/main/java/org/overture/ego/service/UserService.java deleted file mode 100644 index f2804541b..000000000 --- a/src/main/java/org/overture/ego/service/UserService.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.service; - -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.overture.ego.model.entity.User; -import org.overture.ego.model.entity.UserPermission; -import org.overture.ego.model.enums.PolicyMask; -import org.overture.ego.model.enums.UserRole; -import org.overture.ego.model.enums.UserStatus; -import org.overture.ego.model.params.Scope; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.repository.UserRepository; -import org.overture.ego.repository.queryspecification.UserSpecification; -import org.overture.ego.token.IDToken; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import static java.util.UUID.fromString; -import static org.springframework.data.jpa.domain.Specifications.where; - -@Slf4j -@Service -@Transactional -@RequiredArgsConstructor(onConstructor = @__({@Autowired})) -public class UserService extends BaseService { - /* - Constants - */ - // DEFAULTS - @Value("${default.user.role}") - private String DEFAULT_USER_ROLE; - @Value("${default.user.status}") - private String DEFAULT_USER_STATUS; - - // DEMO USER - private final static String DEMO_USER_NAME = "Demo.User@example.com"; - private final static String DEMO_USER_EMAIL = "Demo.User@example.com"; - private final static String DEMO_FIRST_NAME = "Demo"; - private final static String DEMO_LAST_NAME = "User"; - private final static String DEMO_USER_ROLE = UserRole.ADMIN.toString(); - private final static String DEMO_USER_STATUS = UserStatus.APPROVED.toString(); - - /* - Dependencies - */ - private final UserRepository userRepository; - private final GroupService groupService; - private final ApplicationService applicationService; - private final PolicyService policyService; - private final SimpleDateFormat formatter; - - public User create(@NonNull User userInfo) { - // Set Created At date to Now - userInfo.setCreatedAt(new Date()); - - // Set UserName to equal the email. - userInfo.setName(userInfo.getEmail()); - - return userRepository.save(userInfo); - } - - public User createFromIDToken(IDToken idToken) { - val userInfo = new User(); - userInfo.setName(idToken.getEmail()); - userInfo.setEmail(idToken.getEmail()); - userInfo.setFirstName(StringUtils.isEmpty(idToken.getGiven_name()) ? "" : idToken.getGiven_name()); - userInfo.setLastName(StringUtils.isEmpty(idToken.getFamily_name()) ? "" : idToken.getFamily_name()); - userInfo.setStatus(DEFAULT_USER_STATUS); - userInfo.setCreatedAt(new Date()); - userInfo.setLastLogin(null); - userInfo.setRole(DEFAULT_USER_ROLE); - return this.create(userInfo); - } - - public User getOrCreateDemoUser() { - User output = getByName(DEMO_USER_NAME); - - if (output != null) { - // Force the demo user to be ADMIN and APPROVED to allow demo access, - // even if these values have previously been modified for the demo user. - output.setStatus(DEMO_USER_STATUS); - output.setRole(DEMO_USER_ROLE); - } else { - val userInfo = new User(); - userInfo.setName(DEMO_USER_NAME); - userInfo.setEmail(DEMO_USER_EMAIL); - userInfo.setFirstName(DEMO_FIRST_NAME); - userInfo.setLastName(DEMO_LAST_NAME); - userInfo.setStatus(UserStatus.APPROVED.toString()); - userInfo.setCreatedAt(new Date()); - userInfo.setLastLogin(null); - userInfo.setRole(UserRole.ADMIN.toString()); - output = this.create(userInfo); - } - - return output; - } - - public User addUserToGroups(@NonNull String userId, @NonNull List groupIDs){ - val user = getById(userRepository, fromString(userId)); - groupIDs.forEach(grpId -> { - val group = groupService.get(grpId); - user.addNewGroup(group); - }); - return userRepository.save(user); - } - - public User addUserToApps(@NonNull String userId, @NonNull List appIDs){ - val user = getById(userRepository, fromString(userId)); - appIDs.forEach(appId -> { - val app = applicationService.get(appId); - user.addNewApplication(app); - }); - return userRepository.save(user); - } - - public User addUserPermissions(@NonNull String userId, @NonNull List permissions) { - val user = getById(userRepository, fromString(userId)); - permissions.forEach(permission -> { - user.addNewPermission(policyService.get(permission.getAclEntityId()), PolicyMask.fromValue(permission.getMask())); - }); - return userRepository.save(user); - } - - public User get(@NonNull String userId) { - return getById(userRepository, fromString(userId)); - } - - public User getByName(@NonNull String userName) { - return userRepository.findOneByNameIgnoreCase(userName); - } - - public User update(@NonNull User updatedUserInfo) { - val user = getById(userRepository, updatedUserInfo.getId()); - if(UserRole.USER.toString().equals(updatedUserInfo.getRole().toUpperCase())) - updatedUserInfo.setRole(UserRole.USER.toString()); - else if(UserRole.ADMIN.toString().equals(updatedUserInfo.getRole().toUpperCase())) - updatedUserInfo.setRole(UserRole.ADMIN.toString()); - user.update(updatedUserInfo); - return userRepository.save(user); - } - - public void delete(@NonNull String userId) { - userRepository.deleteById(fromString(userId)); - } - - public Page listUsers(@NonNull List filters,@NonNull Pageable pageable) { - return userRepository.findAll(UserSpecification.filterBy(filters), pageable); - } - - public Page findUsers(@NonNull String query, @NonNull List filters, @NonNull Pageable pageable) { - return userRepository.findAll( - where(UserSpecification.containsText(query)) - .and(UserSpecification.filterBy(filters)), pageable); - } - - public void deleteUserFromGroups(@NonNull String userId, @NonNull List groupIDs) { - val user = getById(userRepository, fromString(userId)); - groupIDs.forEach(grpId -> { - user.removeGroup(fromString(grpId)); - }); - userRepository.save(user); - } - - public void deleteUserFromApps(@NonNull String userId, @NonNull List appIDs) { - val user = getById(userRepository, fromString(userId)); - appIDs.forEach(appId -> { - user.removeApplication(fromString(appId)); - }); - userRepository.save(user); - } - - public void deleteUserPermissions(@NonNull String userId, @NonNull List permissionsIds) { - val user = getById(userRepository, fromString(userId)); - permissionsIds.forEach(permissionsId -> { - user.removePermission(fromString(permissionsId)); - }); - userRepository.save(user); - } - - public Page findGroupUsers(@NonNull String groupId, @NonNull List filters, - @NonNull Pageable pageable){ - return userRepository.findAll( - where(UserSpecification.inGroup(fromString(groupId))) - .and(UserSpecification.filterBy(filters)), - pageable); - } - - public Page findGroupUsers(@NonNull String groupId, @NonNull String query, - @NonNull List filters, @NonNull Pageable pageable){ - return userRepository.findAll( - where(UserSpecification.inGroup(fromString(groupId))) - .and(UserSpecification.containsText(query)) - .and(UserSpecification.filterBy(filters)), - pageable); - } - - public Page findAppUsers(@NonNull String appId, @NonNull List filters, - @NonNull Pageable pageable){ - return userRepository.findAll( - where(UserSpecification.ofApplication(fromString(appId))) - .and(UserSpecification.filterBy(filters)), - pageable); - } - - public Page findAppUsers(@NonNull String appId, @NonNull String query, - @NonNull List filters, - @NonNull Pageable pageable){ - return userRepository.findAll( - where(UserSpecification.ofApplication(fromString(appId))) - .and(UserSpecification.containsText(query)) - .and(UserSpecification.filterBy(filters)), - pageable); - } - - public Page getUserPermissions(@NonNull String userId, @NonNull Pageable pageable) { - val userPermissions = getById(userRepository, fromString(userId)).getUserPermissions(); - return new PageImpl<>(userPermissions, pageable, userPermissions.size()); - } -} diff --git a/src/main/java/org/overture/ego/token/TokenService.java b/src/main/java/org/overture/ego/token/TokenService.java deleted file mode 100644 index 0ea8be2df..000000000 --- a/src/main/java/org/overture/ego/token/TokenService.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.token; - -import io.jsonwebtoken.*; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.overture.ego.model.entity.Application; -import org.overture.ego.model.entity.User; -import org.overture.ego.reactor.events.UserEvents; -import org.overture.ego.service.UserService; -import org.overture.ego.token.app.AppJWTAccessToken; -import org.overture.ego.token.app.AppTokenClaims; -import org.overture.ego.token.app.AppTokenContext; -import org.overture.ego.token.signer.TokenSigner; -import org.overture.ego.token.user.UserJWTAccessToken; -import org.overture.ego.token.user.UserTokenClaims; -import org.overture.ego.token.user.UserTokenContext; -import org.overture.ego.utils.TypeUtils; -import org.overture.ego.view.Views; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.security.InvalidKeyException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Map; - -@Slf4j -@Service -public class TokenService { - - @Value("${demo:false}") - private boolean demo; - - @Value("${jwt.duration:86400000}") - private int DURATION; - @Autowired - private UserService userService; - @Autowired - private UserEvents userEvents; - @Autowired - TokenSigner tokenSigner; - @Autowired - private SimpleDateFormat dateFormatter; - /* - Constant - */ - private static final String ISSUER_NAME="ego"; - - - public String generateUserToken(IDToken idToken){ - // If the demo flag is set, all tokens will be generated as the Demo User, - // otherwise, get the user associated with their idToken - User user; - - if (demo) { - user = userService.getOrCreateDemoUser(); - } else { - val userName = idToken.getEmail(); - user = userService.getByName(userName); - if (user == null) { - user = userService.createFromIDToken(idToken); - } - } - - // Update user.lastLogin in the DB - // Use events as these are async: - // the DB call won't block returning the Token - user.setLastLogin(new Date()); - userEvents.update(user); - - return generateUserToken(user); - } - - @SneakyThrows - public String generateUserToken(User u) { - val tokenContext = new UserTokenContext(u); - val tokenClaims = new UserTokenClaims(); - tokenClaims.setIss(ISSUER_NAME); - tokenClaims.setValidDuration(DURATION); - tokenClaims.setContext(tokenContext); - return getSignedToken(tokenClaims); - } - - @SneakyThrows - public String generateAppToken(Application application) { - val tokenContext = new AppTokenContext(application); - val tokenClaims = new AppTokenClaims(); - tokenClaims.setIss(ISSUER_NAME); - tokenClaims.setValidDuration(DURATION); - tokenClaims.setContext(tokenContext); - return getSignedToken(tokenClaims); - } - - public boolean validateToken(String token) { - - Jws decodedToken = null; - try{ - decodedToken = Jwts.parser() - .setSigningKey(tokenSigner.getKey().get()) - .parseClaimsJws(token); - } catch (Exception ex){ - log.error("Error parsing JWT: {}", ex); - } - return (decodedToken != null); - } - - public User getTokenUserInfo(String token) { - try { - Claims body = getTokenClaims(token); - val tokenClaims = TypeUtils.convertToAnotherType(body, UserTokenClaims.class, Views.JWTAccessToken.class); - return userService.get(tokenClaims.getSub()); - } catch (JwtException | ClassCastException e) { - return null; - } - } - - @SneakyThrows - public Claims getTokenClaims(String token) { - - if(tokenSigner.getKey().isPresent()) { - return Jwts.parser() - .setSigningKey(tokenSigner.getKey().get()) - .parseClaimsJws(token) - .getBody(); - } else { - throw new InvalidKeyException("Invalid signing key for the token."); - } - } - - public UserJWTAccessToken getUserAccessToken(String token){ - return new UserJWTAccessToken(token, this); - } - - public AppJWTAccessToken getAppAccessToken(String token){ - return new AppJWTAccessToken(token, this); - } - - @SneakyThrows - private String getSignedToken(TokenClaims claims){ - if(tokenSigner.getKey().isPresent()) { - return Jwts.builder() - .setClaims(TypeUtils.convertToAnotherType(claims, Map.class, Views.JWTAccessToken.class)) - .signWith(SignatureAlgorithm.RS256, tokenSigner.getKey().get()) - .compact(); - } else { - throw new InvalidKeyException("Invalid signing key for the token."); - } - } -} diff --git a/src/main/java/org/overture/ego/utils/AclPermissionUtils.java b/src/main/java/org/overture/ego/utils/AclPermissionUtils.java deleted file mode 100644 index 2220d16fd..000000000 --- a/src/main/java/org/overture/ego/utils/AclPermissionUtils.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.overture.ego.utils; - -import org.overture.ego.model.entity.Permission; - -import java.util.List; -import java.util.stream.Collectors; - -public class AclPermissionUtils { - public static String extractPermissionString(Permission permission) { - return String.format("%s.%s", permission.getEntity().getName(), permission.getMask().toString()); - } - - public static List extractPermissionStrings(List permissions) { - return permissions.stream().map(AclPermissionUtils::extractPermissionString) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/org/overture/ego/utils/FieldUtils.java b/src/main/java/org/overture/ego/utils/FieldUtils.java deleted file mode 100644 index 70091b314..000000000 --- a/src/main/java/org/overture/ego/utils/FieldUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2017. The Ontario Institute for Cancer Research. 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. - */ - -package org.overture.ego.utils; - -import lombok.extern.slf4j.Slf4j; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -@Slf4j -public class FieldUtils { - - public static List getStaticFieldList(Class c){ - return Arrays.stream(c.getDeclaredFields()).map(f -> f).collect(Collectors.toList()); - } - - public static List getStaticFieldValueList(Class c){ - return Arrays.stream(c.getDeclaredFields()).map(f -> getFieldValue(f)).collect(Collectors.toList()); - } - - public static String getFieldValue(Field field){ - try{ - return field.get(null).toString(); - } catch(IllegalAccessException ex){ - log.warn("Illegal access exception. Variable: {} is either private or non-static", field.getName()); - return ""; - } - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0d4108df7..a778e890b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,22 +1,27 @@ server: - port: 8081 + port: 8088 jwt: secret: testsecretisalsoasecret duration: 86400000 #in milliseconds 86400000 = 1day, max = 2147483647 +apitoken: + duration: 365 # in days + # security auth: token: prefix: +spring.main.allow-bean-definition-overriding: true + # Datasource spring.datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://localhost:5432/ego?stringtype=unspecified + url: jdbc:postgresql://localhost:8432/ego?stringtype=unspecified username: postgres - password: + password: password max-active: 10 max-idle: 1 min-idle: 1 @@ -24,27 +29,76 @@ spring.datasource: spring: flyway: enabled: false + jackson: + deserialization: + FAIL_ON_UNKNOWN_PROPERTIES: true + FAIL_ON_NULL_FOR_PRIMITIVES: true + FAIL_ON_NUMBERS_FOR_ENUMS: true + FAIL_ON_READING_DUP_TREE_KEY: true + # set this flag in Spring 2.0 because of this open issue: https://hibernate.atlassian.net/browse/HHH-12368 spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: true +log4j: + logger: + org: + hibernate: TRACE + + +oauth: + redirectFrontendUri: http://localhost:3501 -# Facebook Connection Details facebook: client: - id: 140524976574963 - secret: 2439abe7ae008bda7ab5cfdf706b4d66 + clientId: + clientSecret: accessTokenUri: https://graph.facebook.com/oauth/access_token + userAuthorizationUri: https://www.facebook.com/dialog/oauth tokenValidateUri: https://graph.facebook.com/debug_token + tokenName: oauth_token + authenticationScheme: query + clientAuthenticationScheme: form + scope: + - email timeout: connect: 5000 read: 5000 + user-authorization-uri: https://www.facebook.com/dialog/oauth resource: - userInfoUri: https://graph.facebook.com/me + userInfoUri: https://graph.facebook.com/me?fields=email,first_name,last_name -# Google Connection Details google: client: - Ids: 144611473365-k1aarg8qs6rlh67r3t7dssi1e34b6061.apps.googleusercontent.com + clientId: 144611473365-k1aarg8qs6rlh67r3t7dssi1e34b6061.apps.googleusercontent.com + clientSecret: + accessTokenUri: https://www.googleapis.com/oauth2/v4/token + userAuthorizationUri: https://accounts.google.com/o/oauth2/v2/auth + clientAuthenticationScheme: form + scope: + - email + resource: + userInfoUri: https://www.googleapis.com/oauth2/v3/userinfo + +linkedIn: + client: + clientID: + clientSecret: + accessTokenUri: https://www.linkedin.com/oauth/v2/accessToken + userAuthorizationUri: https://www.linkedin.com/oauth/v2/authorization + clientAuthenticationScheme: query + resource: + userInfoUri: https://api.linkedin.com/v1/people/~:(email-address,first-name,last-name)?format=json + +github: + client: + clientId: + clientSecret: + accessTokenUri: https://github.com/login/oauth/access_token + userAuthorizationUri: https://github.com/login/oauth/authorize + clientAuthenticationScheme: form + scope: "user:email" + resource: + userInfoUri: https://api.github.com/user # Logging settings. logging: @@ -55,9 +109,19 @@ logging: "org.skife.jdbi.v2": TRACE level: root: ERROR - org.springframework.web.filter.CommonsRequestLoggingFilter: DEBUG - org.springframework.boot: INFO - org.overture.ego: INFO + #org.springframework.web.filter.CommonsRequestLoggingFilter: DEBUG + #org.springframework.web.filter.CommonsRequestLoggingFilter: DEBUG + #org.springframework.boot: INFO + bio.overture.ego: INFO + +# Hibernate SQL Debugging +#spring.jpa.properties.hibernate.format_sql: true +#logging.level.org.hibernate.SQL: DEBUG +#logging.level.org.hibernate.type.descriptor.sql: TRACE + + +# When you are desperate, use this... +#logging.level.org.hibernate: TRACE token: private-key: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDSU6oy48sJW6xzqzOSU1dAvUUeFKQSBHsCf7wGWUGpOxEczhtFiiyx4YUJtg+fyvwWxa4wO3GnQLBPIxBHY8JsnvjQN2lsTUoLqMB9nGpwF617uA/S2igm1u+cDpfi82kbi6SG1Sg30PM047R6oxTRGDLLkeMRF1gRaTBM0HfSL0j6ccU5KPgwYsFLE2We6jeR56iYJGC2KYLH4v8rcc2jRAdMbUntHMtUByF9BPSW7elQnyQH5Qzr/o0b59XLKwnJFn2Bp2yviC8cdyTDyhQGna0e+oESQR1j6u3Ux/mOmm3slRXscA8sH+pHmOEAtjYVf/ww36U8uZv+ctBCJyFVAgMBAAECggEBALrEeJqAFUfWFCkSmdUSFKT0bW/svFUTjXgGnZy1ncz9GpENpMH3lQDQVibteKpYwcom+Cr0XlQ66VUcudPrDjcOY7vhuMfnSh1YWLYyM4IeRHtcUxDVkFoM+vEFNHLf2zIOqqbgmboW3iDVIurT7iRO7KxAe/YtWJL9aVqMtBn7Lu7S7OvAU4ji5iLIBxjl82JYA+9lu/aQ6YGaoZuSO7bcU8Sivi+DKAahqN9XMKiB1XpC+PpaS/aec2S7xIlTdzoDGxEALRGlMe+xBEeQTBVJHBWrRIDPoHLTREeRC/9Pp+1Y4Dz8hd5Bi0n8/5r/q0liD+0vtmjsdU4E2QrktYECgYEA73qWvhCYHPMREAFtwz1mpp9ZhDCW6SF+njG7fBKcjz8OLcy15LXiTGc268ewtQqTMjPQlm1n2C6hGccGAIlMibQJo3KZHlTs125FUzDpTVgdlei6vU7M+gmfRSZed00J6jC04/qMR1tnV3HME3np7eRTKTA6Ts+zBwEvkbCetSkCgYEA4NY5iSBO1ybouIecDdD15uI2ItLPCBNMzu7IiK7IygIzuf+SyKyjhtFSR4vEi0gScOM7UMlwCMOVU10e4nMDknIWCDG9iFvmIEkGHGxgRrN5hX1Wrq74wF212lvvagH1IVWSHa8cVpMe+UwKu5Q1h4yzuYt6Q9wPQ7Qtn5emBE0CgYB2syispMUA9GnsqQii0Xhj9nAEWaEzhOqhtrzbTs5TIkoA4Yr3BkBY5oAOdjhcRBWZuJ0XMrtaKCKqCEAtW+CYEKkGXvMOWcHbNkkeZwv8zkQ73dNRqhFnjgVn3RDNyV20uteueK23YNLkQP+KV89fnuCpdcIw9joiqq/NYuIHoQKBgB5WaZ8KH/lCA8babYEjv/pubZWXUl4plISbja17wBYZ4/bl+F1hhhMr7Wk//743dF2NG7TT6W0VTvHXr9IoaMP65uQmKgfbNpsGn294ZClGEFClz+t0KpZyTpZvL0fjibr8u+GLfkxkP5qt2wjif7KRlrKjklTTva+KAVn2cW1FAoGBAMkX9ekIwhx/7uY6ndxKl8ZMDerjr6MhV0b08hHp3RxHbYVbcpN0UKspoYvZVgHwP18xlDij8yWRE2fapwgi4m82ZmYlg0qqJmyqIU9vBB3Jow903h1KPQrkmQEZxJ/4H8yrbgVf2HT+WUfjTFgaDZRl01bI3YkydCw91/Ub9HU6 @@ -66,8 +130,8 @@ token: # Default values available for creation of entities default: user: - role: USER - status: Approved + type: USER + status: APPROVED --- ############################################################################### # Profile - "jks" @@ -108,19 +172,19 @@ logging: spring: profiles: demo -# demo flag -demo: false - --- ############################################################################### # Profile - "test" ############################################################################### spring: profiles: test +logging.test.controller.enable: true + + spring.datasource: driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver - url: jdbc:tc:postgresql:9.5.13://localhost:5432/ego?TC_INITFUNCTION=org.overture.ego.test.FlywayInit::initTestContainers + url: jdbc:tc:postgresql:9.5.13://localhost:5432/ego?TC_INITFUNCTION=bio.overture.ego.test.FlywayInit::initTestContainers username: postgres password: diff --git a/src/main/resources/bootstrap-iam.properties b/src/main/resources/bootstrap-iam.properties index 88c8079a5..4d382ebda 100644 --- a/src/main/resources/bootstrap-iam.properties +++ b/src/main/resources/bootstrap-iam.properties @@ -1,7 +1,5 @@ spring.cloud.vault.enabled=true - spring.application.name=development/oicr/ego - spring.cloud.vault.generic.default-context=${spring.application.name} spring.cloud.vault.uri="" spring.cloud.vault.authentication=AWS_IAM diff --git a/src/main/resources/bootstrap-token.properties b/src/main/resources/bootstrap-token.properties index fbcf49292..b2fe81456 100644 --- a/src/main/resources/bootstrap-token.properties +++ b/src/main/resources/bootstrap-token.properties @@ -1,7 +1,5 @@ spring.cloud.vault.enabled=true - spring.application.name=development/oicr/ego - spring.cloud.vault.generic.default-context=${spring.application.name} spring.cloud.vault.scheme=http spring.cloud.vault.host=localhost diff --git a/src/main/resources/flyway/conf/flyway.conf b/src/main/resources/flyway/conf/flyway.conf index 8bceccd47..e6a8f3438 100644 --- a/src/main/resources/flyway/conf/flyway.conf +++ b/src/main/resources/flyway/conf/flyway.conf @@ -36,13 +36,13 @@ # SQLite : jdbc:sqlite: # Sybase ASE : jdbc:jtds:sybase://:/ # Redshift* : jdbc:redshift://:/ -flyway.url=jdbc:postgresql://localhost:5432/ego?stringtype=unspecified +flyway.url = jdbc:postgresql://localhost:5432/ego?stringtype=unspecified # Fully qualified classname of the JDBC driver (autodetected by default based on flyway.url) # flyway.driver= # User to use to connect to the database. Flyway will prompt you to enter it if not specified. -flyway.user=postgres +flyway.user = postgres # Password to use to connect to the database. Flyway will prompt you to enter it if not specified. # flyway.password= @@ -67,7 +67,7 @@ flyway.user=postgres # Unprefixed locations or locations starting with classpath: point to a package on the classpath and may contain # both sql and java-based migrations. # Locations starting with filesystem: point to a directory on the filesystem and may only contain sql migrations. -# flyway.locations= +flyway.locations = filesystem:src/main/resources/flyway/sql,classpath:db.migration # Comma-separated list of fully qualified class names of custom MigrationResolver to use for resolving migrations. # flyway.resolvers= @@ -259,4 +259,4 @@ flyway.user=postgres # Whether to Flyway's support for Oracle SQL*Plus commands should be activated. (default: false) # Flyway Pro and Flyway Enterprise only -# flyway.oracle.sqlplus= \ No newline at end of file +# flyway.oracle.sqlplus= diff --git a/src/main/resources/flyway/sql/V1_10__remove_apps_from_apitokens.sql b/src/main/resources/flyway/sql/V1_10__remove_apps_from_apitokens.sql new file mode 100644 index 000000000..9d654369b --- /dev/null +++ b/src/main/resources/flyway/sql/V1_10__remove_apps_from_apitokens.sql @@ -0,0 +1 @@ +DROP TABLE tokenapplication; \ No newline at end of file diff --git a/src/main/resources/flyway/sql/V1_11__add_expiry_date_api_tokens.sql b/src/main/resources/flyway/sql/V1_11__add_expiry_date_api_tokens.sql new file mode 100644 index 000000000..172da8bad --- /dev/null +++ b/src/main/resources/flyway/sql/V1_11__add_expiry_date_api_tokens.sql @@ -0,0 +1 @@ +ALTER TABLE token ADD expirydate TIMESTAMP NOT NULL DEFAULT NOW(); \ No newline at end of file diff --git a/src/main/resources/flyway/sql/V1_12__egoapplication_unique_constraints.sql b/src/main/resources/flyway/sql/V1_12__egoapplication_unique_constraints.sql new file mode 100644 index 000000000..e6ec79b3b --- /dev/null +++ b/src/main/resources/flyway/sql/V1_12__egoapplication_unique_constraints.sql @@ -0,0 +1 @@ +ALTER TABLE egoapplication ADD CONSTRAINT egoapplication_name_key UNIQUE (name); diff --git a/src/main/resources/flyway/sql/V1_4__score_integration.sql b/src/main/resources/flyway/sql/V1_4__score_integration.sql new file mode 100644 index 000000000..5b1d79440 --- /dev/null +++ b/src/main/resources/flyway/sql/V1_4__score_integration.sql @@ -0,0 +1,19 @@ +CREATE TABLE TOKEN( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + token VARCHAR(2048) NOT NULL, + owner UUID NOT NULL REFERENCES EGOUSER(ID), + issuedate TIMESTAMP DEFAULT NOW(), + isrevoked BOOLEAN DEFAULT FALSE +); + +CREATE TABLE TOKENSCOPE ( + token_id UUID NOT NULL REFERENCES TOKEN(ID), + policy_id UUID NOT NULL REFERENCES ACLENTITY(ID), + access_level ACLMASK NOT NULL +); + +CREATE TABLE TOKENAPPLICATION ( + tokenid UUID NOT NULL REFERENCES TOKEN(ID), + appid UUID NOT NULL REFERENCES EGOAPPLICATION(ID) +); + diff --git a/src/main/resources/flyway/sql/V1_5__table_renaming.sql b/src/main/resources/flyway/sql/V1_5__table_renaming.sql new file mode 100644 index 000000000..16fb34888 --- /dev/null +++ b/src/main/resources/flyway/sql/V1_5__table_renaming.sql @@ -0,0 +1,44 @@ +ALTER TABLE ACLENTITY RENAME TO POLICY; +ALTER TABLE POLICY RENAME CONSTRAINT ACLENTITY_PKEY TO POLICY_PKEY; +ALTER TABLE POLICY RENAME CONSTRAINT ACLENTITY_NAME_KEY TO POLICY_NAME_KEY; +ALTER TABLE POLICY RENAME CONSTRAINT ACLENTITY_OWNER_FKEY TO POLICY_OWNER_FKEY; + +ALTER TABLE ACLUSERPERMISSION RENAME TO USERPERMISSION; +ALTER TABLE USERPERMISSION RENAME ENTITY TO POLICY_ID; +ALTER TABLE USERPERMISSION RENAME SID TO USER_ID; +ALTER TABLE USERPERMISSION RENAME MASK TO ACCESS_LEVEL; +ALTER TABLE USERPERMISSION RENAME CONSTRAINT ACLUSERPERMISSION_PKEY TO USERPERMISSION_PKEY; +ALTER TABLE USERPERMISSION RENAME CONSTRAINT ACLUSERPERMISSION_ENTITY_FKEY TO USERPERMISSION_POLICY_FKEY; +ALTER TABLE USERPERMISSION RENAME CONSTRAINT ACLUSERPERMISSION_SID_FKEY TO USERPERMISSION_USER_FKEY; + +ALTER TABLE ACLGROUPPERMISSION RENAME TO GROUPPERMISSION; +ALTER TABLE GROUPPERMISSION RENAME ENTITY TO POLICY_ID; +ALTER TABLE GROUPPERMISSION RENAME SID TO GROUP_ID; +ALTER TABLE GROUPPERMISSION RENAME MASK TO ACCESS_LEVEL; + +ALTER TABLE GROUPPERMISSION RENAME CONSTRAINT ACLGROUPPERMISSION_PKEY TO GROUPPERMISSION_PKEY; +ALTER TABLE GROUPPERMISSION RENAME CONSTRAINT ACLGROUPPERMISSION_ENTITY_FKEY TO GROUPPERMISSION_POLICY_FKEY; +ALTER TABLE GROUPPERMISSION RENAME CONSTRAINT ACLGROUPPERMISSION_SID_FKEY TO GROUPPERMISSION_GROUP_FKEY; + +ALTER TABLE USERGROUP RENAME USERID TO USER_ID; +ALTER TABLE USERGROUP RENAME GRPID TO GROUP_ID; +ALTER TABLE USERGROUP RENAME CONSTRAINT USERGROUP_GRPID_FKEY TO USERGROUP_GROUP_FKEY; +ALTER TABLE USERGROUP RENAME CONSTRAINT USERGROUP_USERID_FKEY TO USERGROUP_USER_FKEY; + +ALTER TABLE USERAPPLICATION RENAME USERID TO USER_ID; +ALTER TABLE USERAPPLICATION RENAME APPID TO APPLICATION_ID; +ALTER TABLE USERAPPLICATION RENAME CONSTRAINT USERAPPLICATION_APPID_FKEY TO USERAPPLICATION_APPLICATION_FKEY; +ALTER TABLE USERAPPLICATION RENAME CONSTRAINT USERAPPLICATION_USERID_FKEY TO USERAPPLICATION_USER_FKEY; + +ALTER TABLE GROUPAPPLICATION RENAME GRPID TO GROUP_ID; +ALTER TABLE GROUPAPPLICATION RENAME APPID TO APPLICATION_ID; +ALTER TABLE GROUPAPPLICATION RENAME CONSTRAINT GROUPAPPLICATION_APPID_FKEY TO GROUPAPPLICATION_APPLICATION_FKEY; +ALTER TABLE GROUPAPPLICATION RENAME CONSTRAINT GROUPAPPLICATION_GRPID_FKEY TO GROUPAPPLICATION_GROUP_FKEY; + +ALTER TABLE TOKENAPPLICATION RENAME TOKENID TO TOKEN_ID; +ALTER TABLE TOKENAPPLICATION RENAME APPID TO APPLICATION_ID; +ALTER TABLE TOKENAPPLICATION RENAME CONSTRAINT TOKENAPPLICATION_APPID_FKEY TO TOKENAPPLICATION_APPLICATION_FKEY; +ALTER TABLE TOKENAPPLICATION RENAME CONSTRAINT TOKENAPPLICATION_TOKENID_FKEY TO TOKENAPPLICATION_TOKEN_FKEY; + +ALTER TABLE TOKENSCOPE RENAME CONSTRAINT TOKENSCOPE_POLICY_ID_FKEY TO TOKENSCOPE_POLICY_FKEY; +ALTER TABLE TOKENSCOPE RENAME CONSTRAINT TOKENSCOPE_TOKEN_ID_FKEY TO TOKENSCOPE_TOKEN_FKEY; \ No newline at end of file diff --git a/src/main/resources/flyway/sql/V1_6__add_not_null_constraint.sql b/src/main/resources/flyway/sql/V1_6__add_not_null_constraint.sql new file mode 100644 index 000000000..6fcea5f8a --- /dev/null +++ b/src/main/resources/flyway/sql/V1_6__add_not_null_constraint.sql @@ -0,0 +1,26 @@ +ALTER TABLE EGOAPPLICATION ALTER COLUMN name SET NOT NULL; +ALTER TABLE EGOAPPLICATION ALTER COLUMN clientid SET NOT NULL; +ALTER TABLE EGOAPPLICATION ALTER COLUMN clientsecret SET NOT NULL; +ALTER TABLE EGOAPPLICATION ALTER COLUMN status SET NOT NULL; + +ALTER TABLE EGOGROUP ALTER COLUMN name SET NOT NULL; +ALTER TABLE EGOGROUP ALTER COLUMN status SET NOT NULL; + +ALTER TABLE EGOUSER ALTER COLUMN name SET NOT NULL; +ALTER TABLE EGOUSER ALTER COLUMN email SET NOT NULL; +ALTER TABLE EGOUSER ALTER COLUMN role SET NOT NULL; +ALTER TABLE EGOUSER ALTER COLUMN createdat SET NOT NULL; +-- ALTER TABLE EGOUSER ALTER COLUMN lastlogin SET NOT NULL; +ALTER TABLE EGOUSER ALTER COLUMN status SET NOT NULL; +-- ALTER TABLE EGOUSER ALTER COLUMN preferredlanguage SET NOT NULL; + +ALTER TABLE GROUPAPPLICATION ALTER COLUMN group_id SET NOT NULL; +ALTER TABLE GROUPAPPLICATION ALTER COLUMN application_id SET NOT NULL; + +ALTER TABLE TOKEN ALTER COLUMN issuedate SET NOT NULL; +ALTER TABLE TOKEN ALTER COLUMN isrevoked SET NOT NULL; + +ALTER TABLE USERAPPLICATION ALTER COLUMN application_id SET NOT NULL; + +ALTER TABLE USERGROUP ALTER COLUMN group_id SET NOT NULL; + diff --git a/src/main/resources/flyway/sql/V1_7__token_modification.sql b/src/main/resources/flyway/sql/V1_7__token_modification.sql new file mode 100644 index 000000000..d82704213 --- /dev/null +++ b/src/main/resources/flyway/sql/V1_7__token_modification.sql @@ -0,0 +1,3 @@ +ALTER TABLE token RENAME COLUMN token TO name; +ALTER TABLE token ADD CONSTRAINT token_name_key UNIQUE (name); +ALTER TABLE token ADD description VARCHAR(255); diff --git a/src/main/resources/flyway/sql/V1_8__application_types.sql b/src/main/resources/flyway/sql/V1_8__application_types.sql new file mode 100644 index 000000000..a17ab0935 --- /dev/null +++ b/src/main/resources/flyway/sql/V1_8__application_types.sql @@ -0,0 +1,3 @@ +CREATE TYPE APPLICATIONTYPE AS ENUM('CLIENT','ADMIN'); +ALTER TABLE EGOUSER RENAME COLUMN role to usertype; +ALTER TABLE EGOAPPLICATION add column applicationtype APPLICATIONTYPE not null DEFAULT 'CLIENT'; diff --git a/src/main/resources/flyway/sql/V1_9__new_enum_types.sql b/src/main/resources/flyway/sql/V1_9__new_enum_types.sql new file mode 100644 index 000000000..c1fa183dc --- /dev/null +++ b/src/main/resources/flyway/sql/V1_9__new_enum_types.sql @@ -0,0 +1,59 @@ +-- Create new types +CREATE TYPE statustype AS ENUM('APPROVED', 'REJECTED', 'DISABLED', 'PENDING'); +CREATE TYPE usertype AS ENUM('USER', 'ADMIN'); +CREATE TYPE languagetype AS ENUM('ENGLISH', 'FRENCH', 'SPANISH'); + +-- Convert the Users status column to be of type statustype +ALTER TABLE egouser DROP CONSTRAINT egouser_status_check; +UPDATE egouser SET status = 'APPROVED' WHERE status = 'Approved'; +UPDATE egouser SET status = 'REJECTED' WHERE status = 'Rejected'; +UPDATE egouser SET status = 'DISABLED' WHERE status = 'Disabled'; +UPDATE egouser SET status = 'PENDING' WHERE status = 'Pending'; +ALTER TABLE egouser ALTER COLUMN status TYPE statustype USING status::statustype; +ALTER TABLE egouser ALTER COLUMN status SET NOT NULL; +ALTER TABLE egouser ALTER COLUMN status SET DEFAULT 'PENDING'; + +-- Convert the Applications status column to be of type statustype +ALTER TABLE egoapplication DROP CONSTRAINT egoapplication_status_check; +UPDATE egoapplication SET status = 'APPROVED' WHERE status = 'Approved'; +UPDATE egoapplication SET status = 'REJECTED' WHERE status = 'Rejected'; +UPDATE egoapplication SET status = 'DISABLED' WHERE status = 'Disabled'; +UPDATE egoapplication SET status = 'PENDING' WHERE status = 'Pending' OR status IS NULL; +ALTER TABLE egoapplication ALTER COLUMN status TYPE statustype USING status::statustype; +ALTER TABLE egoapplication ALTER COLUMN status SET NOT NULL; +ALTER TABLE egoapplication ALTER COLUMN status SET DEFAULT 'PENDING'; + +-- Convert the Group 'status' column to be of type statustype +ALTER TABLE egogroup DROP CONSTRAINT egogroup_status_check; +UPDATE egogroup SET status = 'APPROVED' WHERE status = 'Approved'; +UPDATE egogroup SET status = 'REJECTED' WHERE status = 'Rejected'; +UPDATE egogroup SET status = 'DISABLED' WHERE status = 'Disabled'; +UPDATE egogroup SET status = 'PENDING' WHERE status = 'Pending'; +ALTER TABLE egogroup ALTER COLUMN status TYPE statustype USING status::statustype; +ALTER TABLE egogroup ALTER COLUMN status SET NOT NULL; +ALTER TABLE egogroup ALTER COLUMN status SET DEFAULT 'PENDING'; + +-- Rename the User 'usertype' column to 'type' since 'usertype' is redundant in the context of a user +ALTER TABLE egouser RENAME usertype TO type; + +-- Change the User 'type' column to be of type usertype +ALTER TABLE egouser ALTER COLUMN type TYPE usertype USING type::usertype; +ALTER TABLE egouser ALTER COLUMN type SET NOT NULL; +ALTER TABLE egouser ALTER COLUMN type SET DEFAULT 'USER'; + +-- Convert the User 'preferredlanguage' column to be of type languagetype +ALTER TABLE egouser DROP CONSTRAINT egouser_preferredlanguage_check; +UPDATE egouser SET preferredlanguage = 'ENGLISH' WHERE preferredlanguage = 'English'; +UPDATE egouser SET preferredlanguage = 'FRENCH' WHERE preferredlanguage = 'French'; +UPDATE egouser SET preferredlanguage = 'SPANISH' WHERE preferredlanguage = 'Spanish'; +ALTER TABLE egouser ALTER COLUMN preferredlanguage TYPE languagetype USING preferredlanguage::languagetype; + +-- Rename the Application 'applicationtype' column to 'type' since 'applicationtype' is redundant in the context of an application +ALTER TABLE egoapplication RENAME applicationtype TO type; + +-- Add default uuid4 generation to other tables just like for Application and Group +ALTER TABLE egouser ALTER COLUMN id SET DEFAULT uuid_generate_v4(); +ALTER TABLE policy ALTER COLUMN id SET DEFAULT uuid_generate_v4(); +ALTER TABLE grouppermission ALTER COLUMN id SET DEFAULT uuid_generate_v4(); +ALTER TABLE userpermission ALTER COLUMN id SET DEFAULT uuid_generate_v4(); + diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 57404b939..64468e541 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -17,9 +17,9 @@ --> - - - + + + @@ -40,6 +40,6 @@ - + diff --git a/src/test/java/bio/overture/ego/controller/AbstractControllerTest.java b/src/test/java/bio/overture/ego/controller/AbstractControllerTest.java new file mode 100644 index 000000000..120958b45 --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/AbstractControllerTest.java @@ -0,0 +1,275 @@ +package bio.overture.ego.controller; + +import static bio.overture.ego.utils.Converters.convertToIds; +import static bio.overture.ego.utils.Joiners.COMMA; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; + +import bio.overture.ego.model.dto.CreateApplicationRequest; +import bio.overture.ego.model.dto.GroupRequest; +import bio.overture.ego.model.dto.MaskDTO; +import bio.overture.ego.model.dto.UpdateApplicationRequest; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.AccessLevel; +import bio.overture.ego.utils.web.BasicWebResource; +import bio.overture.ego.utils.web.ResponseOption; +import bio.overture.ego.utils.web.StringResponseOption; +import bio.overture.ego.utils.web.StringWebResource; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Collection; +import java.util.UUID; +import lombok.Getter; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Before; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.HttpHeaders; + +@Slf4j +public abstract class AbstractControllerTest { + + /** Constants */ + public static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final String ACCESS_TOKEN = "TestToken"; + + /** Config */ + + /** State */ + @LocalServerPort private int port; + + private TestRestTemplate restTemplate = new TestRestTemplate(); + + @Getter private HttpHeaders headers = new HttpHeaders(); + + @Before + public void setup() { + headers.add(AUTHORIZATION, "Bearer " + ACCESS_TOKEN); + headers.setContentType(APPLICATION_JSON); + beforeTest(); + } + + /** Additional setup before each test */ + protected abstract void beforeTest(); + + protected abstract boolean enableLogging(); + + public StringWebResource initStringRequest() { + val out = initStringRequest(this.headers); + return enableLogging() ? out.prettyLogging() : out; + } + + public StringWebResource initStringRequest(HttpHeaders headers) { + return new StringWebResource(restTemplate, getServerUrl()).headers(headers); + } + + public > BasicWebResource initRequest( + @NonNull Class responseType) { + return initRequest(responseType, this.headers); + } + + public > BasicWebResource initRequest( + @NonNull Class responseType, HttpHeaders headers) { + return new BasicWebResource(restTemplate, getServerUrl(), responseType).headers(headers); + } + + public String getServerUrl() { + return "http://localhost:" + port; + } + + @SneakyThrows + protected StringResponseOption addGroupPermissionToGroupPostRequestAnd( + Group g, Policy p, AccessLevel mask) { + val body = MaskDTO.builder().mask(mask).build(); + return initStringRequest() + .endpoint("/policies/%s/permission/group/%s", p.getId(), g.getId()) + .body(body) + .postAnd(); + } + + protected StringResponseOption addApplicationsToGroupPostRequestAnd( + Group g, Collection applications) { + val appIds = convertToIds(applications); + return addApplicationsToGroupPostRequestAnd(g.getId(), appIds); + } + + protected StringResponseOption addApplicationsToGroupPostRequestAnd( + UUID groupId, Collection applicationIds) { + return initStringRequest() + .endpoint("/groups/%s/applications", groupId) + .body(applicationIds) + .postAnd(); + } + + protected StringResponseOption deleteUsersFromGroupDeleteRequestAnd( + UUID groupId, Collection userIds) { + return initStringRequest() + .endpoint("/groups/%s/users/%s", groupId, COMMA.join(userIds)) + .deleteAnd(); + } + + protected StringResponseOption deleteUsersFromGroupDeleteRequestAnd( + Group g, Collection users) { + val userIds = convertToIds(users); + return deleteUsersFromGroupDeleteRequestAnd(g.getId(), userIds); + } + + protected StringResponseOption createApplicationPostRequestAnd(CreateApplicationRequest r) { + return initStringRequest().endpoint("/applications").body(r).postAnd(); + } + + protected StringResponseOption getGroupPermissionsForGroupGetRequestAnd(Group g) { + return initStringRequest().endpoint("/groups/%s/permissions", g.getId()).getAnd(); + } + + protected StringResponseOption addUsersToGroupPostRequestAnd( + UUID groupId, Collection userIds) { + return initStringRequest().endpoint("/groups/%s/users", groupId).body(userIds).postAnd(); + } + + protected StringResponseOption addUsersToGroupPostRequestAnd(Group g, Collection users) { + val userIds = convertToIds(users); + return addUsersToGroupPostRequestAnd(g.getId(), userIds); + } + + protected StringResponseOption getApplicationsForUserGetRequestAnd(User u) { + return initStringRequest().endpoint("/users/%s/applications", u.getId()).getAnd(); + } + + protected StringWebResource getUsersForApplicationEndpoint(UUID appId) { + return initStringRequest().endpoint("/applications/%s/users", appId); + } + + protected StringResponseOption getUsersForApplicationGetRequestAnd(UUID appId) { + return getUsersForApplicationEndpoint(appId).getAnd(); + } + + protected StringResponseOption getUsersForApplicationGetRequestAnd(Application a) { + return getUsersForApplicationGetRequestAnd(a.getId()); + } + + protected StringResponseOption getApplicationsForGroupGetRequestAnd(Group g) { + return initStringRequest().endpoint("/groups/%s/applications", g.getId()).getAnd(); + } + + protected StringResponseOption addApplicationsToUserPostRequestAnd( + UUID userId, Collection appIds) { + return initStringRequest().endpoint("/users/%s/applications", userId).body(appIds).postAnd(); + } + + protected StringResponseOption addApplicationsToUserPostRequestAnd( + User u, Collection apps) { + return addApplicationsToUserPostRequestAnd(u.getId(), convertToIds(apps)); + } + + protected StringResponseOption getUsersForGroupGetRequestAnd(UUID groupId) { + return initStringRequest().endpoint("/groups/%s/users", groupId).getAnd(); + } + + protected StringResponseOption getUsersForGroupGetRequestAnd(Group g) { + return getUsersForGroupGetRequestAnd(g.getId()); + } + + protected StringResponseOption deleteGroupDeleteRequestAnd(UUID groupId) { + return initStringRequest().endpoint("/groups/%s", groupId).deleteAnd(); + } + + protected StringResponseOption deleteGroupDeleteRequestAnd(Group g) { + return deleteGroupDeleteRequestAnd(g.getId()); + } + + protected StringResponseOption partialUpdateGroupPutRequestAnd( + UUID groupId, GroupRequest updateRequest) { + return initStringRequest().endpoint("/groups/%s", groupId).body(updateRequest).putAnd(); + } + + protected StringResponseOption partialUpdateApplicationPutRequestAnd( + UUID applicationId, UpdateApplicationRequest updateRequest) { + return initStringRequest() + .endpoint("/applications/%s", applicationId) + .body(updateRequest) + .putAnd(); + } + + protected StringResponseOption getGroupEntityGetRequestAnd(Group g) { + return initStringRequest().endpoint("/groups/%s", g.getId()).getAnd(); + } + + protected StringResponseOption createGroupPostRequestAnd(GroupRequest g) { + return initStringRequest().endpoint("/groups").body(g).postAnd(); + } + + protected StringResponseOption getUserEntityGetRequestAnd(User u) { + return initStringRequest().endpoint("/users/%s", u.getId()).getAnd(); + } + + protected StringResponseOption getApplicationEntityGetRequestAnd(UUID appId) { + return initStringRequest().endpoint("/applications/%s", appId).getAnd(); + } + + protected StringResponseOption getApplicationEntityGetRequestAnd(Application a) { + return getApplicationEntityGetRequestAnd(a.getId()); + } + + protected StringResponseOption getPolicyGetRequestAnd(Policy p) { + return initStringRequest().endpoint("/policies/%s", p.getId()).getAnd(); + } + + protected StringResponseOption getGroupsForUserGetRequestAnd(User u) { + return initStringRequest().endpoint("/users/%s/groups", u.getId()).getAnd(); + } + + protected StringWebResource getGroupsForApplicationEndpoint(UUID appId) { + return initStringRequest().endpoint("/applications/%s/groups", appId); + } + + protected StringResponseOption getGroupsForApplicationGetRequestAnd(UUID appId) { + return getGroupsForApplicationEndpoint(appId).getAnd(); + } + + protected StringResponseOption getGroupsForApplicationGetRequestAnd(Application a) { + return getGroupsForApplicationGetRequestAnd(a.getId()); + } + + protected StringResponseOption deleteApplicationFromGroupDeleteRequestAnd( + Group g, Application a) { + return initStringRequest() + .endpoint("/groups/%s/applications/%s", g.getId(), a.getId()) + .deleteAnd(); + } + + protected StringResponseOption deleteApplicationDeleteRequestAnd(Application a) { + return deleteApplicationDeleteRequestAnd(a.getId()); + } + + protected StringResponseOption deleteApplicationDeleteRequestAnd(UUID applicationId) { + return initStringRequest().endpoint("/applications/%s", applicationId).deleteAnd(); + } + + protected StringResponseOption deleteApplicationsFromGroupDeleteRequestAnd( + Group g, Collection apps) { + val appIdsToDelete = convertToIds(apps); + return deleteApplicationsFromGroupDeleteRequestAnd(g.getId(), appIdsToDelete); + } + + protected StringResponseOption deleteApplicationsFromGroupDeleteRequestAnd( + UUID groupId, Collection appIds) { + return initStringRequest() + .endpoint("/groups/%s/applications/%s", groupId, COMMA.join(appIds)) + .deleteAnd(); + } + + protected StringWebResource listGroupsEndpointAnd() { + return initStringRequest().endpoint("/groups"); + } + + protected StringWebResource listApplicationsEndpointAnd() { + return initStringRequest().endpoint("/applications"); + } +} diff --git a/src/test/java/bio/overture/ego/controller/AbstractPermissionControllerTest.java b/src/test/java/bio/overture/ego/controller/AbstractPermissionControllerTest.java new file mode 100644 index 000000000..9294226c7 --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/AbstractPermissionControllerTest.java @@ -0,0 +1,990 @@ +package bio.overture.ego.controller; + +import static bio.overture.ego.model.enums.AccessLevel.DENY; +import static bio.overture.ego.model.enums.AccessLevel.WRITE; +import static bio.overture.ego.utils.CollectionUtils.mapToList; +import static bio.overture.ego.utils.Collectors.toImmutableList; +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentId; +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentName; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CONFLICT; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.OK; + +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.entity.AbstractPermission; +import bio.overture.ego.model.entity.Identifiable; +import bio.overture.ego.model.entity.NameableEntity; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.enums.AccessLevel; +import bio.overture.ego.service.AbstractPermissionService; +import bio.overture.ego.service.NamedService; +import bio.overture.ego.service.PolicyService; +import bio.overture.ego.utils.EntityGenerator; +import bio.overture.ego.utils.Streams; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.Sets; +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import java.util.stream.IntStream; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.testcontainers.shaded.com.google.common.collect.ImmutableList; + +@Slf4j +public abstract class AbstractPermissionControllerTest< + O extends NameableEntity, P extends AbstractPermission> + extends AbstractControllerTest { + + /** Constants */ + private static final String INVALID_UUID = "invalidUUID000"; + + /** State */ + private O owner1; + + private O owner2; + private List policies; + private List permissionRequests; + + @Override + protected void beforeTest() { + // Initial setup of entities (run once) + this.owner1 = generateOwner(generateNonExistentOwnerName()); + this.owner2 = generateOwner(generateNonExistentOwnerName()); + this.policies = + IntStream.range(0, 2) + .boxed() + .map(x -> generateNonExistentName(getPolicyService())) + .map(x -> getEntityGenerator().setupSinglePolicy(x)) + .collect(toImmutableList()); + + this.permissionRequests = + ImmutableList.builder() + .add(PermissionRequest.builder().policyId(policies.get(0).getId()).mask(WRITE).build()) + .add(PermissionRequest.builder().policyId(policies.get(1).getId()).mask(DENY).build()) + .build(); + + // Sanity check + assertThat(getOwnerService().isExist(owner1.getId())).isTrue(); + policies.forEach(p -> assertThat(getPolicyService().isExist(p.getId())).isTrue()); + } + + /** Add permissions to a non-existent owner */ + @Test + public void addPermissionsToOwner_NonExistentOwner_NotFound() { + val nonExistentOwnerId = generateNonExistentId(getOwnerService()); + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(nonExistentOwnerId)) + .body(permissionRequests) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(r1.getBody()).contains(nonExistentOwnerId.toString()); + } + + /** Attempt to add an empty list of permission request to an owner */ + @Test + @SneakyThrows + public void addPermissionsToOwner_EmptyPermissionRequests_Conflict() { + // Add some of the permissions + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(newArrayList()) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(BAD_REQUEST); + } + + /** Add permissions to an owner that has SOME those permissions */ + @Test + @SneakyThrows + public void addPermissionsToOwner_SomeAlreadyExists_Conflict() { + val somePermissionRequests = ImmutableList.of(permissionRequests.get(0)); + + // Add some of the permissions + val r1 = + initRequest(getOwnerType()) + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(somePermissionRequests) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + assertThat(r1.getBody()).isNotNull(); + val r1body = r1.getBody(); + assertThat(r1body.getId()).isEqualTo(owner1.getId()); + + // Add all the permissions, including the one before + val r2 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(permissionRequests) + .post(); + assertThat(r2.getStatusCode()).isEqualTo(CONFLICT); + assertThat(r2.getBody()).isNotNull(); + } + + /** Add permissions to an owner that has all those permissions */ + @Test + @SneakyThrows + public void addPermissionsToOwner_DuplicateRequest_Conflict() { + log.info("Initially adding permissions to the owner"); + val r1 = + initRequest(getOwnerType()) + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(permissionRequests) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + assertThat(r1.getBody()).isNotNull(); + val r1body = r1.getBody(); + assertThat(r1body.getId()).isEqualTo(owner1.getId()); + + log.info("Add the same permissions to the owner. This means duplicates are being added"); + val r2 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(permissionRequests) + .post(); + assertThat(r2.getStatusCode()).isEqualTo(CONFLICT); + assertThat(r2.getBody()).isNotNull(); + } + + /** + * Create permissions for the owner, using one addPermissionRequest with multiple masks for a + * policyId + */ + @Test + public void addPermissionsToOwner_MultipleMasks_Conflict() { + val result = + stream(AccessLevel.values()) + .filter(x -> !x.equals(permissionRequests.get(0).getMask())) + .findAny(); + assertThat(result).isNotEmpty(); + val differentMask = result.get(); + + val newPermRequest = + PermissionRequest.builder() + .mask(differentMask) + .policyId(permissionRequests.get(0).getPolicyId()) + .build(); + + val newPolicyIdStringWithAccessLevel = + ImmutableList.builder() + .addAll(permissionRequests) + .add(newPermRequest) + .build(); + + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(newPolicyIdStringWithAccessLevel) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(CONFLICT); + assertThat(r1.getBody()).isNotNull(); + } + + /** Add permissions containing a non-existing policyId to an owner */ + @Test + public void addPermissionsToOwner_NonExistentPolicy_NotFound() { + val nonExistentPolicyId = generateNonExistentId(getPolicyService()); + + // inject a non existent id + permissionRequests.get(1).setPolicyId(nonExistentPolicyId); + + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(permissionRequests) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(r1.getBody()).contains(nonExistentPolicyId.toString()); + assertThat(r1.getBody()).doesNotContain(permissionRequests.get(0).getPolicyId().toString()); + } + + @Test + @SneakyThrows + public void addPermissions_CreateAndUpdate_Success() { + val permRequest1 = permissionRequests.get(0); + val permRequest2 = permissionRequests.get(1); + assertThat(permRequest1.getMask()).isNotEqualTo(permRequest2.getMask()); + + // Add initial Permission + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(ImmutableList.of(permRequest1)) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + // Update permRequest1 locally + val updatePermRequest1 = + PermissionRequest.builder() + .policyId(permRequest1.getPolicyId()) + .mask(permRequest2.getMask()) + .build(); + + // call addPerms for [updatedPermRequest1, permRequest2] + val r2 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(ImmutableList.of(updatePermRequest1, permRequest2)) + .post(); + assertThat(r2.getStatusCode()).isEqualTo(OK); + + // Get permissions for owner + val r3 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r3.getStatusCode()).isEqualTo(OK); + assertThat(r3.getBody()).isNotNull(); + + // Assert created permission is correct mask + val page = MAPPER.readTree(r3.getBody()); + val existingPermissionIndex = + Streams.stream(page.path("resultSet").iterator()) + .map(x -> MAPPER.convertValue(x, getPermissionType())) + .collect(toMap(x -> x.getPolicy().getId(), identity())); + assertThat(existingPermissionIndex.values()).hasSize(2); + + // verify permission with permRequest1.getPolicyId() and owner, has same mask as + // updatedPermRequest1.getMask() + assertThat(existingPermissionIndex).containsKey(updatePermRequest1.getPolicyId()); + assertThat(existingPermissionIndex.get(updatePermRequest1.getPolicyId()).getAccessLevel()) + .isEqualTo(updatePermRequest1.getMask()); + + // verify permission with permRequest2.getPolicyId() and owner, has same mask as + // permRequest2.getMask(); + assertThat(existingPermissionIndex).containsKey(permRequest2.getPolicyId()); + assertThat(existingPermissionIndex.get(permRequest2.getPolicyId()).getAccessLevel()) + .isEqualTo(permRequest2.getMask()); + } + + /** Happy path Add non-existent permissions to an owner, and read it back */ + @Test + @SneakyThrows + public void addPermissionsToOwner_Unique_Success() { + // Add Permissions to owner + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(permissionRequests) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + // Get the policies for this owner + val r3 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r3.getStatusCode()).isEqualTo(OK); + + // Analyze results + val page = MAPPER.readTree(r3.getBody()); + assertThat(page).isNotNull(); + assertThat(page.get("count").asInt()).isEqualTo(2); + val outputMap = + Streams.stream(page.path("resultSet").iterator()) + .collect( + toMap( + x -> x.path("policy").path("id").asText(), + x -> x.path("accessLevel").asText())); + assertThat(outputMap) + .containsKeys(policies.get(0).getId().toString(), policies.get(1).getId().toString()); + assertThat(outputMap.get(policies.get(0).getId().toString())).isEqualTo(WRITE.toString()); + assertThat(outputMap.get(policies.get(1).getId().toString())).isEqualTo(DENY.toString()); + } + + @Test + @SneakyThrows + public void deletePolicyWithPermissions_AlreadyExists_Success() { + // Add Permissions to owner + val permRequest = permissionRequests.get(0); + val body = ImmutableList.of(permRequest); + val r1 = + initStringRequest().endpoint(getAddPermissionsEndpoint(owner1.getId())).body(body).post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + // Get the policies for this owner + val r2 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r2.getStatusCode()).isEqualTo(OK); + + // Assert the expected permission ids exist + val page = MAPPER.readTree(r2.getBody()); + val existingPermissionIds = + Streams.stream(page.path("resultSet").iterator()) + .map(x -> x.get("id")) + .map(JsonNode::asText) + .collect(toImmutableSet()); + assertThat(existingPermissionIds).hasSize(1); + + // Delete the policy + val r3 = initStringRequest().endpoint("policies/%s", permRequest.getPolicyId()).delete(); + assertThat(r3.getStatusCode()).isEqualTo(OK); + + // Assert that the policy deletion cascaded the delete to the permissions + existingPermissionIds.stream() + .map(UUID::fromString) + .forEach(x -> assertThat(getPermissionService().isExist(x)).isFalse()); + + // Assert that the policy deletion DID NOT cascade past the permissions and delete the owner + assertThat(getOwnerService().isExist(owner1.getId())).isTrue(); + } + + @Test + @SneakyThrows + public void deleteOwnerWithPermissions_AlreadyExists_Success() { + // Add Permissions to owner + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(permissionRequests) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + // Get the policies for this owner + val r2 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r2.getStatusCode()).isEqualTo(OK); + + // Assert the expected permission ids exist + val page = MAPPER.readTree(r2.getBody()); + val existingPermissionIds = + Streams.stream(page.path("resultSet").iterator()) + .map(x -> x.get("id")) + .map(JsonNode::asText) + .collect(toImmutableSet()); + assertThat(existingPermissionIds).hasSize(permissionRequests.size()); + + // Delete the owner + val r3 = initStringRequest().endpoint(getDeleteOwnerEndpoint(owner1.getId())).delete(); + assertThat(r3.getStatusCode()).isEqualTo(OK); + + // Assert that the owner deletion cascaded the delete to the permissions + existingPermissionIds.stream() + .map(UUID::fromString) + .forEach(x -> assertThat(getPermissionService().isExist(x)).isFalse()); + + // Assert that the owner deletion DID NOT cascade past the permission and deleted policies + permissionRequests.stream() + .map(PermissionRequest::getPolicyId) + .distinct() + .forEach(x -> assertThat(getPolicyService().isExist(x)).isTrue()); + } + + @Test + @SneakyThrows + public void deletePermissionsForOwner_NonExistent_NotFound() { + // Add permissions to owner + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(permissionRequests) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + // Get permissions for owner + val r2 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r2.getStatusCode()).isEqualTo(OK); + assertThat(r2.getBody()).isNotNull(); + + // Assert the expected permission ids exist + val page = MAPPER.readTree(r2.getBody()); + val existingPermissionIds = + Streams.stream(page.path("resultSet").iterator()) + .map(x -> x.get("id")) + .map(JsonNode::asText) + .map(UUID::fromString) + .collect(toImmutableSet()); + assertThat(existingPermissionIds).hasSize(permissionRequests.size()); + + // Attempt to delete permissions for a nonExistent owner + val nonExistentOwnerId = generateNonExistentId(getOwnerService()); + val r3 = + initStringRequest() + .endpoint(getDeletePermissionsEndpoint(nonExistentOwnerId, existingPermissionIds)) + .delete(); + assertThat(r3.getStatusCode()).isEqualTo(NOT_FOUND); + + // Attempt to delete permissions for an existing owner but a non-existent permission id + val nonExistentPermissionId = generateNonExistentId(getPermissionService()); + val someExistingPermissionIds = Sets.newHashSet(); + someExistingPermissionIds.addAll(existingPermissionIds); + someExistingPermissionIds.add(nonExistentPermissionId); + assertThat(getOwnerService().isExist(owner1.getId())).isTrue(); + val r4 = + initStringRequest() + .endpoint(getDeletePermissionsEndpoint(owner1.getId(), someExistingPermissionIds)) + .delete(); + assertThat(r4.getStatusCode()).isEqualTo(NOT_FOUND); + } + + @Test + @SneakyThrows + public void deletePermissionsForPolicy_NonExistentOwner_NotFound() { + val permRequest = permissionRequests.get(0); + val policyId = permRequest.getPolicyId(); + val nonExistingOwnerId = generateNonExistentId(getOwnerService()); + val r3 = + initStringRequest() + .endpoint(getDeletePermissionEndpoint(policyId, nonExistingOwnerId)) + .delete(); + assertThat(r3.getStatusCode()).isEqualTo(NOT_FOUND); + } + + @Test + @SneakyThrows + public void deletePermissionsForPolicy_NonExistentPolicy_NotFound() { + val nonExistentPolicyId = generateNonExistentId(getPolicyService()); + val r3 = + initStringRequest() + .endpoint(getDeletePermissionEndpoint(nonExistentPolicyId, owner1.getId())) + .delete(); + assertThat(r3.getStatusCode()).isEqualTo(NOT_FOUND); + } + + @Test + @SneakyThrows + public void deletePermissionsForPolicy_DuplicateRequest_NotFound() { + val permRequest = permissionRequests.get(0); + val policyId = permRequest.getPolicyId(); + val mask = permRequest.getMask(); + + // Create a permission + val r1 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(policyId, owner1.getId())) + .body(createMaskJson(mask.toString())) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + // Assert the permission exists + val r2 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r2.getStatusCode()).isEqualTo(OK); + assertThat(r2.getBody()).isNotNull(); + val page = MAPPER.readTree(r2.getBody()); + val existingPermissionIds = + Streams.stream(page.path("resultSet").iterator()) + .map(x -> x.get("id")) + .map(JsonNode::asText) + .collect(toImmutableSet()); + assertThat(existingPermissionIds).hasSize(1); + + // Delete an existing permission + val r3 = + initStringRequest() + .endpoint(getDeletePermissionEndpoint(policyId, owner1.getId())) + .delete(); + assertThat(r3.getStatusCode()).isEqualTo(OK); + assertThat(r3.getBody()).isNotNull(); + + // Assert the permission no longer exists + val r4 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r4.getStatusCode()).isEqualTo(OK); + assertThat(r4.getBody()).isNotNull(); + val page2 = MAPPER.readTree(r4.getBody()); + val actualPermissionIds = + Streams.stream(page2.path("resultSet").iterator()) + .map(x -> x.get("id")) + .map(JsonNode::asText) + .collect(toImmutableSet()); + assertThat(actualPermissionIds).isEmpty(); + + // Delete an existing permission + val r5 = + initStringRequest() + .endpoint(getDeletePermissionEndpoint(policyId, owner1.getId())) + .delete(); + assertThat(r5.getStatusCode()).isEqualTo(NOT_FOUND); + assertThat(r5.getBody()).isNotNull(); + } + + @Test + @SneakyThrows + public void deletePermissionsForPolicy_AlreadyExists_Success() { + val permRequest = permissionRequests.get(0); + val policyId = permRequest.getPolicyId(); + val mask = permRequest.getMask(); + + // Create a permission + val r1 = + initStringRequest() + .endpoint(getDeletePermissionEndpoint(policyId, owner1.getId())) + .body(createMaskJson(mask.toString())) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + assertThat(r1.getBody()).isNotNull(); + + // Assert the permission exists + val r2 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r2.getStatusCode()).isEqualTo(OK); + assertThat(r2.getBody()).isNotNull(); + val page = MAPPER.readTree(r2.getBody()); + val existingPermissionIds = + Streams.stream(page.path("resultSet").iterator()) + .map(x -> x.get("id")) + .map(JsonNode::asText) + .collect(toImmutableSet()); + assertThat(existingPermissionIds).hasSize(1); + + // Delete an existing permission + val r3 = + initStringRequest() + .endpoint(getDeletePermissionEndpoint(policyId, owner1.getId())) + .delete(); + assertThat(r3.getStatusCode()).isEqualTo(OK); + assertThat(r3.getBody()).isNotNull(); + + // Assert the permission no longer exists + val r4 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r4.getStatusCode()).isEqualTo(OK); + assertThat(r4.getBody()).isNotNull(); + val page2 = MAPPER.readTree(r4.getBody()); + val actualPermissionIds = + Streams.stream(page2.path("resultSet").iterator()) + .map(x -> x.get("id")) + .map(JsonNode::asText) + .collect(toImmutableSet()); + assertThat(actualPermissionIds).isEmpty(); + } + + @Test + @SneakyThrows + public void deletePermissionsForOwner_AlreadyExists_Success() { + // Add owner permissions + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(permissionRequests) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + // Get permissions for the owner + val r2 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r2.getStatusCode()).isEqualTo(OK); + assertThat(r2.getBody()).isNotNull(); + + // Assert the expected permission ids exist + val page = MAPPER.readTree(r2.getBody()); + val existingPermissionIds = + Streams.stream(page.path("resultSet").iterator()) + .map(x -> x.get("id")) + .map(JsonNode::asText) + .map(UUID::fromString) + .collect(toImmutableSet()); + assertThat(existingPermissionIds).hasSize(permissionRequests.size()); + + // Delete the permissions for the owner + val r3 = + initStringRequest() + .endpoint(getDeletePermissionsEndpoint(owner1.getId(), existingPermissionIds)) + .delete(); + assertThat(r3.getStatusCode()).isEqualTo(OK); + + // Assert the expected permissions were deleted + val r4 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r4.getStatusCode()).isEqualTo(OK); + assertThat(r4.getBody()).isNotNull(); + val page4 = MAPPER.readTree(r4.getBody()); + val existingPermissionIds4 = + Streams.stream(page4.path("resultSet").iterator()) + .map(x -> x.get("id")) + .map(JsonNode::asText) + .collect(toImmutableSet()); + assertThat(existingPermissionIds4).isEmpty(); + + // Assert that the policies still exists + policies.forEach( + p -> { + val r5 = initStringRequest().endpoint("policies/%s", p.getId().toString()).get(); + assertThat(r5.getStatusCode()).isEqualTo(OK); + assertThat(r5.getBody()).isNotNull(); + }); + + // Assert the owner still exists + assertThat(getOwnerService().isExist(owner1.getId())).isTrue(); + } + + /** Using the owners controller, attempt to read a permission belonging to a non-existent owner */ + @Test + public void readPermissionsForOwner_NonExistent_NotFound() { + val nonExistentOwnerId = generateNonExistentId(getOwnerService()); + val r1 = initStringRequest().endpoint(getReadPermissionsEndpoint(nonExistentOwnerId)).get(); + assertThat(r1.getStatusCode()).isEqualTo(NOT_FOUND); + } + + /** PolicyController */ + + /** Using the policy controller, add a single permission for a non-existent owner */ + @Test + public void addPermissionToPolicy_NonExistentOwnerId_NotFound() { + val nonExistentOwnerId = generateNonExistentId(getOwnerService()); + + val r1 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(policies.get(0).getId(), nonExistentOwnerId)) + .body(createMaskJson(DENY.toString())) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(r1.getBody()).contains(nonExistentOwnerId.toString()); + } + + /** Using the policy controller, add a single permission for a non-existent policy */ + @Test + public void addPermissionToPolicy_NonExistentPolicyId_NotFound() { + val nonExistentPolicyId = generateNonExistentId(getPolicyService()); + + val r1 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(nonExistentPolicyId, owner1.getId())) + .body(createMaskJson(DENY.toString())) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(r1.getBody()).contains(nonExistentPolicyId.toString()); + } + + /** Add a single permission using the policy controller */ + @Test + @SneakyThrows + public void addPermissionToPolicy_Unique_Success() { + val permRequest = permissionRequests.get(0); + + // Create 2 requests with same policy but different owners + val r1 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(permRequest.getPolicyId(), owner1.getId())) + .body(createMaskJson(permRequest.getMask().toString())) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + val r2 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(permRequest.getPolicyId(), owner2.getId())) + .body(createMaskJson(permRequest.getMask().toString())) + .post(); + assertThat(r2.getStatusCode()).isEqualTo(OK); + + // Get the owners for the policy previously used + val r3 = + initStringRequest() + .endpoint(getReadOwnersForPolicyEndpoint(permRequest.getPolicyId())) + .get(); + assertThat(r3.getStatusCode()).isEqualTo(OK); + + // Assert that response contains both ownerIds, ownerNames and policyId + val body = MAPPER.readTree(r3.getBody()); + assertThat(body).isNotNull(); + + val expectedMap = uniqueIndex(asList(owner1, owner2), Identifiable::getId); + + Streams.stream(body.iterator()) + .forEach( + n -> { + val actualOwnerId = UUID.fromString(n.path("id").asText()); + val actualOwnerName = n.path("name").asText(); + val actualMask = AccessLevel.fromValue(n.path("mask").asText()); + assertThat(expectedMap).containsKey(actualOwnerId); + val expectedOwner = expectedMap.get(actualOwnerId); + assertThat(actualOwnerName).isEqualTo(expectedOwner.getName()); + assertThat(actualMask).isEqualTo(permRequest.getMask()); + }); + } + + /** Using the owners controller, add a permission with an undefined mask */ + @Test + public void addPermissionsToOwner_IncorrectMask_BadRequest() { + // Corrupt the request + val incorrectMask = "anIncorrectMask"; + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> AccessLevel.fromValue(incorrectMask)); + + val body = MAPPER.valueToTree(permissionRequests); + val firstElement = (ObjectNode) body.get(0); + firstElement.put("mask", incorrectMask); + + val r1 = + initStringRequest().endpoint(getAddPermissionsEndpoint(owner1.getId())).body(body).post(); + assertThat(r1.getStatusCode()).isEqualTo(BAD_REQUEST); + } + + /** Using the policy controller, add a permission with an undefined mask */ + @Test + public void addPermissionsToPolicy_IncorrectMask_BadRequest() { + // Corrupt the request + val incorrectMask = "anIncorrectMask"; + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> AccessLevel.fromValue(incorrectMask)); + + // Using the policy controller + val policyId = permissionRequests.get(0).getPolicyId(); + val r2 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(policyId, owner1.getId())) + .body(createMaskJson(incorrectMask)) + .post(); + assertThat(r2.getStatusCode()).isEqualTo(BAD_REQUEST); + } + + @Test + public void uuidValidationForOwner_MalformedUUID_BadRequest() { + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(INVALID_UUID)) + .body(permissionRequests) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(BAD_REQUEST); + + val r4 = initStringRequest().endpoint(getReadPermissionsEndpoint(INVALID_UUID)).get(); + assertThat(r4.getStatusCode()).isEqualTo(BAD_REQUEST); + + val r5 = + initStringRequest() + .endpoint(getDeletePermissionEndpoint(UUID.randomUUID().toString(), INVALID_UUID)) + .delete(); + assertThat(r5.getStatusCode()).isEqualTo(BAD_REQUEST); + + val r6 = + initStringRequest() + .endpoint(getDeletePermissionEndpoint(INVALID_UUID, UUID.randomUUID().toString())) + .delete(); + assertThat(r6.getStatusCode()).isEqualTo(BAD_REQUEST); + } + + @Test + public void uuidValidationForPolicy_MalformedUUID_BadRequest() { + val r1 = + initStringRequest() + .endpoint(getDeletePermissionEndpoint(UUID.randomUUID().toString(), INVALID_UUID)) + .delete(); + assertThat(r1.getStatusCode()).isEqualTo(BAD_REQUEST); + + val r2 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(UUID.randomUUID().toString(), INVALID_UUID)) + .body(createMaskJson(WRITE.toString())) + .post(); + assertThat(r2.getStatusCode()).isEqualTo(BAD_REQUEST); + + val r3 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(INVALID_UUID, UUID.randomUUID().toString())) + .body(createMaskJson(WRITE.toString())) + .post(); + assertThat(r3.getStatusCode()).isEqualTo(BAD_REQUEST); + + val r4 = + initStringRequest() + .endpoint(getDeletePermissionEndpoint(INVALID_UUID, UUID.randomUUID().toString())) + .delete(); + assertThat(r4.getStatusCode()).isEqualTo(BAD_REQUEST); + } + + @Test + @SneakyThrows + public void addPermissionsToPolicy_DuplicateRequests_Conflict() { + val permRequest = permissionRequests.get(0); + // Create 2 identical requests with same policy and owner + val r1 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(permRequest.getPolicyId(), owner1.getId())) + .body(createMaskJson(permRequest.getMask().toString())) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + val r2 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(permRequest.getPolicyId(), owner1.getId())) + .body(createMaskJson(permRequest.getMask().toString())) + .post(); + assertThat(r2.getStatusCode()).isEqualTo(CONFLICT); + } + + @Test + @SneakyThrows + public void updatePermissionsToOwner_AlreadyExists_Success() { + val permRequest1 = permissionRequests.get(0); + val permRequest2 = permissionRequests.get(1); + val updatedMask = permRequest2.getMask(); + val updatedPermRequest1 = + PermissionRequest.builder().policyId(permRequest1.getPolicyId()).mask(updatedMask).build(); + + assertThat(updatedMask).isNotEqualTo(permRequest1.getMask()); + assertThat(permRequest1.getMask()).isNotEqualTo(permRequest2.getMask()); + + // Create permission for owner + val r1 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(ImmutableList.of(permRequest1)) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + // Get created permissions + val r2 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r2.getStatusCode()).isEqualTo(OK); + assertThat(r2.getBody()).isNotNull(); + + // Assert created permission is correct mask + val page = MAPPER.readTree(r2.getBody()); + val existingPermissions = + Streams.stream(page.path("resultSet").iterator()) + .map(x -> MAPPER.convertValue(x, getPermissionType())) + .collect(toImmutableList()); + assertThat(existingPermissions).hasSize(1); + val permission = existingPermissions.get(0); + assertThat(permission.getAccessLevel()).isEqualTo(permRequest1.getMask()); + + // Update the permission + val r3 = + initStringRequest() + .endpoint(getAddPermissionsEndpoint(owner1.getId())) + .body(ImmutableList.of(updatedPermRequest1)) + .post(); + assertThat(r3.getStatusCode()).isEqualTo(OK); + + // Get updated permissions + val r4 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r4.getStatusCode()).isEqualTo(OK); + assertThat(r4.getBody()).isNotNull(); + + // Assert updated permission is correct mask + val page2 = MAPPER.readTree(r4.getBody()); + val existingPermissions2 = + Streams.stream(page2.path("resultSet").iterator()) + .map(x -> MAPPER.convertValue(x, getPermissionType())) + .collect(toImmutableList()); + assertThat(existingPermissions2).hasSize(1); + val permission2 = existingPermissions2.get(0); + assertThat(permission2.getAccessLevel()).isEqualTo(updatedMask); + } + + @Test + @SneakyThrows + public void updatePermissionsToPolicy_AlreadyExists_Success() { + val permRequest1 = permissionRequests.get(0); + val permRequest2 = permissionRequests.get(1); + assertThat(permRequest1.getMask()).isNotEqualTo(permRequest2.getMask()); + + // Create permission for owner and policy + val r1 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(permRequest1.getPolicyId(), owner1.getId())) + .body(createMaskJson(permRequest1.getMask().toString())) + .post(); + assertThat(r1.getStatusCode()).isEqualTo(OK); + + // Get created permissions + val r2 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r2.getStatusCode()).isEqualTo(OK); + assertThat(r2.getBody()).isNotNull(); + + // Assert created permission is correct mask + val page = MAPPER.readTree(r2.getBody()); + val existingPermissions = + Streams.stream(page.path("resultSet").iterator()) + .map(x -> MAPPER.convertValue(x, getPermissionType())) + .collect(toImmutableList()); + assertThat(existingPermissions).hasSize(1); + val permission = existingPermissions.get(0); + assertThat(permission.getAccessLevel()).isEqualTo(permRequest1.getMask()); + + // Update the permission + val r3 = + initStringRequest() + .endpoint(getAddPermissionEndpoint(permRequest1.getPolicyId(), owner1.getId())) + .body(createMaskJson(permRequest2.getMask().toString())) + .post(); + assertThat(r3.getStatusCode()).isEqualTo(OK); + + // Get updated permissions + val r4 = initStringRequest().endpoint(getReadPermissionsEndpoint(owner1.getId())).get(); + assertThat(r4.getStatusCode()).isEqualTo(OK); + assertThat(r4.getBody()).isNotNull(); + + // Assert updated permission is correct mask + val page2 = MAPPER.readTree(r4.getBody()); + val existingPermissions2 = + Streams.stream(page2.path("resultSet").iterator()) + .map(x -> MAPPER.convertValue(x, getPermissionType())) + .collect(toImmutableList()); + assertThat(existingPermissions2).hasSize(1); + val permission2 = existingPermissions2.get(0); + assertThat(permission2.getAccessLevel()).isEqualTo(permRequest2.getMask()); + } + + /** Necessary abstract methods for a generic abstract test */ + + // Commonly used + protected abstract EntityGenerator getEntityGenerator(); + + protected abstract PolicyService getPolicyService(); + + // Owner specific + protected abstract Class getOwnerType(); + + protected abstract O generateOwner(String name); + + protected abstract NamedService getOwnerService(); + + protected abstract String generateNonExistentOwnerName(); + + // Permission specific + protected abstract Class

getPermissionType(); + + protected abstract AbstractPermissionService getPermissionService(); + + // Endpoints + protected abstract String getAddPermissionsEndpoint(String ownerId); + + protected abstract String getAddPermissionEndpoint(String policyId, String ownerId); + + protected abstract String getReadPermissionsEndpoint(String ownerId); + + protected abstract String getDeleteOwnerEndpoint(String ownerId); + + protected abstract String getDeletePermissionsEndpoint( + String ownerId, Collection permissionIds); + + protected abstract String getDeletePermissionEndpoint(String policyId, String ownerId); + + protected abstract String getReadOwnersForPolicyEndpoint(String policyId); + + /** For convenience */ + private String getReadOwnersForPolicyEndpoint(UUID policyId) { + return getReadOwnersForPolicyEndpoint(policyId.toString()); + } + + private String getAddPermissionsEndpoint(UUID ownerId) { + return getAddPermissionsEndpoint(ownerId.toString()); + } + + private String getAddPermissionEndpoint(UUID policyId, UUID ownerId) { + return getAddPermissionEndpoint(policyId.toString(), ownerId.toString()); + } + + private String getReadPermissionsEndpoint(UUID ownerId) { + return getReadPermissionsEndpoint(ownerId.toString()); + } + + private String getDeleteOwnerEndpoint(UUID ownerId) { + return getDeleteOwnerEndpoint(ownerId.toString()); + } + + private String getDeletePermissionsEndpoint(UUID ownerId, Collection permissionIds) { + return getDeletePermissionsEndpoint( + ownerId.toString(), mapToList(permissionIds, UUID::toString)); + } + + private String getDeletePermissionEndpoint(UUID policyId, UUID ownerId) { + return getDeletePermissionEndpoint(policyId.toString(), ownerId.toString()); + } + + public static ObjectNode createMaskJson(String maskStringValue) { + return MAPPER.createObjectNode().put("mask", maskStringValue); + } +} diff --git a/src/test/java/bio/overture/ego/controller/ApplicationControllerTest.java b/src/test/java/bio/overture/ego/controller/ApplicationControllerTest.java new file mode 100644 index 000000000..86e0f0c4c --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/ApplicationControllerTest.java @@ -0,0 +1,647 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.controller; + +import static bio.overture.ego.controller.resolver.PageableResolver.LIMIT; +import static bio.overture.ego.controller.resolver.PageableResolver.OFFSET; +import static bio.overture.ego.model.enums.JavaFields.GROUPAPPLICATIONS; +import static bio.overture.ego.model.enums.JavaFields.ID; +import static bio.overture.ego.model.enums.JavaFields.NAME; +import static bio.overture.ego.model.enums.JavaFields.STATUS; +import static bio.overture.ego.model.enums.JavaFields.USERS; +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static bio.overture.ego.utils.CollectionUtils.repeatedCallsOf; +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentClientId; +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentId; +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentName; +import static bio.overture.ego.utils.EntityGenerator.randomApplicationType; +import static bio.overture.ego.utils.EntityGenerator.randomEnum; +import static bio.overture.ego.utils.EntityGenerator.randomEnumExcluding; +import static bio.overture.ego.utils.EntityGenerator.randomStatusType; +import static bio.overture.ego.utils.EntityGenerator.randomStringNoSpaces; +import static bio.overture.ego.utils.EntityGenerator.randomStringWithSpaces; +import static bio.overture.ego.utils.Streams.stream; +import static com.google.common.collect.Lists.newArrayList; +import static org.assertj.core.api.Assertions.assertThat; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.model.dto.CreateApplicationRequest; +import bio.overture.ego.model.dto.UpdateApplicationRequest; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.ApplicationType; +import bio.overture.ego.model.enums.StatusType; +import bio.overture.ego.service.ApplicationService; +import bio.overture.ego.utils.EntityGenerator; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.List; +import lombok.Builder; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.commons.lang.NotImplementedException; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@Slf4j +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ApplicationControllerTest extends AbstractControllerTest { + + private static boolean hasRunEntitySetup = false; + + /** Dependencies */ + @Autowired private EntityGenerator entityGenerator; + + @Autowired private ApplicationService applicationService; + + @Value("${logging.test.controller.enable}") + private boolean enableLogging; + + @Override + protected void beforeTest() { + // Initial setup of entities (run once + if (!hasRunEntitySetup) { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + entityGenerator.setupTestGroups(); + hasRunEntitySetup = true; + } + } + + @Override + protected boolean enableLogging() { + return enableLogging; + } + + @Test + @SneakyThrows + public void addApplication_Success() { + val app = + Application.builder() + .name("addApplication_Success") + .clientId("addApplication_Success") + .clientSecret("addApplication_Success") + .redirectUri("http://example.com") + .status(APPROVED) + .type(ApplicationType.CLIENT) + .build(); + + val response = initStringRequest().endpoint("/applications").body(app).post(); + + val responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + val responseJson = MAPPER.readTree(response.getBody()); + assertThat(responseJson.get("name").asText()).isEqualTo("addApplication_Success"); + } + + @Test + @SneakyThrows + public void addDuplicateApplication_Conflict() { + val app1 = + Application.builder() + .name("addDuplicateApplication") + .clientId("addDuplicateApplication") + .clientSecret("addDuplicateApplication") + .redirectUri("http://example.com") + .status(APPROVED) + .type(ApplicationType.CLIENT) + .build(); + + val app2 = + Application.builder() + .name("addDuplicateApplication") + .clientId("addDuplicateApplication") + .clientSecret("addDuplicateApplication") + .redirectUri("http://example.com") + .status(APPROVED) + .type(ApplicationType.CLIENT) + .build(); + + val response1 = initStringRequest().endpoint("/applications").body(app1).post(); + + val responseStatus1 = response1.getStatusCode(); + assertThat(responseStatus1).isEqualTo(HttpStatus.OK); + + val response2 = initStringRequest().endpoint("/applications").body(app2).post(); + val responseStatus2 = response2.getStatusCode(); + assertThat(responseStatus2).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + @SneakyThrows + public void getApplication_Success() { + val application = applicationService.getByClientId("111111"); + getApplicationEntityGetRequestAnd(application) + .assertEntityOfType(Application.class) + .isEqualToIgnoringGivenFields(application, GROUPAPPLICATIONS, USERS); + } + + @Test + public void findApplications_FindAllQuery_Success() { + // Generate data + val data = generateUniqueTestApplicationData(); + + // Get total count of applications + val totalApplications = (int) applicationService.getRepository().count(); + + // List all applications + val actualApps = + listApplicationsEndpointAnd() + .queryParam("offset", 0) + .queryParam("limit", totalApplications) + .getAnd() + .extractPageResults(Application.class); + + // Assert the generated applications are included in the list + assertThat(actualApps).hasSize(totalApplications); + assertThat(actualApps).containsAll(data.getApplications()); + } + + @Test + @Ignore("Should be tested") + public void findApplications_FindSomeQuery_Success() { + throw new NotImplementedException( + "need to implement the test 'getApplications_FindSomeQuery_Success'"); + } + + @Test + @Ignore("Should be tested") + public void getGroupsFromApplication_FindSomeQuery_Success() { + throw new NotImplementedException( + "need to implement the test 'getGroupsFromApplication_FindSomeQuery_Success'"); + } + + @Test + @Ignore("Should be tested") + public void getUsersFromApplication_FindSomeQuery_Success() { + throw new NotImplementedException( + "need to implement the test 'getUsersFromApplication_FindSomeQuery_Success'"); + } + + @Test + public void createApplication_NullValuesForRequiredFields_BadRequest() { + // Create with null values + val r1 = CreateApplicationRequest.builder().build(); + + // Assert that a bad request is returned + createApplicationPostRequestAnd(r1).assertBadRequest(); + } + + @Test + public void createApplication_NonExisting_Success() { + // Create application request + val createRequest = + CreateApplicationRequest.builder() + .clientId(randomStringNoSpaces(6)) + .clientSecret(randomStringNoSpaces(6)) + .name(randomStringNoSpaces(6)) + .status(randomStatusType()) + .type(randomApplicationType()) + .build(); + + // Create the application using the request + val app = createApplicationPostRequestAnd(createRequest).extractOneEntity(Application.class); + assertThat(app).isEqualToIgnoringGivenFields(createRequest, ID, GROUPAPPLICATIONS, USERS); + + // Get the application + getApplicationEntityGetRequestAnd(app) + .assertEntityOfType(Application.class) + .isEqualToIgnoringGivenFields(createRequest, ID, GROUPAPPLICATIONS, USERS); + } + + @Test + public void createApplication_NameAlreadyExists_Conflict() { + val name = generateNonExistentName(applicationService); + // Create application request + val createRequest = + CreateApplicationRequest.builder() + .clientId(randomStringNoSpaces(6)) + .clientSecret(randomStringNoSpaces(6)) + .name(name) + .status(randomStatusType()) + .type(randomApplicationType()) + .build(); + + // Create the application using the request + val expectedApp = + createApplicationPostRequestAnd(createRequest).extractOneEntity(Application.class); + + // Assert app exists + getApplicationEntityGetRequestAnd(expectedApp).assertOk(); + + // Create another create request with the same name + val createRequest2 = + CreateApplicationRequest.builder() + .clientId(randomStringNoSpaces(2)) + .clientSecret(randomStringNoSpaces(6)) + .name(name) + .status(randomStatusType()) + .type(randomApplicationType()) + .build(); + + // Assert that creating an application with an existing name, results in a CONFLICT + createApplicationPostRequestAnd(createRequest2).assertConflict(); + } + + @Test + public void createApplication_ClientIdAlreadyExists_Conflict() { + val clientId = generateNonExistentClientId(applicationService); + val name1 = generateNonExistentName(applicationService); + // Create application request + val createRequest = + CreateApplicationRequest.builder() + .clientId(clientId) + .clientSecret(randomStringNoSpaces(6)) + .name(name1) + .status(randomStatusType()) + .type(randomApplicationType()) + .build(); + + // Create the application using the request + val expectedApp = + createApplicationPostRequestAnd(createRequest).extractOneEntity(Application.class); + + // Assert app exists + getApplicationEntityGetRequestAnd(expectedApp).assertOk(); + + val name2 = generateNonExistentName(applicationService); + // Create another create request with the same name + val createRequest2 = + CreateApplicationRequest.builder() + .clientId(clientId) + .clientSecret(randomStringNoSpaces(6)) + .name(name2) + .status(randomStatusType()) + .type(randomApplicationType()) + .build(); + + // Assert that creating an application with an existing clientId, results in a CONFLICT + createApplicationPostRequestAnd(createRequest2).assertConflict(); + } + + @Test + public void deleteApplication_NonExisting_NotFound() { + // Create an non-existing application Id + val nonExistentId = generateNonExistentId(applicationService); + + // Assert that deleting a non-existing applicationId results in NOT_FOUND error + deleteApplicationDeleteRequestAnd(nonExistentId).assertNotFound(); + } + + @Test + public void deleteApplicationAndRelationshipsOnly_AlreadyExisting_Success() { + // Generate data + val data = generateUniqueTestApplicationData(); + val group0 = data.getGroups().get(0); + val app0 = data.getApplications().get(0); + val user0 = data.getUsers().get(0); + + // Add Applications to Group0 + addApplicationsToGroupPostRequestAnd(group0, newArrayList(app0)).assertOk(); + + // Assert group0 was added to app0 + getGroupsForApplicationGetRequestAnd(app0) + .assertPageResultsOfType(Group.class) + .containsExactly(group0); + + // Add user0 to app0 + addApplicationsToUserPostRequestAnd(user0, newArrayList(app0)).assertOk(); + + // Assert user0 was added to app0 + getUsersForApplicationGetRequestAnd(app0) + .assertPageResultsOfType(User.class) + .containsExactly(user0); + + // Delete App0 + deleteApplicationDeleteRequestAnd(app0).assertOk(); + + // Assert app0 was deleted + getApplicationEntityGetRequestAnd(app0).assertNotFound(); + + // Assert user0 still exists + getUserEntityGetRequestAnd(user0).assertOk(); + + // Assert group0 still exists + getGroupEntityGetRequestAnd(group0).assertOk(); + + // Assert user0 is associated with 0 applications + getApplicationsForUserGetRequestAnd(user0).assertPageResultsOfType(Application.class).isEmpty(); + + // Assert group0 is associated with 0 applications + getApplicationsForGroupGetRequestAnd(group0).assertPageResultsOfType(Group.class).isEmpty(); + } + + @Test + public void getApplication_ExistingApplication_Success() { + val data = generateUniqueTestApplicationData(); + val app0 = data.getApplications().get(0); + + // Assert app0 can be read + getApplicationEntityGetRequestAnd(app0) + .assertEntityOfType(Application.class) + .isEqualToIgnoringGivenFields(app0, GROUPAPPLICATIONS, USERS); + } + + @Test + public void getApplication_NonExistentApplication_NotFound() { + // Create non-existing application id + val nonExistingId = generateNonExistentId(applicationService); + + // Assert that the id cannot be read and throws a NOT_FOUND error + getApplicationEntityGetRequestAnd(nonExistingId).assertNotFound(); + } + + @Test + public void UUIDValidation_MalformedUUID_BadRequest() { + val badUUID = "123sksk"; + + initStringRequest().endpoint("/applications/%s", badUUID).deleteAnd().assertBadRequest(); + + initStringRequest().endpoint("/applications/%s", badUUID).getAnd().assertBadRequest(); + + val dummyUpdateRequest = UpdateApplicationRequest.builder().build(); + initStringRequest() + .endpoint("/applications/%s", badUUID) + .body(dummyUpdateRequest) + .putAnd() + .assertBadRequest(); + + initStringRequest().endpoint("/applications/%s/groups", badUUID).getAnd().assertBadRequest(); + + initStringRequest().endpoint("/applications/%s/users", badUUID).getAnd().assertBadRequest(); + } + + private UpdateApplicationRequest randomUpdateApplicationRequest() { + val name = generateNonExistentName(applicationService); + return UpdateApplicationRequest.builder() + .name(name) + .status(randomEnum(StatusType.class)) + .clientId(randomStringNoSpaces(7)) + .clientSecret(randomStringNoSpaces(7)) + .redirectUri(randomStringNoSpaces(7)) + .description(randomStringWithSpaces(100)) + .type(randomEnum(ApplicationType.class)) + .build(); + } + + @Test + public void updateApplication_ExistingApplication_Success() { + // Generate data + val data = generateUniqueTestApplicationData(); + val app0 = data.getApplications().get(0); + + // Create updateRequest1 + val updateRequest1 = + UpdateApplicationRequest.builder() + .name(generateNonExistentName(applicationService)) + .build(); + assertThat(app0.getName()).isNotEqualTo(updateRequest1.getName()); + + // Update app0 with updateRequest1, and assert the name changed + val app0_before0 = getApplicationEntityGetRequestAnd(app0).extractOneEntity(Application.class); + partialUpdateApplicationPutRequestAnd(app0.getId(), updateRequest1).assertOk(); + val app0_after0 = getApplicationEntityGetRequestAnd(app0).extractOneEntity(Application.class); + assertThat(app0_before0) + .isEqualToIgnoringGivenFields(app0_after0, ID, GROUPAPPLICATIONS, USERS, NAME); + + // Update app0 with empty update request, and assert nothing changed + val app0_before1 = getApplicationEntityGetRequestAnd(app0).extractOneEntity(Application.class); + partialUpdateApplicationPutRequestAnd(app0.getId(), UpdateApplicationRequest.builder().build()) + .assertOk(); + val app0_after1 = getApplicationEntityGetRequestAnd(app0).extractOneEntity(Application.class); + assertThat(app0_before1).isEqualTo(app0_after1); + + // Update the status field, and assert only that was updated + val app0_before2 = getApplicationEntityGetRequestAnd(app0).extractOneEntity(Application.class); + val updateRequest2 = + UpdateApplicationRequest.builder() + .status(randomEnumExcluding(StatusType.class, app0_before2.getStatus())) + .build(); + partialUpdateApplicationPutRequestAnd(app0.getId(), updateRequest2).assertOk(); + val app0_after2 = getApplicationEntityGetRequestAnd(app0).extractOneEntity(Application.class); + assertThat(app0_before2) + .isEqualToIgnoringGivenFields(app0_after2, ID, GROUPAPPLICATIONS, USERS, STATUS); + assertThat(app0_before2.getStatus()).isNotEqualTo(app0_after2.getStatus()); + assertThat(app0_after2.getStatus()).isEqualTo(updateRequest2.getStatus()); + } + + @Test + public void updateApplication_NonExistentApplication_NotFound() { + // Generate a non-existing applicaiton Id + val nonExistentId = generateNonExistentId(applicationService); + + // Assert that updating a non-existing application results in NOT_FOUND error + partialUpdateApplicationPutRequestAnd(nonExistentId, UpdateApplicationRequest.builder().build()) + .assertNotFound(); + } + + @Test + public void updateApplication_NameAlreadyExists_Conflict() { + // Generate data + val data = generateUniqueTestApplicationData(); + val app0 = data.getApplications().get(0); + val app1 = data.getApplications().get(1); + + // Create update request with the same name as app1 + val updateRequest = UpdateApplicationRequest.builder().name(app1.getName()).build(); + + // Update app0 with the same name as app1, and assert a CONFLICT + partialUpdateApplicationPutRequestAnd(app0.getId(), updateRequest).assertConflict(); + } + + @Test + public void updateApplication_ClientIdAlreadyExists_Conflict() { + // Generate data + val data = generateUniqueTestApplicationData(); + val app0 = data.getApplications().get(0); + val app1 = data.getApplications().get(1); + + // Create update request with the same name as app1 + val updateRequest = UpdateApplicationRequest.builder().clientId(app1.getClientId()).build(); + + // Update app0 with the same clientId as app1, and assert a CONFLICT + partialUpdateApplicationPutRequestAnd(app0.getId(), updateRequest).assertConflict(); + } + + @Test + public void statusValidation_MalformedStatus_BadRequest() { + // Assert the invalid status is actually invalid + val invalidStatus = "something123"; + val match = stream(StatusType.values()).anyMatch(x -> x.toString().equals(invalidStatus)); + assertThat(match).isFalse(); + + // Generate data + val data = generateUniqueTestApplicationData(); + val app0 = data.getApplications().get(0); + + // Build application create request with invalid status + val createRequest = + CreateApplicationRequest.builder() + .name(randomStringNoSpaces(7)) + .clientId(randomStringNoSpaces(7)) + .clientSecret(randomStringNoSpaces(7)) + .type(randomEnum(ApplicationType.class)) + .build(); + val createRequestJson = (ObjectNode) MAPPER.valueToTree(createRequest); + createRequestJson.put("status", invalidStatus); + + // Create application with invalid request, and assert BAD_REQUEST + initStringRequest() + .endpoint("/applications") + .body(createRequestJson) + .postAnd() + .assertBadRequest(); + + // Build application update request with invalid status + val updateRequestJson = MAPPER.createObjectNode().put("status", invalidStatus); + initStringRequest() + .endpoint("/applications/%s", app0.getId()) + .body(updateRequestJson) + .putAnd() + .assertBadRequest(); + } + + @Test + public void applicationTypeValidation_MalformedApplicationType_BadRequest() { + // Assert the invalid status is actually invalid + val invalidApplicationType = "something123"; + val match = + stream(ApplicationType.values()).anyMatch(x -> x.toString().equals(invalidApplicationType)); + assertThat(match).isFalse(); + + // Generate data + val data = generateUniqueTestApplicationData(); + val app0 = data.getApplications().get(0); + + // Build application create request with invalid application Type + val createRequest = + CreateApplicationRequest.builder() + .name(randomStringNoSpaces(7)) + .clientId(randomStringNoSpaces(7)) + .clientSecret(randomStringNoSpaces(7)) + .status(randomEnum(StatusType.class)) + .build(); + val createRequestJson = (ObjectNode) MAPPER.valueToTree(createRequest); + createRequestJson.put("type", invalidApplicationType); + + // Create application with invalid request, and assert BAD_REQUEST + initStringRequest() + .endpoint("/applications") + .body(createRequestJson) + .postAnd() + .assertBadRequest(); + + // Build application update request with invalid status + val updateRequestJson = MAPPER.createObjectNode().put("type", invalidApplicationType); + initStringRequest() + .endpoint("/applications/%s", app0.getId()) + .body(updateRequestJson) + .putAnd() + .assertBadRequest(); + } + + @Test + public void getGroupsFromApplication_FindAllQuery_Success() { + // Generate data + val data = generateUniqueTestApplicationData(); + val app0 = data.getApplications().get(0); + val groups = data.getGroups(); + + // Add groups to app0 + groups.forEach(g -> addApplicationsToGroupPostRequestAnd(g, newArrayList(app0)).assertOk()); + + // Assert all associated groups with the application can be read + getGroupsForApplicationEndpoint(app0.getId()) + .queryParam(OFFSET, 0) + .queryParam(LIMIT, groups.size() + 100) + .getAnd() + .assertPageResultsOfType(Group.class) + .containsExactlyInAnyOrderElementsOf(groups); + } + + @Test + public void getGroupsFromApplication_NonExistentApplication_NotFound() { + // Generate non existing applicaition id + val nonExistentId = generateNonExistentId(applicationService); + + // Read non existing application id and assert its NOT_FOUND + getGroupsForApplicationGetRequestAnd(nonExistentId).assertNotFound(); + } + + @Test + public void getUsersFromApplication_FindAllQuery_Success() { + // Generate data + val data = generateUniqueTestApplicationData(); + val app0 = data.getApplications().get(0); + val users = data.getUsers(); + + // Add users to app0 + users.forEach(u -> addApplicationsToUserPostRequestAnd(u, newArrayList(app0)).assertOk()); + + // Assert all associated users with the application can be read + getUsersForApplicationEndpoint(app0.getId()) + .queryParam(OFFSET, 0) + .queryParam(LIMIT, users.size() + 100) + .getAnd() + .assertPageResultsOfType(User.class) + .containsExactlyInAnyOrderElementsOf(users); + } + + @Test + public void getUsersFromApplication_NonExistentGroup_NotFound() { + // Generate non existing applicaition id + val nonExistentId = generateNonExistentId(applicationService); + + // Read non existing application id and assert its NOT_FOUND + getUsersForApplicationGetRequestAnd(nonExistentId).assertNotFound(); + } + + @SneakyThrows + private TestApplicationData generateUniqueTestApplicationData() { + val applications = repeatedCallsOf(() -> entityGenerator.generateRandomApplication(), 2); + val groups = repeatedCallsOf(() -> entityGenerator.generateRandomGroup(), 3); + val users = repeatedCallsOf(() -> entityGenerator.generateRandomUser(), 3); + + return TestApplicationData.builder() + .applications(applications) + .users(users) + .groups(groups) + .build(); + } + + @lombok.Value + @Builder + public static class TestApplicationData { + @NonNull private final List groups; + @NonNull private final List applications; + @NonNull private final List users; + } +} diff --git a/src/test/java/bio/overture/ego/controller/GroupControllerTest.java b/src/test/java/bio/overture/ego/controller/GroupControllerTest.java new file mode 100644 index 000000000..c9881acda --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/GroupControllerTest.java @@ -0,0 +1,1135 @@ +package bio.overture.ego.controller; + +import static bio.overture.ego.model.enums.AccessLevel.DENY; +import static bio.overture.ego.model.enums.AccessLevel.READ; +import static bio.overture.ego.model.enums.AccessLevel.WRITE; +import static bio.overture.ego.model.enums.JavaFields.DESCRIPTION; +import static bio.overture.ego.model.enums.JavaFields.GROUPAPPLICATIONS; +import static bio.overture.ego.model.enums.JavaFields.NAME; +import static bio.overture.ego.model.enums.JavaFields.PERMISSIONS; +import static bio.overture.ego.model.enums.JavaFields.STATUS; +import static bio.overture.ego.model.enums.JavaFields.USERGROUPS; +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static bio.overture.ego.model.enums.StatusType.DISABLED; +import static bio.overture.ego.model.enums.StatusType.PENDING; +import static bio.overture.ego.model.enums.StatusType.REJECTED; +import static bio.overture.ego.utils.CollectionUtils.concatToSet; +import static bio.overture.ego.utils.CollectionUtils.mapToImmutableSet; +import static bio.overture.ego.utils.CollectionUtils.mapToList; +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static bio.overture.ego.utils.CollectionUtils.repeatedCallsOf; +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static bio.overture.ego.utils.Converters.convertToIds; +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentId; +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentName; +import static bio.overture.ego.utils.EntityGenerator.randomEnum; +import static bio.overture.ego.utils.EntityGenerator.randomEnumExcluding; +import static bio.overture.ego.utils.EntityTools.extractAppIds; +import static bio.overture.ego.utils.EntityTools.extractGroupIds; +import static bio.overture.ego.utils.EntityTools.extractIDs; +import static bio.overture.ego.utils.Joiners.COMMA; +import static bio.overture.ego.utils.Streams.stream; +import static com.google.common.collect.Lists.newArrayList; +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toList; +import static net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER; +import static net.javacrumbs.jsonunit.core.Option.IGNORING_EXTRA_ARRAY_ITEMS; +import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.model.dto.GroupRequest; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.GroupPermission; +import bio.overture.ego.model.entity.Identifiable; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.AccessLevel; +import bio.overture.ego.model.enums.StatusType; +import bio.overture.ego.model.join.GroupApplication; +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.repository.GroupPermissionRepository; +import bio.overture.ego.repository.GroupRepository; +import bio.overture.ego.service.ApplicationService; +import bio.overture.ego.service.GroupService; +import bio.overture.ego.service.UserService; +import bio.overture.ego.utils.EntityGenerator; +import java.util.List; +import java.util.UUID; +import lombok.Builder; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.commons.lang.NotImplementedException; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@Slf4j +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class GroupControllerTest extends AbstractControllerTest { + + private boolean hasRunEntitySetup = false; + + /** Dependencies */ + @Autowired private EntityGenerator entityGenerator; + + @Autowired private GroupService groupService; + @Autowired private UserService userService; + @Autowired private ApplicationService applicationService; + @Autowired private GroupPermissionRepository groupPermissionRepository; + @Autowired private GroupRepository groupRepository; + + @Value("${logging.test.controller.enable}") + private boolean enableLogging; + + @Override + protected boolean enableLogging() { + return enableLogging; + } + + @Override + protected void beforeTest() { + // Initial setup of entities (run once + if (!hasRunEntitySetup) { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + entityGenerator.setupTestGroups(); + hasRunEntitySetup = true; + } + } + + @Test + public void addGroup() { + val groupRequest = + GroupRequest.builder().name("Wizards").status(PENDING).description("").build(); + createGroupPostRequestAnd(groupRequest).assertOk(); + } + + @Test + public void addUniqueGroup() { + val group = entityGenerator.setupGroup("SameSame"); + val groupRequest = + GroupRequest.builder() + .name(group.getName()) + .status(group.getStatus()) + .description(group.getDescription()) + .build(); + + createGroupPostRequestAnd(groupRequest).assertConflict(); + } + + @Test + public void getGroup() { + // Groups created in setup + val group = groupService.getByName("Group One"); + val groupId = group.getId(); + + val actualBody = + getGroupEntityGetRequestAnd(group).assertOk().assertHasBody().getResponse().getBody(); + + val expectedBody = + format( + "{\"id\":\"%s\",\"name\":\"Group One\",\"description\":\"\",\"status\":\"PENDING\"}", + groupId); + + assertThat(actualBody).isEqualTo(expectedBody); + } + + @Test + public void getGroupNotFound() { + initStringRequest().endpoint("/groups/%s", UUID.randomUUID()).getAnd().assertNotFound(); + } + + @Test + public void listGroups() { + + val totalGroups = groupService.getRepository().count(); + + // Get all groups + + val actualBody = + listGroupsEndpointAnd() + .queryParam("offset", 0) + .queryParam("limit", totalGroups) + .getAnd() + .assertOk() + .assertHasBody() + .getResponse() + .getBody(); + + val expectedBody = + format( + "[{\"id\":\"%s\",\"name\":\"Group One\",\"description\":\"\",\"status\":\"PENDING\"}, {\"id\":\"%s\",\"name\":\"Group Two\",\"description\":\"\",\"status\":\"PENDING\"}, {\"id\":\"%s\",\"name\":\"Group Three\",\"description\":\"\",\"status\":\"PENDING\"}]", + groupService.getByName("Group One").getId(), + groupService.getByName("Group Two").getId(), + groupService.getByName("Group Three").getId()); + + assertThatJson(actualBody) + .when(IGNORING_EXTRA_ARRAY_ITEMS, IGNORING_ARRAY_ORDER) + .node("resultSet") + .isEqualTo(expectedBody); + } + + @Test + public void deleteOne() { + val group = entityGenerator.setupGroup("DeleteOne"); + val groupId = group.getId(); + + // Users for test + val userOne = entityGenerator.setupUser("TempGroup User"); + + // Application for test + val appOne = entityGenerator.setupApplication("TempGroupApp"); + + // REST to get users/app in group + val usersBody = singletonList(userOne); + val appsBody = singletonList(appOne); + + addUsersToGroupPostRequestAnd(group, usersBody).assertOk(); + addApplicationsToGroupPostRequestAnd(group, appsBody).assertOk(); + + // Check user-group relationship is there + val userWithGroup = userService.getByName("TempGroupUser@domain.com"); + val expectedGroups = mapToSet(userWithGroup.getUserGroups(), UserGroup::getGroup); + assertThat(extractGroupIds(expectedGroups)).contains(groupId); + + // Check app-group relationship is there + val applicationWithGroup = applicationService.getByClientId("TempGroupApp"); + val groups = + mapToImmutableSet(applicationWithGroup.getGroupApplications(), GroupApplication::getGroup); + assertThat(extractGroupIds(groups)).contains(groupId); + + deleteGroupDeleteRequestAnd(group).assertOk(); + + // Check user-group relationship is also deleted + val usersWithoutGroupBody = + getGroupsForUserGetRequestAnd(userOne).assertOk().assertHasBody().getResponse().getBody(); + assertThat(usersWithoutGroupBody).doesNotContain(groupId.toString()); + + // Check user-application relationship is also deleted + val applicationWithoutGroupBody = + getGroupsForApplicationGetRequestAnd(appOne) + .assertOk() + .assertHasBody() + .getResponse() + .getBody(); + assertThat(applicationWithoutGroupBody).doesNotContain(groupId.toString()); + + // Check group is deleted + getGroupEntityGetRequestAnd(group).assertNotFound(); + } + + @Test + public void addUsersToGroup() { + + val group = entityGenerator.setupGroup("GroupWithUsers"); + + val userOne = userService.getByName("FirstUser@domain.com"); + val userTwo = userService.getByName("SecondUser@domain.com"); + + val body = asList(userOne, userTwo); + addUsersToGroupPostRequestAnd(group, body).assertOk(); + + // Check that Group is associated with Users + val groupWithUsers = groupService.getByName("GroupWithUsers"); + assertThat(extractIDs(mapToSet(groupWithUsers.getUserGroups(), UserGroup::getUser))) + .contains(userOne.getId(), userTwo.getId()); + + // Check that each user is associated with the group + val userOneWithGroups = userService.getByName("FirstUser@domain.com"); + val userTwoWithGroups = userService.getByName("SecondUser@domain.com"); + + assertThat(mapToSet(userOneWithGroups.getUserGroups(), UserGroup::getGroup)).contains(group); + assertThat(mapToSet(userTwoWithGroups.getUserGroups(), UserGroup::getGroup)).contains(group); + } + + @Test + @SneakyThrows + public void deleteUserFromGroup() { + val group = entityGenerator.setupGroup("RemoveGroupUsers"); + val deleteUser = entityGenerator.setupUser("Delete This"); + val remainUser = entityGenerator.setupUser("Keep This"); + + val body = asList(deleteUser, remainUser); + + addUsersToGroupPostRequestAnd(group, body).assertOk(); + + getUsersForGroupGetRequestAnd(group).assertPageResultsOfType(User.class).hasSize(2); + + deleteUsersFromGroupDeleteRequestAnd(group, asList(deleteUser)).assertOk(); + + val actualUsersForGroup = getUsersForGroupGetRequestAnd(group).extractPageResults(User.class); + assertThat(actualUsersForGroup).hasSize(1); + assertThat(actualUsersForGroup.stream().findAny().get().getId()).isEqualTo(remainUser.getId()); + } + + @Test + public void addAppsToGroup() { + + val group = entityGenerator.setupGroup("GroupWithApps"); + + val appOne = applicationService.getByClientId("111111"); + val appTwo = applicationService.getByClientId("222222"); + + val body = asList(appOne, appTwo); + addApplicationsToGroupPostRequestAnd(group, body).assertOk(); + + // Check that Group is associated with Users + val groupWithApps = groupService.getByName("GroupWithApps"); + val applications = + mapToImmutableSet(groupWithApps.getGroupApplications(), GroupApplication::getApplication); + assertThat(extractAppIds(applications)).contains(appOne.getId(), appTwo.getId()); + + // Check that each user is associated with the group + val appOneWithGroups = applicationService.getByClientId("111111"); + val appTwoWithGroups = applicationService.getByClientId("222222"); + + assertThat( + mapToImmutableSet(appOneWithGroups.getGroupApplications(), GroupApplication::getGroup)) + .contains(group); + assertThat( + mapToImmutableSet(appTwoWithGroups.getGroupApplications(), GroupApplication::getGroup)) + .contains(group); + } + + @Test + @SneakyThrows + public void deleteAppFromGroup() { + val group = entityGenerator.setupGroup("RemoveGroupApps"); + val groupId = group.getId(); + val deleteApp = entityGenerator.setupApplication("DeleteThis"); + val remainApp = entityGenerator.setupApplication("KeepThis"); + + val body = asList(deleteApp, remainApp); + addApplicationsToGroupPostRequestAnd(group, body).assertOk(); + + getApplicationsForGroupGetRequestAnd(group) + .assertPageResultsOfType(Application.class) + .hasSize(2); + + deleteApplicationFromGroupDeleteRequestAnd(group, deleteApp).assertOk(); + + val actualApps = + getApplicationsForGroupGetRequestAnd(group).extractPageResults(Application.class); + assertThat(actualApps).hasSize(1); + assertThat(actualApps.stream().findAny().get().getId()).isEqualTo(remainApp.getId()); + } + + @Test + public void createGroup_NonExisting_Success() { + val r = + GroupRequest.builder().name(generateNonExistentName(groupService)).status(APPROVED).build(); + + val group1 = createGroupPostRequestAnd(r).extractOneEntity(Group.class); + + getGroupEntityGetRequestAnd(group1).assertOk().assertHasBody(); + + assertThat(r).isEqualToComparingFieldByField(group1); + } + + @Test + public void createGroup_NameAlreadyExists_Conflict() { + val existingGroup = entityGenerator.generateRandomGroup(); + val createRequest = + GroupRequest.builder().name(existingGroup.getName()).status(APPROVED).build(); + + createGroupPostRequestAnd(createRequest).assertConflict(); + } + + public void createGroup_NullValueButRequired_BadRequest() { + // Create an empty createRequest for groups + val createRequest = GroupRequest.builder().build(); + + // Assert that an empty request results in a badrequest + createGroupPostRequestAnd(createRequest).assertBadRequest(); + } + + @Test + public void deleteGroup_NonExisting_Conflict() { + val nonExistingId = generateNonExistentId(groupService); + deleteGroupDeleteRequestAnd(nonExistingId).assertNotFound(); + } + + @Test + public void deleteGroupAndRelationshipsOnly_AlreadyExisting_Success() { + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + + // Add Applications to Group0 + addApplicationsToGroupPostRequestAnd(group0, data.getApplications()).assertOk(); + + // Assert the applications were add to Group0 + getApplicationsForGroupGetRequestAnd(group0) + .assertPageResultsOfType(Application.class) + .hasSize(data.getApplications().size()); + + // Add Users to Group0 + addUsersToGroupPostRequestAnd(group0, data.getUsers()).assertOk(); + + // Assert the users were added to Group0 + getUsersForGroupGetRequestAnd(group0) + .assertPageResultsOfType(User.class) + .hasSize(data.getUsers().size()); + + // Add Permissions to Group0 + addGroupPermissionToGroupPostRequestAnd(group0, data.getPolicies().get(0), DENY).assertOk(); + + addGroupPermissionToGroupPostRequestAnd(group0, data.getPolicies().get(1), WRITE).assertOk(); + + // Assert the permissions were added to Group0 + getGroupPermissionsForGroupGetRequestAnd(group0) + .assertPageResultsOfType(GroupPermission.class) + .hasSize(2); + + // Delete the group + deleteGroupDeleteRequestAnd(group0).assertOk(); + + // Assert the group was deleted + getGroupEntityGetRequestAnd(group0).assertNotFound(); + + // Assert no group permissions for the group + val results = groupPermissionRepository.findAllByOwner_Id(group0.getId()); + assertThat(results).hasSize(0); + + // Assert getGroupUsers returns NOT_FOUND + getUsersForGroupGetRequestAnd(group0).assertNotFound(); + + // Assert getGroupApplications returns NotFound + getApplicationsForGroupGetRequestAnd(group0).assertNotFound(); + + // Assert all users still exist + data.getUsers().forEach(u -> getUserEntityGetRequestAnd(u).assertOk()); + + // Assert all applications still exist + data.getApplications().forEach(a -> getApplicationEntityGetRequestAnd(a).assertOk()); + + // Assert all policies still exist + data.getPolicies().forEach(p -> getPolicyGetRequestAnd(p).assertOk()); + } + + @Test + public void getGroups_FindAllQuery_Success() { + val expectedGroups = repeatedCallsOf(() -> entityGenerator.generateRandomGroup(), 4); + val numGroups = groupService.getRepository().count(); + listGroupsEndpointAnd() + .queryParam("limit", numGroups) + .queryParam("offset", 0) + .getAnd() + .assertPageResultsOfType(Group.class) + .containsAll(expectedGroups); + } + + @Test + public void getGroups_FindSomeQuery_Success() { + val g1 = + createGroupPostRequestAnd( + GroupRequest.builder() + .name("abc11") + .status(APPROVED) + .description("blueberry banana") + .build()) + .extractOneEntity(Group.class); + + val g2 = + createGroupPostRequestAnd( + GroupRequest.builder() + .name("abc21") + .status(APPROVED) + .description("blueberry orange") + .build()) + .extractOneEntity(Group.class); + + val g3 = + createGroupPostRequestAnd( + GroupRequest.builder() + .name("abc22") + .status(REJECTED) + .description("orange banana") + .build()) + .extractOneEntity(Group.class); + + val numGroups = groupPermissionRepository.count(); + + listGroupsEndpointAnd() + .queryParam("query", "abc") + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(Group.class) + .containsExactlyInAnyOrder(g1, g2, g3); + + listGroupsEndpointAnd() + .queryParam("query", "abc2") + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(Group.class) + .containsExactlyInAnyOrder(g3, g2); + + val r3 = + listGroupsEndpointAnd() + .queryParam("query", "abc") + .queryParam("status", REJECTED) + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .extractPageResults(Group.class); + val rejectedGroups = + r3.stream().filter(x -> x.getStatus() == REJECTED).collect(toImmutableSet()); + assertThat(rejectedGroups.size()).isGreaterThanOrEqualTo(1); + + listGroupsEndpointAnd() + .queryParam("query", "blueberry") + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(Group.class) + .contains(g1, g2); + + listGroupsEndpointAnd() + .queryParam("query", "orange") + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(Group.class) + .contains(g3, g2); + + listGroupsEndpointAnd() + .queryParam("query", "orange") + .queryParam("status", REJECTED) + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(Group.class) + .contains(g3); + + listGroupsEndpointAnd() + .queryParam("query", "blue") + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(Group.class) + .contains(g1); + } + + @Test + public void addUsersToGroup_NonExistentGroup_NotFound() { + val data = generateUniqueTestGroupData(); + val nonExistentId = generateNonExistentId(groupService); + val nonExistentGroup = Group.builder().id(nonExistentId).build(); + addUsersToGroupPostRequestAnd(nonExistentGroup, data.getUsers()).assertNotFound(); + } + + @Test + public void addUsersToGroup_AllExistingUnassociatedUsers_Success() { + // Generate test data + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + + // Assert there are no users for the group + getUsersForGroupGetRequestAnd(group0).assertPageResultsOfType(User.class).isEmpty(); + + // Add the users to the group + addUsersToGroupPostRequestAnd(group0, data.getUsers()).assertOk(); + + // Assert the users were added + getUsersForGroupGetRequestAnd(group0) + .assertPageResultsOfType(User.class) + .containsExactlyInAnyOrderElementsOf(data.getUsers()); + } + + @Test + public void addUsersToGroup_SomeExistingUsersButAllUnassociated_NotFound() { + // Setup data + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + + val existingUserIds = convertToIds(data.getUsers()); + val someNonExistingUserIds = repeatedCallsOf(() -> generateNonExistentId(userService), 3); + val mergedUserIds = concatToSet(existingUserIds, someNonExistingUserIds); + + // Attempt to add nonexistent users to the group + addUsersToGroupPostRequestAnd(group0.getId(), mergedUserIds).assertNotFound(); + } + + @Test + public void addUsersToGroup_AllExsitingUsersButSomeAlreadyAssociated_Conflict() { + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + + // Assert there are no users for the group + getUsersForGroupGetRequestAnd(group0).assertPageResultsOfType(User.class).isEmpty(); + + // Add some new unassociated users + val someUsers = newArrayList(data.getUsers().get(0)); + addUsersToGroupPostRequestAnd(group0, someUsers).assertOk(); + + // Assert that adding already associated users returns a conflict + addUsersToGroupPostRequestAnd(group0, data.getUsers()).assertConflict(); + } + + @Test + public void removeUsersFromGroup_AllExistingAssociatedUsers_Success() { + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + + // Assert there are no users for the group + getUsersForGroupGetRequestAnd(group0).assertPageResultsOfType(User.class).isEmpty(); + ; + + // Add users to group + addUsersToGroupPostRequestAnd(group0, data.getUsers()).assertOk(); + + // Delete all users + deleteUsersFromGroupDeleteRequestAnd(group0, data.getUsers()).assertOk(); + + // Assert there are no users for the group + getUsersForGroupGetRequestAnd(group0).assertPageResultsOfType(User.class).isEmpty(); + } + + @Test + public void removeUsersFromGroup_AllExistingUsersButSomeNotAssociated_NotFound() { + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + + // Assert there are no users for the group + getUsersForGroupGetRequestAnd(group0).assertPageResultsOfType(User.class).isEmpty(); + + // Add some users to group + addUsersToGroupPostRequestAnd(group0, newArrayList(data.getUsers().get(0))).assertOk(); + + // Delete all users + deleteUsersFromGroupDeleteRequestAnd(group0, data.getUsers()).assertNotFound(); + } + + @Test + public void removeUsersFromGroup_SomeNonExistingUsersButAllAssociated_NotFound() { + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + + // Assert there are no users for the group + getUsersForGroupGetRequestAnd(group0).assertPageResultsOfType(User.class).isEmpty(); + + // Add all users to group + addUsersToGroupPostRequestAnd(group0, data.getUsers()).assertOk(); + + // Create list of userIds to delete, including one non existent id + val userIdsToDelete = data.getUsers().stream().map(Identifiable::getId).collect(toList()); + userIdsToDelete.add(generateNonExistentId(userService)); + + // Delete existing associated users and non-existing users, and assert a not found error + deleteUsersFromGroupDeleteRequestAnd(group0.getId(), userIdsToDelete).assertNotFound(); + } + + @Test + public void removeUsersFromGroup_NonExistentGroup_NotFound() { + val data = generateUniqueTestGroupData(); + val existingUserIds = convertToIds(data.getUsers()); + val nonExistentId = generateNonExistentId(groupService); + + deleteUsersFromGroupDeleteRequestAnd(nonExistentId, existingUserIds).assertNotFound(); + } + + @Test + public void getUsersFromGroup_FindAllQuery_Success() { + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + + // Assert without using a controller, there are no users for the group + val beforeGroup = groupService.getWithRelationships(group0.getId()); + assertThat(beforeGroup.getUserGroups()).isEmpty(); + + // Add users to group + addUsersToGroupPostRequestAnd(group0, data.getUsers()).assertOk(); + + // Assert without using a controller, there are users for the group + val afterGroup = groupService.getWithRelationships(group0.getId()); + val expectedUsers = mapToSet(afterGroup.getUserGroups(), UserGroup::getUser); + assertThat(expectedUsers).containsExactlyInAnyOrderElementsOf(data.getUsers()); + + // Get user for a group using a controller + getUsersForGroupGetRequestAnd(group0) + .assertPageResultsOfType(User.class) + .containsExactlyInAnyOrderElementsOf(data.getUsers()); + } + + @Test + public void getUsersFromGroup_NonExistentGroup_NotFound() { + val nonExistentId = generateNonExistentId(groupService); + getUsersForGroupGetRequestAnd(nonExistentId).assertNotFound(); + } + + @Test + public void getUsersFromGroup_FindSomeQuery_Success() { + + // Create users and groups + val g1 = entityGenerator.generateRandomGroup(); + val u1 = entityGenerator.setupUser("blueberry banana"); + val u2 = entityGenerator.setupUser("blueberry orange"); + val u3 = entityGenerator.setupUser("banana orange"); + + // Update their status + u1.setStatus(APPROVED); + u2.setStatus(APPROVED); + u3.setStatus(DISABLED); + + // Add users to group + addUsersToGroupPostRequestAnd(g1, newArrayList(u1, u2, u3)).assertOk(); + + val numGroups = groupRepository.count(); + + // Search users + initStringRequest() + .endpoint("groups/%s/users", g1.getId()) + .queryParam("query", "orange") + .queryParam("status", DISABLED) + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(User.class) + .contains(u3); + + initStringRequest() + .endpoint("groups/%s/users", g1.getId()) + .queryParam("query", "orange") + .queryParam("status", APPROVED) + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(User.class) + .contains(u2); + + initStringRequest() + .endpoint("groups/%s/users", g1.getId()) + .queryParam("status", APPROVED) + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(User.class) + .contains(u1, u2); + + initStringRequest() + .endpoint("groups/%s/users", g1.getId()) + .queryParam("query", "blueberry") + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(User.class) + .contains(u1, u2); + + initStringRequest() + .endpoint("groups/%s/users", g1.getId()) + .queryParam("query", "banana") + .queryParam("offset", 0) + .queryParam("length", numGroups) + .getAnd() + .assertPageResultsOfType(User.class) + .contains(u1, u3); + } + + @Test + public void getGroup_ExistingGroup_Success() { + val group = entityGenerator.generateRandomGroup(); + assertThat(groupService.isExist(group.getId())).isTrue(); + getGroupEntityGetRequestAnd(group).assertOk(); + } + + @Test + public void getGroup_NonExistentGroup_Success() { + val nonExistentId = generateNonExistentId(groupService); + val r1 = initStringRequest().endpoint("groups/%s", nonExistentId).getAnd().assertNotFound(); + } + + @Test + public void UUIDValidation_MalformedUUID_BadRequest() { + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + val badUUID = "123sksk"; + + initStringRequest().endpoint("/groups/%s", badUUID).deleteAnd().assertBadRequest(); + + initStringRequest().endpoint("/groups/%s", badUUID).getAnd().assertBadRequest(); + + initStringRequest().endpoint("/groups/%s/applications", badUUID).getAnd().assertBadRequest(); + + initStringRequest().endpoint("/groups/%s/applications", badUUID).postAnd().assertBadRequest(); + + val appIds = mapToList(data.getApplications(), x -> x.getId().toString()); + appIds.add(badUUID); + + // Test when an id in the payload is not a uuid + initStringRequest() + .endpoint("/groups/%s/applications", group0.getId()) + .body(appIds) + .postAnd() + .assertBadRequest(); + + initStringRequest() + .endpoint("/groups/%s/applications/%s", badUUID, data.getApplications().get(0).getId()) + .deleteAnd() + .assertBadRequest(); + + initStringRequest() + .endpoint("/groups/%s/applications/%s", group0.getId(), COMMA.join(appIds)) + .deleteAnd() + .assertBadRequest(); + + initStringRequest().endpoint("groups/%s/permissions", badUUID).getAnd().assertBadRequest(); + + initStringRequest().endpoint("groups/%s/permissions", badUUID).postAnd().assertBadRequest(); + + // Test when an id in the payload is not a uuid + val body = + MAPPER + .createArrayNode() + .add( + MAPPER + .createObjectNode() + .put("mask", READ.toString()) + .put("policyId", data.getPolicies().get(0).getId().toString())) + .add(MAPPER.createObjectNode().put("mask", READ.toString()).put("policyId", badUUID)); + initStringRequest() + .endpoint("groups/%s/permissions", group0.getId()) + .body(body) + .postAnd() + .assertBadRequest(); + + addGroupPermissionToGroupPostRequestAnd(group0, data.getPolicies().get(0), READ).assertOk(); + + val actualPermissions = + getGroupPermissionsForGroupGetRequestAnd(group0).extractPageResults(GroupPermission.class); + assertThat(actualPermissions).hasSize(1); + val existingPermissionId = actualPermissions.get(0).getId(); + + initStringRequest() + .endpoint("groups/%s/permissions/%s", badUUID, existingPermissionId) + .deleteAnd() + .assertBadRequest(); + + initStringRequest() + .endpoint("groups/%s/permissions/%s", group0.getId(), badUUID + "," + existingPermissionId) + .deleteAnd() + .assertBadRequest(); + + initStringRequest().endpoint("/groups/%s/users", badUUID).getAnd().assertBadRequest(); + + initStringRequest().endpoint("/groups/%s/users", badUUID).postAnd().assertBadRequest(); + + val userIds = mapToList(data.getUsers(), x -> x.getId().toString()); + userIds.add(badUUID); + + // Test when an id in the payload is not a uuid + initStringRequest() + .endpoint("/groups/%s/users", group0.getId()) + .body(userIds) + .postAnd() + .assertBadRequest(); + + initStringRequest() + .endpoint("/groups/%s/users/%s", badUUID, data.getUsers().get(0).getId()) + .deleteAnd() + .assertBadRequest(); + + initStringRequest() + .endpoint("/groups/%s/users/%s", group0.getId(), COMMA.join(userIds)) + .deleteAnd() + .assertBadRequest(); + } + + @Test + public void updateGroup_ExistingGroup_Success() { + val g = entityGenerator.generateRandomGroup(); + + val updateRequest1 = + GroupRequest.builder() + .name(generateNonExistentName(groupService)) + .status(null) + .description(null) + .build(); + + val updatedGroup1 = + partialUpdateGroupPutRequestAnd(g.getId(), updateRequest1).extractOneEntity(Group.class); + assertThat(updatedGroup1) + .isEqualToIgnoringGivenFields(g, NAME, PERMISSIONS, GROUPAPPLICATIONS, USERGROUPS); + assertThat(updatedGroup1.getName()).isEqualTo(updateRequest1.getName()); + + val updateRequest2 = + GroupRequest.builder() + .name(null) + .status(randomEnumExcluding(StatusType.class, g.getStatus())) + .description(null) + .build(); + val updatedGroup2 = + partialUpdateGroupPutRequestAnd(g.getId(), updateRequest2).extractOneEntity(Group.class); + assertThat(updatedGroup2) + .isEqualToIgnoringGivenFields( + updatedGroup1, STATUS, PERMISSIONS, GROUPAPPLICATIONS, USERGROUPS); + assertThat(updatedGroup2.getStatus()).isEqualTo(updateRequest2.getStatus()); + + val description = "my description"; + val updateRequest3 = + GroupRequest.builder().name(null).status(null).description(description).build(); + val updatedGroup3 = + partialUpdateGroupPutRequestAnd(g.getId(), updateRequest3).extractOneEntity(Group.class); + assertThat(updatedGroup3) + .isEqualToIgnoringGivenFields( + updatedGroup2, DESCRIPTION, PERMISSIONS, GROUPAPPLICATIONS, USERGROUPS); + assertThat(updatedGroup3.getDescription()).isEqualTo(updateRequest3.getDescription()); + } + + @Test + public void updateGroup_NonExistentGroup_NotFound() { + val nonExistentId = generateNonExistentId(groupService); + val updateRequest = GroupRequest.builder().build(); + partialUpdateGroupPutRequestAnd(nonExistentId, updateRequest).assertNotFound(); + } + + @Test + public void updateGroup_NameAlreadyExists_Conflict() { + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + val group1 = data.getGroups().get(1); + val updateRequest = GroupRequest.builder().name(group1.getName()).build(); + partialUpdateGroupPutRequestAnd(group0.getId(), updateRequest).assertConflict(); + } + + @Test + public void statusValidation_MalformedStatus_BadRequest() { + val invalidStatus = "something123"; + val match = stream(StatusType.values()).anyMatch(x -> x.toString().equals(invalidStatus)); + assertThat(match).isFalse(); + + val data = generateUniqueTestGroupData(); + val group = data.getGroups().get(0); + + // createGroup: POST /groups + val createRequest = + MAPPER + .createObjectNode() + .put("name", generateNonExistentName(groupService)) + .put("status", invalidStatus); + + initStringRequest().endpoint("/groups").body(createRequest).postAnd().assertBadRequest(); + + // updateGroup: PUT /groups + val updateRequest = MAPPER.createObjectNode().put("status", invalidStatus); + initStringRequest() + .endpoint("/groups/%s", group.getId()) + .body(updateRequest) + .putAnd() + .assertBadRequest(); + } + + @Test + public void getScopes_FindAllQuery_Success() { + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + + // Assert without using a controller, there are no users for the group + val beforeGroup = groupService.getWithRelationships(group0.getId()); + assertThat(beforeGroup.getPermissions()).isEmpty(); + + // Add policies to group + data.getPolicies() + .forEach( + p -> { + val randomMask = randomEnum(AccessLevel.class); + addGroupPermissionToGroupPostRequestAnd(group0, p, randomMask).assertOk(); + }); + + // Assert without using a controller, there are users for the group + val afterGroup = groupService.getWithRelationships(group0.getId()); + assertThat(afterGroup.getPermissions()).hasSize(2); + + // Get permissions for a group using a controller + getGroupPermissionsForGroupGetRequestAnd(group0) + .assertPageResultsOfType(GroupPermission.class) + .containsExactlyInAnyOrderElementsOf(afterGroup.getPermissions()); + } + + @Test + @Ignore("should be tested") + public void getScopes_FindSomeQuery_Success() { + throw new NotImplementedException( + "need to implement the test 'getScopes_FindSomeQuery_Success'"); + } + + @Test + public void addAppsToGroup_NonExistentGroup_NotFound() { + val data = generateUniqueTestGroupData(); + val nonExistentId = generateNonExistentId(groupService); + val appIds = convertToIds(data.getApplications()); + + initStringRequest() + .endpoint("/groups/%s/applications", nonExistentId) + .body(appIds) + .postAnd() + .assertNotFound(); + } + + @Test + public void addAppsToGroup_AllExistingUnassociatedApps_Success() { + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + val appIds = convertToIds(data.getApplications()); + + // Assert without using the controller, that the group is not associated with any apps + val beforeGroup = groupService.getWithRelationships(group0.getId()); + assertThat(beforeGroup.getGroupApplications()).isEmpty(); + + // Add applications to the group + addApplicationsToGroupPostRequestAnd(group0, data.getApplications()).assertOk(); + + // Assert without usign the controller, that the group IS associated with all the apps + val afterGroup = groupService.getWithRelationships(group0.getId()); + val expectedApplications = + mapToImmutableSet(afterGroup.getGroupApplications(), GroupApplication::getApplication); + assertThat(expectedApplications).containsExactlyInAnyOrderElementsOf(data.getApplications()); + } + + @Test + public void addAppsToGroup_SomeExistingAppsButAllUnassociated_NotFound() { + + // Setup data + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + val existingAppIds = convertToIds(data.getApplications()); + val nonExistingAppIds = repeatedCallsOf(() -> generateNonExistentId(applicationService), 3); + val appIdsToAssociate = concatToSet(existingAppIds, nonExistingAppIds); + + // Add some existing and non-existing app ids to an existing group + addApplicationsToGroupPostRequestAnd(group0.getId(), appIdsToAssociate).assertNotFound(); + } + + @Test + public void removeAppsFromGroup_AllExistingAssociatedApps_Success() { + // Setup data + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + + // Add all apps to the group0 + addApplicationsToGroupPostRequestAnd(group0, data.getApplications()).assertOk(); + + // Assert the group has all the apps + getApplicationsForGroupGetRequestAnd(group0) + .assertPageResultsOfType(Application.class) + .containsExactlyInAnyOrderElementsOf(data.getApplications()); + + // Remove all apps + deleteApplicationsFromGroupDeleteRequestAnd(group0, data.getApplications()).assertOk(); + + // Assert the group has 0 apps + getApplicationsForGroupGetRequestAnd(group0) + .assertPageResultsOfType(Application.class) + .isEmpty(); + } + + @Test + public void removeAppsFromGroup_AllExistingAppsButSomeNotAssociated_NotFound() { + // Setup data + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + val app0Id = data.getApplications().get(0).getId(); + val app1Id = data.getApplications().get(1).getId(); + + // Add app0 to the group0 + addApplicationsToGroupPostRequestAnd(group0.getId(), newArrayList(app0Id)).assertOk(); + + // Remove associated and non-associated apps from the group, however all are existing + val appIdsToRemove = newArrayList(app0Id, app1Id); + deleteApplicationsFromGroupDeleteRequestAnd(group0.getId(), appIdsToRemove).assertNotFound(); + } + + @Test + public void removeAppsFromGroup_SomeNonExistingAppsButAllAssociated_NotFound() { + + // Setup data + val data = generateUniqueTestGroupData(); + val group0 = data.getGroups().get(0); + val existingAppIds = convertToIds(data.getApplications()); + val nonExistingAppIds = repeatedCallsOf(() -> generateNonExistentId(applicationService), 3); + val appIdsToDisassociate = concatToSet(existingAppIds, nonExistingAppIds); + + // Add all existing apps to group + addApplicationsToGroupPostRequestAnd(group0.getId(), existingAppIds).assertOk(); + + // Attempt to disassociate existing associated apps and non-exisiting apps from the group, and + // fail + deleteApplicationsFromGroupDeleteRequestAnd(group0.getId(), appIdsToDisassociate) + .assertNotFound(); + } + + @Test + public void removeAppsFromGroup_NonExistentGroup_NotFound() { + val nonExistentId = generateNonExistentId(groupService); + val data = generateUniqueTestGroupData(); + val existingApplicationIds = convertToIds(data.getApplications()); + + // Assert that the group does not exist + deleteApplicationsFromGroupDeleteRequestAnd(nonExistentId, existingApplicationIds) + .assertNotFound(); + } + + @Test + @Ignore("should be tested") + public void getAppsFromGroup_FindAllQuery_Success() { + throw new NotImplementedException( + "need to implement the test 'getAppsFromGroup_FindAllQuery_Success'"); + } + + @Test + public void getAppsFromGroup_NonExistentGroup_NotFound() { + val nonExistentId = generateNonExistentId(groupService); + + // Attempt to get applications for non existent group, and fail + initStringRequest() + .endpoint("/groups/%s/applications", nonExistentId) + .getAnd() + .assertNotFound(); + } + + @Test + @Ignore("should be tested") + public void getAppsFromGroup_FindSomeQuery_Success() { + throw new NotImplementedException( + "need to implement the test 'getAppsFromGroup_FindSomeQuery_Success'"); + } + + @SneakyThrows + private TestGroupData generateUniqueTestGroupData() { + val groups = repeatedCallsOf(() -> entityGenerator.generateRandomGroup(), 2); + val applications = repeatedCallsOf(() -> entityGenerator.generateRandomApplication(), 2); + val users = repeatedCallsOf(() -> entityGenerator.generateRandomUser(), 2); + val policies = repeatedCallsOf(() -> entityGenerator.generateRandomPolicy(), 2); + + return TestGroupData.builder() + .groups(groups) + .applications(applications) + .users(users) + .policies(policies) + .build(); + } + + @lombok.Value + @Builder + public static class TestGroupData { + @NonNull private final List groups; + @NonNull private final List applications; + @NonNull private final List users; + @NonNull private final List policies; + } +} diff --git a/src/test/java/bio/overture/ego/controller/GroupPermissionControllerTest.java b/src/test/java/bio/overture/ego/controller/GroupPermissionControllerTest.java new file mode 100644 index 000000000..68412ae76 --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/GroupPermissionControllerTest.java @@ -0,0 +1,127 @@ +package bio.overture.ego.controller; + +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentName; +import static bio.overture.ego.utils.Joiners.COMMA; +import static java.lang.String.format; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.GroupPermission; +import bio.overture.ego.service.AbstractPermissionService; +import bio.overture.ego.service.GroupPermissionService; +import bio.overture.ego.service.GroupService; +import bio.overture.ego.service.NamedService; +import bio.overture.ego.service.PolicyService; +import bio.overture.ego.utils.EntityGenerator; +import java.util.Collection; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +@Slf4j +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@TestExecutionListeners(listeners = DependencyInjectionTestExecutionListener.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class GroupPermissionControllerTest + extends AbstractPermissionControllerTest { + + /** Dependencies */ + @Autowired private EntityGenerator entityGenerator; + + @Autowired private PolicyService policyService; + @Autowired private GroupService groupService; + @Autowired private GroupPermissionService groupPermissionService; + + @Value("${logging.test.controller.enable}") + private boolean enableLogging; + + @Override + protected boolean enableLogging() { + return enableLogging; + } + + @Override + protected EntityGenerator getEntityGenerator() { + return entityGenerator; + } + + @Override + protected PolicyService getPolicyService() { + return policyService; + } + + @Override + protected Class getOwnerType() { + return Group.class; + } + + @Override + protected Class getPermissionType() { + return GroupPermission.class; + } + + @Override + protected Group generateOwner(String name) { + return entityGenerator.setupGroup(name); + } + + @Override + protected String generateNonExistentOwnerName() { + return generateNonExistentName(groupService); + } + + @Override + protected NamedService getOwnerService() { + return groupService; + } + + @Override + protected AbstractPermissionService getPermissionService() { + return groupPermissionService; + } + + @Override + protected String getAddPermissionsEndpoint(String ownerId) { + return format("groups/%s/permissions", ownerId); + } + + @Override + protected String getAddPermissionEndpoint(String policyId, String ownerId) { + return format("policies/%s/permission/group/%s", policyId, ownerId); + } + + @Override + protected String getReadPermissionsEndpoint(String ownerId) { + return format("groups/%s/permissions", ownerId); + } + + @Override + protected String getDeleteOwnerEndpoint(String ownerId) { + return format("groups/%s", ownerId); + } + + @Override + protected String getDeletePermissionsEndpoint(String ownerId, Collection permissionIds) { + return format("groups/%s/permissions/%s", ownerId, COMMA.join(permissionIds)); + } + + @Override + protected String getDeletePermissionEndpoint(String policyId, String ownerId) { + return format("policies/%s/permission/group/%s", policyId, ownerId); + } + + @Override + protected String getReadOwnersForPolicyEndpoint(String policyId) { + return format("policies/%s/groups", policyId); + } +} diff --git a/src/test/java/bio/overture/ego/controller/PolicyControllerTest.java b/src/test/java/bio/overture/ego/controller/PolicyControllerTest.java new file mode 100644 index 000000000..fd22a3e9e --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/PolicyControllerTest.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.controller; + +import static bio.overture.ego.controller.AbstractPermissionControllerTest.createMaskJson; +import static bio.overture.ego.model.enums.AccessLevel.READ; +import static bio.overture.ego.model.enums.AccessLevel.WRITE; +import static org.assertj.core.api.Assertions.assertThat; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.model.dto.PolicyRequest; +import bio.overture.ego.service.PolicyService; +import bio.overture.ego.utils.EntityGenerator; +import com.fasterxml.jackson.databind.node.ArrayNode; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@Slf4j +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class PolicyControllerTest extends AbstractControllerTest { + + private static boolean hasRunEntitySetup = false; + + /** Dependencies */ + @Autowired private EntityGenerator entityGenerator; + + @Autowired private PolicyService policyService; + + @Value("${logging.test.controller.enable}") + private boolean enableLogging; + + @Override + protected boolean enableLogging() { + return enableLogging; + } + + @Override + protected void beforeTest() { + // Initial setup of entities (run once + if (!hasRunEntitySetup) { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + entityGenerator.setupTestPolicies(); + hasRunEntitySetup = true; + } + } + + @Test + @SneakyThrows + public void addpolicy_Success() { + val policy = PolicyRequest.builder().name("AddPolicy").build(); + + val response = initStringRequest().endpoint("/policies").body(policy).post(); + + val responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + val responseJson = MAPPER.readTree(response.getBody()); + + log.info(response.getBody()); + + assertThat(responseJson.get("name").asText()).isEqualTo("AddPolicy"); + } + + @Test + @SneakyThrows + public void addDuplicatePolicy_Conflict() { + val policy1 = PolicyRequest.builder().name("PolicyUnique").build(); + val policy2 = PolicyRequest.builder().name("PolicyUnique").build(); + + val response1 = initStringRequest().endpoint("/policies").body(policy1).post(); + + val responseStatus1 = response1.getStatusCode(); + assertThat(responseStatus1).isEqualTo(HttpStatus.OK); + + val response2 = initStringRequest().endpoint("/policies").body(policy2).post(); + + val responseStatus2 = response2.getStatusCode(); + assertThat(responseStatus2).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + @SneakyThrows + public void getPolicy_Success() { + val policyId = policyService.getByName("Study001").getId(); + val response = initStringRequest().endpoint("/policies/%s", policyId).get(); + + val responseStatus = response.getStatusCode(); + val responseJson = MAPPER.readTree(response.getBody()); + + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + assertThat(responseJson.get("name").asText()).isEqualTo("Study001"); + } + + @Test + @SneakyThrows + public void associatePermissionsWithGroup_ExistingEntitiesButNonExistingRelationship_Success() { + val policyId = entityGenerator.setupSinglePolicy("AddGroupPermission").getId().toString(); + val groupId = entityGenerator.setupGroup("GroupPolicyAdd").getId().toString(); + + val response = + initStringRequest() + .endpoint("/policies/%s/permission/group/%s", policyId, groupId) + .body(createMaskJson(WRITE.toString())) + .post(); + + val responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + + val getResponse = initStringRequest().endpoint("/policies/%s/groups", policyId).get(); + + val getResponseStatus = getResponse.getStatusCode(); + val getResponseJson = MAPPER.readTree(getResponse.getBody()); + val groupPermissionJson = getResponseJson.get(0); + + assertThat(getResponseStatus).isEqualTo(HttpStatus.OK); + assertThat(groupPermissionJson.get("id").asText()).isEqualTo(groupId); + assertThat(groupPermissionJson.get("mask").asText()).isEqualTo("WRITE"); + } + + @Test + @SneakyThrows + public void disassociatePermissionsFromGroup_EntitiesAndRelationshipsExisting_Success() { + val policyId = entityGenerator.setupSinglePolicy("DeleteGroupPermission").getId().toString(); + val groupId = entityGenerator.setupGroup("GroupPolicyDelete").getId().toString(); + + val response = + initStringRequest() + .endpoint("/policies/%s/permission/group/%s", policyId, groupId) + .body(createMaskJson(WRITE.toString())) + .post(); + + val responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + + val deleteResponse = + initStringRequest() + .endpoint("/policies/%s/permission/group/%s", policyId, groupId) + .delete(); + + val deleteResponseStatus = deleteResponse.getStatusCode(); + assertThat(deleteResponseStatus).isEqualTo(HttpStatus.OK); + + val getResponse = initStringRequest().endpoint("/policies/%s/groups", policyId).get(); + + val getResponseStatus = getResponse.getStatusCode(); + val getResponseJson = (ArrayNode) MAPPER.readTree(getResponse.getBody()); + + assertThat(getResponseStatus).isEqualTo(HttpStatus.OK); + assertThat(getResponseJson.size()).isEqualTo(0); + } + + @Test + @SneakyThrows + public void associatePermissionsWithUser_ExistingEntitiesButNoRelationship_Success() { + val policyId = entityGenerator.setupSinglePolicy("AddUserPermission").getId().toString(); + val userId = entityGenerator.setupUser("UserPolicy Add").getId().toString(); + + val response = + initStringRequest() + .endpoint("/policies/%s/permission/user/%s", policyId, userId) + .body(createMaskJson(READ.toString())) + .post(); + + val responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + // TODO: Fix it so that POST returns JSON, not just random string message + + val getResponse = initStringRequest().endpoint("/policies/%s/users", policyId).get(); + + val getResponseStatus = getResponse.getStatusCode(); + val getResponseJson = MAPPER.readTree(getResponse.getBody()); + val groupPermissionJson = getResponseJson.get(0); + + assertThat(getResponseStatus).isEqualTo(HttpStatus.OK); + assertThat(groupPermissionJson.get("id").asText()).isEqualTo(userId); + assertThat(groupPermissionJson.get("mask").asText()).isEqualTo("READ"); + } + + @Test + @SneakyThrows + public void disassociatePermissionsFromUser_ExistingEntitiesAndRelationships_Success() { + val policyId = entityGenerator.setupSinglePolicy("DeleteGroupPermission").getId().toString(); + val userId = entityGenerator.setupUser("UserPolicy Delete").getId().toString(); + + val response = + initStringRequest() + .endpoint("/policies/%s/permission/user/%s", policyId, userId) + .body(createMaskJson(WRITE.toString())) + .post(); + + val responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + // TODO: Fix it so that POST returns JSON, not just random string message + + val deleteResponse = + initStringRequest().endpoint("/policies/%s/permission/user/%s", policyId, userId).delete(); + + val deleteResponseStatus = deleteResponse.getStatusCode(); + assertThat(deleteResponseStatus).isEqualTo(HttpStatus.OK); + + val getResponse = initStringRequest().endpoint("/policies/%s/users", policyId).get(); + + val getResponseStatus = getResponse.getStatusCode(); + val getResponseJson = (ArrayNode) MAPPER.readTree(getResponse.getBody()); + + assertThat(getResponseStatus).isEqualTo(HttpStatus.OK); + assertThat(getResponseJson.size()).isEqualTo(0); + } +} diff --git a/src/test/java/bio/overture/ego/controller/RevokeTokenControllerTest.java b/src/test/java/bio/overture/ego/controller/RevokeTokenControllerTest.java new file mode 100644 index 000000000..71fc051af --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/RevokeTokenControllerTest.java @@ -0,0 +1,233 @@ +package bio.overture.ego.controller; + +import static bio.overture.ego.model.enums.ApplicationType.CLIENT; +import static bio.overture.ego.model.enums.UserType.USER; +import static java.util.UUID.randomUUID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.service.TokenService; +import bio.overture.ego.utils.EntityGenerator; +import bio.overture.ego.utils.TestData; +import bio.overture.ego.utils.WithMockCustomApplication; +import bio.overture.ego.utils.WithMockCustomUser; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +@Slf4j +@ActiveProfiles("test") +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ContextConfiguration +@Transactional +public class RevokeTokenControllerTest { + + @Autowired private TokenService tokenService; + + @Autowired private EntityGenerator entityGenerator; + + @Autowired private WebApplicationContext webApplicationContext; + + private MockMvc mockMvc; + + private TestData test; + + private static final String ACCESS_TOKEN = "TestToken"; + + @Before + public void initTest() { + test = new TestData(entityGenerator); + this.mockMvc = + MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(springSecurity()) + .alwaysDo(print()) + .build(); + } + + @WithMockCustomUser + @SneakyThrows + @Test + public void revokeAnyTokenAsAdminUser() { + // Admin users can revoke other users' tokens. + + val randomTokenName = randomUUID().toString(); + val adminTokenName = randomUUID().toString(); + val scopes = test.getScopes("song.READ"); + val randomScopes = test.getScopes("song.READ"); + + val randomToken = + entityGenerator.setupToken( + test.regularUser, randomTokenName, false, 1000, "random token", randomScopes); + entityGenerator.setupToken(test.adminUser, adminTokenName, false, 1000, "test token", scopes); + + assertThat(randomToken.isRevoked()).isFalse(); + + mockMvc + .perform( + MockMvcRequestBuilders.delete("/o/token") + .param("token", randomTokenName) + .header(AUTHORIZATION, ACCESS_TOKEN)) + .andExpect(status().isOk()); + + val revokedToken = + tokenService + .findByTokenString(randomTokenName) + .orElseThrow(() -> new InvalidTokenException("Token Not Found!")); + assertThat(revokedToken.isRevoked()).isTrue(); + } + + @WithMockCustomUser + @SneakyThrows + @Test + public void revokeOwnTokenAsAdminUser() { + // Admin users can revoke their own tokens. + + val tokenName = randomUUID().toString(); + val scopes = test.getScopes("song.READ", "collab.READ", "id.WRITE"); + val token = + entityGenerator.setupToken(test.adminUser, tokenName, false, 1000, "test token", scopes); + + assertThat(token.isRevoked()).isFalse(); + + mockMvc + .perform( + MockMvcRequestBuilders.delete("/o/token") + .param("token", tokenName) + .header(AUTHORIZATION, ACCESS_TOKEN)) + .andExpect(status().isOk()); + + val revokedToken = + tokenService + .findByTokenString(tokenName) + .orElseThrow(() -> new InvalidTokenException("Token Not Found!")); + assertThat(revokedToken.isRevoked()).isTrue(); + } + + @WithMockCustomUser(firstName = "Regular", lastName = "User", type = USER) + @SneakyThrows + @Test + public void revokeAnyTokenAsRegularUser() { + // Regular user cannot revoke other people's token + + val tokenName = randomUUID().toString(); + val scopes = test.getScopes("id.WRITE"); + val token = + entityGenerator.setupToken(test.user1, tokenName, false, 1000, "test token", scopes); + + assertThat(token.isRevoked()).isFalse(); + + mockMvc + .perform( + MockMvcRequestBuilders.delete("/o/token") + .param("token", tokenName) + .header(AUTHORIZATION, ACCESS_TOKEN)) + .andExpect(status().isUnauthorized()); + val revokedToken = + tokenService + .findByTokenString(tokenName) + .orElseThrow(() -> new InvalidTokenException("Token Not Found!")); + assertThat(revokedToken.isRevoked()).isFalse(); + } + + @WithMockCustomUser(firstName = "Regular", lastName = "User", type = USER) + @SneakyThrows + @Test + public void revokeOwnTokenAsRegularUser() { + // Regular users can only revoke tokens that belong to them. + + val tokenName = randomUUID().toString(); + val scopes = test.getScopes("song.READ"); + val token = + entityGenerator.setupToken(test.regularUser, tokenName, false, 1000, "test token", scopes); + + assertThat(token.isRevoked()).isFalse(); + + mockMvc + .perform( + MockMvcRequestBuilders.delete("/o/token") + .param("token", tokenName) + .header(AUTHORIZATION, ACCESS_TOKEN)) + .andExpect(status().isOk()); + + val revokedToken = + tokenService + .findByTokenString(tokenName) + .orElseThrow(() -> new InvalidTokenException("Token Not Found!")); + assertThat(revokedToken.isRevoked()).isTrue(); + } + + @WithMockCustomApplication + @SneakyThrows + @Test + public void revokeAnyTokenAsAdminApp() { + val tokenName = randomUUID().toString(); + val scopes = test.getScopes("song.READ"); + val token = + entityGenerator.setupToken(test.regularUser, tokenName, false, 1000, "test token", scopes); + + assertThat(token.isRevoked()).isFalse(); + + mockMvc + .perform( + MockMvcRequestBuilders.delete("/o/token") + .param("token", tokenName) + .header(AUTHORIZATION, ACCESS_TOKEN)) + .andExpect(status().isOk()); + + val revokedToken = + tokenService + .findByTokenString(tokenName) + .orElseThrow(() -> new InvalidTokenException("Token Not Found!")); + assertThat(revokedToken.isRevoked()).isTrue(); + } + + @WithMockCustomApplication( + name = "song", + clientId = "song", + clientSecret = "La la la!;", + type = CLIENT) + @SneakyThrows + @Test + public void revokeTokenAsClientApp() { + val tokenName = randomUUID().toString(); + val scopes = test.getScopes("song.READ"); + val token = + entityGenerator.setupToken(test.regularUser, tokenName, false, 1000, "test token", scopes); + + assertThat(token.isRevoked()).isFalse(); + + mockMvc + .perform( + MockMvcRequestBuilders.delete("/o/token") + .param("token", tokenName) + .header(AUTHORIZATION, ACCESS_TOKEN)) + .andExpect(status().isBadRequest()); + + val revokedToken = + tokenService + .findByTokenString(tokenName) + .orElseThrow(() -> new InvalidTokenException("Token Not Found!")); + assertThat(revokedToken.isRevoked()).isFalse(); + } +} diff --git a/src/test/java/bio/overture/ego/controller/TokenControllerTest.java b/src/test/java/bio/overture/ego/controller/TokenControllerTest.java new file mode 100644 index 000000000..61fc42f00 --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/TokenControllerTest.java @@ -0,0 +1,480 @@ +package bio.overture.ego.controller; + +import static bio.overture.ego.model.enums.AccessLevel.DENY; +import static bio.overture.ego.model.enums.AccessLevel.READ; +import static bio.overture.ego.model.enums.AccessLevel.WRITE; +import static java.util.Arrays.asList; +import static net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER; +import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.service.PolicyService; +import bio.overture.ego.service.TokenService; +import bio.overture.ego.service.UserPermissionService; +import bio.overture.ego.service.UserService; +import bio.overture.ego.utils.EntityGenerator; +import bio.overture.ego.utils.TestData; +import java.util.UUID; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.LinkedMultiValueMap; + +@Slf4j +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class TokenControllerTest extends AbstractControllerTest { + + @Autowired private PolicyService policyService; + + @Autowired private UserService userService; + + @Autowired private UserPermissionService userPermissionService; + + @Autowired private EntityGenerator entityGenerator; + + @Autowired private TokenService tokenService; + + @Value("${logging.test.controller.enable}") + private boolean enableLogging; + + private TestData test; + + private final String DESCRIPTION = "This is a Test Token"; + + @Override + protected boolean enableLogging() { + return enableLogging; + } + + @Override + protected void beforeTest() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestPolicies(); + test = new TestData(entityGenerator); + } + + @Test + public void issueTokenShouldRevokeRedundantTokens() { + val user = entityGenerator.setupUser("Test User"); + val userId = user.getId(); + val standByUser = entityGenerator.setupUser("Test User2"); + entityGenerator.setupPolicies("aws,no-be-used", "collab,no-be-used"); + entityGenerator.addPermissions(user, entityGenerator.getScopes("aws.READ", "collab.READ")); + + val tokenRevoke = + entityGenerator.setupToken( + user, "token 1", false, 1000, "", entityGenerator.getScopes("collab.READ", "aws.READ")); + + val otherToken = + entityGenerator.setupToken( + standByUser, + "token not be affected", + false, + 1000, + "", + entityGenerator.getScopes("collab.READ", "aws.READ")); + + val otherToken2 = + entityGenerator.setupToken( + user, + "token 2 not be affected", + false, + 1000, + "", + entityGenerator.getScopes("collab.READ")); + + assertThat(tokenService.getById(tokenRevoke.getId()).isRevoked()).isFalse(); + assertThat(tokenService.getById(otherToken.getId()).isRevoked()).isFalse(); + assertThat(tokenService.getById(otherToken2.getId()).isRevoked()).isFalse(); + + val scopes = "collab.READ,aws.READ"; + val params = new LinkedMultiValueMap(); + params.add("user_id", userId.toString()); + params.add("scopes", scopes); + params.add("description", DESCRIPTION); + super.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + val response = initStringRequest().endpoint("o/token").body(params).post(); + val responseStatus = response.getStatusCode(); + + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + assertThat(tokenService.getById(tokenRevoke.getId()).isRevoked()).isTrue(); + assertThat(tokenService.getById(otherToken.getId()).isRevoked()).isFalse(); + assertThat(tokenService.getById(otherToken2.getId()).isRevoked()).isFalse(); + } + + @SneakyThrows + @Test + public void issueTokenExactScope() { + // if scopes are exactly the same as user scopes, issue token should be successful, + + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + val study001 = policyService.getByName("Study001"); + val study001id = study001.getId(); + val study002 = policyService.getByName("Study002"); + val study002id = study002.getId(); + val study003 = policyService.getByName("Study003"); + val study003id = study003.getId(); + + val permissions = + asList( + new PermissionRequest(study001id, READ), + new PermissionRequest(study002id, WRITE), + new PermissionRequest(study003id, DENY)); + + userPermissionService.addPermissions(user.getId(), permissions); + + val scopes = "Study001.READ,Study002.WRITE"; + val params = new LinkedMultiValueMap(); + params.add("user_id", userId.toString()); + params.add("scopes", scopes); + params.add("description", DESCRIPTION); + + super.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + val response = initStringRequest().endpoint("o/token").body(params).post(); + val statusCode = response.getStatusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.OK); + assertThatJson(response.getBody()) + .when(IGNORING_ARRAY_ORDER) + .node("scope") + .isEqualTo("[\"Study002.WRITE\",\"Study001.READ\"]") + .node("description") + .isEqualTo(DESCRIPTION); + } + + @SneakyThrows + @Test + public void issueTokenWithExcessiveScope() { + // If token has scopes that user doesn't, token won't be issued. + + val user = userService.getByName("SecondUser@domain.com"); + val userId = user.getId(); + val study001 = policyService.getByName("Study001"); + val study001id = study001.getId(); + val study002 = policyService.getByName("Study002"); + val study002id = study002.getId(); + + val permissions = + asList(new PermissionRequest(study001id, READ), new PermissionRequest(study002id, READ)); + + userPermissionService.addPermissions(user.getId(), permissions); + + val scopes = "Study001.WRITE,Study002.WRITE"; + val params = new LinkedMultiValueMap(); + params.add("user_id", userId.toString()); + params.add("scopes", scopes); + params.add("description", DESCRIPTION); + + super.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + val response = initStringRequest().endpoint("o/token").body(params).post(); + val statusCode = response.getStatusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + + val jsonResponse = MAPPER.readTree(response.getBody()); + assertThat(jsonResponse.get("error").asText()) + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()); + } + + @SneakyThrows + @Test + public void issueTokenForLimitedScopes() { + // if scopes are subset of user scopes, issue token should be successful + + val user = userService.getByName("UserTwo@domain.com"); + val userId = user.getId(); + + val study001 = policyService.getByName("Study001"); + val study001id = study001.getId(); + + val study002 = policyService.getByName("Study002"); + val study002id = study002.getId(); + + val study003 = policyService.getByName("Study003"); + val study003id = study003.getId(); + + val permissions = + asList( + new PermissionRequest(study001id, READ), + new PermissionRequest(study002id, WRITE), + new PermissionRequest(study003id, READ)); + + userPermissionService.addPermissions(user.getId(), permissions); + + val scopes = "Study001.READ,Study002.WRITE"; + val params = new LinkedMultiValueMap(); + params.add("user_id", userId.toString()); + params.add("scopes", scopes); + params.add("description", DESCRIPTION); + + super.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + val response = initStringRequest().endpoint("o/token").body(params).post(); + val statusCode = response.getStatusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.OK); + assertThatJson(response.getBody()) + .when(IGNORING_ARRAY_ORDER) + .node("scope") + .isEqualTo("[\"Study002.WRITE\",\"Study001.READ\"]") + .node("description") + .isEqualTo(DESCRIPTION); + } + + @SneakyThrows + @Test + public void issueTokenForInvalidScope() { + // If requested scopes don't exist, should get 404 + + val user = userService.getByName("UserOne@domain.com"); + val userId = user.getId(); + + val study001 = policyService.getByName("Study001"); + val study001id = study001.getId(); + + val study002 = policyService.getByName("Study002"); + val study002id = study002.getId(); + + val study003 = policyService.getByName("Study003"); + val study003id = study003.getId(); + + val permissions = + asList( + new PermissionRequest(study001id, READ), + new PermissionRequest(study002id, WRITE), + new PermissionRequest(study003id, READ)); + + userPermissionService.addPermissions(user.getId(), permissions); + + val scopes = "Study001.READ,Invalid.WRITE"; + val params = new LinkedMultiValueMap(); + params.add("user_id", userId.toString()); + params.add("scopes", scopes); + params.add("description", DESCRIPTION); + + super.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + val response = initStringRequest().endpoint("o/token").body(params).post(); + + val statusCode = response.getStatusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.NOT_FOUND); + val jsonResponse = MAPPER.readTree(response.getBody()); + assertThat(jsonResponse.get("error").asText()) + .isEqualTo(HttpStatus.NOT_FOUND.getReasonPhrase()); + } + + @SneakyThrows + @Test + public void issueTokenForInvalidUser() { + val userId = UUID.randomUUID(); + val scopes = "Study001.READ,Invalid.WRITE"; + val params = new LinkedMultiValueMap(); + params.add("user_id", userId.toString()); + params.add("scopes", scopes); + params.add("description", DESCRIPTION); + + super.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + val response = initStringRequest().endpoint("o/token").body(params).post(); + + val statusCode = response.getStatusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + + val jsonResponse = MAPPER.readTree(response.getBody()); + assertThat(jsonResponse.get("error").asText()) + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()); + } + + @SneakyThrows + @Test + public void checkRevokedToken() { + val user = userService.getByName("UserThree@domain.com"); + val tokenName = "601044a1-3ffd-4164-a6a0-0e1e666b28dc"; + val scopes = test.getScopes("song.WRITE", "id.WRITE", "portal.WRITE"); + entityGenerator.setupToken(user, tokenName, true, 1000, "test token", scopes); + + val params = new LinkedMultiValueMap(); + params.add("token", tokenName); + super.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + super.getHeaders().set("Authorization", test.songAuth); + + val response = initStringRequest().endpoint("o/check_token").body(params).post(); + + val statusCode = response.getStatusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @SneakyThrows + @Test + public void checkValidToken() { + val user = userService.getByName("UserThree@domain.com"); + val tokenName = "501044a1-3ffd-4164-a6a0-0e1e666b28dc"; + val scopes = test.getScopes("song.WRITE", "id.WRITE", "portal.WRITE"); + entityGenerator.setupToken(user, tokenName, false, 1000, "test token", scopes); + + val params = new LinkedMultiValueMap(); + params.add("token", tokenName); + super.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + super.getHeaders().set("Authorization", test.songAuth); + + val response = initStringRequest().endpoint("o/check_token").body(params).post(); + + val statusCode = response.getStatusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.MULTI_STATUS); + } + + @SneakyThrows + @Test + public void checkInvalidToken() { + val randomToken = UUID.randomUUID().toString(); + val params = new LinkedMultiValueMap(); + params.add("token", randomToken); + + super.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + super.getHeaders().set("Authorization", test.songAuth); + + val response = initStringRequest().endpoint("o/check_token").body(params).post(); + + val statusCode = response.getStatusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @SneakyThrows + @Test + public void getUserScope() { + val user = userService.getByName("ThirdUser@domain.com"); + val userName = "ThirdUser@domain.com"; + + val study001 = policyService.getByName("Study001"); + val study001id = study001.getId(); + + val study002 = policyService.getByName("Study002"); + val study002id = study002.getId(); + + val study003 = policyService.getByName("Study003"); + val study003id = study003.getId(); + + val permissions = + asList( + new PermissionRequest(study001id, READ), + new PermissionRequest(study002id, WRITE), + new PermissionRequest(study003id, DENY)); + + userPermissionService.addPermissions(user.getId(), permissions); + + val response = initStringRequest().endpoint("o/scopes?userName=%s", userName).get(); + + val statusCode = response.getStatusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.OK); + assertThatJson(response.getBody()) + .when(IGNORING_ARRAY_ORDER) + .node("scopes") + .isEqualTo("[\"Study002.WRITE\",\"Study001.READ\",\"Study003.DENY\"]"); + } + + @SneakyThrows + @Test + public void getUserScopeInvalidUserName() { + val userName = "randomUser@domain.com"; + val response = initStringRequest().endpoint("o/scopes?userName=%s", userName).get(); + + val statusCode = response.getStatusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.NOT_FOUND); + } + + @SneakyThrows + @Test + public void listToken() { + val user = entityGenerator.setupUser("List Token"); + val userId = user.getId().toString(); + + val tokenString1 = "791044a1-3ffd-4164-a6a0-0e1e666b28dc"; + val tokenString2 = "891044a1-3ffd-4164-a6a0-0e1e666b28dc"; + val tokenString3 = "491044a1-3ffd-4164-a6a0-0e1e666b28dc"; + + val scopes1 = test.getScopes("song.READ"); + val scopes2 = test.getScopes("collab.READ"); + val scopes3 = test.getScopes("id.WRITE"); + + entityGenerator.setupToken(user, tokenString1, false, 1000, "test token 1", scopes1); + entityGenerator.setupToken(user, tokenString2, false, 1000, "test token 2", scopes2); + entityGenerator.setupToken(user, tokenString3, true, 1000, "revoked token 3", scopes3); + + val response = initStringRequest().endpoint("o/token?user_id=%s", userId).get(); + + val statusCode = response.getStatusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.OK); + + // Result should only have unrevoked tokens, ignoring the "exp" field. + val expected = + "[{\"accessToken\":\"891044a1-3ffd-4164-a6a0-0e1e666b28dc\"," + + "\"scope\":[\"collab.READ\"]," + + "\"exp\":\"${json-unit.ignore}\"," + + "\"description\":\"test token 2\"}," + + "{\"accessToken\":\"791044a1-3ffd-4164-a6a0-0e1e666b28dc\"," + + "\"scope\":[\"song.READ\"]," + + "\"exp\":\"${json-unit.ignore}\"," + + "\"description\":\"test token 1\"}]"; + assertThatJson(response.getBody()).when(IGNORING_ARRAY_ORDER).isEqualTo(expected); + } + + @SneakyThrows + @Test + public void listTokenEmptyToken() { + val userId = test.adminUser.getId().toString(); + val response = initStringRequest().endpoint("o/token?user_id=%s", userId).get(); + + val statusCode = response.getStatusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("[]"); + } + + @SneakyThrows + @Test + public void tokenShouldHaveNonZeroExpiry() { + val user = entityGenerator.setupUser("NonZero User"); + entityGenerator.setupSinglePolicy("NonZeroExpiryPolicy"); + entityGenerator.addPermissions(user, entityGenerator.getScopes("NonZeroExpiryPolicy.READ")); + + val scopes = "NonZeroExpiryPolicy.READ"; + val params = new LinkedMultiValueMap(); + params.add("user_id", user.getId().toString()); + params.add("scopes", scopes); + params.add("description", DESCRIPTION); + super.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + val response = initStringRequest().endpoint("o/token").body(params).post(); + val responseStatus = response.getStatusCode(); + + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + + val listResponse = + initStringRequest().endpoint("o/token?user_id=%s", user.getId().toString()).get(); + val listStatusCode = listResponse.getStatusCode(); + assertThat(listStatusCode).isEqualTo(HttpStatus.OK); + + log.info(listResponse.getBody()); + val responseJson = MAPPER.readTree(listResponse.getBody()); + val exp = responseJson.get(0).get("exp").asInt(); + assertThat(exp).isNotZero(); + assertThat(exp).isPositive(); + } +} diff --git a/src/test/java/bio/overture/ego/controller/TokensOnPermissionsChangeTest.java b/src/test/java/bio/overture/ego/controller/TokensOnPermissionsChangeTest.java new file mode 100644 index 000000000..b70b113c1 --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/TokensOnPermissionsChangeTest.java @@ -0,0 +1,547 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.AccessLevel; +import bio.overture.ego.model.enums.ApplicationType; +import bio.overture.ego.utils.EntityGenerator; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.google.common.collect.ImmutableList; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@Slf4j +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class TokensOnPermissionsChangeTest extends AbstractControllerTest { + + /** Config */ + @Value("${logging.test.controller.enable}") + private boolean enableLogging; + + /** Dependencies */ + @Autowired private EntityGenerator entityGenerator; + + private HttpHeaders tokenHeaders = new HttpHeaders(); + + @Override + protected void beforeTest() { + entityGenerator.setupApplication("tokenClient", "tokenSecret", ApplicationType.ADMIN); + tokenHeaders.add(AUTHORIZATION, "Basic dG9rZW5DbGllbnQ6dG9rZW5TZWNyZXQ="); + tokenHeaders.setContentType(APPLICATION_JSON); + } + + @Override + protected boolean enableLogging() { + return enableLogging; + } + + /** + * Scenario: Delete a user permission for a user who as an active access token using a scope from + * that permission. Expected Behavior: Token should be revoked. + */ + @Test + @SneakyThrows + public void deletePermissionFromUser_ExistingToken_RevokeSuccess() { + val user = entityGenerator.setupUser("UserFoo DeletePermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForSingleUserDeletePermission"); + val accessToken = userPermissionTestSetup(user, policy, AccessLevel.WRITE, "WRITE"); + + val getPermissionsResponse = + initStringRequest().endpoint("/users/%s/permissions", user.getId()).get(); + val permissionJson = MAPPER.readTree(getPermissionsResponse.getBody()); + val results = (ArrayNode) permissionJson.get("resultSet"); + val permissionId = results.elements().next().get("id").asText(); + + val deletePermissionResponse = + initStringRequest() + .endpoint("/users/%s/permissions/%s", user.getId(), permissionId) + .delete(); + val deleteStatusCode = deletePermissionResponse.getStatusCode(); + assertThat(deleteStatusCode).isEqualTo(HttpStatus.OK); + + val checkTokenAfterDeleteResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + // Should be revoked + assertThat(checkTokenAfterDeleteResponse.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + /** + * Scenario: Upgrade a user permission from READ to WRITE. The user had a token using READ before + * the upgrade. Expected Behavior: Token should be remain active. + */ + @Test + @SneakyThrows + public void upgradePermissionFromUser_ExistingToken_KeepTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo UpgradePermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForSingleUserUpgradePermission"); + val accessToken = userPermissionTestSetup(user, policy, AccessLevel.READ, "READ"); + + val permissionUpgradeRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(AccessLevel.WRITE).build()); + val upgradeResponse = + initStringRequest() + .endpoint("/users/%s/permissions", user.getId().toString()) + .body(permissionUpgradeRequest) + .post(); + + val upgradeStatusCode = upgradeResponse.getStatusCode(); + assertThat(upgradeStatusCode).isEqualTo(HttpStatus.OK); + + val checkTokenAfterUpgradeResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + val statusCode = checkTokenAfterUpgradeResponse.getStatusCode(); + + // Should be valid + assertThat(statusCode).isEqualTo(HttpStatus.MULTI_STATUS); + } + + /** + * Scenario: Downgrade a user permission from WRITE to READ. The user had a token using WRITE + * before the upgrade. Expected Behavior: Token should be revoked. + */ + @Test + @SneakyThrows + public void downgradePermissionFromUser_ExistingToken_RevokeTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo DowngradePermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForSingleUserDowngradePermission"); + val accessToken = userPermissionTestSetup(user, policy, AccessLevel.WRITE, "WRITE"); + + val permissionDowngradeRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(AccessLevel.READ).build()); + val upgradeResponse = + initStringRequest() + .endpoint("/users/%s/permissions", user.getId().toString()) + .body(permissionDowngradeRequest) + .post(); + + val downgradeStatusCode = upgradeResponse.getStatusCode(); + assertThat(downgradeStatusCode).isEqualTo(HttpStatus.OK); + + val checkTokenAfterUpgradeResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + val statusCode = checkTokenAfterUpgradeResponse.getStatusCode(); + + // Should be revoked + assertThat(statusCode).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + /** + * Scenario: DENY a user on a policy. The user had a token using WRITE before the DENY. Expected + * Behavior: Token should be revoked. + */ + @Test + @SneakyThrows + public void denyPermissionFromUser_ExistingToken_RevokeTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo DenyPermission"); + val policy = entityGenerator.setupSinglePolicy("song.abc"); + val accessToken = userPermissionTestSetup(user, policy, AccessLevel.WRITE, "WRITE"); + + val permissionDenyRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(AccessLevel.DENY).build()); + val upgradeResponse = + initStringRequest() + .endpoint("/users/%s/permissions", user.getId().toString()) + .body(permissionDenyRequest) + .post(); + + val denyStatusCode = upgradeResponse.getStatusCode(); + assertThat(denyStatusCode).isEqualTo(HttpStatus.OK); + + val checkTokenAfterUpgradeResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + val statusCode = checkTokenAfterUpgradeResponse.getStatusCode(); + + // Should be revoked + assertThat(statusCode).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + /** + * Scenario: User is part of a group that has a WRITE permission. User has a token using this + * scope. The group then has the permission deleted. Behavior: Token should be revoked. + */ + @Test + @SneakyThrows + public void deleteGroupPermission_ExistingToken_RevokeTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo DeleteGroupPermission"); + val group = entityGenerator.setupGroup("DeleteGroupPermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForGroupDeletePermission"); + + val accessToken = groupPermissionTestSetup(user, group, policy, AccessLevel.WRITE, "READ"); + + val getPermissionsResponse = + initStringRequest().endpoint("/groups/%s/permissions", group.getId()).get(); + val permissionJson = MAPPER.readTree(getPermissionsResponse.getBody()); + val results = (ArrayNode) permissionJson.get("resultSet"); + val permissionId = results.elements().next().get("id").asText(); + + val deletePermissionResponse = + initStringRequest() + .endpoint("/groups/%s/permissions/%s", group.getId(), permissionId) + .delete(); + assertThat(deletePermissionResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + val checkTokenAfterDeleteResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + // Should be revoked + assertThat(checkTokenAfterDeleteResponse.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + /** + * Scenario: User is part of a group that has a READ permission. User has a token using this + * scope. The group then has the permission upgraded to WRITE. Behavior: Token should remain + * valid. + */ + @Test + @SneakyThrows + public void upgradeGroupPermission_ExistingToken_KeepTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo UpgradeGroupPermission"); + val group = entityGenerator.setupGroup("UpgradeGroupPermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForGroupUpgradePermission"); + + val accessToken = groupPermissionTestSetup(user, group, policy, AccessLevel.READ, "READ"); + + val permissionUpgradeRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(AccessLevel.WRITE).build()); + val upgradeResponse = + initStringRequest() + .endpoint("/groups/%s/permissions", group.getId().toString()) + .body(permissionUpgradeRequest) + .post(); + assertThat(upgradeResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + val checkTokenAfterUpgradeResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + // Should be valid + assertThat(checkTokenAfterUpgradeResponse.getStatusCode()).isEqualTo(HttpStatus.MULTI_STATUS); + } + + /** + * Scenario: User is part of a group that has a WRITE permission. User has a token using this + * scope. The group then has the permission downgraded to READ. Behavior: Token should be revoked. + */ + @Test + @SneakyThrows + public void downgradeGroupPermission_ExistingToken_RevokeTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo DowngradeGroupPermission"); + val group = entityGenerator.setupGroup("DowngradeGroupPermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForGroupDowngradePermission"); + + val accessToken = groupPermissionTestSetup(user, group, policy, AccessLevel.WRITE, "WRITE"); + + val permissionDowngradeRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(AccessLevel.READ).build()); + val downgradeResponse = + initStringRequest() + .endpoint("/groups/%s/permissions", group.getId().toString()) + .body(permissionDowngradeRequest) + .post(); + assertThat(downgradeResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + val checkTokenAfterUpgradeResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + // Should be revoked + assertThat(checkTokenAfterUpgradeResponse.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + /** + * Scenario: User is part of a group that has a WRITE permission. User has a token using this + * scope. The group then has the permission downgraded to DENY. Behavior: Token should be revoked. + */ + @Test + @SneakyThrows + public void denyGroupPermission_ExistingToken_RevokeTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo DenyGroupPermission"); + val group = entityGenerator.setupGroup("DenyGroupPermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForGroupDenyPermission"); + + val accessToken = groupPermissionTestSetup(user, group, policy, AccessLevel.WRITE, "WRITE"); + + val permissionDenyRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(AccessLevel.DENY).build()); + val denyResponse = + initStringRequest() + .endpoint("/groups/%s/permissions", group.getId().toString()) + .body(permissionDenyRequest) + .post(); + assertThat(denyResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + val checkTokenAfterUpgradeResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + // Should be revoked + assertThat(checkTokenAfterUpgradeResponse.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + /** + * Scenario: User is part of a group that has a WRITE permission. User has a token using this + * scope. The user is then removed from this group. Behavior: Token should be revoked. + */ + @Test + @SneakyThrows + public void removeUserFromGroupPermission_ExistingToken_RevokeTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo removeGroupPermission"); + val group = entityGenerator.setupGroup("RemoveGroupPermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForGroupRemovePermission"); + + val accessToken = groupPermissionTestSetup(user, group, policy, AccessLevel.WRITE, "WRITE"); + + val removeUserFromGroupResponse = + initStringRequest() + .endpoint("/users/%s/groups/%s", user.getId().toString(), group.getId().toString()) + .delete(); + assertThat(removeUserFromGroupResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + val checkTokenAfterUpgradeResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + // Should be revoked + assertThat(checkTokenAfterUpgradeResponse.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + /** + * Scenario: User is part of a group that has a WRITE permission. User has a token using this + * scope. The user is then added to a group that has the DENY permission. Behavior: Token should + * be revoked. + */ + @Test + @SneakyThrows + public void addUserToDenyGroupPermission_ExistingToken_RevokeTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo addDenyGroupPermission"); + val group = entityGenerator.setupGroup("GoodExistingGroupPermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForDenyGroupAddPermission"); + + val accessToken = groupPermissionTestSetup(user, group, policy, AccessLevel.WRITE, "WRITE"); + + val groupDeny = entityGenerator.setupGroup("AddDenyGroupPermission"); + + val permissionRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(AccessLevel.DENY).build()); + initStringRequest() + .endpoint("/groups/%s/permissions", groupDeny.getId().toString()) + .body(permissionRequest) + .post(); + + val groupRequest = ImmutableList.of(groupDeny.getId()); + val groupResponse = + initStringRequest() + .endpoint("/users/%s/groups", user.getId().toString()) + .body(groupRequest) + .post(); + assertThat(groupResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + val checkTokenAfterUpgradeResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + // Should be revoked + assertThat(checkTokenAfterUpgradeResponse.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + /** + * Scenario: User is part of a group that has a READ permission. User has a token using this + * scope. The user is then added to a group that has the WRITE permission. Behavior: Token should + * remain valid. + */ + @Test + @SneakyThrows + public void addUserToWriteGroupPermission_ExistingToken_KeepTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo addDenyGroupPermission"); + val group = entityGenerator.setupGroup("GoodExistingReadGroupPermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForWriteGroupUpgradeAddPermission"); + + val accessToken = groupPermissionTestSetup(user, group, policy, AccessLevel.READ, "READ"); + + val groupWrite = entityGenerator.setupGroup("AddWriteUpgradeGroupPermission"); + + val permissionRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(AccessLevel.WRITE).build()); + initStringRequest() + .endpoint("/groups/%s/permissions", groupWrite.getId().toString()) + .body(permissionRequest) + .post(); + + val groupRequest = ImmutableList.of(groupWrite.getId()); + val groupResponse = + initStringRequest() + .endpoint("/users/%s/groups", user.getId().toString()) + .body(groupRequest) + .post(); + assertThat(groupResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + val checkTokenAfterUpgradeResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + // Should be valid + assertThat(checkTokenAfterUpgradeResponse.getStatusCode()).isEqualTo(HttpStatus.MULTI_STATUS); + } + + /** + * Scenario: User is part of a group that has a READ permission. User has a token using this + * scope. The group is then deleted. Behavior: Token should be revoked. + */ + @Test + @SneakyThrows + public void deleteGroupWithUserAndPermission_ExistingToken_RevokeTokenSuccess() { + val user = entityGenerator.setupUser("UserFoo deleteGroupWithUserPermission"); + val group = entityGenerator.setupGroup("DeleteGroupWithUserPermission"); + val policy = entityGenerator.setupSinglePolicy("PolicyForDeleteGroupWithUserPermission"); + + val accessToken = groupPermissionTestSetup(user, group, policy, AccessLevel.READ, "READ"); + + val deleteGroupResponse = + initStringRequest() + .endpoint("/users/%s/groups/%s", user.getId().toString(), group.getId().toString()) + .delete(); + assertThat(deleteGroupResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + val checkTokenAfterGroupDeleteResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + // Should be revoked + assertThat(checkTokenAfterGroupDeleteResponse.getStatusCode()) + .isEqualTo(HttpStatus.UNAUTHORIZED); + } + + /** + * This helper method is responsible for executing the pre-conditions of the scenario for user + * permission mutations. + * + * @param user User that will have heir permissions mutated. + * @param policy Policy that the permissions will be against. + * @param initalAccess The initial access level (MASK) that a user will have to the policy. + * @param tokenScopeSuffix The scope suffix that the token will be created with. + * @return The access token + */ + @SneakyThrows + private String userPermissionTestSetup( + User user, Policy policy, AccessLevel initalAccess, String tokenScopeSuffix) { + val permissionRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(initalAccess).build()); + initStringRequest() + .endpoint("/users/%s/permissions", user.getId().toString()) + .body(permissionRequest) + .post(); + + val createTokenResponse = + initStringRequest() + .endpoint( + "/o/token?user_id=%s&scopes=%s", + user.getId().toString(), policy.getName() + "." + tokenScopeSuffix) + .post(); + + val tokenResponseJson = MAPPER.readTree(createTokenResponse.getBody()); + val accessToken = tokenResponseJson.get("accessToken").asText(); + + val checkTokenResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + val checkStatusCode = checkTokenResponse.getStatusCode(); + assertThat(checkStatusCode).isEqualTo(HttpStatus.MULTI_STATUS); + assertThat(checkTokenResponse.getBody()).contains(policy.getName() + "." + tokenScopeSuffix); + + return accessToken; + } + + /** + * This helper method is responsible for executing the pre-conditions of the scenario for group + * permission mutations. + * + * @param user The user that will belong to the group which is having the permissions mutated. + * @param group The group to which the group permissions are assigned. + * @param policy The policy that the permission is relevant to. + * @param initalAccess The initial access level (MASK) for the group on the policy. + * @param tokenScopeSuffix The scope suffix that the token will be created with for the user. + * @return The access token + */ + @SneakyThrows + private String groupPermissionTestSetup( + User user, Group group, Policy policy, AccessLevel initalAccess, String tokenScopeSuffix) { + + // Associate User with Group + val groupRequest = ImmutableList.of(group.getId()); + val groupResponse = + initStringRequest() + .endpoint("/users/%s/groups", user.getId().toString()) + .body(groupRequest) + .post(); + assertThat(groupResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + // Create Group Permission + val permissionRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(initalAccess).build()); + initStringRequest() + .endpoint("/groups/%s/permissions", group.getId().toString()) + .body(permissionRequest) + .post(); + + val createTokenResponse = + initStringRequest() + .endpoint( + "/o/token?user_id=%s&scopes=%s", + user.getId().toString(), policy.getName() + "." + tokenScopeSuffix) + .post(); + + val tokenResponseJson = MAPPER.readTree(createTokenResponse.getBody()); + val accessToken = tokenResponseJson.get("accessToken").asText(); + + val checkTokenResponse = + initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", accessToken).post(); + + val checkStatusCode = checkTokenResponse.getStatusCode(); + assertThat(checkStatusCode).isEqualTo(HttpStatus.MULTI_STATUS); + assertThat(checkTokenResponse.getBody()).contains(policy.getName() + "." + tokenScopeSuffix); + + return accessToken; + } +} diff --git a/src/test/java/bio/overture/ego/controller/TokensOnUserAndPolicyDeletes.java b/src/test/java/bio/overture/ego/controller/TokensOnUserAndPolicyDeletes.java new file mode 100644 index 000000000..4fe12bd8d --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/TokensOnUserAndPolicyDeletes.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.AccessLevel; +import bio.overture.ego.model.enums.ApplicationType; +import bio.overture.ego.utils.EntityGenerator; +import com.google.common.collect.ImmutableList; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@Slf4j +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class TokensOnUserAndPolicyDeletes extends AbstractControllerTest { + + /** Config */ + @Value("${logging.test.controller.enable}") + private boolean enableLogging; + + /** Dependencies */ + @Autowired private EntityGenerator entityGenerator; + + private HttpHeaders tokenHeaders = new HttpHeaders(); + + @Override + protected void beforeTest() { + entityGenerator.setupApplication("tokenClient", "tokenSecret", ApplicationType.ADMIN); + tokenHeaders.add(AUTHORIZATION, "Basic dG9rZW5DbGllbnQ6dG9rZW5TZWNyZXQ="); + tokenHeaders.setContentType(APPLICATION_JSON); + } + + @Override + protected boolean enableLogging() { + return enableLogging; + } + + /** + * Scenario: Two users with tokens that have a scope on a policy. Delete one user. Expected + * Behavior: Deleted user shold also have tokens deleted, other user's tokens should remain valid. + */ + @Test + public void deleteUser_ExistingTokens_TokensDeletedSuccess() { + val userDelete = entityGenerator.setupUser("UserTokens DeleteUser"); + val userKeep = entityGenerator.setupUser("UserTokens DontDeleteUser"); + val policy = entityGenerator.setupSinglePolicy("PolicyForUserDeleteTest"); + + val tokenToDelete = setupUserWithToken(userDelete, policy); + val tokenToKeep = setupUserWithToken(userKeep, policy); + + val deleteUserResponse = initStringRequest().endpoint("/users/%s", userDelete.getId()).delete(); + + val deleteStatusCode = deleteUserResponse.getStatusCode(); + assertThat(deleteStatusCode).isEqualTo(HttpStatus.OK); + + val checkTokenAfterDeleteResponse = checkToken(tokenToDelete); + // Should be revoked + assertThat(checkTokenAfterDeleteResponse.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + + val checkTokenRemainedAfterDeleteResponse = checkToken(tokenToKeep); + // Should be valid + assertThat(checkTokenRemainedAfterDeleteResponse.getStatusCode()) + .isEqualTo(HttpStatus.MULTI_STATUS); + } + + /** + * Scenario: User1 has token for policy1. User2 has token for policy2. Delete policy1. Expected + * Behavior: User1 should have token deleted, user2's token should remain valid. + */ + @Test + public void deletePolicy_ExistingTokens_TokensDeletedSuccess() { + val user1 = entityGenerator.setupUser("UserTokens ForDeletedPolicy"); + val user2 = entityGenerator.setupUser("UserTokens ForKeptPolicy"); + val policy1 = entityGenerator.setupSinglePolicy("PolicyToBeDeletedForTokens"); + val policy2 = entityGenerator.setupSinglePolicy("PolicyToBeKeptForTokens"); + + val tokenToDelete = setupUserWithToken(user1, policy1); + val tokenToKeep = setupUserWithToken(user2, policy2); + + val deletePolicyResponse = + initStringRequest().endpoint("/policies/%s", policy1.getId()).delete(); + val deleteStatusCode = deletePolicyResponse.getStatusCode(); + assertThat(deleteStatusCode).isEqualTo(HttpStatus.OK); + + val checkTokenAfterDeleteResponse = checkToken(tokenToDelete); + // Should be revoked + assertThat(checkTokenAfterDeleteResponse.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + + val checkTokenRemainedAfterDeleteResponse = checkToken(tokenToKeep); + // Should be valid + assertThat(checkTokenRemainedAfterDeleteResponse.getStatusCode()) + .isEqualTo(HttpStatus.MULTI_STATUS); + } + + /** + * This helper method is responsible for executing the pre-conditions of the scenario for user + * permission mutations. + * + * @param user User that will have heir permissions mutated. + * @param policy Policy that the permissions will be against. + * @return The access token for the user. + */ + @SneakyThrows + private String setupUserWithToken(User user, Policy policy) { + val permissionRequest = + ImmutableList.of( + PermissionRequest.builder().policyId(policy.getId()).mask(AccessLevel.WRITE).build()); + initStringRequest() + .endpoint("/users/%s/permissions", user.getId().toString()) + .body(permissionRequest) + .post(); + + val createTokenResponse = + initStringRequest() + .endpoint( + "/o/token?user_id=%s&scopes=%s", + user.getId().toString(), policy.getName() + "." + "WRITE") + .post(); + + val tokenResponseJson = MAPPER.readTree(createTokenResponse.getBody()); + val accessToken = tokenResponseJson.get("accessToken").asText(); + + val checkTokenResponse = checkToken(accessToken); + + val checkStatusCode = checkTokenResponse.getStatusCode(); + assertThat(checkStatusCode).isEqualTo(HttpStatus.MULTI_STATUS); + assertThat(checkTokenResponse.getBody()).contains(policy.getName() + "." + "WRITE"); + + return accessToken; + } + + private ResponseEntity checkToken(String token) { + return initStringRequest(tokenHeaders).endpoint("/o/check_token?token=%s", token).post(); + } +} diff --git a/src/test/java/bio/overture/ego/controller/UserControllerTest.java b/src/test/java/bio/overture/ego/controller/UserControllerTest.java new file mode 100644 index 000000000..667a63f54 --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/UserControllerTest.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.controller; + +import static bio.overture.ego.model.enums.LanguageType.ENGLISH; +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static bio.overture.ego.model.enums.StatusType.REJECTED; +import static bio.overture.ego.model.enums.UserType.USER; +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static bio.overture.ego.utils.Collectors.toImmutableList; +import static bio.overture.ego.utils.EntityTools.extractUserIds; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.model.dto.CreateUserRequest; +import bio.overture.ego.model.dto.UpdateUserRequest; +import bio.overture.ego.model.join.UserGroup; +import bio.overture.ego.service.ApplicationService; +import bio.overture.ego.service.GroupService; +import bio.overture.ego.service.UserService; +import bio.overture.ego.utils.EntityGenerator; +import bio.overture.ego.utils.Streams; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.UUID; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@Slf4j +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class UserControllerTest extends AbstractControllerTest { + + private static boolean hasRunEntitySetup = false; + + /** Dependencies */ + @Autowired private EntityGenerator entityGenerator; + + @Autowired private UserService userService; + @Autowired private ApplicationService applicationService; + @Autowired private GroupService groupService; + + @Value("${logging.test.controller.enable}") + private boolean enableLogging; + + @Override + protected boolean enableLogging() { + return enableLogging; + } + + @Override + protected void beforeTest() { + // Initial setup of entities (run once + if (!hasRunEntitySetup) { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + entityGenerator.setupTestGroups(); + hasRunEntitySetup = true; + } + } + + @Test + public void addUser() { + + val user = + CreateUserRequest.builder() + .firstName("foo") + .lastName("bar") + .email("foobar@foo.bar") + .preferredLanguage(ENGLISH) + .type(USER) + .status(APPROVED) + .build(); + + val response = initStringRequest().endpoint("/users").body(user).post(); + + val responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + } + + @Test + public void addUniqueUser() { + val user1 = + CreateUserRequest.builder() + .firstName("unique") + .lastName("unique") + .email("unique@unique.com") + .preferredLanguage(ENGLISH) + .type(USER) + .status(APPROVED) + .build(); + val user2 = + CreateUserRequest.builder() + .firstName("unique") + .lastName("unique") + .email("unique@unique.com") + .preferredLanguage(ENGLISH) + .type(USER) + .status(APPROVED) + .build(); + + val response1 = initStringRequest().endpoint("/users").body(user1).post(); + val responseStatus1 = response1.getStatusCode(); + + assertThat(responseStatus1).isEqualTo(HttpStatus.OK); + + // Return a 409 conflict because email already exists for a registered user. + val response2 = initStringRequest().endpoint("/users").body(user2).post(); + val responseStatus2 = response2.getStatusCode(); + assertThat(responseStatus2).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + @SneakyThrows + public void getUser() { + + // Users created in setup + val userId = userService.getByName("FirstUser@domain.com").getId(); + val response = initStringRequest().endpoint("/users/%s", userId).get(); + + val responseStatus = response.getStatusCode(); + val responseJson = MAPPER.readTree(response.getBody()); + + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + assertThat(responseJson.get("firstName").asText()).isEqualTo("First"); + assertThat(responseJson.get("lastName").asText()).isEqualTo("User"); + assertThat(responseJson.get("name").asText()).isEqualTo("FirstUser@domain.com"); + assertThat(responseJson.get("preferredLanguage").asText()).isEqualTo(ENGLISH.toString()); + assertThat(responseJson.get("status").asText()).isEqualTo(APPROVED.toString()); + assertThat(responseJson.get("id").asText()).isEqualTo(userId.toString()); + } + + @Test + public void getUser404() { + val response = initStringRequest().endpoint("/users/%s", UUID.randomUUID().toString()).get(); + + val responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + @SneakyThrows + public void listUsersNoFilter() { + val numUsers = userService.getRepository().count(); + + // Since previous test may introduce new users. If there are more users than the default page + // size, only a subset will be returned and could cause a test failure. + val response = initStringRequest().endpoint("/users?offset=0&limit=%s", numUsers).get(); + + val responseStatus = response.getStatusCode(); + val responseJson = MAPPER.readTree(response.getBody()); + + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + assertThat(responseJson.get("count").asInt()).isGreaterThanOrEqualTo(3); + assertThat(responseJson.get("resultSet").isArray()).isTrue(); + + // Verify that the returned Users are the ones from the setup. + Iterable resultSetIterable = () -> responseJson.get("resultSet").iterator(); + val actualUserNames = + Streams.stream(resultSetIterable) + .map(j -> j.get("name").asText()) + .collect(toImmutableList()); + assertThat(actualUserNames) + .contains("FirstUser@domain.com", "SecondUser@domain.com", "ThirdUser@domain.com"); + } + + @Test + @SneakyThrows + public void listUsersWithQuery() { + val response = initStringRequest().endpoint("/users?query=FirstUser").get(); + + val responseStatus = response.getStatusCode(); + val responseJson = MAPPER.readTree(response.getBody()); + + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + assertThat(responseJson.get("count").asInt()).isEqualTo(1); + assertThat(responseJson.get("resultSet").isArray()).isTrue(); + assertThat(responseJson.get("resultSet").elements().next().get("name").asText()) + .isEqualTo("FirstUser@domain.com"); + } + + @Test + public void updateUser() { + val user = entityGenerator.setupUser("update test"); + val update = UpdateUserRequest.builder().status(REJECTED).build(); + + val response = initStringRequest().endpoint("/users/%s", user.getId()).body(update).put(); + + val responseBody = response.getBody(); + + HttpStatus responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + assertThatJson(responseBody).node("id").isEqualTo(user.getId()); + assertThatJson(responseBody).node("status").isEqualTo(REJECTED.toString()); + } + + @Test + @SneakyThrows + public void addGroupToUser() { + val userId = entityGenerator.setupUser("Group1 User").getId(); + val groupId = entityGenerator.setupGroup("Addone Group").getId().toString(); + + val response = + initStringRequest() + .endpoint("/users/%s/groups", userId) + .body(singletonList(groupId)) + .post(); + + val responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + + val groupResponse = initStringRequest().endpoint("/users/%s/groups", userId).get(); + + val groupResponseStatus = groupResponse.getStatusCode(); + assertThat(groupResponseStatus).isEqualTo(HttpStatus.OK); + + val groupResponseJson = MAPPER.readTree(groupResponse.getBody()); + assertThat(groupResponseJson.get("count").asInt()).isEqualTo(1); + assertThat(groupResponseJson.get("resultSet").elements().next().get("id").asText()) + .isEqualTo(groupId); + } + + @Test + @SneakyThrows + public void deleteGroupFromUser() { + val userId = entityGenerator.setupUser("DeleteGroup User").getId(); + val deleteGroup = entityGenerator.setupGroup("Delete One Group").getId().toString(); + val remainGroup = entityGenerator.setupGroup("Don't Delete This One").getId().toString(); + + initStringRequest() + .endpoint("/users/%s/groups", userId) + .body(asList(deleteGroup, remainGroup)) + .post(); + + val groupResponse = initStringRequest().endpoint("/users/%s/groups", userId).get(); + + val groupResponseStatus = groupResponse.getStatusCode(); + assertThat(groupResponseStatus).isEqualTo(HttpStatus.OK); + val groupResponseJson = MAPPER.readTree(groupResponse.getBody()); + assertThat(groupResponseJson.get("count").asInt()).isEqualTo(2); + + val deleteResponse = + initStringRequest().endpoint("/users/%s/groups/%s", userId, deleteGroup).delete(); + + val deleteResponseStatus = deleteResponse.getStatusCode(); + assertThat(deleteResponseStatus).isEqualTo(HttpStatus.OK); + + val secondGetResponse = initStringRequest().endpoint("/users/%s/groups", userId).get(); + val secondGetResponseStatus = deleteResponse.getStatusCode(); + assertThat(secondGetResponseStatus).isEqualTo(HttpStatus.OK); + val secondGetResponseJson = MAPPER.readTree(secondGetResponse.getBody()); + assertThat(secondGetResponseJson.get("count").asInt()).isEqualTo(1); + assertThat(secondGetResponseJson.get("resultSet").elements().next().get("id").asText()) + .isEqualTo(remainGroup); + } + + @Test + @SneakyThrows + public void addApplicationToUser() { + val userId = entityGenerator.setupUser("AddApp1 User").getId(); + val appId = entityGenerator.setupApplication("app1").getId().toString(); + + val response = + initStringRequest() + .endpoint("/users/%s/applications", userId) + .body(singletonList(appId)) + .post(); + + val responseStatus = response.getStatusCode(); + assertThat(responseStatus).isEqualTo(HttpStatus.OK); + + val appResponse = initStringRequest().endpoint("/users/%s/applications", userId).get(); + + val appResponseStatus = appResponse.getStatusCode(); + assertThat(appResponseStatus).isEqualTo(HttpStatus.OK); + + val groupResponseJson = MAPPER.readTree(appResponse.getBody()); + assertThat(groupResponseJson.get("count").asInt()).isEqualTo(1); + assertThat(groupResponseJson.get("resultSet").elements().next().get("id").asText()) + .isEqualTo(appId); + } + + @Test + @SneakyThrows + public void deleteApplicationFromUser() { + val userId = entityGenerator.setupUser("App2 User").getId(); + val deleteApp = entityGenerator.setupApplication("deleteApp").getId().toString(); + val remainApp = entityGenerator.setupApplication("remainApp").getId().toString(); + + val appResponse = + initStringRequest() + .endpoint("/users/%s/applications", userId) + .body(asList(deleteApp, remainApp)) + .post(); + + log.info(appResponse.getBody()); + + val appResponseStatus = appResponse.getStatusCode(); + assertThat(appResponseStatus).isEqualTo(HttpStatus.OK); + + val deleteResponse = + initStringRequest().endpoint("/users/%s/applications/%s", userId, deleteApp).delete(); + + val deleteResponseStatus = deleteResponse.getStatusCode(); + assertThat(deleteResponseStatus).isEqualTo(HttpStatus.OK); + + val secondGetResponse = initStringRequest().endpoint("/users/%s/applications", userId).get(); + + val secondGetResponseStatus = deleteResponse.getStatusCode(); + assertThat(secondGetResponseStatus).isEqualTo(HttpStatus.OK); + val secondGetResponseJson = MAPPER.readTree(secondGetResponse.getBody()); + assertThat(secondGetResponseJson.get("count").asInt()).isEqualTo(1); + assertThat(secondGetResponseJson.get("resultSet").elements().next().get("id").asText()) + .isEqualTo(remainApp); + } + + @Test + @SneakyThrows + public void deleteUser() { + val userId = entityGenerator.setupUser("User ToDelete").getId(); + + // Add application to user + val appOne = entityGenerator.setupApplication("TempGroupApp"); + val appBody = singletonList(appOne.getId().toString()); + val addAppToUserResponse = + initStringRequest().endpoint("/users/%s/applications", userId).body(appBody).post(); + val addAppToUserResponseStatus = addAppToUserResponse.getStatusCode(); + assertThat(addAppToUserResponseStatus).isEqualTo(HttpStatus.OK); + + // Make sure user-application relationship is there + val appWithUser = applicationService.getByClientId("TempGroupApp"); + assertThat(extractUserIds(appWithUser.getUsers())).contains(userId); + + // Add group to user + val groupOne = entityGenerator.setupGroup("GroupOne"); + val groupBody = singletonList(groupOne.getId().toString()); + val addGroupToUserResponse = + initStringRequest().endpoint("/users/%s/groups", userId).body(groupBody).post(); + val addGroupToUserResponseStatus = addGroupToUserResponse.getStatusCode(); + assertThat(addGroupToUserResponseStatus).isEqualTo(HttpStatus.OK); + // Make sure user-group relationship is there + val expectedUserGroups = groupService.getByName("GroupOne").getUserGroups(); + val expectedUsers = mapToSet(expectedUserGroups, UserGroup::getUser); + assertThat(extractUserIds(expectedUsers)).contains(userId); + + // delete user + val deleteResponse = initStringRequest().endpoint("/users/%s", userId).delete(); + val deleteResponseStatus = deleteResponse.getStatusCode(); + assertThat(deleteResponseStatus).isEqualTo(HttpStatus.OK); + + // verify if user is deleted + val getUserResponse = initStringRequest().endpoint("/users/%s", userId).get(); + val getUserResponseStatus = getUserResponse.getStatusCode(); + assertThat(getUserResponseStatus).isEqualTo(HttpStatus.NOT_FOUND); + val jsonResponse = MAPPER.readTree(getUserResponse.getBody()); + assertThat(jsonResponse.get("error").asText()) + .isEqualTo(HttpStatus.NOT_FOUND.getReasonPhrase()); + + // check if user - group is deleted + val groupWithoutUser = groupService.getByName("GroupOne"); + assertThat(groupWithoutUser.getUserGroups()).isEmpty(); + + // make sure user - application is deleted + val appWithoutUser = applicationService.getByClientId("TempGroupApp"); + assertThat(appWithoutUser.getUsers()).isEmpty(); + } +} diff --git a/src/test/java/bio/overture/ego/controller/UserPermissionControllerTest.java b/src/test/java/bio/overture/ego/controller/UserPermissionControllerTest.java new file mode 100644 index 000000000..396183813 --- /dev/null +++ b/src/test/java/bio/overture/ego/controller/UserPermissionControllerTest.java @@ -0,0 +1,126 @@ +package bio.overture.ego.controller; + +import static bio.overture.ego.utils.Joiners.COMMA; +import static java.lang.String.format; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.entity.UserPermission; +import bio.overture.ego.service.AbstractPermissionService; +import bio.overture.ego.service.NamedService; +import bio.overture.ego.service.PolicyService; +import bio.overture.ego.service.UserPermissionService; +import bio.overture.ego.service.UserService; +import bio.overture.ego.utils.EntityGenerator; +import java.util.Collection; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +@Slf4j +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@TestExecutionListeners(listeners = DependencyInjectionTestExecutionListener.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class UserPermissionControllerTest + extends AbstractPermissionControllerTest { + + /** Dependencies */ + @Autowired private EntityGenerator entityGenerator; + + @Autowired private PolicyService policyService; + @Autowired private UserService userService; + @Autowired private UserPermissionService userPermissionService; + + @Value("${logging.test.controller.enable}") + private boolean enableLogging; + + @Override + protected boolean enableLogging() { + return enableLogging; + } + + @Override + protected EntityGenerator getEntityGenerator() { + return entityGenerator; + } + + @Override + protected PolicyService getPolicyService() { + return policyService; + } + + @Override + protected Class getOwnerType() { + return User.class; + } + + @Override + protected Class getPermissionType() { + return UserPermission.class; + } + + @Override + protected User generateOwner(String name) { + return entityGenerator.setupUser(name); + } + + @Override + protected String generateNonExistentOwnerName() { + return entityGenerator.generateNonExistentUserName(); + } + + @Override + protected NamedService getOwnerService() { + return userService; + } + + @Override + protected AbstractPermissionService getPermissionService() { + return userPermissionService; + } + + @Override + protected String getAddPermissionsEndpoint(String ownerId) { + return format("users/%s/permissions", ownerId); + } + + @Override + protected String getAddPermissionEndpoint(String policyId, String ownerId) { + return format("policies/%s/permission/user/%s", policyId, ownerId); + } + + @Override + protected String getReadPermissionsEndpoint(String ownerId) { + return format("users/%s/permissions", ownerId); + } + + @Override + protected String getDeleteOwnerEndpoint(String ownerId) { + return format("users/%s", ownerId); + } + + @Override + protected String getDeletePermissionsEndpoint(String ownerId, Collection permissionIds) { + return format("users/%s/permissions/%s", ownerId, COMMA.join(permissionIds)); + } + + @Override + protected String getDeletePermissionEndpoint(String policyId, String ownerId) { + return format("policies/%s/permission/user/%s", policyId, ownerId); + } + + @Override + protected String getReadOwnersForPolicyEndpoint(String policyId) { + return format("policies/%s/users", policyId); + } +} diff --git a/src/test/java/bio/overture/ego/model/entity/ScopeTest.java b/src/test/java/bio/overture/ego/model/entity/ScopeTest.java new file mode 100644 index 000000000..90b06b7fd --- /dev/null +++ b/src/test/java/bio/overture/ego/model/entity/ScopeTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2018. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.model.entity; + +import static bio.overture.ego.utils.CollectionUtils.listOf; +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static org.junit.Assert.*; + +import bio.overture.ego.model.dto.Scope; +import bio.overture.ego.utils.EntityGenerator; +import bio.overture.ego.utils.TestData; +import java.util.HashSet; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@SpringBootTest +@RunWith(SpringRunner.class) +@ActiveProfiles("test") +@Transactional +public class ScopeTest { + @Autowired private EntityGenerator entityGenerator; + public static TestData test; + + @Before + public void initDb() { + if (test == null) { + test = new TestData(entityGenerator); + } + } + + public void testMissing(String msg, Set have, Set want, Set expected) { + val result = Scope.missingScopes(have, want); + assertEquals(msg, expected, result); + } + + public void testEffective(String msg, Set have, Set want, Set expected) { + val result = Scope.effectiveScopes(have, want); + assertEquals(msg, expected, result); + } + + @Test + public void testMissingSame() { + // Test for missing exactly what we have. + // It should return an empty set. + val have = getScopes("song.WRITE", "collab.READ"); + val expected = new HashSet(); + testMissing("Same set", have, have, expected); + } + + @Test + public void testEffectiveSame() { + // Basic sanity check. If what we have and want are the same, that's what our effective scope + // should be. + val have = getScopes("song.WRITE", "collab.READ"); + testEffective("Same set", have, have, have); + } + + @Test + public void testMissingSubset() { + // Test missing + // Test for when what we have is a subset of what we want, + // (ie. permissions are otherwise identical). + + // We should get the set difference. + val have = getScopes("song.WRITE", "collab.READ"); + val want = getScopes("song.WRITE", "collab.READ", "id.READ"); + val expected = getScopes("id.READ"); + testMissing("Subset", have, want, expected); + } + + @Test + public void testEffectiveSubset() { + // When the permissions we have is a subset of what we want, + // our effective permissions are limited to what we have. + val have = getScopes("song.WRITE", "collab.READ"); + val want = getScopes("song.WRITE", "collab.READ", "id.READ"); + testEffective("Subset", have, want, have); + } + + @Test + public void testMissingSuperset() { + // Test to see what happens if what we have is a superset of what we want. + // We should get an empty set (nothing missing). + val have = getScopes("song.WRITE", "collab.READ", "id.READ"); + val want = getScopes("song.WRITE", "collab.READ"); + val expected = new HashSet(); + testMissing("Superset", have, want, expected); + } + + @Test + public void testEffectiveSuperset() { + // When the permissions we have exceed those we want, + // our effective permissions should be limited to those we want. + val have = getScopes("song.WRITE", "collab.READ", "id.READ"); + val want = getScopes("song.WRITE", "collab.READ"); + testEffective("Superset", have, want, want); + } + + @Test + public void testMissingExcessPermissions() { + // Test what happens if we have more permissions that we want + // We should have an empty set (nothing missing) + val have = getScopes("song.WRITE"); + val want = getScopes("song.READ"); + val expected = new HashSet(); + testMissing("Excess Permission", have, want, expected); + } + + @Test + public void testEffectiveExcessPermissions() { + // If we have more permissions that we want, + // our effective permissions should be those we want. + val have = getScopes("song.WRITE"); + val want = getScopes("song.READ"); + testEffective("Excess Permission", have, want, want); + } + + @Test + public void testMissingInsufficientPermissions() { + // Test what happens if we have fewer permissions that we want + // We should get back the scope with the permission that isn't available) + val have = getScopes("song.READ"); + val want = getScopes("song.WRITE"); + val expected = want; + testMissing("Insufficient Permission", have, want, expected); + } + + @Test + public void testEffectiveInsufficientPermissions() { + // If we have lesser permissions than those we want, + // our effective permission should be those we have. + val have = getScopes("song.READ"); + val want = getScopes("song.WRITE"); + testEffective("Insufficient Permission", have, want, have); + } + + @Test + public void testMissingWithDeny() { + // If we have deny in the list of permissions we have, + // it should always be missing from our list of permissions. + + } + + @Test + public void testEffective() { + val have = getScopes("song.WRITE", "collab.READ"); + val want = getScopes("song.READ"); + + val e = Scope.effectiveScopes(have, want); + val expected = getScopes("song.READ"); + assertTrue(e.equals(expected)); + } + + @Test + public void testExplicit() { + val have = getScopes("song.READ", "collab.WRITE"); + + val e = Scope.explicitScopes(have); + val expected = getScopes("song.READ", "collab.READ", "collab.WRITE"); + assertEquals(expected, e); + } + + Set getScopes(String... scopes) { + return mapToSet(listOf(scopes), test::getScope); + } +} diff --git a/src/test/java/bio/overture/ego/model/enums/AccessLevelTest.java b/src/test/java/bio/overture/ego/model/enums/AccessLevelTest.java new file mode 100644 index 000000000..1658c2cd3 --- /dev/null +++ b/src/test/java/bio/overture/ego/model/enums/AccessLevelTest.java @@ -0,0 +1,53 @@ +package bio.overture.ego.model.enums; + +import static bio.overture.ego.model.enums.AccessLevel.DENY; +import static bio.overture.ego.model.enums.AccessLevel.READ; +import static bio.overture.ego.model.enums.AccessLevel.WRITE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@SpringBootTest +@RunWith(SpringRunner.class) +@ActiveProfiles("test") +@Transactional +public class AccessLevelTest { + @Test + public void testFromValue() { + assertThat(AccessLevel.fromValue("read")).isEqualByComparingTo(AccessLevel.READ); + assertThat(AccessLevel.fromValue("write")).isEqualByComparingTo(AccessLevel.WRITE); + assertThat(AccessLevel.fromValue("deny")).isEqualByComparingTo(AccessLevel.DENY); + } + + @Test + public void testAllows() { + allows(READ, READ); + allows(WRITE, READ); + denies(DENY, READ); + + denies(READ, WRITE); + allows(WRITE, WRITE); + denies(DENY, WRITE); + + denies(READ, DENY); + denies(WRITE, DENY); + denies(DENY, DENY); + } + + public void allows(AccessLevel have, AccessLevel want) { + assertTrue(AccessLevel.allows(have, want)); + } + + public void denies(AccessLevel have, AccessLevel want) { + assertFalse(AccessLevel.allows(have, want)); + } +} diff --git a/src/test/java/bio/overture/ego/model/params/ScopeNameTest.java b/src/test/java/bio/overture/ego/model/params/ScopeNameTest.java new file mode 100644 index 000000000..342bf01ed --- /dev/null +++ b/src/test/java/bio/overture/ego/model/params/ScopeNameTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.model.params; + +import static org.junit.Assert.assertEquals; + +import bio.overture.ego.model.enums.AccessLevel; +import lombok.val; +import org.junit.Test; + +public class ScopeNameTest { + @Test + public void testRead() { + val s = new ScopeName("song.READ"); + assertEquals("song", s.getName()); + assertEquals(AccessLevel.READ, s.getAccessLevel()); + } + + @Test + public void testNamedStudy() { + val s = new ScopeName("song.ABC.WRITE"); + assertEquals("song.ABC", s.getName()); + assertEquals(AccessLevel.WRITE, s.getAccessLevel()); + } +} diff --git a/src/test/java/bio/overture/ego/selenium/AbstractSeleniumTest.java b/src/test/java/bio/overture/ego/selenium/AbstractSeleniumTest.java new file mode 100644 index 000000000..b54c98c6d --- /dev/null +++ b/src/test/java/bio/overture/ego/selenium/AbstractSeleniumTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.selenium; + +import bio.overture.ego.AuthorizationServiceMain; +import bio.overture.ego.selenium.driver.WebDriverFactory; +import bio.overture.ego.selenium.rule.AssumingSeleniumEnvironment; +import bio.overture.ego.selenium.rule.SeleniumEnvironmentChecker; +import java.util.HashMap; +import java.util.Map; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.*; +import org.junit.runner.RunWith; +import org.openqa.selenium.WebDriver; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.testcontainers.containers.GenericContainer; + +@Slf4j +@ActiveProfiles({"test", "secure", "auth"}) +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = AuthorizationServiceMain.class, + properties = {"server.port=19001"}, + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@Ignore +public abstract class AbstractSeleniumTest { + + public int port = 19001; + public static WebDriver driver; + + private static final WebDriverFactory FACTORY = new WebDriverFactory(); + + @ClassRule + public static AssumingSeleniumEnvironment seleniumEnvironment = + new AssumingSeleniumEnvironment(new SeleniumEnvironmentChecker()); + + @Rule public GenericContainer uiContainer = createGenericContainer(); + + @BeforeClass + public static void openBrowser() { + driver = FACTORY.createDriver(seleniumEnvironment.getDriverType()); + } + + @AfterClass + public static void tearDown() { + if (driver != null) { + driver.quit(); + } + } + + @SneakyThrows + private GenericContainer createGenericContainer() { + return new GenericContainer("overture/ego-ui:2.0.1") + .withExposedPorts(80) + .withEnv(createEnvMap()); + } + + private Map createEnvMap() { + val envs = new HashMap(); + envs.put("REACT_APP_API", "http://localhost:" + port); + envs.put("REACT_APP_EGO_CLIENT_ID", "seleniumClient"); + return envs; + } +} diff --git a/src/test/java/bio/overture/ego/selenium/LoadAdminUITest.java b/src/test/java/bio/overture/ego/selenium/LoadAdminUITest.java new file mode 100644 index 000000000..4c5ee5849 --- /dev/null +++ b/src/test/java/bio/overture/ego/selenium/LoadAdminUITest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.selenium; + +import static bio.overture.ego.model.enums.ApplicationType.ADMIN; +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static org.assertj.core.api.Assertions.assertThat; + +import bio.overture.ego.model.dto.CreateApplicationRequest; +import bio.overture.ego.service.ApplicationService; +import lombok.SneakyThrows; +import lombok.val; +import org.junit.Test; +import org.openqa.selenium.By; +import org.springframework.beans.factory.annotation.Autowired; + +public class LoadAdminUITest extends AbstractSeleniumTest { + + /** Dependencies */ + @Autowired private ApplicationService applicationService; + + @Test + @SneakyThrows + public void loadAdmin_Success() { + val facebookUser = System.getenv("FACEBOOK_USER"); + val facebookPass = System.getenv("FACEBOOK_PASS"); + + val uiPort = uiContainer.getMappedPort(80); + + applicationService.create( + CreateApplicationRequest.builder() + .clientId("seleniumClient") + .clientSecret("seleniumSecret") + .name("Selenium Tests") + .redirectUri("http://localhost:" + uiPort) + .type(ADMIN) + .status(APPROVED) + .description("testing") + .build()); + + driver.get("http://localhost:" + uiPort); + val titleText = + driver.findElement(By.className("Login")).findElement(By.tagName("h1")).getText(); + assertThat(titleText).isEqualTo("Admin Portal"); + + driver.findElement(By.className("fa-facebook")).click(); + + val email = driver.findElement(By.id("email")); + email.sendKeys(facebookUser); + + val pass = driver.findElement(By.id("pass")); + pass.sendKeys(facebookPass); + + driver.findElement(By.id("loginbutton")).click(); + + Thread.sleep(1000); + + val messageDiv = + driver + .findElement(By.id("root")) + .findElement(By.tagName("div")) + .findElement(By.tagName("div")) + .getText(); + assertThat(messageDiv).contains("Your account does not have an administrator userType."); + + Thread.sleep(1000); + } +} diff --git a/src/test/java/bio/overture/ego/selenium/driver/BrowserStackDriverProxy.java b/src/test/java/bio/overture/ego/selenium/driver/BrowserStackDriverProxy.java new file mode 100644 index 000000000..0a3a3f099 --- /dev/null +++ b/src/test/java/bio/overture/ego/selenium/driver/BrowserStackDriverProxy.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.selenium.driver; + +import com.browserstack.local.Local; +import java.net.URL; +import lombok.SneakyThrows; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.RemoteWebDriver; + +public class BrowserStackDriverProxy extends RemoteWebDriver { + + /** State */ + private final Local local; + + @SneakyThrows + public BrowserStackDriverProxy(URL url, DesiredCapabilities capabilities, Local local) { + super(url, capabilities); + this.local = local; + } + + @Override + @SneakyThrows + public void quit() { + if (local != null) local.stop(); + super.quit(); + } +} diff --git a/src/test/java/bio/overture/ego/selenium/driver/WebDriverFactory.java b/src/test/java/bio/overture/ego/selenium/driver/WebDriverFactory.java new file mode 100644 index 000000000..ba6f56521 --- /dev/null +++ b/src/test/java/bio/overture/ego/selenium/driver/WebDriverFactory.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.selenium.driver; + +import com.browserstack.local.Local; +import java.io.FileReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import net.minidev.json.JSONArray; +import net.minidev.json.JSONObject; +import net.minidev.json.parser.JSONParser; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.remote.DesiredCapabilities; + +@Slf4j +public class WebDriverFactory { + + private static final int TIMEOUT_SECONDS = 15; + private static final int PAGELOAD_TIMEOUT = 30; + + public WebDriver createDriver(DriverType type) { + switch (type) { + case LOCAL: + return createChromeDriver(); + case BROWSERSTACK: + return createBrowserStackDriver(); + default: + throw new IllegalStateException("How did you get here?"); + } + } + + private WebDriver createChromeDriver() { + val chromeDriverPath = System.getenv("CHROME_DRIVER_PATH"); + if (chromeDriverPath == null) + throw new RuntimeException("Please set the CHROME_DRIVER_PATH environment variable"); + System.setProperty("webdriver.chrome.driver", chromeDriverPath); + val driver = new ChromeDriver(); + driver + .manage() + .timeouts() + .implicitlyWait(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .pageLoadTimeout(PAGELOAD_TIMEOUT, TimeUnit.SECONDS); + return driver; + } + + @SneakyThrows + private WebDriver createBrowserStackDriver() { + val parser = new JSONParser(JSONParser.MODE_PERMISSIVE); + val config = (JSONObject) parser.parse(new FileReader("src/test/resources/conf/bs.conf.json")); + + val envs = (JSONArray) config.get("environments"); + val capabilities = new DesiredCapabilities(); + + // TODO: Allow for many environments. + val envCapabilities = (Map) envs.get(0); + val it1 = envCapabilities.entrySet().iterator(); + while (it1.hasNext()) { + val pair = it1.next(); + capabilities.setCapability(pair.getKey(), pair.getValue()); + } + + Map commonCapabilities = (Map) config.get("capabilities"); + val it2 = commonCapabilities.entrySet().iterator(); + while (it2.hasNext()) { + Map.Entry pair = it2.next(); + if (capabilities.getCapability(pair.getKey().toString()) == null) { + capabilities.setCapability(pair.getKey().toString(), pair.getValue().toString()); + } + } + + String username = System.getenv("BROWSERSTACK_USERNAME"); + String accessKey = System.getenv("BROWSERSTACK_ACCESS_KEY"); + + val options = new HashMap(); + options.put("key", accessKey); + val local = new Local(); + local.start(options); + + log.info(capabilities.toString()); + + val driver = + new BrowserStackDriverProxy( + new URL( + "http://" + username + ":" + accessKey + "@" + config.get("server") + "/wd/hub"), + capabilities, + local); + + driver + .manage() + .timeouts() + .implicitlyWait(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .pageLoadTimeout(PAGELOAD_TIMEOUT, TimeUnit.SECONDS); + + return driver; + } + + public enum DriverType { + LOCAL, + BROWSERSTACK + } +} diff --git a/src/test/java/bio/overture/ego/selenium/rule/AssumingSeleniumEnvironment.java b/src/test/java/bio/overture/ego/selenium/rule/AssumingSeleniumEnvironment.java new file mode 100644 index 000000000..1f4f8c818 --- /dev/null +++ b/src/test/java/bio/overture/ego/selenium/rule/AssumingSeleniumEnvironment.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.selenium.rule; + +import bio.overture.ego.selenium.driver.WebDriverFactory.DriverType; +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class AssumingSeleniumEnvironment implements TestRule { + + private SeleniumEnvironmentChecker checker; + + public AssumingSeleniumEnvironment(SeleniumEnvironmentChecker checker) { + this.checker = checker; + } + + public DriverType getDriverType() { + return checker.getType(); + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + if (!checker.shouldRunTest()) { + throw new AssumptionViolatedException("Could not connect. Skipping test!"); + } else { + base.evaluate(); + } + } + }; + } +} diff --git a/src/test/java/bio/overture/ego/selenium/rule/SeleniumEnvironmentChecker.java b/src/test/java/bio/overture/ego/selenium/rule/SeleniumEnvironmentChecker.java new file mode 100644 index 000000000..988487d71 --- /dev/null +++ b/src/test/java/bio/overture/ego/selenium/rule/SeleniumEnvironmentChecker.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019. The Ontario Institute for Cancer Research. 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. + * + */ + +package bio.overture.ego.selenium.rule; + +import bio.overture.ego.selenium.driver.WebDriverFactory.DriverType; +import lombok.Getter; + +public class SeleniumEnvironmentChecker { + + @Getter private DriverType type; + + public SeleniumEnvironmentChecker() { + String envVar = System.getenv("SELENIUM_TEST_TYPE"); + if (envVar != null) { + type = DriverType.valueOf(envVar); + } + } + + public boolean shouldRunTest() { + if (type == DriverType.BROWSERSTACK || type == DriverType.LOCAL) { + return true; + } else { + return false; + } + } +} diff --git a/src/test/java/bio/overture/ego/service/ApplicationServiceTest.java b/src/test/java/bio/overture/ego/service/ApplicationServiceTest.java new file mode 100644 index 000000000..8ef45882f --- /dev/null +++ b/src/test/java/bio/overture/ego/service/ApplicationServiceTest.java @@ -0,0 +1,581 @@ +package bio.overture.ego.service; + +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static bio.overture.ego.model.enums.StatusType.DISABLED; +import static bio.overture.ego.model.enums.StatusType.PENDING; +import static bio.overture.ego.model.enums.StatusType.REJECTED; +import static bio.overture.ego.service.ApplicationService.APPLICATION_CONVERTER; +import static bio.overture.ego.utils.CollectionUtils.setOf; +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentId; +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Collections.singletonList; +import static java.util.UUID.randomUUID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import bio.overture.ego.controller.resolver.PageableResolver; +import bio.overture.ego.model.dto.CreateApplicationRequest; +import bio.overture.ego.model.dto.UpdateApplicationRequest; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.exceptions.NotFoundException; +import bio.overture.ego.model.exceptions.UniqueViolationException; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.repository.ApplicationRepository; +import bio.overture.ego.token.app.AppTokenClaims; +import bio.overture.ego.utils.EntityGenerator; +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.provider.ClientRegistrationException; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@SpringBootTest +@RunWith(SpringRunner.class) +@ActiveProfiles("test") +@Transactional +@Ignore("replace with controller tests.") +public class ApplicationServiceTest { + + @Autowired private ApplicationService applicationService; + @Autowired private ApplicationRepository applicationRepository; + + @Autowired private UserService userService; + + @Autowired private GroupService groupService; + + @Autowired private EntityGenerator entityGenerator; + + @Test + public void applicationConversion_UpdateApplicationRequest_Application() { + val id = randomUUID(); + val clientId = randomUUID().toString(); + val clientSecret = randomUUID().toString(); + val name = randomUUID().toString(); + val status = PENDING; + + val app = + Application.builder() + .id(id) + .clientId(clientId) + .clientSecret(clientSecret) + .name(name) + .status(status) + .redirectUri(null) + .users(null) + .build(); + + val newName = randomUUID().toString(); + assertThat(newName).isNotEqualTo(name); + val partialAppUpdateRequest = + UpdateApplicationRequest.builder() + .name(newName) + .status(APPROVED) + .redirectUri(randomUUID().toString()) + .build(); + APPLICATION_CONVERTER.updateApplication(partialAppUpdateRequest, app); + + assertThat(app.getDescription()).isNull(); + assertThat(app.getGroupApplications()).isEmpty(); + assertThat(app.getClientSecret()).isEqualTo(clientSecret); + assertThat(app.getClientId()).isEqualTo(clientId); + assertThat(app.getRedirectUri()).isNotNull(); + assertThat(app.getStatus()).isEqualTo(APPROVED); + assertThat(app.getId()).isEqualTo(id); + assertThat(app.getName()).isEqualTo(newName); + assertThat(app.getUsers()).isNull(); + } + + @Test + public void applicationConversion_CreateApplicationRequest_Application() { + val req = + CreateApplicationRequest.builder() + .status(PENDING) + .clientSecret(randomUUID().toString()) + .clientId(randomUUID().toString()) + .name(randomUUID().toString()) + .redirectUri("") + .build(); + val app = APPLICATION_CONVERTER.convertToApplication(req); + assertThat(app.getId()).isNull(); + assertThat(app.getGroupApplications()).isEmpty(); + assertThat(app.getClientId()).isEqualTo(req.getClientId()); + assertThat(app.getName()).isEqualTo(req.getName()); + assertThat(app.getUsers()).isEmpty(); + assertThat(app.getClientSecret()).isEqualTo(req.getClientSecret()); + assertThat(app.getStatus()).isEqualTo(req.getStatus()); + assertThat(app.getDescription()).isNull(); + assertThat(app.getRedirectUri()).isEqualTo(""); + } + + // Create + @Test + public void testCreate() { + val application = entityGenerator.setupApplication("123456"); + assertThat(application.getClientId()).isEqualTo("123456"); + } + + // Get + @Test + public void testGet() { + val application = entityGenerator.setupApplication("123456"); + val savedApplication = applicationService.getById(application.getId()); + assertThat(savedApplication.getClientId()).isEqualTo("123456"); + } + + @Test + public void testGetEntityNotFoundException() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> applicationService.getById(randomUUID())); + } + + @Test + public void testGetByName() { + entityGenerator.setupApplication("123456"); + val savedApplication = applicationService.getByName("Application 123456"); + assertThat(savedApplication.getClientId()).isEqualTo("123456"); + } + + @Test + public void testGetByNameAllCaps() { + entityGenerator.setupApplication("123456"); + val savedApplication = applicationService.getByName("APPLICATION 123456"); + assertThat(savedApplication.getClientId()).isEqualTo("123456"); + } + + @Test + @Ignore + public void testGetByNameNotFound() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> applicationService.getByName("Application 123456")); + } + + @Test + public void testGetByClientId() { + entityGenerator.setupApplication("123456"); + val savedApplication = applicationService.getByClientId("123456"); + assertThat(savedApplication.getClientId()).isEqualTo("123456"); + } + + @Test + @Ignore + public void testGetByClientIdNotFound() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> applicationService.getByClientId("123456")); + } + + // List + @Test + public void testListAppsNoFilters() { + val expectedApplications = newArrayList(applicationRepository.findAll()); + val actualApplicationsPage = + applicationService.listApps(Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(actualApplicationsPage.getTotalElements()).isEqualTo(expectedApplications.size()); + assertThat(actualApplicationsPage.getContent()) + .containsExactlyInAnyOrderElementsOf(expectedApplications); + } + + @Test + public void testListAppsFiltered() { + entityGenerator.setupTestApplications(); + val clientIdFilter = new SearchFilter("clientId", "333333"); + val applications = + applicationService.listApps( + singletonList(clientIdFilter), new PageableResolver().getPageable()); + assertThat(applications.getTotalElements()).isEqualTo(1L); + assertThat(applications.getContent().get(0).getClientId()).isEqualTo("333333"); + } + + @Test + public void testListAppsFilteredEmptyResult() { + entityGenerator.setupTestApplications(); + val clientIdFilter = new SearchFilter("clientId", "666666"); + val applications = + applicationService.listApps( + singletonList(clientIdFilter), new PageableResolver().getPageable()); + assertThat(applications.getTotalElements()).isEqualTo(0L); + } + + // Find + @Test + public void testFindAppsNoFilters() { + entityGenerator.setupTestApplications(); + val applications = + applicationService.findApps( + "222222", Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(applications.getTotalElements()).isEqualTo(1L); + assertThat(applications.getContent().get(0).getClientId()).isEqualTo("222222"); + } + + @Test + public void testFindAppsFiltered() { + entityGenerator.setupTestApplications(); + val clientIdFilter = new SearchFilter("clientId", "333333"); + val applications = + applicationService.findApps( + "222222", singletonList(clientIdFilter), new PageableResolver().getPageable()); + // Expect empty list + assertThat(applications.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindUsersAppsNoQueryNoFilters() { + entityGenerator.setupTestApplications(); + entityGenerator.setupTestUsers(); + + val user = userService.getByName("FirstUser@domain.com"); + val userTwo = userService.getByName("SecondUser@domain.com"); + + val application = applicationService.getByClientId("444444"); + + userService.addUserToApps(user.getId(), newArrayList(application.getId())); + userService.addUserToApps(userTwo.getId(), newArrayList(application.getId())); + + val applications = + applicationService.findApplicationsForUser( + user.getId(), Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(applications.getTotalElements()).isEqualTo(1L); + assertThat(applications.getContent().get(0).getClientId()).isEqualTo("444444"); + } + + @Test + public void testFindUsersAppsNoQueryNoFiltersNoUser() { + entityGenerator.setupTestApplications(); + entityGenerator.setupTestUsers(); + + val user = userService.getByName("FirstUser@domain.com"); + val applications = + applicationService.findApplicationsForUser( + user.getId(), Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(applications.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindUsersAppsNoQueryFilters() { + entityGenerator.setupTestApplications(); + entityGenerator.setupTestUsers(); + + val user = userService.getByName("FirstUser@domain.com"); + val applicationOne = applicationService.getByClientId("111111"); + val applicationTwo = applicationService.getByClientId("555555"); + + userService.addUserToApps( + user.getId(), newArrayList(applicationOne.getId(), applicationTwo.getId())); + + val clientIdFilter = new SearchFilter("clientId", "111111"); + + val applications = + applicationService.findApplicationsForUser( + user.getId(), singletonList(clientIdFilter), new PageableResolver().getPageable()); + + assertThat(applications.getTotalElements()).isEqualTo(1L); + assertThat(applications.getContent().get(0).getClientId()).isEqualTo("111111"); + } + + @Test + public void testFindUsersAppsQueryAndFilters() { + entityGenerator.setupTestApplications(); + entityGenerator.setupTestUsers(); + + val user = userService.getByName("FirstUser@domain.com"); + val applicationOne = applicationService.getByClientId("333333"); + val applicationTwo = applicationService.getByClientId("444444"); + + userService.addUserToApps( + user.getId(), newArrayList(applicationOne.getId(), applicationTwo.getId())); + + val clientIdFilter = new SearchFilter("clientId", "333333"); + + val applications = + applicationService.findApplicationsForUser( + user.getId(), + "444444", + singletonList(clientIdFilter), + new PageableResolver().getPageable()); + + assertThat(applications.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindUsersAppsQueryNoFilters() { + entityGenerator.setupTestApplications(); + entityGenerator.setupTestUsers(); + + val user = userService.getByName("FirstUser@domain.com"); + val applicationOne = applicationService.getByClientId("222222"); + val applicationTwo = applicationService.getByClientId("444444"); + + userService.addUserToApps( + user.getId(), newArrayList(applicationOne.getId(), applicationTwo.getId())); + + val applications = + applicationService.findApplicationsForUser( + user.getId(), "222222", Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(applications.getTotalElements()).isEqualTo(1L); + assertThat(applications.getContent().get(0).getClientId()).isEqualTo("222222"); + } + + @Test + public void testFindGroupsAppsNoQueryNoFilters() { + entityGenerator.setupTestApplications(); + entityGenerator.setupTestGroups(); + + val group = groupService.getByName("Group One"); + val groupTwo = groupService.getByName("Group Two"); + + val application = applicationService.getByClientId("111111"); + + groupService.associateApplicationsWithGroup(group.getId(), newArrayList(application.getId())); + groupService.associateApplicationsWithGroup( + groupTwo.getId(), newArrayList(application.getId())); + + val applications = + applicationService.findApplicationsForGroup( + group.getId(), Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(applications.getTotalElements()).isEqualTo(1L); + assertThat(applications.getContent().get(0).getClientId()).isEqualTo("111111"); + } + + @Test + public void testFindGroupsAppsNoQueryNoFiltersNoGroup() { + entityGenerator.setupTestApplications(); + entityGenerator.setupTestGroups(); + + val group = groupService.getByName("Group One"); + val applications = + applicationService.findApplicationsForGroup( + group.getId(), Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(applications.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindGroupsAppsNoQueryFilters() { + entityGenerator.setupTestApplications(); + entityGenerator.setupTestGroups(); + + val group = groupService.getByName("Group One"); + val applicationOne = applicationService.getByClientId("222222"); + val applicationTwo = applicationService.getByClientId("333333"); + + groupService.associateApplicationsWithGroup( + group.getId(), newArrayList(applicationOne.getId(), applicationTwo.getId())); + + val clientIdFilter = new SearchFilter("clientId", "333333"); + + val applications = + applicationService.findApplicationsForGroup( + group.getId(), singletonList(clientIdFilter), new PageableResolver().getPageable()); + + assertThat(applications.getTotalElements()).isEqualTo(1L); + assertThat(applications.getContent().get(0).getClientId()).isEqualTo("333333"); + } + + @Test + public void testFindGroupsAppsQueryAndFilters() { + entityGenerator.setupTestApplications(); + entityGenerator.setupTestGroups(); + + val group = groupService.getByName("Group Three"); + val applicationOne = applicationService.getByClientId("333333"); + val applicationTwo = applicationService.getByClientId("444444"); + + groupService.associateApplicationsWithGroup( + group.getId(), newArrayList(applicationOne.getId(), applicationTwo.getId())); + + val clientIdFilter = new SearchFilter("clientId", "333333"); + + val applications = + applicationService.findApplicationsForGroup( + group.getId(), + "444444", + singletonList(clientIdFilter), + new PageableResolver().getPageable()); + + assertThat(applications.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindGroupsAppsQueryNoFilters() { + entityGenerator.setupTestApplications(); + entityGenerator.setupTestGroups(); + + val group = groupService.getByName("Group One"); + val applicationOne = applicationService.getByClientId("444444"); + val applicationTwo = applicationService.getByClientId("555555"); + + groupService.associateApplicationsWithGroup( + group.getId(), newArrayList(applicationOne.getId(), applicationTwo.getId())); + + val applications = + applicationService.findApplicationsForGroup( + group.getId(), "555555", Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(applications.getTotalElements()).isEqualTo(1L); + assertThat(applications.getContent().get(0).getClientId()).isEqualTo("555555"); + } + + // Update + @Test + public void testUpdate() { + val application = entityGenerator.setupApplication("123456"); + val updateRequest = UpdateApplicationRequest.builder().name("New Name").build(); + val updated = applicationService.partialUpdate(application.getId(), updateRequest); + assertThat(updated.getName()).isEqualTo("New Name"); + } + + @Test + public void testUpdateNonexistentEntity() { + val nonExistentId = generateNonExistentId(applicationService); + val updateRequest = + UpdateApplicationRequest.builder() + .clientId("123456") + .name("DoesNotExist") + .clientSecret("654321") + .build(); + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> applicationService.partialUpdate(nonExistentId, updateRequest)); + } + + @Test + public void uniqueClientIdCheck_CreateApplication_ThrowsUniqueConstraintException() { + val r1 = + CreateApplicationRequest.builder() + .clientId(UUID.randomUUID().toString()) + .clientSecret(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .status(PENDING) + .build(); + + val a1 = applicationService.create(r1); + assertThat(applicationService.isExist(a1.getId())).isTrue(); + + assertThat(a1.getClientId()).isEqualTo(r1.getClientId()); + assertThatExceptionOfType(UniqueViolationException.class) + .isThrownBy(() -> applicationService.create(r1)); + } + + @Test + public void uniqueClientIdCheck_UpdateApplication_ThrowsUniqueConstraintException() { + val clientId1 = UUID.randomUUID().toString(); + val clientId2 = UUID.randomUUID().toString(); + val cr1 = + CreateApplicationRequest.builder() + .clientId(clientId1) + .clientSecret(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .status(PENDING) + .build(); + + val cr2 = + CreateApplicationRequest.builder() + .clientId(clientId2) + .clientSecret(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .status(APPROVED) + .build(); + + val a1 = applicationService.create(cr1); + assertThat(applicationService.isExist(a1.getId())).isTrue(); + val a2 = applicationService.create(cr2); + assertThat(applicationService.isExist(a2.getId())).isTrue(); + + val ur3 = UpdateApplicationRequest.builder().clientId(clientId1).build(); + + assertThat(a1.getClientId()).isEqualTo(ur3.getClientId()); + assertThat(a2.getClientId()).isNotEqualTo(ur3.getClientId()); + assertThatExceptionOfType(UniqueViolationException.class) + .isThrownBy(() -> applicationService.partialUpdate(a2.getId(), ur3)); + } + + // Delete + @Test + public void testDelete() { + entityGenerator.setupTestApplications(); + + val application = applicationService.getByClientId("222222"); + applicationService.delete(application.getId()); + + val applications = + applicationService.listApps(Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(applications.getTotalElements()).isEqualTo(4L); + assertThat(applications.getContent()).doesNotContain(application); + } + + @Test + public void testDeleteNonExisting() { + entityGenerator.setupTestApplications(); + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> applicationService.delete(randomUUID())); + } + + // Special (LoadClient) + @Test + public void testLoadClientByClientId() { + val application = entityGenerator.setupApplication("123456"); + val updateRequest = UpdateApplicationRequest.builder().status(APPROVED).build(); + applicationService.partialUpdate(application.getId(), updateRequest); + + val client = applicationService.loadClientByClientId("123456"); + + assertThat(client.getClientId()).isEqualToIgnoringCase("123456"); + assertThat( + client + .getAuthorizedGrantTypes() + .containsAll(Arrays.asList(AppTokenClaims.AUTHORIZED_GRANTS))); + assertThat(client.getScope().containsAll(Arrays.asList(AppTokenClaims.SCOPES))); + assertThat(client.getRegisteredRedirectUri()).isEqualTo(setOf(application.getRedirectUri())); + assertThat(client.getAuthorities()) + .containsExactly(new SimpleGrantedAuthority(AppTokenClaims.ROLE)); + } + + @Test + public void testLoadClientByClientIdNotFound() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> applicationService.loadClientByClientId("123456")) + .withMessage("The 'Application' entity with clientId '123456' was not found"); + } + + @Test + public void testLoadClientByClientIdEmptyString() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> applicationService.loadClientByClientId("")) + .withMessage("The 'Application' entity with clientId '' was not found"); + } + + @Test + public void testLoadClientByClientIdNotApproved() { + val application = entityGenerator.setupApplication("123456"); + val updateRequest = UpdateApplicationRequest.builder().status(PENDING).build(); + applicationService.partialUpdate(application.getId(), updateRequest); + assertThatExceptionOfType(ClientRegistrationException.class) + .isThrownBy(() -> applicationService.loadClientByClientId("123456")) + .withMessage("Client Access is not approved."); + + updateRequest.setStatus(REJECTED); + applicationService.partialUpdate(application.getId(), updateRequest); + assertThatExceptionOfType(ClientRegistrationException.class) + .isThrownBy(() -> applicationService.loadClientByClientId("123456")) + .withMessage("Client Access is not approved."); + + updateRequest.setStatus(DISABLED); + applicationService.partialUpdate(application.getId(), updateRequest); + assertThatExceptionOfType(ClientRegistrationException.class) + .isThrownBy(() -> applicationService.loadClientByClientId("123456")) + .withMessage("Client Access is not approved."); + } +} diff --git a/src/test/java/bio/overture/ego/service/GroupsServiceTest.java b/src/test/java/bio/overture/ego/service/GroupsServiceTest.java new file mode 100644 index 000000000..14c5628de --- /dev/null +++ b/src/test/java/bio/overture/ego/service/GroupsServiceTest.java @@ -0,0 +1,693 @@ +package bio.overture.ego.service; + +import static bio.overture.ego.model.enums.AccessLevel.DENY; +import static bio.overture.ego.model.enums.AccessLevel.READ; +import static bio.overture.ego.model.enums.AccessLevel.WRITE; +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static bio.overture.ego.model.enums.StatusType.PENDING; +import static bio.overture.ego.utils.CollectionUtils.mapToImmutableSet; +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentId; +import static bio.overture.ego.utils.EntityTools.extractGroupNames; +import static com.google.common.collect.Lists.newArrayList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import bio.overture.ego.controller.resolver.PageableResolver; +import bio.overture.ego.model.dto.GroupRequest; +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.entity.AbstractPermission; +import bio.overture.ego.model.exceptions.NotFoundException; +import bio.overture.ego.model.exceptions.UniqueViolationException; +import bio.overture.ego.model.join.GroupApplication; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.repository.join.UserGroupRepository; +import bio.overture.ego.utils.EntityGenerator; +import bio.overture.ego.utils.PolicyPermissionUtils; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@SpringBootTest +@RunWith(SpringRunner.class) +@ActiveProfiles("test") +@Transactional +@Ignore("replace with controller tests.") +public class GroupsServiceTest { + @Autowired private ApplicationService applicationService; + + @Autowired private UserService userService; + + @Autowired private GroupService groupService; + @Autowired private GroupPermissionService groupPermissionService; + + @Autowired private PolicyService policyService; + + @Autowired private EntityGenerator entityGenerator; + @Autowired private UserGroupRepository userGroupRepository; + + // Create + @Test + public void testCreate() { + val group = entityGenerator.setupGroup("Group One"); + assertThat(group.getName()).isEqualTo("Group One"); + } + + @Test + public void uniqueNameCheck_CreateGroup_ThrowsUniqueConstraintException() { + val r1 = GroupRequest.builder().name(UUID.randomUUID().toString()).status(PENDING).build(); + + val g1 = groupService.create(r1); + assertThat(groupService.isExist(g1.getId())).isTrue(); + + assertThat(g1.getName()).isEqualTo(r1.getName()); + assertThatExceptionOfType(UniqueViolationException.class) + .isThrownBy(() -> groupService.create(r1)); + } + + @Test + public void uniqueClientIdCheck_UpdateGroup_ThrowsUniqueConstraintException() { + val name1 = UUID.randomUUID().toString(); + val name2 = UUID.randomUUID().toString(); + val cr1 = GroupRequest.builder().name(name1).status(PENDING).build(); + + val cr2 = GroupRequest.builder().name(name2).status(APPROVED).build(); + + val g1 = groupService.create(cr1); + assertThat(groupService.isExist(g1.getId())).isTrue(); + val g2 = groupService.create(cr2); + assertThat(groupService.isExist(g2.getId())).isTrue(); + + val ur3 = GroupRequest.builder().name(name1).build(); + + assertThat(g1.getName()).isEqualTo(ur3.getName()); + assertThat(g2.getName()).isNotEqualTo(ur3.getName()); + assertThatExceptionOfType(UniqueViolationException.class) + .isThrownBy(() -> groupService.partialUpdate(g2.getId(), ur3)); + } + + // Get + @Test + public void testGet() { + val group = entityGenerator.setupGroup("Group One"); + val saveGroup = groupService.getById(group.getId()); + assertThat(saveGroup.getName()).isEqualTo("Group One"); + } + + @Test + public void testGetNotFoundException() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> groupService.getById(UUID.randomUUID())); + } + + @Test + public void testGetByName() { + entityGenerator.setupGroup("Group One"); + val saveGroup = groupService.getByName("Group One"); + assertThat(saveGroup.getName()).isEqualTo("Group One"); + } + + @Test + public void testGetByNameAllCaps() { + entityGenerator.setupGroup("Group One"); + val saveGroup = groupService.getByName("GROUP ONE"); + assertThat(saveGroup.getName()).isEqualTo("Group One"); + } + + @Test + @Ignore + public void testGetByNameNotFound() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> groupService.getByName("Group One")); + } + + // List Groups + @Test + public void testListGroupsNoFilters() { + entityGenerator.setupTestGroups(); + val groups = + groupService.listGroups(Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(groups.getTotalElements()).isEqualTo(3L); + } + + @Test + public void testListGroupsNoFiltersEmptyResult() { + val groups = + groupService.listGroups(Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(groups.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testListGroupsFiltered() { + entityGenerator.setupTestGroups(); + val groupNameFilter = new SearchFilter("name", "Group One"); + val groups = + groupService.listGroups( + Arrays.asList(groupNameFilter), new PageableResolver().getPageable()); + assertThat(groups.getTotalElements()).isEqualTo(1L); + assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); + } + + @Test + public void testListGroupsFilteredEmptyResult() { + entityGenerator.setupTestGroups(); + val groupNameFilter = new SearchFilter("name", "Group Four"); + val groups = + groupService.listGroups( + Arrays.asList(groupNameFilter), new PageableResolver().getPageable()); + assertThat(groups.getTotalElements()).isEqualTo(0L); + } + + // Find Groups + @Test + public void testFindGroupsNoFilters() { + entityGenerator.setupTestGroups(); + val groups = + groupService.findGroups( + "One", Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(groups.getTotalElements()).isEqualTo(1L); + assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); + } + + @Test + public void testFindGroupsFiltered() { + entityGenerator.setupTestGroups(); + val groupNameFilter = new SearchFilter("name", "Group One"); + val groups = + groupService.findGroups( + "Two", Arrays.asList(groupNameFilter), new PageableResolver().getPageable()); + // Expect empty list + assertThat(groups.getTotalElements()).isEqualTo(0L); + } + + // Find User's Groups + @Test + public void testFindUsersGroupsNoQueryNoFilters() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestUsers(); + + val userId = userService.getByName("FirstUser@domain.com").getId(); + val userTwoId = userService.getByName("SecondUser@domain.com").getId(); + val groupId = groupService.getByName("Group One").getId(); + + userService.associateGroupsWithUser(userId, Arrays.asList(groupId)); + userService.associateGroupsWithUser(userTwoId, Arrays.asList(groupId)); + + val groups = + groupService.findGroupsForUser( + userId, ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(groups.getTotalElements()).isEqualTo(1L); + assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); + } + + @Test + public void testFindUsersGroupsNoQueryNoFiltersNoGroupsFound() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestUsers(); + + val userId = userService.getByName("FirstUser@domain.com").getId(); + + val groups = + groupService.findGroupsForUser( + userId, ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(groups.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindUsersGroupsNoQueryFilters() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestUsers(); + + val userId = userService.getByName("FirstUser@domain.com").getId(); + val groupId = groupService.getByName("Group One").getId(); + val groupTwoId = groupService.getByName("Group Two").getId(); + + userService.associateGroupsWithUser(userId, Arrays.asList(groupId, groupTwoId)); + + val groupsFilters = new SearchFilter("name", "Group One"); + + val groups = + groupService.findGroupsForUser( + userId, ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(groups.getTotalElements()).isEqualTo(1L); + assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); + } + + @Test + public void testFindUsersGroupsQueryAndFilters() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestUsers(); + + val userId = userService.getByName("FirstUser@domain.com").getId(); + val groupId = groupService.getByName("Group One").getId(); + val groupTwoId = groupService.getByName("Group Two").getId(); + + userService.associateGroupsWithUser(userId, Arrays.asList(groupId, groupTwoId)); + + val groupsFilters = new SearchFilter("name", "Group One"); + + val groups = + groupService.findGroupsForUser( + userId, "Two", ImmutableList.of(groupsFilters), new PageableResolver().getPageable()); + + assertThat(groups.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindUsersGroupsQueryNoFilters() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestUsers(); + + val userId = userService.getByName("FirstUser@domain.com").getId(); + val groupId = groupService.getByName("Group One").getId(); + val groupTwoId = groupService.getByName("Group Two").getId(); + + userService.associateGroupsWithUser(userId, Arrays.asList(groupId, groupTwoId)); + + val groups = + groupService.findGroupsForUser( + userId, "Two", ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(groups.getTotalElements()).isEqualTo(1L); + assertThat(groups.getContent().get(0).getName()).isEqualTo("Group Two"); + } + + // Find Application's Groups + @Test + public void testFindApplicationsGroupsNoQueryNoFilters() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestApplications(); + + val groupId = groupService.getByName("Group One").getId(); + val groupTwoId = groupService.getByName("Group Two").getId(); + val applicationId = applicationService.getByClientId("111111").getId(); + val applicationTwoId = applicationService.getByClientId("222222").getId(); + + groupService.associateApplicationsWithGroup(groupId, Arrays.asList(applicationId)); + groupService.associateApplicationsWithGroup(groupTwoId, Arrays.asList(applicationTwoId)); + + val groups = + groupService.findGroupsForApplication( + applicationId, ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(extractGroupNames(groups.getContent())).contains("Group One"); + assertThat(extractGroupNames(groups.getContent())).doesNotContain("Group Two"); + } + + @Test + public void testFindApplicationsGroupsNoQueryNoFiltersNoGroup() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestApplications(); + + val applicationId = applicationService.getByClientId("111111").getId(); + + val groups = + groupService.findGroupsForApplication( + applicationId, ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(groups.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindApplicationsGroupsNoQueryFilters() { + entityGenerator.setupTestGroups("testFindApplicationsGroupsNoQueryFilters"); + entityGenerator.setupTestApplications("testFindApplicationsGroupsNoQueryFilters"); + + val groupId = + groupService.getByName("Group One_testFindApplicationsGroupsNoQueryFilters").getId(); + val groupTwoId = + groupService.getByName("Group Two_testFindApplicationsGroupsNoQueryFilters").getId(); + val applicationId = + applicationService.getByClientId("111111_testFindApplicationsGroupsNoQueryFilters").getId(); + + groupService.associateApplicationsWithGroup(groupId, Arrays.asList(applicationId)); + groupService.associateApplicationsWithGroup(groupTwoId, Arrays.asList(applicationId)); + + val groupsFilters = + new SearchFilter("name", "Group One_testFindApplicationsGroupsNoQueryFilters"); + + val groups = + groupService.findGroupsForApplication( + applicationId, ImmutableList.of(groupsFilters), new PageableResolver().getPageable()); + + assertThat(groups.getTotalElements()).isEqualTo(1L); + assertThat(groups.getContent().get(0).getName()) + .isEqualTo("Group One_testFindApplicationsGroupsNoQueryFilters"); + } + + @Test + public void testFindApplicationsGroupsQueryAndFilters() { + entityGenerator.setupTestGroups("testFindApplicationsGroupsQueryAndFilters"); + entityGenerator.setupTestApplications("testFindApplicationsGroupsQueryAndFilters"); + + val groupId = + groupService.getByName("Group One_testFindApplicationsGroupsQueryAndFilters").getId(); + val groupTwoId = + groupService.getByName("Group Two_testFindApplicationsGroupsQueryAndFilters").getId(); + val applicationId = + applicationService + .getByClientId("111111_testFindApplicationsGroupsQueryAndFilters") + .getId(); + + groupService.associateApplicationsWithGroup(groupId, Arrays.asList(applicationId)); + groupService.associateApplicationsWithGroup(groupTwoId, Arrays.asList(applicationId)); + + val groupsFilters = + new SearchFilter("name", "Group One_testFindApplicationsGroupsQueryAndFilters"); + + val groups = + groupService.findGroupsForApplication( + applicationId, + "Two", + ImmutableList.of(groupsFilters), + new PageableResolver().getPageable()); + + assertThat(groups.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindApplicationsGroupsQueryNoFilters() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestApplications(); + + val groupId = groupService.getByName("Group One").getId(); + val groupTwoId = groupService.getByName("Group Two").getId(); + val applicationId = applicationService.getByClientId("111111").getId(); + + groupService.associateApplicationsWithGroup(groupId, Arrays.asList(applicationId)); + groupService.associateApplicationsWithGroup(groupTwoId, Arrays.asList(applicationId)); + + val groups = + groupService.findGroupsForApplication( + applicationId, "Group One", ImmutableList.of(), new PageableResolver().getPageable()); + assertThat(groups.getTotalElements()).isEqualTo(1L); + assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); + } + + // Update + @Test + public void testUpdate() { + val group = entityGenerator.setupGroup("Group One"); + val updateRequest = GroupRequest.builder().description("New Description").build(); + val updated = groupService.partialUpdate(group.getId(), updateRequest); + assertThat(updated.getDescription()).isEqualTo("New Description"); + } + + @Test + public void testUpdateNonexistentEntity() { + val nonExistentId = generateNonExistentId(groupService); + val nonExistentEntity = + GroupRequest.builder().name("NonExistent").status(PENDING).description("").build(); + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> groupService.partialUpdate(nonExistentId, nonExistentEntity)); + } + + // Add Apps to Group + @Test + public void addAppsToGroup() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestApplications(); + + val groupId = groupService.getByName("Group One").getId(); + val application = applicationService.getByClientId("111111"); + val applicationId = application.getId(); + + groupService.associateApplicationsWithGroup(groupId, Arrays.asList(applicationId)); + + val group = groupService.getWithApplications(groupId); + + assertThat(mapToImmutableSet(group.getGroupApplications(), GroupApplication::getApplication)) + .contains(applicationService.getByClientId("111111")); + } + + @Test + public void addAppsToGroupNoGroup() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestApplications(); + val applicationId = applicationService.getByClientId("111111").getId(); + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy( + () -> + groupService.associateApplicationsWithGroup( + UUID.randomUUID(), Arrays.asList(applicationId))); + } + + @Test + public void addAppsToGroupNoApp() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestApplications(); + + val groupId = groupService.getByName("Group One").getId(); + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy( + () -> + groupService.associateApplicationsWithGroup( + groupId, Arrays.asList(UUID.randomUUID()))); + } + + @Test + public void addAppsToGroupEmptyAppList() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestApplications(); + + val group = groupService.getByName("Group One"); + val groupId = group.getId(); + + groupService.associateApplicationsWithGroup(groupId, Collections.emptyList()); + + val nonUpdated = groupService.getByName("Group One"); + assertThat(nonUpdated).isEqualTo(group); + } + + // Delete + @Test + public void testDelete() { + entityGenerator.setupTestGroups(); + + val group = groupService.getByName("Group One"); + + groupService.delete(group.getId()); + + val groups = + groupService.listGroups(Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(groups.getTotalElements()).isEqualTo(2L); + assertThat(groups.getContent()).doesNotContain(group); + } + + @Test + public void testDeleteNonExisting() { + entityGenerator.setupTestGroups(); + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> groupService.delete(UUID.randomUUID())); + } + + // Delete Apps from Group + @Test + public void testDeleteAppFromGroup() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestApplications(); + + val groupId = groupService.getByName("Group One").getId(); + val application = applicationService.getByClientId("111111"); + val applicationId = application.getId(); + + groupService.associateApplicationsWithGroup(groupId, Arrays.asList(applicationId)); + + val group = groupService.getWithApplications(groupId); + assertThat(group.getGroupApplications().size()).isEqualTo(1); + + groupService.disassociateApplicationsFromGroup(groupId, Arrays.asList(applicationId)); + + val groupWithDeleteApp = groupService.getWithApplications(groupId); + assertThat(groupWithDeleteApp.getGroupApplications().size()).isEqualTo(0); + } + + @Test + public void testDeleteAppsFromGroupNoGroup() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestApplications(); + + val groupId = groupService.getByName("Group One").getId(); + val application = applicationService.getByClientId("111111"); + val applicationId = application.getId(); + + groupService.associateApplicationsWithGroup(groupId, Arrays.asList(applicationId)); + + val group = groupService.getWithApplications(groupId); + assertThat(group.getGroupApplications().size()).isEqualTo(1); + + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy( + () -> + groupService.disassociateApplicationsFromGroup( + UUID.randomUUID(), Arrays.asList(applicationId))); + } + + @Test + public void testDeleteAppsFromGroupEmptyAppsList() { + entityGenerator.setupTestGroups(); + entityGenerator.setupTestApplications(); + + val groupId = groupService.getByName("Group One").getId(); + val application = applicationService.getByClientId("111111"); + val applicationId = application.getId(); + + groupService.associateApplicationsWithGroup(groupId, Arrays.asList(applicationId)); + + val group = groupService.getWithApplications(groupId); + assertThat(group.getGroupApplications().size()).isEqualTo(1); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> groupService.disassociateApplicationsFromGroup(groupId, Arrays.asList())); + } + + /** This test guards against bad cascades against users */ + @Test + public void testDeleteGroupWithUserRelations() { + val user = entityGenerator.setupUser("foo bar"); + val group = entityGenerator.setupGroup("testGroup"); + + val updatedGroup = + userService.associateGroupsWithUser(group.getId(), newArrayList(user.getId())); + + groupService.delete(updatedGroup.getId()); + assertThat(userService.getById(user.getId())).isNotNull(); + } + + /** This test guards against bad cascades against applications */ + @Test + public void testDeleteGroupWithApplicationRelations() { + val app = entityGenerator.setupApplication("foobar"); + val group = entityGenerator.setupGroup("testGroup"); + + val updatedGroup = + groupService.associateApplicationsWithGroup(group.getId(), newArrayList(app.getId())); + + groupService.delete(updatedGroup.getId()); + assertThat(applicationService.getById(app.getId())).isNotNull(); + } + + @Test + public void testAddGroupPermissions() { + entityGenerator.setupTestGroups(); + val groups = + groupService + .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) + .getContent(); + entityGenerator.setupTestPolicies(); + + val study001 = policyService.getByName("Study001"); + val study001id = study001.getId(); + + val study002 = policyService.getByName("Study002"); + val study002id = study002.getId(); + + val study003 = policyService.getByName("Study003"); + val study003id = study003.getId(); + + val permissions = + Arrays.asList( + new PermissionRequest(study001id, READ), + new PermissionRequest(study002id, WRITE), + new PermissionRequest(study003id, DENY)); + + val firstGroup = groups.get(0); + + groupPermissionService.addPermissions(firstGroup.getId(), permissions); + + assertThat(PolicyPermissionUtils.extractPermissionStrings(firstGroup.getPermissions())) + .containsExactlyInAnyOrder("Study001.READ", "Study002.WRITE", "Study003.DENY"); + } + + @Test + public void testDeleteGroupPermissions() { + entityGenerator.setupTestGroups(); + val groups = + groupService + .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) + .getContent(); + entityGenerator.setupTestPolicies(); + + val firstGroup = groups.get(0); + + val study001 = policyService.getByName("Study001"); + val study001id = study001.getId(); + + val study002 = policyService.getByName("Study002"); + val study002id = study002.getId(); + + val study003 = policyService.getByName("Study003"); + val study003id = study003.getId(); + + val permissions = + Arrays.asList( + new PermissionRequest(study001id, READ), + new PermissionRequest(study002id, WRITE), + new PermissionRequest(study003id, DENY)); + + groupPermissionService.addPermissions(firstGroup.getId(), permissions); + + val groupPermissionsToRemove = + firstGroup.getPermissions().stream() + .filter(p -> !p.getPolicy().getName().equals("Study001")) + .map(AbstractPermission::getId) + .collect(Collectors.toList()); + + groupPermissionService.deletePermissions(firstGroup.getId(), groupPermissionsToRemove); + + assertThat(PolicyPermissionUtils.extractPermissionStrings(firstGroup.getPermissions())) + .containsExactlyInAnyOrder("Study001.READ"); + } + + @Test + public void testGetGroupPermissions() { + entityGenerator.setupPolicies( + "testGetGroupPermissions_Study001, testGetGroupPermissions_Group", + "testGetGroupPermissions_Study002, testGetGroupPermissions_Group", + "testGetGroupPermissions_Study003, testGetGroupPermissions_Group"); + + val testGroup = entityGenerator.setupGroup("testGetGroupPermissions_Group"); + + val study001 = policyService.getByName("testGetGroupPermissions_Study001"); + val study001id = study001.getId(); + + val study002 = policyService.getByName("testGetGroupPermissions_Study002"); + val study002id = study002.getId(); + + val study003 = policyService.getByName("testGetGroupPermissions_Study003"); + val study003id = study003.getId(); + + val permissions = + Arrays.asList( + new PermissionRequest(study001id, READ), + new PermissionRequest(study002id, WRITE), + new PermissionRequest(study003id, DENY)); + + groupPermissionService.addPermissions(testGroup.getId(), permissions); + + val pagedGroupPermissions = + groupPermissionService.getPermissions( + testGroup.getId(), new PageableResolver().getPageable()); + + assertThat(pagedGroupPermissions.getTotalElements()).isEqualTo(1L); + assertThat(pagedGroupPermissions.getContent().get(0).getAccessLevel().toString()) + .isEqualToIgnoringCase("READ"); + assertThat(pagedGroupPermissions.getContent().get(0).getPolicy().getName()) + .isEqualToIgnoringCase("testGetGroupPermissions_Study001"); + } +} diff --git a/src/test/java/bio/overture/ego/service/PermissionServiceTest.java b/src/test/java/bio/overture/ego/service/PermissionServiceTest.java new file mode 100644 index 000000000..bcace9a07 --- /dev/null +++ b/src/test/java/bio/overture/ego/service/PermissionServiceTest.java @@ -0,0 +1,93 @@ +package bio.overture.ego.service; + +import static bio.overture.ego.model.enums.AccessLevel.READ; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.dto.PolicyResponse; +import bio.overture.ego.utils.EntityGenerator; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@SpringBootTest +@RunWith(SpringRunner.class) +@ActiveProfiles("test") +@Transactional +@Ignore("replace with controller tests.") +public class PermissionServiceTest { + @Autowired private UserService userService; + + @Autowired private GroupService groupService; + + @Autowired private PolicyService policyService; + + @Autowired private UserPermissionService userPermissionService; + + @Autowired private GroupPermissionService groupPermissionService; + + @Autowired private EntityGenerator entityGenerator; + + @Test + public void testFindGroupIdsByPolicy() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + entityGenerator.setupTestPolicies(); + + val policy = policyService.getByName("Study001"); + + val name1 = "Group One"; + val name2 = "Group Three"; + + val group1 = groupService.getByName(name1); + val group2 = groupService.getByName(name2); + + val permissions = asList(new PermissionRequest(policy.getId(), READ)); + groupPermissionService.addPermissions(group1.getId(), permissions); + groupPermissionService.addPermissions(group2.getId(), permissions); + + val expected = + asList( + new PolicyResponse(group1.getId().toString(), name1, READ), + new PolicyResponse(group2.getId().toString(), name2, READ)); + + val actual = groupPermissionService.findByPolicy(policy.getId()); + + assertThat(actual).containsExactlyInAnyOrderElementsOf(expected); + } + + @Test + public void testFindUserIdsByPolicy() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + entityGenerator.setupTestPolicies(); + + val policy = policyService.getByName("Study001"); + val name1 = "FirstUser@domain.com"; + val name2 = "SecondUser@domain.com"; + val user1 = userService.getByName(name1); + val user2 = userService.getByName(name2); + + val permissions = asList(new PermissionRequest(policy.getId(), READ)); + userPermissionService.addPermissions(user1.getId(), permissions); + userPermissionService.addPermissions(user2.getId(), permissions); + + val expected = + asList( + new PolicyResponse(user1.getId().toString(), name1, READ), + new PolicyResponse(user2.getId().toString(), name2, READ)); + + val actual = userPermissionService.findByPolicy(policy.getId()); + System.out.printf("%s", actual.get(0).toString()); + assertThat(actual).containsExactlyInAnyOrderElementsOf(expected); + } +} diff --git a/src/test/java/bio/overture/ego/service/PolicyServiceTest.java b/src/test/java/bio/overture/ego/service/PolicyServiceTest.java new file mode 100644 index 000000000..7adc23751 --- /dev/null +++ b/src/test/java/bio/overture/ego/service/PolicyServiceTest.java @@ -0,0 +1,177 @@ +package bio.overture.ego.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import bio.overture.ego.controller.resolver.PageableResolver; +import bio.overture.ego.model.dto.PolicyRequest; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.exceptions.NotFoundException; +import bio.overture.ego.model.exceptions.UniqueViolationException; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.utils.EntityGenerator; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@SpringBootTest +@RunWith(SpringRunner.class) +@ActiveProfiles("test") +@Transactional +@Ignore("To be replaced with controller tests") +public class PolicyServiceTest { + + @Autowired private PolicyService policyService; + + @Autowired private EntityGenerator entityGenerator; + + private List groups; + + @Before + public void setUp() { + groups = entityGenerator.setupGroups("Group One", "GroupTwo", "Group Three"); + } + + // Create + @Test + public void testCreate() { + val policy = entityGenerator.setupPolicy("Study001,Group One"); + assertThat(policy.getName()).isEqualTo("Study001"); + } + + // Read + @Test + public void testGet() { + val policy = entityGenerator.setupPolicy("Study001", groups.get(0).getName()); + val savedPolicy = policyService.getById(policy.getId()); + assertThat(savedPolicy.getName()).isEqualTo("Study001"); + } + + @Test + public void testGetNotFoundException() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> policyService.getById(UUID.randomUUID())); + } + + @Test + public void testGetByName() { + entityGenerator.setupPolicy("Study001", groups.get(0).getName()); + val savedUser = policyService.getByName("Study001"); + assertThat(savedUser.getName()).isEqualTo("Study001"); + } + + @Test + public void testGetByNameAllCaps() { + entityGenerator.setupPolicy("Study001", groups.get(0).getName()); + val savedUser = policyService.getByName("STUDY001"); + assertThat(savedUser.getName()).isEqualTo("Study001"); + } + + @Test + @Ignore + public void testGetByNameNotFound() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> policyService.getByName("Study000")); + } + + @Test + public void testListUsersNoFilters() { + entityGenerator.setupTestPolicies(); + val aclEntities = + policyService.listPolicies(Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(aclEntities.getTotalElements()).isEqualTo(3L); + } + + @Test + public void testListUsersNoFiltersEmptyResult() { + val aclEntities = + policyService.listPolicies(Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(aclEntities.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testListUsersFiltered() { + entityGenerator.setupTestPolicies(); + val userFilter = new SearchFilter("name", "Study001"); + val aclEntities = + policyService.listPolicies(Arrays.asList(userFilter), new PageableResolver().getPageable()); + assertThat(aclEntities.getTotalElements()).isEqualTo(1L); + } + + @Test + public void uniqueNameCheck_CreatePolicy_ThrowsUniqueConstraintException() { + val r1 = PolicyRequest.builder().name(UUID.randomUUID().toString()).build(); + + val p1 = policyService.create(r1); + assertThat(policyService.isExist(p1.getId())).isTrue(); + + assertThat(p1.getName()).isEqualTo(r1.getName()); + assertThatExceptionOfType(UniqueViolationException.class) + .isThrownBy(() -> policyService.create(r1)); + } + + @Test + public void uniqueNameCheck_UpdatePolicy_ThrowsUniqueConstraintException() { + val name1 = UUID.randomUUID().toString(); + val name2 = UUID.randomUUID().toString(); + val cr1 = PolicyRequest.builder().name(name1).build(); + + val cr2 = PolicyRequest.builder().name(name2).build(); + + val p1 = policyService.create(cr1); + assertThat(policyService.isExist(p1.getId())).isTrue(); + val p2 = policyService.create(cr2); + assertThat(policyService.isExist(p2.getId())).isTrue(); + + val ur3 = PolicyRequest.builder().name(name1).build(); + + assertThat(p1.getName()).isEqualTo(ur3.getName()); + assertThat(p2.getName()).isNotEqualTo(ur3.getName()); + assertThatExceptionOfType(UniqueViolationException.class) + .isThrownBy(() -> policyService.partialUpdate(p2.getId(), ur3)); + } + + @Test + public void testListUsersFilteredEmptyResult() { + entityGenerator.setupTestPolicies(); + val userFilter = new SearchFilter("name", "Study004"); + val aclEntities = + policyService.listPolicies(Arrays.asList(userFilter), new PageableResolver().getPageable()); + assertThat(aclEntities.getTotalElements()).isEqualTo(0L); + } + + // Update + @Test + public void testUpdate() { + val policy = entityGenerator.setupPolicy("Study001", groups.get(0).getName()); + val updateRequest = PolicyRequest.builder().name("StudyOne").build(); + val updated = policyService.partialUpdate(policy.getId(), updateRequest); + assertThat(updated.getName()).isEqualTo("StudyOne"); + } + + // Delete + @Test + public void testDelete() { + entityGenerator.setupTestPolicies(); + val policy = policyService.getByName("Study001"); + policyService.delete(policy.getId()); + + val remainingAclEntities = + policyService.listPolicies(Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(remainingAclEntities.getTotalElements()).isEqualTo(2L); + assertThat(remainingAclEntities.getContent()).doesNotContain(policy); + } +} diff --git a/src/test/java/bio/overture/ego/service/UserServiceTest.java b/src/test/java/bio/overture/ego/service/UserServiceTest.java new file mode 100644 index 000000000..0b8aeaf30 --- /dev/null +++ b/src/test/java/bio/overture/ego/service/UserServiceTest.java @@ -0,0 +1,927 @@ +package bio.overture.ego.service; + +import static bio.overture.ego.model.enums.AccessLevel.DENY; +import static bio.overture.ego.model.enums.AccessLevel.READ; +import static bio.overture.ego.model.enums.AccessLevel.WRITE; +import static bio.overture.ego.model.enums.LanguageType.ENGLISH; +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static bio.overture.ego.model.enums.StatusType.DISABLED; +import static bio.overture.ego.model.enums.StatusType.PENDING; +import static bio.overture.ego.model.enums.UserType.ADMIN; +import static bio.overture.ego.model.enums.UserType.USER; +import static bio.overture.ego.service.UserService.USER_CONVERTER; +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static bio.overture.ego.utils.EntityGenerator.generateNonExistentId; +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.UUID.randomUUID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import bio.overture.ego.controller.resolver.PageableResolver; +import bio.overture.ego.model.dto.CreateUserRequest; +import bio.overture.ego.model.dto.PermissionRequest; +import bio.overture.ego.model.dto.UpdateUserRequest; +import bio.overture.ego.model.entity.AbstractPermission; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.exceptions.NotFoundException; +import bio.overture.ego.model.exceptions.UniqueViolationException; +import bio.overture.ego.model.search.SearchFilter; +import bio.overture.ego.token.IDToken; +import bio.overture.ego.utils.EntityGenerator; +import bio.overture.ego.utils.PolicyPermissionUtils; +import com.google.common.collect.ImmutableList; +import java.util.Collections; +import java.util.Date; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@SpringBootTest +@RunWith(SpringRunner.class) +@ActiveProfiles("test") +@Transactional +@Ignore("replace with controller tests.") +public class UserServiceTest { + + private static final UUID NON_EXISTENT_USER = + UUID.fromString("827fae28-7fb8-11e8-adc0-fa7ae01bbebc"); + + @Autowired private ApplicationService applicationService; + @Autowired private UserService userService; + @Autowired private GroupService groupService; + @Autowired private PolicyService policyService; + @Autowired private EntityGenerator entityGenerator; + @Autowired private UserPermissionService userPermissionService; + + @Test + public void userConverter_UpdateUserRequest_User() { + val email = System.currentTimeMillis() + "@gmail.com"; + val firstName = "John"; + val lastName = "Doe"; + val userType = ADMIN; + val status = APPROVED; + val preferredLanguage = ENGLISH; + val id = randomUUID(); + val createdAt = new Date(); + + val applications = + IntStream.range(0, 3) + .boxed() + .map(x -> Application.builder().id(randomUUID()).build()) + .collect(toImmutableSet()); + + val user = + User.builder() + .email(email) + .firstName(firstName) + .lastName(lastName) + .type(userType) + .status(status) + .preferredLanguage(preferredLanguage) + .id(id) + .createdAt(createdAt) + .applications(applications) + .userPermissions(null) + .build(); + + val partialUserUpdateRequest = + UpdateUserRequest.builder().firstName("Rob").status(DISABLED).build(); + USER_CONVERTER.updateUser(partialUserUpdateRequest, user); + + assertThat(user.getPreferredLanguage()).isEqualTo(preferredLanguage); + assertThat(user.getCreatedAt()).isEqualTo(createdAt); + assertThat(user.getStatus()).isEqualTo(DISABLED); + assertThat(user.getLastName()).isEqualTo(lastName); + assertThat(user.getName()).isEqualTo(email); + assertThat(user.getEmail()).isEqualTo(email); + assertThat(user.getFirstName()).isEqualTo("Rob"); + assertThat(user.getType()).isEqualTo(userType); + assertThat(user.getId()).isEqualTo(id); + assertThat(user.getApplications()).containsExactlyInAnyOrderElementsOf(applications); + assertThat(user.getUserPermissions()).isNull(); + assertThat(user.getUserGroups()).isEmpty(); + } + + @Test + public void userConversion_CreateUserRequest_User() { + val t = System.currentTimeMillis(); + val request = + CreateUserRequest.builder() + .email(t + "@gmail.com") + .firstName("John") + .type(ADMIN) + .status(APPROVED) + .preferredLanguage(ENGLISH) + .build(); + val user = USER_CONVERTER.convertToUser(request); + assertThat(user.getEmail()).isEqualTo(request.getEmail()); + assertThat(user.getName()).isEqualTo(user.getEmail()); + assertThat(user.getCreatedAt()).isNotNull(); + assertThat(user.getId()).isNull(); + assertThat(user.getLastName()).isNull(); + assertThat(user.getFirstName()).isEqualTo(request.getFirstName()); + assertThat(user.getType()).isEqualTo(request.getType()); + assertThat(user.getStatus()).isEqualTo(request.getStatus()); + assertThat(user.getPreferredLanguage()).isEqualTo(request.getPreferredLanguage()); + assertThat(user.getUserGroups()).isEmpty(); + assertThat(user.getUserPermissions()).isEmpty(); + assertThat(user.getApplications()).isEmpty(); + } + + // Create + @Test + public void testCreate() { + val user = entityGenerator.setupUser("Demo User"); + // UserName == UserEmail + assertThat(user.getName()).isEqualTo("DemoUser@domain.com"); + } + + @Test + public void testCreateFromIDToken() { + val idToken = + IDToken.builder() + .email("UserOne@domain.com") + .given_name("User") + .family_name("User") + .build(); + + val idTokenUser = userService.createFromIDToken(idToken); + + assertThat(idTokenUser.getName()).isEqualTo("UserOne@domain.com"); + assertThat(idTokenUser.getEmail()).isEqualTo("UserOne@domain.com"); + assertThat(idTokenUser.getFirstName()).isEqualTo("User"); + assertThat(idTokenUser.getLastName()).isEqualTo("User"); + assertThat(idTokenUser.getStatus()).isEqualTo("Approved"); + assertThat(idTokenUser.getType()).isEqualTo("USER"); + } + + @Test + public void testCreateFromIDTokenUniqueNameAndEmail() { + // Note: This test has one strike due to Hibernate Cache. + entityGenerator.setupUser("User One"); + val idToken = + IDToken.builder().email("UserOne@domain.com").given_name("User").family_name("One").build(); + assertThatExceptionOfType(UniqueViolationException.class) + .isThrownBy(() -> userService.createFromIDToken(idToken)); + } + + // Get + @Test + public void testGet() { + val user = entityGenerator.setupUser("User One"); + val savedUser = userService.getById(user.getId()); + assertThat(savedUser.getName()).isEqualTo("UserOne@domain.com"); + } + + @Test + public void testGetNotFoundException() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> userService.getById(NON_EXISTENT_USER)); + } + + @Test + public void testGetByName() { + entityGenerator.setupUser("User One"); + val savedUser = userService.getByName("UserOne@domain.com"); + assertThat(savedUser.getName()).isEqualTo("UserOne@domain.com"); + } + + @Test + public void testGetByNameAllCaps() { + entityGenerator.setupUser("User One"); + val savedUser = userService.getByName("USERONE@DOMAIN.COM"); + assertThat(savedUser.getName()).isEqualTo("UserOne@domain.com"); + } + + @Test + @Ignore + public void testGetByNameNotFound() { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> userService.getByName("UserOne@domain.com")); + } + + // List Users + @Test + public void testListUsersNoFilters() { + entityGenerator.setupTestUsers(); + val users = + userService.listUsers(Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(users.getTotalElements()).isEqualTo(3L); + } + + @Test + public void testListUsersNoFiltersEmptyResult() { + val users = + userService.listUsers(Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(users.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testListUsersFiltered() { + entityGenerator.setupTestUsers(); + val userFilter = new SearchFilter("email", "FirstUser@domain.com"); + val users = + userService.listUsers(singletonList(userFilter), new PageableResolver().getPageable()); + assertThat(users.getTotalElements()).isEqualTo(1L); + } + + @Test + public void testListUsersFilteredEmptyResult() { + entityGenerator.setupTestUsers(); + val userFilter = new SearchFilter("email", "FourthUser@domain.com"); + val users = + userService.listUsers(singletonList(userFilter), new PageableResolver().getPageable()); + assertThat(users.getTotalElements()).isEqualTo(0L); + } + + // Find Users + @Test + public void testFindUsersNoFilters() { + entityGenerator.setupTestUsers(); + val users = + userService.findUsers( + "First", Collections.emptyList(), new PageableResolver().getPageable()); + assertThat(users.getTotalElements()).isEqualTo(1L); + assertThat(users.getContent().get(0).getName()).isEqualTo("FirstUser@domain.com"); + } + + @Test + public void testFindUsersFiltered() { + entityGenerator.setupTestUsers(); + val userFilter = new SearchFilter("email", "FirstUser@domain.com"); + val users = + userService.findUsers( + "Second", singletonList(userFilter), new PageableResolver().getPageable()); + // Expect empty list + assertThat(users.getTotalElements()).isEqualTo(0L); + } + // Find Group Users + @Test + public void testFindGroupUsersNoQueryNoFilters() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val user = userService.getByName("FirstUser@domain.com"); + val userTwo = (userService.getByName("SecondUser@domain.com")); + val groupId = groupService.getByName("Group One").getId(); + + userService.associateGroupsWithUser(user.getId(), singletonList(groupId)); + userService.associateGroupsWithUser(userTwo.getId(), singletonList(groupId)); + + val users = + userService.findUsersForGroup( + groupId, ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(users.getTotalElements()).isEqualTo(2L); + assertThat(users.getContent()).contains(user, userTwo); + } + + @Test + public void testFindGroupUsersNoQueryNoFiltersNoUsersFound() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val groupId = groupService.getByName("Group One").getId(); + + val users = + userService.findUsersForGroup( + groupId, ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(users.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindGroupUsersNoQueryFilters() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val user = userService.getByName("FirstUser@domain.com"); + val userTwo = userService.getByName("SecondUser@domain.com"); + val groupId = groupService.getByName("Group One").getId(); + + userService.associateGroupsWithUser(user.getId(), newArrayList(groupId)); + userService.associateGroupsWithUser(userTwo.getId(), newArrayList(groupId)); + + val userFilters = new SearchFilter("name", "First"); + + val users = + userService.findUsersForGroup( + groupId, ImmutableList.of(userFilters), new PageableResolver().getPageable()); + + assertThat(users.getTotalElements()).isEqualTo(1L); + assertThat(users.getContent()).contains(user); + } + + @Test + public void testFindGroupUsersQueryAndFilters() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val user = userService.getByName("FirstUser@domain.com"); + val userTwo = (userService.getByName("SecondUser@domain.com")); + val groupId = groupService.getByName("Group One").getId(); + + userService.associateGroupsWithUser(user.getId(), singletonList(groupId)); + userService.associateGroupsWithUser(userTwo.getId(), singletonList(groupId)); + + val userFilters = new SearchFilter("name", "First"); + + val users = + userService.findUsersForGroup( + groupId, "Second", ImmutableList.of(userFilters), new PageableResolver().getPageable()); + + assertThat(users.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindGroupUsersQueryNoFilters() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val user = userService.getByName("FirstUser@domain.com"); + val userTwo = (userService.getByName("SecondUser@domain.com")); + val groupId = groupService.getByName("Group One").getId(); + + userService.associateGroupsWithUser(user.getId(), singletonList(groupId)); + userService.associateGroupsWithUser(userTwo.getId(), singletonList(groupId)); + val users = + userService.findUsersForGroup( + groupId, "Second", ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(users.getTotalElements()).isEqualTo(1L); + assertThat(users.getContent()).contains(userTwo); + } + + // Find App Users + + @Test + public void testFindAppUsersNoQueryNoFilters() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val user = userService.getByName("FirstUser@domain.com"); + val userTwo = (userService.getByName("SecondUser@domain.com")); + val appId = applicationService.getByClientId("111111").getId(); + + userService.addUserToApps(user.getId(), singletonList(appId)); + userService.addUserToApps(userTwo.getId(), singletonList(appId)); + + val users = + userService.findUsersForApplication( + appId, Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(users.getTotalElements()).isEqualTo(2L); + assertThat(users.getContent()).contains(user, userTwo); + } + + @Test + public void testFindAppUsersNoQueryNoFiltersNoUser() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val appId = applicationService.getByClientId("111111").getId(); + + val users = + userService.findUsersForApplication( + appId, Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(users.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindAppUsersNoQueryFilters() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val user = userService.getByName("FirstUser@domain.com"); + val userTwo = (userService.getByName("SecondUser@domain.com")); + val appId = applicationService.getByClientId("111111").getId(); + + userService.addUserToApps(user.getId(), singletonList(appId)); + userService.addUserToApps(userTwo.getId(), singletonList(appId)); + + val userFilters = new SearchFilter("name", "First"); + + val users = + userService.findUsersForApplication( + appId, singletonList(userFilters), new PageableResolver().getPageable()); + + assertThat(users.getTotalElements()).isEqualTo(1L); + assertThat(users.getContent()).contains(user); + } + + @Test + public void testFindAppUsersQueryAndFilters() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val user = userService.getByName("FirstUser@domain.com"); + val userTwo = (userService.getByName("SecondUser@domain.com")); + val appId = applicationService.getByClientId("111111").getId(); + + userService.addUserToApps(user.getId(), singletonList(appId)); + userService.addUserToApps(userTwo.getId(), singletonList(appId)); + + val userFilters = new SearchFilter("name", "First"); + + val users = + userService.findUsersForApplication( + appId, "Second", singletonList(userFilters), new PageableResolver().getPageable()); + + assertThat(users.getTotalElements()).isEqualTo(0L); + } + + @Test + public void testFindAppUsersQueryNoFilters() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val user = userService.getByName("FirstUser@domain.com"); + val userTwo = (userService.getByName("SecondUser@domain.com")); + val appId = applicationService.getByClientId("111111").getId(); + + userService.addUserToApps(user.getId(), singletonList(appId)); + userService.addUserToApps(userTwo.getId(), singletonList(appId)); + + val users = + userService.findUsersForApplication( + appId, "First", Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(users.getTotalElements()).isEqualTo(1L); + assertThat(users.getContent()).contains(user); + } + + // Update + @Test + public void testUpdate() { + val user = entityGenerator.setupUser("First User"); + val updated = + userService.partialUpdate( + user.getId(), UpdateUserRequest.builder().firstName("NotFirst").build()); + assertThat(updated.getFirstName()).isEqualTo("NotFirst"); + } + + @Test + public void testUpdateTypeUser() { + val user = entityGenerator.setupUser("First User"); + val updated = + userService.partialUpdate(user.getId(), UpdateUserRequest.builder().type(USER).build()); + assertThat(updated.getType()).isEqualTo("USER"); + } + + @Test + public void testUpdateUserTypeAdmin() { + val user = entityGenerator.setupUser("First User"); + val updated = + userService.partialUpdate(user.getId(), UpdateUserRequest.builder().type(ADMIN).build()); + assertThat(updated.getType()).isEqualTo("ADMIN"); + } + + @Test + public void uniqueEmailCheck_CreateUser_ThrowsUniqueConstraintException() { + val r1 = + CreateUserRequest.builder() + .preferredLanguage(ENGLISH) + .type(ADMIN) + .status(APPROVED) + .email(UUID.randomUUID() + "@gmail.com") + .build(); + + val u1 = userService.create(r1); + assertThat(userService.isExist(u1.getId())).isTrue(); + r1.setType(USER); + r1.setStatus(PENDING); + + assertThat(u1.getEmail()).isEqualTo(r1.getEmail()); + assertThatExceptionOfType(UniqueViolationException.class) + .isThrownBy(() -> userService.create(r1)); + } + + @Test + public void uniqueEmailCheck_UpdateUser_ThrowsUniqueConstraintException() { + val e1 = UUID.randomUUID().toString() + "@something.com"; + val e2 = UUID.randomUUID().toString() + "@something.com"; + val cr1 = + CreateUserRequest.builder() + .preferredLanguage(ENGLISH) + .type(ADMIN) + .status(APPROVED) + .email(e1) + .build(); + + val cr2 = + CreateUserRequest.builder() + .preferredLanguage(ENGLISH) + .type(USER) + .status(PENDING) + .email(e2) + .build(); + + val u1 = userService.create(cr1); + assertThat(userService.isExist(u1.getId())).isTrue(); + val u2 = userService.create(cr2); + assertThat(userService.isExist(u2.getId())).isTrue(); + + val ur3 = UpdateUserRequest.builder().email(e1).build(); + + assertThat(u1.getEmail()).isEqualTo(ur3.getEmail()); + assertThat(u2.getEmail()).isNotEqualTo(ur3.getEmail()); + assertThatExceptionOfType(UniqueViolationException.class) + .isThrownBy(() -> userService.partialUpdate(u2.getId(), ur3)); + } + + @Test + public void testUpdateNonexistentEntity() { + val nonExistentId = generateNonExistentId(userService); + val updateRequest = + UpdateUserRequest.builder() + .firstName("Doesnot") + .lastName("Exist") + .status(APPROVED) + .preferredLanguage(ENGLISH) + .lastLogin(null) + .type(ADMIN) + .build(); + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> userService.partialUpdate(nonExistentId, updateRequest)); + } + + // Add User to Groups + @Test + public void addUserToGroups() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val group = groupService.getByName("Group One"); + val groupId = group.getId(); + val groupTwo = groupService.getByName("Group Two"); + val groupTwoId = groupTwo.getId(); + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + userService.associateGroupsWithUser(userId, asList(groupId, groupTwoId)); + + val groups = + groupService.findGroupsForUser( + userId, ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(groups.getContent()).contains(group, groupTwo); + } + + @Test + public void addUserToGroupsNoUser() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val group = groupService.getByName("Group One"); + val groupId = group.getId(); + + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy( + () -> userService.associateGroupsWithUser(NON_EXISTENT_USER, singletonList(groupId))); + } + + @Test + public void addUserToGroupsWithGroupsListOneEmptyString() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> userService.associateGroupsWithUser(userId, ImmutableList.of())); + } + + @Test + public void addUserToGroupsEmptyGroupsList() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + userService.associateGroupsWithUser(userId, Collections.emptyList()); + + val nonUpdated = userService.getByName("FirstUser@domain.com"); + assertThat(nonUpdated).isEqualTo(user); + } + + // Add User to Apps + @Test + public void addUserToApps() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val app = applicationService.getByClientId("111111"); + val appId = app.getId(); + val appTwo = applicationService.getByClientId("222222"); + val appTwoId = appTwo.getId(); + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + userService.addUserToApps(userId, asList(appId, appTwoId)); + + val apps = + applicationService.findApplicationsForUser( + userId, Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(apps.getContent()).contains(app, appTwo); + } + + @Test + public void addUserToAppsNoUser() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val app = applicationService.getByClientId("111111"); + val appId = app.getId(); + + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> userService.addUserToApps(NON_EXISTENT_USER, singletonList(appId))); + } + + @Test + public void addUserToAppsWithAppsListOneEmptyString() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> userService.addUserToApps(userId, ImmutableList.of())); + } + + @Test + public void addUserToAppsEmptyAppsList() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + userService.addUserToApps(userId, Collections.emptyList()); + + val nonUpdated = userService.getByName("FirstUser@domain.com"); + assertThat(nonUpdated).isEqualTo(user); + } + + // Delete + @Test + public void testDelete() { + entityGenerator.setupTestUsers(); + + val usersBefore = + userService.listUsers(Collections.emptyList(), new PageableResolver().getPageable()); + + val user = userService.getByName("FirstUser@domain.com"); + + userService.delete(user.getId()); + + val usersAfter = + userService.listUsers(Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(usersBefore.getTotalElements() - usersAfter.getTotalElements()).isEqualTo(1L); + assertThat(usersAfter.getContent()).doesNotContain(user); + } + + @Test + public void testDeleteNonExisting() { + entityGenerator.setupTestUsers(); + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> userService.delete(NON_EXISTENT_USER)); + } + + // Delete User from Group + @Test + public void testDeleteUserFromGroup() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val group = groupService.getByName("Group One"); + val groupId = group.getId(); + val groupTwo = groupService.getByName("Group Two"); + val groupTwoId = groupTwo.getId(); + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + userService.associateGroupsWithUser(userId, asList(groupId, groupTwoId)); + + userService.disassociateGroupsFromUser(userId, singletonList(groupId)); + + val groupWithoutUser = + groupService.findGroupsForUser( + userId, ImmutableList.of(), new PageableResolver().getPageable()); + + assertThat(groupWithoutUser.getContent()).containsOnly(groupTwo); + } + + @Test + public void testDeleteUserFromGroupNoUser() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val group = groupService.getByName("Group One"); + val groupId = group.getId(); + val groupTwo = groupService.getByName("Group Two"); + val groupTwoId = groupTwo.getId(); + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + userService.associateGroupsWithUser(userId, asList(groupId, groupTwoId)); + + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy( + () -> + userService.disassociateGroupsFromUser(NON_EXISTENT_USER, singletonList(groupId))); + } + + @Test + public void testDeleteUserFromGroupEmptyGroupsList() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + val group = groupService.getByName("Group One"); + val groupId = group.getId(); + + userService.associateGroupsWithUser(userId, singletonList(groupId)); + assertThat(user.getUserGroups().size()).isEqualTo(1); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> userService.disassociateGroupsFromUser(userId, ImmutableList.of())); + } + + // Delete User from App + @Test + public void testDeleteUserFromApp() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val app = applicationService.getByClientId("111111"); + val appId = app.getId(); + val appTwo = applicationService.getByClientId("222222"); + val appTwoId = appTwo.getId(); + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + userService.addUserToApps(userId, asList(appId, appTwoId)); + + userService.deleteUserFromApps(userId, singletonList(appId)); + + val groupWithoutUser = + applicationService.findApplicationsForUser( + userId, Collections.emptyList(), new PageableResolver().getPageable()); + + assertThat(groupWithoutUser.getContent()).containsOnly(appTwo); + } + + @Test + public void testDeleteUserFromAppNoUser() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val app = applicationService.getByClientId("111111"); + val appId = app.getId(); + val appTwo = applicationService.getByClientId("222222"); + val appTwoId = appTwo.getId(); + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + userService.addUserToApps(userId, asList(appId, appTwoId)); + + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> userService.deleteUserFromApps(NON_EXISTENT_USER, singletonList(appId))); + } + + @Test + public void testDeleteUserFromAppEmptyAppsList() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestApplications(); + + val app = applicationService.getByClientId("111111"); + val appId = app.getId(); + val appTwo = applicationService.getByClientId("222222"); + val appTwoId = appTwo.getId(); + val user = userService.getByName("FirstUser@domain.com"); + val userId = user.getId(); + + userService.addUserToApps(userId, asList(appId, appTwoId)); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> userService.deleteUserFromApps(userId, ImmutableList.of())); + } + + @Test + public void testAddUserPermissions() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + entityGenerator.setupTestPolicies(); + + val user = userService.getByName("FirstUser@domain.com"); + + val study001 = policyService.getByName("Study001"); + val study001id = study001.getId(); + + val study002 = policyService.getByName("Study002"); + val study002id = study002.getId(); + + val study003 = policyService.getByName("Study003"); + val study003id = study003.getId(); + + val permissions = + asList( + new PermissionRequest(study001id, READ), + new PermissionRequest(study002id, WRITE), + new PermissionRequest(study003id, DENY)); + + userPermissionService.addPermissions(user.getId(), permissions); + + assertThat(PolicyPermissionUtils.extractPermissionStrings(user.getUserPermissions())) + .containsExactlyInAnyOrder("Study001.READ", "Study002.WRITE", "Study003.DENY"); + } + + @Test + public void testRemoveUserPermissions() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + entityGenerator.setupTestPolicies(); + + val user = userService.getByName("FirstUser@domain.com"); + + val study001 = policyService.getByName("Study001"); + val study001id = study001.getId(); + + val study002 = policyService.getByName("Study002"); + val study002id = study002.getId(); + + val study003 = policyService.getByName("Study003"); + val study003id = study003.getId(); + + val permissions = + asList( + new PermissionRequest(study001id, READ), + new PermissionRequest(study002id, WRITE), + new PermissionRequest(study003id, DENY)); + + userPermissionService.addPermissions(user.getId(), permissions); + + val userPermissionsToRemove = + user.getUserPermissions().stream() + .filter(p -> !p.getPolicy().getName().equals("Study001")) + .map(AbstractPermission::getId) + .collect(Collectors.toList()); + + userPermissionService.deletePermissions(user.getId(), userPermissionsToRemove); + + assertThat(PolicyPermissionUtils.extractPermissionStrings(user.getUserPermissions())) + .containsExactlyInAnyOrder("Study001.READ"); + } + + @Test + public void testGetUserPermissions() { + entityGenerator.setupTestUsers(); + entityGenerator.setupTestGroups(); + entityGenerator.setupTestPolicies(); + + val user = userService.getByName("FirstUser@domain.com"); + + val study001 = policyService.getByName("Study001"); + val study001id = study001.getId(); + + val study002 = policyService.getByName("Study002"); + val study002id = study002.getId(); + + val study003 = policyService.getByName("Study003"); + val study003id = study003.getId(); + + val permissions = + asList( + new PermissionRequest(study001id, READ), + new PermissionRequest(study002id, WRITE), + new PermissionRequest(study003id, DENY)); + + userPermissionService.addPermissions(user.getId(), permissions); + + val pagedUserPermissions = + userPermissionService.getPermissions(user.getId(), new PageableResolver().getPageable()); + + assertThat(pagedUserPermissions.getTotalElements()).isEqualTo(3L); + } +} diff --git a/src/test/java/org/overture/ego/test/FlywayInit.java b/src/test/java/bio/overture/ego/test/FlywayInit.java similarity index 94% rename from src/test/java/org/overture/ego/test/FlywayInit.java rename to src/test/java/bio/overture/ego/test/FlywayInit.java index 6a665e5c6..f1de4d774 100644 --- a/src/test/java/org/overture/ego/test/FlywayInit.java +++ b/src/test/java/bio/overture/ego/test/FlywayInit.java @@ -1,12 +1,11 @@ -package org.overture.ego.test; +package bio.overture.ego.test; +import java.sql.Connection; +import java.sql.SQLException; import lombok.extern.slf4j.Slf4j; import org.flywaydb.core.Flyway; import org.springframework.jdbc.datasource.SingleConnectionDataSource; -import java.sql.Connection; -import java.sql.SQLException; - @Slf4j public class FlywayInit { @@ -18,5 +17,4 @@ public static void initTestContainers(Connection connection) throws SQLException flyway.setDataSource(new SingleConnectionDataSource(connection, true)); flyway.migrate(); } - } diff --git a/src/test/java/bio/overture/ego/token/LastloginTest.java b/src/test/java/bio/overture/ego/token/LastloginTest.java new file mode 100644 index 000000000..54213e4ee --- /dev/null +++ b/src/test/java/bio/overture/ego/token/LastloginTest.java @@ -0,0 +1,57 @@ +package bio.overture.ego.token; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import bio.overture.ego.model.entity.User; +import bio.overture.ego.service.TokenService; +import bio.overture.ego.service.UserService; +import bio.overture.ego.utils.EntityGenerator; +import java.util.Date; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@Slf4j +@SpringBootTest +@RunWith(SpringRunner.class) +@ActiveProfiles("test") +public class LastloginTest { + + @Autowired private TokenService tokenService; + + @Autowired private UserService userService; + + @Autowired private EntityGenerator entityGenerator; + + @Test + @SneakyThrows + public void testLastloginUpdate() { + + val idToken = new IDToken(); + idToken.setFamily_name("foo"); + idToken.setGiven_name("bar"); + idToken.setEmail("foobar@domain.com"); + User user = entityGenerator.setupUser("foo bar"); + + assertNull( + " Verify before generatedUserToken, last login after fetching the user should be null. ", + userService.getByName(idToken.getEmail()).getLastLogin()); + + tokenService.generateUserToken(idToken); + + val lastLogin = userService.getByName(idToken.getEmail()).getLastLogin(); + userService.delete(user.getId()); + + assertNotNull("Verify after generatedUserToken, last login is not null.", lastLogin); + val tolerance = 2 * 1000; // 2 seconds + val now = new Date().getTime(); + assert (now - lastLogin.getTime()) <= tolerance; + } +} diff --git a/src/test/java/bio/overture/ego/token/ListTokenTest.java b/src/test/java/bio/overture/ego/token/ListTokenTest.java new file mode 100644 index 000000000..b6fe98063 --- /dev/null +++ b/src/test/java/bio/overture/ego/token/ListTokenTest.java @@ -0,0 +1,97 @@ +package bio.overture.ego.token; + +import static bio.overture.ego.utils.CollectionUtils.mapToSet; +import static org.junit.Assert.assertTrue; + +import bio.overture.ego.model.dto.Scope; +import bio.overture.ego.model.dto.TokenResponse; +import bio.overture.ego.model.entity.Token; +import bio.overture.ego.service.TokenService; +import bio.overture.ego.utils.EntityGenerator; +import bio.overture.ego.utils.TestData; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@SpringBootTest +@RunWith(SpringRunner.class) +@Transactional +@ActiveProfiles("test") +@Ignore +public class ListTokenTest { + + public static TestData test = null; + @Autowired private EntityGenerator entityGenerator; + @Autowired private TokenService tokenService; + @Rule public ExpectedException exception = ExpectedException.none(); + + @Before + public void setUp() { + test = new TestData(entityGenerator); + } + + @Test + public void testListToken() { + val tokenString1 = "791044a1-3ffd-4164-a6a0-0e1e666b28dc"; + val tokenString2 = "891044a1-3ffd-4164-a6a0-0e1e666b28dc"; + + val scopes1 = test.getScopes("song.WRITE", "id.WRITE"); + val scopes2 = test.getScopes("song.READ", "id.READ"); + + Set scopeString1 = mapToSet(scopes1, Scope::toString); + Set scopeString2 = mapToSet(scopes2, Scope::toString); + + val userToken1 = + entityGenerator.setupToken( + test.regularUser, tokenString1, false, 1000, "Test token 1.", scopes1); + val userToken2 = + entityGenerator.setupToken( + test.regularUser, tokenString2, false, 1000, "Test token 2.", scopes2); + + Set tokens = new HashSet<>(); + tokens.add(userToken1); + tokens.add(userToken2); + test.regularUser.setTokens(tokens); + + val responseList = tokenService.listToken(test.regularUser.getId()); + + List expected = new ArrayList<>(); + expected.add( + TokenResponse.builder() + .accessToken(tokenString1) + .scope(scopeString1) + .exp(userToken1.getSecondsUntilExpiry()) + .description("Test token 1.") + .build()); + expected.add( + TokenResponse.builder() + .accessToken(tokenString2) + .scope(scopeString2) + .exp(userToken2.getSecondsUntilExpiry()) + .description("Test token 2.") + .build()); + + assertTrue((responseList.stream().allMatch(expected::contains))); + } + + @Test + public void testEmptyTokenList() { + val tokens = tokenService.listToken(test.regularUser.getId()); + assertTrue(tokens.isEmpty()); + } +} diff --git a/src/test/java/bio/overture/ego/utils/EntityGenerator.java b/src/test/java/bio/overture/ego/utils/EntityGenerator.java new file mode 100644 index 000000000..62b0ea68c --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/EntityGenerator.java @@ -0,0 +1,482 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.model.enums.LanguageType.ENGLISH; +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static bio.overture.ego.model.enums.StatusType.PENDING; +import static bio.overture.ego.model.enums.UserType.ADMIN; +import static bio.overture.ego.utils.CollectionUtils.listOf; +import static bio.overture.ego.utils.CollectionUtils.mapToList; +import static bio.overture.ego.utils.Splitters.COMMA_SPLITTER; +import static com.google.common.collect.Lists.newArrayList; +import static java.lang.Integer.MAX_VALUE; +import static java.lang.Math.abs; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; + +import bio.overture.ego.model.dto.CreateApplicationRequest; +import bio.overture.ego.model.dto.CreateUserRequest; +import bio.overture.ego.model.dto.GroupRequest; +import bio.overture.ego.model.dto.PolicyRequest; +import bio.overture.ego.model.dto.Scope; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.Token; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.entity.UserPermission; +import bio.overture.ego.model.enums.AccessLevel; +import bio.overture.ego.model.enums.ApplicationType; +import bio.overture.ego.model.enums.LanguageType; +import bio.overture.ego.model.enums.StatusType; +import bio.overture.ego.model.enums.UserType; +import bio.overture.ego.model.params.ScopeName; +import bio.overture.ego.service.ApplicationService; +import bio.overture.ego.service.BaseService; +import bio.overture.ego.service.GroupService; +import bio.overture.ego.service.NamedService; +import bio.overture.ego.service.PolicyService; +import bio.overture.ego.service.TokenService; +import bio.overture.ego.service.TokenStoreService; +import bio.overture.ego.service.UserPermissionService; +import bio.overture.ego.service.UserService; +import com.google.common.collect.ImmutableSet; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import java.util.function.Supplier; +import lombok.NonNull; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +/** + * * For this class, we follow the following naming conventions: createEntity: returns a new object + * of applicationType Entity. setupEntity: Create an policy, saves it using Hibernate, & returns it. + * setupEntities: Sets up multiple entities at once setupTestEntities: Sets up specific entities + * used in our unit tests + */ +public class EntityGenerator { + + private static final String DICTIONARY = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-abcdefghijklmnopqrstuvwxyz"; + + @Autowired private TokenService tokenService; + + @Autowired private ApplicationService applicationService; + + @Autowired private UserService userService; + + @Autowired private GroupService groupService; + + @Autowired private PolicyService policyService; + + @Autowired private TokenStoreService tokenStoreService; + + @Autowired private UserPermissionService userPermissionService; + + public Application setupApplication(String clientId) { + return applicationService + .findByClientId(clientId) + .orElseGet( + () -> { + val request = createApplicationCreateRequest(clientId); + return applicationService.create(request); + }); + } + + public List setupApplications(String... clientIds) { + return mapToList(listOf(clientIds), this::setupApplication); + } + + public void setupTestApplications(String postfix) { + setupApplications( + String.format("111111_%s", postfix), + String.format("222222_%s", postfix), + String.format("333333_%s", postfix), + String.format("444444_%s", postfix), + String.format("555555_%s", postfix)); + } + + public void setupTestApplications() { + setupApplications("111111", "222222", "333333", "444444", "555555"); + } + + public Application setupApplication( + String clientId, String clientSecret, ApplicationType applicationType) { + return applicationService + .findByClientId(clientId) + .orElseGet( + () -> { + val request = + CreateApplicationRequest.builder() + .name(clientId) + .type(applicationType) + .clientSecret(clientSecret) + .clientId(clientId) + .status(APPROVED) + .build(); + return applicationService.create(request); + }); + } + + public User setupUser(String name) { + val names = name.split(" ", 2); + val userName = String.format("%s%s@domain.com", names[0], names[1]); + return userService + .findByName(userName) + .orElseGet( + () -> { + val createUserRequest = createUser(name); + return userService.create(createUserRequest); + }); + } + + public List setupUsers(String... users) { + return mapToList(listOf(users), this::setupUser); + } + + public void setupTestUsers() { + setupUsers("First User", "Second User", "Third User"); + } + + public Group setupGroup(String name) { + return groupService + .findByName(name) + .orElseGet( + () -> { + val group = createGroupRequest(name); + return groupService.create(group); + }); + } + + private CreateUserRequest createUser(String firstName, String lastName) { + return CreateUserRequest.builder() + .email(String.format("%s%s@domain.com", firstName, lastName)) + .firstName(firstName) + .lastName(lastName) + .status(APPROVED) + .preferredLanguage(ENGLISH) + .type(ADMIN) + .build(); + } + + private CreateUserRequest createUser(String name) { + val names = name.split(" ", 2); + return createUser(names[0], names[1]); + } + + private GroupRequest createGroupRequest(String name) { + return GroupRequest.builder().name(name).status(PENDING).description("").build(); + } + + public static > E randomEnum(Class e) { + val enums = e.getEnumConstants(); + val r = new Random(); + val randomPos = abs(r.nextInt()) % enums.length; + return enums[randomPos]; + } + + public static StatusType randomStatusType() { + return randomEnum(StatusType.class); + } + + private static String internalRandomString(String dictionary, int length) { + val r = new Random(); + val sb = new StringBuilder(); + r.ints(length, 0, dictionary.length()).map(dictionary::charAt).forEach(sb::append); + return sb.toString(); + } + + public static String randomStringWithSpaces(int length) { + val newDictionary = DICTIONARY + " "; + return internalRandomString(newDictionary, length); + } + + public static String randomStringNoSpaces(int length) { + return internalRandomString(DICTIONARY, length); + } + + public Group generateRandomGroup() { + val request = + GroupRequest.builder() + .name(generateNonExistentName(groupService)) + .status(randomStatusType()) + .description(randomStringWithSpaces(15)) + .build(); + return groupService.create(request); + } + + public static ApplicationType randomApplicationType() { + return randomEnum(ApplicationType.class); + } + + public static UserType randomUserType() { + return randomEnum(UserType.class); + } + + public static LanguageType randomLanguageType() { + return randomEnum(LanguageType.class); + } + + public static AccessLevel randomAccessLevel() { + return randomEnum(AccessLevel.class); + } + + public Application generateRandomApplication() { + val request = + CreateApplicationRequest.builder() + .clientId(randomStringNoSpaces(10)) + .clientSecret(randomStringNoSpaces(10)) + .name(generateNonExistentName(applicationService)) + .type(randomApplicationType()) + .status(randomStatusType()) + .redirectUri("https://ego.com/" + randomStringNoSpaces(7)) + .description(randomStringWithSpaces(15)) + .build(); + return applicationService.create(request); + } + + private String randomUserEmail() { + String email; + Optional result; + + do { + email = randomStringNoSpaces(5) + "@xyz.com"; + result = userService.findByName(email); + } while (result.isPresent()); + + return email; + } + + public User generateRandomUser() { + val request = + CreateUserRequest.builder() + .email(randomUserEmail()) + .status(randomStatusType()) + .type(randomUserType()) + .preferredLanguage(randomLanguageType()) + .firstName(randomStringNoSpaces(5)) + .lastName(randomStringNoSpaces(6)) + .build(); + return userService.create(request); + } + + public Policy generateRandomPolicy() { + val request = PolicyRequest.builder().name(generateNonExistentName(policyService)).build(); + return policyService.create(request); + } + + public List setupGroups(String... groupNames) { + return mapToList(listOf(groupNames), this::setupGroup); + } + + public void setupTestGroups(String postfix) { + setupGroups( + String.format("Group One_%s", postfix), + String.format("Group Two_%s", postfix), + String.format("Group Three_%s", postfix)); + } + + public void setupTestGroups() { + setupGroups("Group One", "Group Two", "Group Three"); + } + + public Policy setupSinglePolicy(String name) { + return policyService + .findByName(name) + .orElseGet( + () -> { + val createRequest = createPolicyRequest(name); + return policyService.create(createRequest); + }); + } + + public Policy setupPolicy(String name, String groupName) { + return policyService + .findByName(name) + .orElseGet( + () -> { + val createRequest = createPolicyRequest(name); + return policyService.create(createRequest); + }); + } + + public Policy setupPolicy(@NonNull String csv) { + val args = newArrayList(COMMA_SPLITTER.split(csv)); + assertThat(args).hasSize(2); + val name = args.get(0); + val groupName = args.get(1); + return setupPolicy(name, groupName); + } + + public List setupPolicies(String... names) { + return mapToList(listOf(names), this::setupPolicy); + } + + public void setupTestPolicies() { + setupPolicies("Study001,Group One", "Study002,Group Two", "Study003,Group Three"); + } + + public Token setupToken( + User user, + String token, + boolean isRevoked, + long duration, + String description, + Set scopes) { + val tokenObject = + Token.builder() + .name(token) + .isRevoked(isRevoked) + .owner(user) + .description(description) + .issueDate(Date.from(Instant.now())) + .expiryDate(Date.from(Instant.now().plus(365, ChronoUnit.DAYS))) + .build(); + + tokenObject.setScopes(scopes); + + return tokenStoreService.create(tokenObject); + } + + public void addPermissions(User user, Set scopes) { + val userPermissions = + scopes.stream() + .map( + s -> { + UserPermission up = new UserPermission(); + up.setPolicy(s.getPolicy()); + up.setAccessLevel(s.getAccessLevel()); + up.setOwner(user); + userPermissionService.getRepository().save(up); + return up; + }) + .collect(toSet()); + user.getUserPermissions().addAll(userPermissions); + userService.getRepository().save(user); + } + + public String generateNonExistentUserName() { + val r = new Random(); + String name; + Optional result; + + do { + name = generateRandomUserName(r, 5); + result = userService.findByName(name); + } while (result.isPresent()); + + return name; + } + + public Set getScopes(String... scope) { + return tokenService.getScopes(ImmutableSet.copyOf(scopeNames(scope))); + } + + private CreateApplicationRequest createApplicationCreateRequest(String clientId) { + return CreateApplicationRequest.builder() + .name(createApplicationName(clientId)) + .type(ApplicationType.CLIENT) + .clientId(clientId) + .clientSecret(reverse(clientId)) + .status(PENDING) + .build(); + } + + private String createApplicationName(String clientId) { + return String.format("Application %s", clientId); + } + + private String reverse(String value) { + return new StringBuilder(value).reverse().toString(); + } + + private PolicyRequest createPolicyRequest(String name) { + return PolicyRequest.builder().name(name).build(); + } + + public static List scopeNames(String... strings) { + return mapToList(listOf(strings), ScopeName::new); + } + + public static > E randomEnumExcluding( + @NonNull Class enumClass, @NonNull E enumToExclude) { + val list = + stream(enumClass.getEnumConstants()).filter(x -> x != enumToExclude).collect(toList()); + return randomElementOf(list); + } + + public static T randomNull(Supplier callback) { + return randomBoundedInt(2) == 0 ? null : callback.get(); + } + + public static int randomBoundedInt(int maxExclusive) { + return abs(new Random().nextInt()) % maxExclusive; + } + + public static int randomBoundedInt(int minInclusive, int maxExclusive) { + assertThat(MAX_VALUE - maxExclusive).isGreaterThan(minInclusive); + return minInclusive + randomBoundedInt(maxExclusive); + } + + public static T randomElementOf(List list) { + return list.get(randomBoundedInt(list.size())); + } + + public static T randomElementOf(T... objects) { + return objects[randomBoundedInt(objects.length)]; + } + + public static String generateNonExistentClientId( + NamedService applicationService) { + val r = new Random(); + String clientId = generateRandomName(r, 15); + Optional result = applicationService.findByName(clientId); + + while (result.isPresent()) { + clientId = generateRandomName(r, 15); + result = applicationService.findByName(clientId); + } + return clientId; + } + + public static String generateNonExistentName(NamedService namedService) { + val r = new Random(); + String name = generateRandomName(r, 15); + Optional result = namedService.findByName(name); + + while (result.isPresent()) { + name = generateRandomName(r, 15); + result = namedService.findByName(name); + } + return name; + } + + public static UUID generateNonExistentId(BaseService baseService) { + UUID id = UUID.randomUUID(); + while (baseService.isExist(id)) { + id = UUID.randomUUID(); + } + return id; + } + + private static String generateRandomName(Random r, int length) { + val sb = new StringBuilder(); + r.ints(length, 65, 90).forEach(sb::append); + return sb.toString(); + } + + private static String generateRandomUserName(Random r, int length) { + val fn = generateRandomName(r, length); + val ln = generateRandomName(r, length); + return fn + " " + ln; + } +} diff --git a/src/test/java/bio/overture/ego/utils/EntityTools.java b/src/test/java/bio/overture/ego/utils/EntityTools.java new file mode 100644 index 000000000..55ec424bf --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/EntityTools.java @@ -0,0 +1,33 @@ +package bio.overture.ego.utils; + +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Group; +import bio.overture.ego.model.entity.Identifiable; +import bio.overture.ego.model.entity.User; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class EntityTools { + public static List extractGroupIds(Set entities) { + return entities.stream().map(Group::getId).collect(java.util.stream.Collectors.toList()); + } + + public static List extractGroupNames(List entities) { + return entities.stream().map(Group::getName).collect(java.util.stream.Collectors.toList()); + } + + public static List extractUserIds(Set entities) { + return entities.stream().map(User::getId).collect(java.util.stream.Collectors.toList()); + } + + public static List extractAppIds(Set entities) { + return entities.stream().map(Application::getId).collect(Collectors.toList()); + } + + public static > List extractIDs(Collection entities) { + return entities.stream().map(Identifiable::getId).collect(Collectors.toList()); + } +} diff --git a/src/test/java/bio/overture/ego/utils/TestData.java b/src/test/java/bio/overture/ego/utils/TestData.java new file mode 100644 index 000000000..25594708a --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/TestData.java @@ -0,0 +1,96 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.model.enums.StatusType.APPROVED; +import static bio.overture.ego.model.enums.UserType.ADMIN; +import static bio.overture.ego.model.enums.UserType.USER; +import static bio.overture.ego.utils.CollectionUtils.listOf; +import static bio.overture.ego.utils.CollectionUtils.mapToSet; + +import bio.overture.ego.model.dto.Scope; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.model.entity.Policy; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.model.enums.ApplicationType; +import bio.overture.ego.model.params.ScopeName; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import lombok.val; + +public class TestData { + public Application song; + public String songId; + public String songAuth; + + public String scoreId; + public Application score; + public String scoreAuth; + + public Application adminApp; + + private Map policyMap; + + public User user1, user2, user3, regularUser, adminUser; + + public TestData(EntityGenerator entityGenerator) { + songId = "song"; + val songSecret = "La la la!;"; + songAuth = authToken(songId, songSecret); + + song = entityGenerator.setupApplication(songId, songSecret, ApplicationType.CLIENT); + + scoreId = "score"; + val scoreSecret = "She shoots! She scores!"; + scoreAuth = authToken(scoreId, scoreSecret); + + score = entityGenerator.setupApplication(scoreId, scoreSecret, ApplicationType.CLIENT); + val developers = entityGenerator.setupGroup("developers"); + + val adminAppId = "Admin-App-ID"; + val adminAppSecret = "Admin-App-Secret"; + adminApp = entityGenerator.setupApplication(adminAppId, adminAppSecret, ApplicationType.ADMIN); + + val allPolicies = listOf("song", "id", "collab", "aws", "portal"); + + policyMap = new HashMap<>(); + for (val p : allPolicies) { + val policy = entityGenerator.setupPolicy(p, "admin"); + policyMap.put(p, policy); + } + + user1 = entityGenerator.setupUser("User One"); + // user1.addNewGroup(developers); + // entityGenerator.addPermissions( + // user1, getScopes("id.WRITE", "song.WRITE", "collab.WRITE", "portal.READ")); + + user2 = entityGenerator.setupUser("User Two"); + // entityGenerator.addPermissions(user2, getScopes("song.READ", "collab.READ", "id.WRITE")); + + user3 = entityGenerator.setupUser("User Three"); + + regularUser = entityGenerator.setupUser("Regular User"); + regularUser.setType(USER); + regularUser.setStatus(APPROVED); + entityGenerator.addPermissions(regularUser, getScopes("song.READ", "collab.READ")); + + adminUser = entityGenerator.setupUser("Admin User"); + adminUser.setType(ADMIN); + adminUser.setStatus(APPROVED); + entityGenerator.addPermissions(adminUser, getScopes("song.READ", "collab.READ", "id.WRITE")); + } + + public Set getScopes(String... scopeNames) { + return mapToSet(listOf(scopeNames), this::getScope); + } + + public Scope getScope(String name) { + val s = new ScopeName(name); + return new Scope(policyMap.get(s.getName()), s.getAccessLevel()); + } + + private String authToken(String clientId, String clientSecret) { + val s = clientId + ":" + clientSecret; + return "Basic " + Base64.getEncoder().encodeToString(s.getBytes()); + } +} diff --git a/src/test/java/bio/overture/ego/utils/WithMockCustomApplication.java b/src/test/java/bio/overture/ego/utils/WithMockCustomApplication.java new file mode 100644 index 000000000..24f1f9b2f --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/WithMockCustomApplication.java @@ -0,0 +1,25 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.model.enums.ApplicationType.ADMIN; + +import bio.overture.ego.model.enums.ApplicationType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.security.test.context.support.WithSecurityContext; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockCustomApplicationSecurityContextFactory.class) +public @interface WithMockCustomApplication { + + String name() default "Admin Security App"; + + String clientId() default "Admin-Security-APP-ID"; + + String clientSecret() default "Admin-Security-APP-Secret"; + + String redirectUri() default "mock.com"; + + String description() default "Mock Application"; + + ApplicationType type() default ADMIN; +} diff --git a/src/test/java/bio/overture/ego/utils/WithMockCustomApplicationSecurityContextFactory.java b/src/test/java/bio/overture/ego/utils/WithMockCustomApplicationSecurityContextFactory.java new file mode 100644 index 000000000..8bc88af06 --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/WithMockCustomApplicationSecurityContextFactory.java @@ -0,0 +1,54 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.model.enums.StatusType.APPROVED; + +import bio.overture.ego.model.dto.CreateApplicationRequest; +import bio.overture.ego.model.entity.Application; +import bio.overture.ego.service.ApplicationService; +import java.util.ArrayList; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +public class WithMockCustomApplicationSecurityContextFactory + implements WithSecurityContextFactory { + + @Autowired private ApplicationService applicationService; + + @Override + public SecurityContext createSecurityContext(WithMockCustomApplication customApplication) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + val principal = setupApplication(customApplication); + Authentication auth = + new UsernamePasswordAuthenticationToken(principal, null, new ArrayList<>()); + context.setAuthentication(auth); + return context; + } + + private Application setupApplication(WithMockCustomApplication customApplication) { + return applicationService + .findByClientId(customApplication.clientId()) + .orElseGet( + () -> { + val request = createApplicationCreateRequest(customApplication); + return applicationService.create(request); + }); + } + + private CreateApplicationRequest createApplicationCreateRequest( + WithMockCustomApplication customApplication) { + return CreateApplicationRequest.builder() + .name(customApplication.clientId()) + .type(customApplication.type()) + .clientId(customApplication.clientId()) + .clientSecret(customApplication.clientSecret()) + .status(APPROVED) + .redirectUri(customApplication.redirectUri()) + .description(customApplication.description()) + .build(); + } +} diff --git a/src/test/java/bio/overture/ego/utils/WithMockCustomUser.java b/src/test/java/bio/overture/ego/utils/WithMockCustomUser.java new file mode 100644 index 000000000..77e3ab65f --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/WithMockCustomUser.java @@ -0,0 +1,19 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.model.enums.UserType.ADMIN; + +import bio.overture.ego.model.enums.UserType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.security.test.context.support.WithSecurityContext; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) +public @interface WithMockCustomUser { + + String firstName() default "Admin"; + + String lastName() default "User"; + + UserType type() default ADMIN; +} diff --git a/src/test/java/bio/overture/ego/utils/WithMockCustomUserSecurityContextFactory.java b/src/test/java/bio/overture/ego/utils/WithMockCustomUserSecurityContextFactory.java new file mode 100644 index 000000000..3691eec20 --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/WithMockCustomUserSecurityContextFactory.java @@ -0,0 +1,55 @@ +package bio.overture.ego.utils; + +import static bio.overture.ego.model.enums.LanguageType.ENGLISH; +import static bio.overture.ego.model.enums.StatusType.APPROVED; + +import bio.overture.ego.model.dto.CreateUserRequest; +import bio.overture.ego.model.entity.User; +import bio.overture.ego.service.UserService; +import java.util.ArrayList; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +public class WithMockCustomUserSecurityContextFactory + implements WithSecurityContextFactory { + + @Autowired private UserService userService; + + @Override + public SecurityContext createSecurityContext(WithMockCustomUser customUser) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + val principal = setupUser(customUser.firstName() + " " + customUser.lastName(), customUser); + Authentication auth = + new UsernamePasswordAuthenticationToken(principal, null, new ArrayList<>()); + context.setAuthentication(auth); + return context; + } + + private User setupUser(String name, WithMockCustomUser customUser) { + val names = name.split(" ", 2); + val userName = String.format("%s%s@domain.com", names[0], names[1]); + return userService + .findByName(userName) + .orElseGet( + () -> { + val createUserRequest = createUser(userName, customUser); + return userService.create(createUserRequest); + }); + } + + private CreateUserRequest createUser(String userName, WithMockCustomUser customUser) { + return CreateUserRequest.builder() + .email(userName) + .firstName(customUser.firstName()) + .lastName(customUser.lastName()) + .status(APPROVED) + .preferredLanguage(ENGLISH) + .type(customUser.type()) + .build(); + } +} diff --git a/src/test/java/bio/overture/ego/utils/web/AbstractWebResource.java b/src/test/java/bio/overture/ego/utils/web/AbstractWebResource.java new file mode 100644 index 000000000..d7d097183 --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/web/AbstractWebResource.java @@ -0,0 +1,174 @@ +package bio.overture.ego.utils.web; + +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static bio.overture.ego.utils.Joiners.AMPERSAND; +import static bio.overture.ego.utils.Joiners.PATH; +import static bio.overture.ego.utils.web.QueryParam.createQueryParam; +import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; +import static com.google.common.collect.Sets.newHashSet; +import static java.lang.String.format; +import static java.util.Objects.isNull; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Optional; +import java.util.Set; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; + +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractWebResource< + T, O extends ResponseOption, W extends AbstractWebResource> { + + private static final ObjectMapper REGULAR_MAPPER = new ObjectMapper(); + private static final ObjectMapper PRETTY_MAPPER = new ObjectMapper(); + + static { + PRETTY_MAPPER.enable(INDENT_OUTPUT); + } + + @NonNull private final TestRestTemplate restTemplate; + @NonNull private final String serverUrl; + @NonNull private final Class responseType; + + private String endpoint; + private Set queryParams = newHashSet(); + private Object body; + private HttpHeaders headers; + private boolean enableLogging = false; + private boolean pretty = false; + + protected abstract O createResponseOption(ResponseEntity responseEntity); + + private W thisInstance() { + return (W) this; + } + + public W endpoint(String formattedEndpoint, Object... args) { + this.endpoint = format(formattedEndpoint, args); + return thisInstance(); + } + + public W body(Object body) { + this.body = body; + return thisInstance(); + } + + public W headers(HttpHeaders httpHeaders) { + this.headers = httpHeaders; + return thisInstance(); + } + + public W logging() { + return configLogging(true, false); + } + + public W prettyLogging() { + return configLogging(true, true); + } + + public W queryParam(String key, Object... values) { + queryParams.add(createQueryParam(key, values)); + return thisInstance(); + } + + private W configLogging(boolean enable, boolean pretty) { + this.enableLogging = enable; + this.pretty = pretty; + return thisInstance(); + } + + public ResponseEntity get() { + return doRequest(null, HttpMethod.GET); + } + + public ResponseEntity put() { + return doRequest(this.body, HttpMethod.PUT); + } + + public ResponseEntity post() { + return doRequest(this.body, HttpMethod.POST); + } + + public ResponseEntity delete() { + return doRequest(null, HttpMethod.DELETE); + } + + public O deleteAnd() { + return createResponseOption(delete()); + } + + public O getAnd() { + return createResponseOption(get()); + } + + public O putAnd() { + return createResponseOption(put()); + } + + public O postAnd() { + return createResponseOption(post()); + } + + private Optional getQuery() { + val queryStrings = queryParams.stream().map(QueryParam::toString).collect(toImmutableSet()); + return queryStrings.isEmpty() ? Optional.empty() : Optional.of(AMPERSAND.join(queryStrings)); + } + + private String getUrl() { + return PATH.join(this.serverUrl, this.endpoint) + getQuery().map(x -> "?" + x).orElse(""); + } + + @SneakyThrows + private ResponseEntity doRequest(Object body, HttpMethod httpMethod) { + logRequest(enableLogging, pretty, httpMethod, getUrl(), body); + val response = + restTemplate.exchange( + getUrl(), httpMethod, new HttpEntity<>(body, this.headers), this.responseType); + logResponse(enableLogging, pretty, response); + return response; + } + + @SneakyThrows + private static void logRequest( + boolean enable, boolean pretty, HttpMethod httpMethod, String url, Object body) { + if (enable) { + if (isNull(body)) { + log.info("[REQUEST] {} {}", httpMethod, url); + } else { + if (pretty) { + log.info( + "[REQUEST] {} {} < \n{}", httpMethod, url, PRETTY_MAPPER.writeValueAsString(body)); + } else { + log.info( + "[REQUEST] {} {} < {}", httpMethod, url, REGULAR_MAPPER.writeValueAsString(body)); + } + } + } + } + + @SneakyThrows + private static void logResponse(boolean enable, boolean pretty, ResponseEntity response) { + if (enable) { + val output = + CleanResponse.builder() + .body(response.hasBody() ? response.getBody() : null) + .statusCodeName(response.getStatusCode().name()) + .statusCodeValue(response.getStatusCodeValue()) + .build(); + if (pretty) { + log.info("[RESPONSE] > \n{}", PRETTY_MAPPER.writeValueAsString(output)); + } else { + log.info("[RESPONSE] > {}", REGULAR_MAPPER.writeValueAsString(output)); + } + } + } +} diff --git a/src/test/java/bio/overture/ego/utils/web/BasicWebResource.java b/src/test/java/bio/overture/ego/utils/web/BasicWebResource.java new file mode 100644 index 000000000..5954d688b --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/web/BasicWebResource.java @@ -0,0 +1,17 @@ +package bio.overture.ego.utils.web; + +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.ResponseEntity; + +public class BasicWebResource> + extends AbstractWebResource> { + + public BasicWebResource(TestRestTemplate restTemplate, String serverUrl, Class responseType) { + super(restTemplate, serverUrl, responseType); + } + + @Override + protected O createResponseOption(ResponseEntity responseEntity) { + return (O) new ResponseOption(responseEntity); + } +} diff --git a/src/test/java/bio/overture/ego/utils/web/CleanResponse.java b/src/test/java/bio/overture/ego/utils/web/CleanResponse.java new file mode 100644 index 000000000..06644d924 --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/web/CleanResponse.java @@ -0,0 +1,13 @@ +package bio.overture.ego.utils.web; + +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +@Value +@Builder +public class CleanResponse { + @NonNull private final String statusCodeName; + private final int statusCodeValue; + private final Object body; +} diff --git a/src/test/java/bio/overture/ego/utils/web/QueryParam.java b/src/test/java/bio/overture/ego/utils/web/QueryParam.java new file mode 100644 index 000000000..83d3ac76c --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/web/QueryParam.java @@ -0,0 +1,24 @@ +package bio.overture.ego.utils.web; + +import static bio.overture.ego.utils.Joiners.COMMA; +import static java.lang.String.format; + +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +@Value +@Builder +public class QueryParam { + @NonNull private final String key; + @NonNull private final Object value; + + public static QueryParam createQueryParam(String key, Object... values) { + return new QueryParam(key, COMMA.join(values)); + } + + @Override + public String toString() { + return format("%s=%s", key, value); + } +} diff --git a/src/test/java/bio/overture/ego/utils/web/ResponseOption.java b/src/test/java/bio/overture/ego/utils/web/ResponseOption.java new file mode 100644 index 000000000..f51e56055 --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/web/ResponseOption.java @@ -0,0 +1,55 @@ +package bio.overture.ego.utils.web; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CONFLICT; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.OK; + +import java.util.function.Function; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RequiredArgsConstructor +public class ResponseOption> { + + @Getter @NonNull private final ResponseEntity response; + + public O assertStatusCode(HttpStatus code) { + assertThat(response.getStatusCode()).isEqualTo(code); + return thisInstance(); + } + + public O assertOk() { + return assertStatusCode(OK); + } + + public O assertNotFound() { + return assertStatusCode(NOT_FOUND); + } + + public O assertConflict() { + return assertStatusCode(CONFLICT); + } + + public O assertBadRequest() { + return assertStatusCode(BAD_REQUEST); + } + + public O assertHasBody() { + assertThat(response.hasBody()).isTrue(); + assertThat(response.getBody()).isNotNull(); + return thisInstance(); + } + + public R map(Function, R> transformingFunction) { + return transformingFunction.apply(getResponse()); + } + + private O thisInstance() { + return (O) this; + } +} diff --git a/src/test/java/bio/overture/ego/utils/web/StringResponseOption.java b/src/test/java/bio/overture/ego/utils/web/StringResponseOption.java new file mode 100644 index 000000000..9385b5d5e --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/web/StringResponseOption.java @@ -0,0 +1,76 @@ +package bio.overture.ego.utils.web; + +import static bio.overture.ego.utils.Collectors.toImmutableList; +import static bio.overture.ego.utils.Collectors.toImmutableSet; +import static bio.overture.ego.utils.Streams.stream; +import static org.assertj.core.api.Assertions.assertThat; + +import bio.overture.ego.utils.Streams; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Set; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.val; +import org.assertj.core.api.ListAssert; +import org.assertj.core.api.ObjectAssert; +import org.springframework.http.ResponseEntity; + +public class StringResponseOption extends ResponseOption { + + public static final ObjectMapper MAPPER = new ObjectMapper(); + + public StringResponseOption(ResponseEntity response) { + super(response); + } + + public R extractOneEntity(@NonNull Class entityClass) { + return assertOk() + .assertHasBody() + .map(x -> internalExtractOneEntityFromResponse(x, entityClass)); + } + + public ListAssert assertPageResultsOfType(Class entityClass) { + return assertThat(extractPageResults(entityClass)); + } + + public ObjectAssert assertEntityOfType(Class entityClass) { + return assertThat(extractOneEntity(entityClass)); + } + + public List extractPageResults(@NonNull Class entityClass) { + return assertOk() + .assertHasBody() + .map(x -> internalExtractPageResultSetFromResponse(x, entityClass)); + } + + public Set extractManyEntities(@NonNull Class entityClass) { + return assertOk() + .assertHasBody() + .map(x -> internalExtractManyEntitiesFromResponse(x, entityClass)); + } + + @SneakyThrows + private static List internalExtractPageResultSetFromResponse( + ResponseEntity r, Class tClass) { + val page = MAPPER.readTree(r.getBody()); + assertThat(page).isNotNull(); + return stream(page.path("resultSet").iterator()) + .map(x -> MAPPER.convertValue(x, tClass)) + .collect(toImmutableList()); + } + + @SneakyThrows + private static T internalExtractOneEntityFromResponse( + ResponseEntity r, Class tClass) { + return MAPPER.readValue(r.getBody(), tClass); + } + + @SneakyThrows + private static Set internalExtractManyEntitiesFromResponse( + ResponseEntity r, Class tClass) { + return Streams.stream(MAPPER.readTree(r.getBody()).iterator()) + .map(x -> MAPPER.convertValue(x, tClass)) + .collect(toImmutableSet()); + } +} diff --git a/src/test/java/bio/overture/ego/utils/web/StringWebResource.java b/src/test/java/bio/overture/ego/utils/web/StringWebResource.java new file mode 100644 index 000000000..c470f1d5a --- /dev/null +++ b/src/test/java/bio/overture/ego/utils/web/StringWebResource.java @@ -0,0 +1,17 @@ +package bio.overture.ego.utils.web; + +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.ResponseEntity; + +public class StringWebResource + extends AbstractWebResource { + + public StringWebResource(TestRestTemplate restTemplate, String serverUrl) { + super(restTemplate, serverUrl, String.class); + } + + @Override + protected StringResponseOption createResponseOption(ResponseEntity responseEntity) { + return new StringResponseOption(responseEntity); + } +} diff --git a/src/test/java/org/overture/ego/model/entity/UserTest.java b/src/test/java/org/overture/ego/model/entity/UserTest.java deleted file mode 100644 index 80826ef0e..000000000 --- a/src/test/java/org/overture/ego/model/entity/UserTest.java +++ /dev/null @@ -1,205 +0,0 @@ -package org.overture.ego.model.entity; - -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.overture.ego.controller.resolver.PageableResolver; -import org.overture.ego.model.params.Scope; -import org.overture.ego.service.PolicyService; -import org.overture.ego.service.GroupService; -import org.overture.ego.service.UserService; -import org.overture.ego.utils.EntityGenerator; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Arrays; -import java.util.Collections; - -import static org.assertj.core.api.Assertions.assertThat; - -@Slf4j -@SpringBootTest -@RunWith(SpringRunner.class) -@ActiveProfiles("test") -@Transactional -public class UserTest { - @Autowired - private UserService userService; - - @Autowired - private GroupService groupService; - - @Autowired - private PolicyService policyService; - - @Autowired - private EntityGenerator entityGenerator; - - @Test - public void testGetPermissionsNoPermissions() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) - .getContent(); - entityGenerator.setupSimpleAclEntities(groups); - - val user = userService.getByName("FirstUser@domain.com"); - - assertThat(user.getPermissions().size()).isEqualTo(0); - } - - @Test - public void testGetPermissionsNoGroups() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) - .getContent(); - entityGenerator.setupSimpleAclEntities(groups); - - val user = userService.getByName("FirstUser@domain.com"); - val study001id = policyService.getByName("Study001").getId().toString(); - - val permissions = Arrays.asList( - new Scope(study001id, "WRITE"), - new Scope(study001id, "READ"), - new Scope(study001id, "DENY") - ); - - userService.addUserPermissions(user.getId().toString(), permissions); - - assertThat(user.getPermissions()).containsExactlyInAnyOrder( - "Study001.DENY" - ); - } - - /** - * This is the acl permission -> JWT output uber test, - * if this passes we can be assured that we are correctly - * coalescing permissions from the individual user and their - * groups, squashing on aclEntity while prioritizing the - * aclMask order of (DENY -> WRITE -> READ) - *

- * Original github issue with manual SQL: - * https://github.com/overture-stack/ego/issues/105 - */ - @Test - public void testGetPermissionsUberTest() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) - .getContent(); - entityGenerator.setupSimpleAclEntities(groups); - - // Get Users and Groups - val alex = userService.getByName("FirstUser@domain.com"); - val alexId = alex.getId().toString(); - - val bob = userService.getByName("SecondUser@domain.com"); - val bobId = bob.getId().toString(); - - val marry = userService.getByName("ThirdUser@domain.com"); - val marryId = marry.getId().toString(); - - val wizards = groups.get(0); - val wizardsId = wizards.getId().toString(); - - val robots = groups.get(1); - val robotsId = robots.getId().toString(); - - // Add user's to their respective groups - userService.addUserToGroups(alexId, Arrays.asList(wizardsId, robotsId)); - userService.addUserToGroups(bobId, Arrays.asList(robotsId)); - userService.addUserToGroups(marryId, Arrays.asList(robotsId)); - - // Get the studies so we can - val study001 = policyService.getByName("Study001"); - val study001id = study001.getId().toString(); - - val study002 = policyService.getByName("Study002"); - val study002id = study002.getId().toString(); - - val study003 = policyService.getByName("Study003"); - val study003id = study003.getId().toString(); - - // Assign ACL Permissions for each user/group - userService.addUserPermissions(alexId, Arrays.asList( - new Scope(study001id, "WRITE"), - new Scope(study002id, "READ"), - new Scope(study003id, "DENY") - )); - - userService.addUserPermissions(bobId, Arrays.asList( - new Scope(study001id, "READ"), - new Scope(study002id, "DENY"), - new Scope(study003id, "WRITE") - )); - - userService.addUserPermissions(marryId, Arrays.asList( - new Scope(study001id, "DENY"), - new Scope(study002id, "WRITE"), - new Scope(study003id, "READ") - )); - - groupService.addGroupPermissions(wizardsId, Arrays.asList( - new Scope(study001id, "WRITE"), - new Scope(study002id, "READ"), - new Scope(study003id, "DENY") - )); - - groupService.addGroupPermissions(robotsId, Arrays.asList( - new Scope(study001id, "DENY"), - new Scope(study002id, "WRITE"), - new Scope(study003id, "READ") - )); - - /** - * Expected Result Computations - * Alex (Wizards and Robots) - * - Study001 (WRITE/WRITE/DENY) == DENY - * - Study002 (READ/READ/WRITE) == WRITE - * - Study003 (DENY/DENY/READ) == DENY - * Bob (Robots) - * - Study001 (READ/DENY) == DENY - * - Study002 (DENY/WRITE) == DENY - * - Study003 (WRITE/READ) == WRITE - * Marry (Robots) - * - Study001 (DENY/DENY) == DENY - * - Study002 (WRITE/WRITE) == WRITE - * - Study003 (READ/READ) == READ - * - * Test Matrix | Group R | Group W | Group D - * ----------------------------------------- - * User R | Marry | Alex | Bob - * User W | Bob | Marry | Alex - * User D | Alex | Bob | Marry - * - */ - - // Test that all is well - assertThat(alex.getPermissions()).containsExactlyInAnyOrder( - "Study001.DENY", - "Study002.WRITE", - "Study003.DENY" - ); - - assertThat(bob.getPermissions()).containsExactlyInAnyOrder( - "Study001.DENY", - "Study002.DENY", - "Study003.WRITE" - ); - - assertThat(marry.getPermissions()).containsExactlyInAnyOrder( - "Study001.DENY", - "Study002.WRITE", - "Study003.READ" - ); - } - -} diff --git a/src/test/java/org/overture/ego/model/enums/ScopeMaskTest.java b/src/test/java/org/overture/ego/model/enums/ScopeMaskTest.java deleted file mode 100644 index 25cbeca4a..000000000 --- a/src/test/java/org/overture/ego/model/enums/ScopeMaskTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.overture.ego.model.enums; - -import lombok.extern.slf4j.Slf4j; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.transaction.annotation.Transactional; - -import static org.assertj.core.api.Assertions.assertThat; - -@Slf4j -@SpringBootTest -@RunWith(SpringRunner.class) -@ActiveProfiles("test") -@Transactional -public class ScopeMaskTest { - - @Test - public void testFromValue() { - assertThat(PolicyMask.fromValue("read")).isEqualByComparingTo(PolicyMask.READ); - assertThat(PolicyMask.fromValue("write")).isEqualByComparingTo(PolicyMask.WRITE); - assertThat(PolicyMask.fromValue("deny")).isEqualByComparingTo(PolicyMask.DENY); - } -} diff --git a/src/test/java/org/overture/ego/service/ApplicationServiceTest.java b/src/test/java/org/overture/ego/service/ApplicationServiceTest.java deleted file mode 100644 index 1fa9c17c3..000000000 --- a/src/test/java/org/overture/ego/service/ApplicationServiceTest.java +++ /dev/null @@ -1,469 +0,0 @@ -package org.overture.ego.service; - -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.overture.ego.controller.resolver.PageableResolver; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.token.app.AppTokenClaims; -import org.overture.ego.utils.EntityGenerator; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.provider.ClientRegistrationException; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.transaction.annotation.Transactional; - -import javax.persistence.EntityNotFoundException; -import java.util.Arrays; -import java.util.Collections; -import java.util.UUID; - -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -@Slf4j -@SpringBootTest -@RunWith(SpringRunner.class) -@ActiveProfiles("test") -@Transactional -public class ApplicationServiceTest { - - @Autowired - private ApplicationService applicationService; - - @Autowired - private UserService userService; - - @Autowired - private GroupService groupService; - - @Autowired - private EntityGenerator entityGenerator; - - // Create - @Test - public void testCreate() { - val application = applicationService.create(entityGenerator.createOneApplication("123456")); - assertThat(application.getClientId()).isEqualTo("123456"); - } - - @Test - @Ignore - public void testCreateUniqueClientId() { -// applicationService.create(entityGenerator.createOneApplication("111111")); -// applicationService.create(entityGenerator.createOneApplication("222222")); -// assertThatExceptionOfType(DataIntegrityViolationException.class) -// .isThrownBy(() -> applicationService.create(entityGenerator.createOneApplication("111111"))); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - // Get - @Test - public void testGet() { - val application = applicationService.create(entityGenerator.createOneApplication("123456")); - val savedApplication = applicationService.get(application.getId().toString()); - assertThat(savedApplication.getClientId()).isEqualTo("123456"); - } - - @Test - public void testGetEntityNotFoundException() { - assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> applicationService.get(UUID.randomUUID().toString())); - } - - @Test - public void testGetByName() { - applicationService.create(entityGenerator.createOneApplication("123456")); - val savedApplication = applicationService.getByName("Application 123456"); - assertThat(savedApplication.getClientId()).isEqualTo("123456"); - } - - @Test - public void testGetByNameAllCaps() { - applicationService.create(entityGenerator.createOneApplication("123456")); - val savedApplication = applicationService.getByName("APPLICATION 123456"); - assertThat(savedApplication.getClientId()).isEqualTo("123456"); - } - - @Test - @Ignore - public void testGetByNameNotFound() { - // TODO Currently returning null, should throw exception (EntityNotFoundException?) - assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> applicationService.getByName("Application 123456")); - } - - @Test - public void testGetByClientId() { - applicationService.create(entityGenerator.createOneApplication("123456")); - val savedApplication = applicationService.getByClientId("123456"); - assertThat(savedApplication.getClientId()).isEqualTo("123456"); - } - - @Test - @Ignore - public void testGetByClientIdNotFound() { - // TODO Currently returning null, should throw exception (EntityNotFoundException?) - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> applicationService.getByClientId("123456")); - } - - // List - @Test - public void testListAppsNoFilters() { - entityGenerator.setupSimpleApplications(); - val applications = applicationService.listApps(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(applications.getTotalElements()).isEqualTo(5L); - } - - @Test - public void testListAppsNoFiltersEmptyResult() { - val applications = applicationService.listApps(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(applications.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testListAppsFiltered() { - entityGenerator.setupSimpleApplications(); - val clientIdFilter = new SearchFilter("clientId", "333333"); - val applications = applicationService.listApps(singletonList(clientIdFilter), new PageableResolver().getPageable()); - assertThat(applications.getTotalElements()).isEqualTo(1L); - assertThat(applications.getContent().get(0).getClientId()).isEqualTo("333333"); - } - - @Test - public void testListAppsFilteredEmptyResult() { - entityGenerator.setupSimpleApplications(); - val clientIdFilter = new SearchFilter("clientId", "666666"); - val applications = applicationService.listApps(singletonList(clientIdFilter), new PageableResolver().getPageable()); - assertThat(applications.getTotalElements()).isEqualTo(0L); - } - - // Find - @Test - public void testFindAppsNoFilters() { - entityGenerator.setupSimpleApplications(); - val applications = applicationService.findApps("222222", Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(applications.getTotalElements()).isEqualTo(1L); - assertThat(applications.getContent().get(0).getClientId()).isEqualTo("222222"); - } - - @Test - public void testFindAppsFiltered() { - entityGenerator.setupSimpleApplications(); - val clientIdFilter = new SearchFilter("clientId", "333333"); - val applications = applicationService.findApps("222222", singletonList(clientIdFilter), new PageableResolver().getPageable()); - // Expect empty list - assertThat(applications.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindUsersAppsNoQueryNoFilters() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleUsers(); - - val user = userService.getByName("FirstUser@domain.com"); - val userTwo = userService.getByName("SecondUser@domain.com"); - - val application = applicationService.getByClientId("444444"); - - user.addNewApplication(application); - userTwo.addNewApplication(application); - - val applications = applicationService.findUserApps(user.getId().toString(), Collections.emptyList(), new PageableResolver().getPageable()); - - assertThat(applications.getTotalElements()).isEqualTo(1L); - assertThat(applications.getContent().get(0).getClientId()).isEqualTo("444444"); - } - - @Test - public void testFindUsersAppsNoQueryNoFiltersNoUser() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleUsers(); - - val user = userService.getByName("FirstUser@domain.com"); - val applications = applicationService.findUserApps(user.getId().toString(), Collections.emptyList(), new PageableResolver().getPageable()); - - assertThat(applications.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindUsersAppsNoQueryNoFiltersEmptyUserString() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleUsers(); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> applicationService.findUserApps("", Collections.emptyList(), new PageableResolver().getPageable())); - } - - @Test - public void testFindUsersAppsNoQueryFilters() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleUsers(); - - val user = userService.getByName("FirstUser@domain.com"); - val applicationOne = applicationService.getByClientId("111111"); - val applicationTwo = applicationService.getByClientId("555555"); - - user.addNewApplication(applicationOne); - user.addNewApplication(applicationTwo); - - val clientIdFilter = new SearchFilter("clientId", "111111"); - - val applications = applicationService.findUserApps(user.getId().toString(), singletonList(clientIdFilter), new PageableResolver().getPageable()); - - assertThat(applications.getTotalElements()).isEqualTo(1L); - assertThat(applications.getContent().get(0).getClientId()).isEqualTo("111111"); - } - - @Test - public void testFindUsersAppsQueryAndFilters() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleUsers(); - - val user = userService.getByName("FirstUser@domain.com"); - val applicationOne = applicationService.getByClientId("333333"); - val applicationTwo = applicationService.getByClientId("444444"); - - user.addNewApplication(applicationOne); - user.addNewApplication(applicationTwo); - - val clientIdFilter = new SearchFilter("clientId", "333333"); - - val applications = applicationService.findUserApps(user.getId().toString(), "444444", singletonList(clientIdFilter), new PageableResolver().getPageable()); - - assertThat(applications.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindUsersAppsQueryNoFilters() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleUsers(); - - val user = userService.getByName("FirstUser@domain.com"); - val applicationOne = applicationService.getByClientId("222222"); - val applicationTwo = applicationService.getByClientId("444444"); - - user.addNewApplication(applicationOne); - user.addNewApplication(applicationTwo); - - val applications = applicationService.findUserApps(user.getId().toString(), "222222", Collections.emptyList(), new PageableResolver().getPageable()); - - assertThat(applications.getTotalElements()).isEqualTo(1L); - assertThat(applications.getContent().get(0).getClientId()).isEqualTo("222222"); - } - - @Test - public void testFindGroupsAppsNoQueryNoFilters() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - val groupTwo = groupService.getByName("Group Two"); - - val application = applicationService.getByClientId("111111"); - - group.addApplication(application); - groupTwo.addApplication(application); - - val applications = applicationService.findGroupApplications(group.getId().toString(), Collections.emptyList(), new PageableResolver().getPageable()); - - assertThat(applications.getTotalElements()).isEqualTo(1L); - assertThat(applications.getContent().get(0).getClientId()).isEqualTo("111111"); - } - - @Test - public void testFindGroupsAppsNoQueryNoFiltersNoGroup() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - val applications = applicationService.findGroupApplications(group.getId().toString(), Collections.emptyList(), new PageableResolver().getPageable()); - - assertThat(applications.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindGroupsAppsNoQueryNoFiltersEmptyGroupString() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleGroups(); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> applicationService.findGroupApplications("", Collections.emptyList(), new PageableResolver().getPageable())); - } - - @Test - public void testFindGroupsAppsNoQueryFilters() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - val applicationOne = applicationService.getByClientId("222222"); - val applicationTwo = applicationService.getByClientId("333333"); - - group.addApplication(applicationOne); - group.addApplication(applicationTwo); - - val clientIdFilter = new SearchFilter("clientId", "333333"); - - val applications = applicationService.findGroupApplications(group.getId().toString(), singletonList(clientIdFilter), new PageableResolver().getPageable()); - - assertThat(applications.getTotalElements()).isEqualTo(1L); - assertThat(applications.getContent().get(0).getClientId()).isEqualTo("333333"); - } - - @Test - public void testFindGroupsAppsQueryAndFilters() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group Three"); - val applicationOne = applicationService.getByClientId("333333"); - val applicationTwo = applicationService.getByClientId("444444"); - - group.addApplication(applicationOne); - group.addApplication(applicationTwo); - - val clientIdFilter = new SearchFilter("clientId", "333333"); - - val applications = applicationService.findGroupApplications(group.getId().toString(), "444444", singletonList(clientIdFilter), new PageableResolver().getPageable()); - - assertThat(applications.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindGroupsAppsQueryNoFilters() { - entityGenerator.setupSimpleApplications(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - val applicationOne = applicationService.getByClientId("444444"); - val applicationTwo = applicationService.getByClientId("555555"); - - group.addApplication(applicationOne); - group.addApplication(applicationTwo); - - val applications = applicationService.findGroupApplications(group.getId().toString(), "555555", Collections.emptyList(), new PageableResolver().getPageable()); - - assertThat(applications.getTotalElements()).isEqualTo(1L); - assertThat(applications.getContent().get(0).getClientId()).isEqualTo("555555"); - } - - // Update - @Test - public void testUpdate() { - val application = applicationService.create(entityGenerator.createOneApplication("123456")); - application.setName("New Name"); - val updated = applicationService.update(application); - assertThat(updated.getName()).isEqualTo("New Name"); - } - - @Test - public void testUpdateNonexistentEntity() { - applicationService.create(entityGenerator.createOneApplication("123456")); - val nonExistentEntity = entityGenerator.createOneApplication("654321"); - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> applicationService.update(nonExistentEntity)); - } - - @Test - public void testUpdateIdNotAllowed() { - val application = applicationService.create(entityGenerator.createOneApplication("123456")); - application.setId(new UUID(12312912931L,12312912931L)); - // New id means new non-existent entity or one that exists and is being overwritten - assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> applicationService.update(application)); - } - - @Test - @Ignore - public void testUpdateClientIdNotAllowed() { -// entityGenerator.setupSimpleApplications(); -// val application = applicationService.getByClientId("111111"); -// application.setClientId("222222"); -// val updated = applicationService.update(application); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - @Test - @Ignore - public void testUpdateStatusNotInAllowedEnum() { -// entityGenerator.setupSimpleApplications(); -// val application = applicationService.getByClientId("111111"); -// application.setStatus("Junk"); -// val updated = applicationService.update(application); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - // Delete - @Test - public void testDelete() { - entityGenerator.setupSimpleApplications(); - - val application = applicationService.getByClientId("222222"); - applicationService.delete(application.getId().toString()); - - val applications = applicationService.listApps(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(applications.getTotalElements()).isEqualTo(4L); - assertThat(applications.getContent()).doesNotContain(application); - } - - @Test - public void testDeleteNonExisting() { - entityGenerator.setupSimpleApplications(); - assertThatExceptionOfType(EmptyResultDataAccessException.class) - .isThrownBy(() -> applicationService.delete(UUID.randomUUID().toString())); - } - - @Test - public void testDeleteEmptyIdString() { - entityGenerator.setupSimpleApplications(); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> applicationService.delete("")); - } - - // Special (LoadClient) - @Test - public void testLoadClientByClientId() { - val application = applicationService.create(entityGenerator.createOneApplication("123456")); - application.setStatus("Approved"); - applicationService.update(application); - - val client = applicationService.loadClientByClientId("123456"); - - assertThat(client.getClientId()).isEqualToIgnoringCase("123456"); - assertThat(client.getAuthorizedGrantTypes().containsAll(Arrays.asList(AppTokenClaims.AUTHORIZED_GRANTS))); - assertThat(client.getScope().containsAll(Arrays.asList(AppTokenClaims.SCOPES))); - assertThat(client.getRegisteredRedirectUri()).isEqualTo(application.getURISet()); - assertThat(client.getAuthorities()).containsExactly(new SimpleGrantedAuthority(AppTokenClaims.ROLE)); - } - - @Test - public void testLoadClientByClientIdNotFound() { - assertThatExceptionOfType(ClientRegistrationException.class).isThrownBy( - () -> applicationService.loadClientByClientId("123456")).withMessage("Client ID not found."); - } - - @Test - public void testLoadClientByClientIdEmptyString() { - assertThatExceptionOfType(ClientRegistrationException.class).isThrownBy( - () -> applicationService.loadClientByClientId("")).withMessage("Client ID not found."); - } - - @Test - public void testLoadClientByClientIdNotApproved() { - val application = applicationService.create(entityGenerator.createOneApplication("123456")); - application.setStatus("Pending"); - applicationService.update(application); - assertThatExceptionOfType(ClientRegistrationException.class).isThrownBy(() -> applicationService.loadClientByClientId("123456")).withMessage("Client Access is not approved."); - application.setStatus("Rejected"); - applicationService.update(application); - assertThatExceptionOfType(ClientRegistrationException.class).isThrownBy(() -> applicationService.loadClientByClientId("123456")).withMessage("Client Access is not approved."); - application.setStatus("Disabled"); - applicationService.update(application); - assertThatExceptionOfType(ClientRegistrationException.class).isThrownBy(() -> applicationService.loadClientByClientId("123456")).withMessage("Client Access is not approved."); - } - -} diff --git a/src/test/java/org/overture/ego/service/GroupsServiceTest.java b/src/test/java/org/overture/ego/service/GroupsServiceTest.java deleted file mode 100644 index 0f22c075d..000000000 --- a/src/test/java/org/overture/ego/service/GroupsServiceTest.java +++ /dev/null @@ -1,736 +0,0 @@ -package org.overture.ego.service; - -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.overture.ego.controller.resolver.PageableResolver; -import org.overture.ego.model.params.Scope; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.utils.EntityGenerator; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.util.Pair; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.transaction.annotation.Transactional; - -import javax.persistence.EntityNotFoundException; -import java.util.Arrays; -import java.util.Collections; -import java.util.UUID; -import java.util.stream.Collectors; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.overture.ego.utils.AclPermissionUtils.extractPermissionStrings; - -@Slf4j -@SpringBootTest -@RunWith(SpringRunner.class) -@ActiveProfiles("test") -@Transactional -public class GroupsServiceTest { - @Autowired - private ApplicationService applicationService; - - @Autowired - private UserService userService; - - @Autowired - private GroupService groupService; - - @Autowired - private PolicyService policyService; - - @Autowired - private EntityGenerator entityGenerator; - - // Create - @Test - public void testCreate() { - val group = groupService.create(entityGenerator.createOneGroup("Group One")); - assertThat(group.getName()).isEqualTo("Group One"); - } - - @Test - @Ignore - public void testCreateUniqueName() { -// groupService.create(entityGenerator.createOneGroup("Group One")); -// groupService.create(entityGenerator.createOneGroup("Group Two")); -// assertThatExceptionOfType(DataIntegrityViolationException.class) -// .isThrownBy(() -> groupService.create(entityGenerator.createOneGroup("Group One"))); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - // Get - @Test - public void testGet() { - val group = groupService.create(entityGenerator.createOneGroup("Group One")); - val saveGroup = groupService.get(group.getId().toString()); - assertThat(saveGroup.getName()).isEqualTo("Group One"); - } - - @Test - public void testGetEntityNotFoundException() { - assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> groupService.get(UUID.randomUUID().toString())); - } - - @Test - public void testGetByName() { - groupService.create(entityGenerator.createOneGroup("Group One")); - val saveGroup = groupService.getByName("Group One"); - assertThat(saveGroup.getName()).isEqualTo("Group One"); - } - - @Test - public void testGetByNameAllCaps() { - groupService.create(entityGenerator.createOneGroup("Group One")); - val saveGroup = groupService.getByName("GROUP ONE"); - assertThat(saveGroup.getName()).isEqualTo("Group One"); - } - - @Test - @Ignore - public void testGetByNameNotFound() { - // TODO Currently returning null, should throw exception (EntityNotFoundException?) - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> groupService.getByName("Group One")); - } - - // List Groups - @Test - public void testListGroupsNoFilters() { - entityGenerator.setupSimpleGroups(); - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(groups.getTotalElements()).isEqualTo(3L); - } - - @Test - public void testListGroupsNoFiltersEmptyResult() { - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(groups.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testListGroupsFiltered() { - entityGenerator.setupSimpleGroups(); - val groupNameFilter = new SearchFilter("name", "Group One"); - val groups = groupService.listGroups(Arrays.asList(groupNameFilter), new PageableResolver().getPageable()); - assertThat(groups.getTotalElements()).isEqualTo(1L); - assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); - } - - @Test - public void testListGroupsFilteredEmptyResult() { - entityGenerator.setupSimpleGroups(); - val groupNameFilter = new SearchFilter("name", "Group Four"); - val groups = groupService.listGroups(Arrays.asList(groupNameFilter), new PageableResolver().getPageable()); - assertThat(groups.getTotalElements()).isEqualTo(0L); - } - - // Find Groups - @Test - public void testFindGroupsNoFilters() { - entityGenerator.setupSimpleGroups(); - val groups = groupService.findGroups("One", Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(groups.getTotalElements()).isEqualTo(1L); - assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); - } - - @Test - public void testFindGroupsFiltered() { - entityGenerator.setupSimpleGroups(); - val groupNameFilter = new SearchFilter("name", "Group One"); - val groups = groupService - .findGroups("Two", Arrays.asList(groupNameFilter), new PageableResolver().getPageable()); - // Expect empty list - assertThat(groups.getTotalElements()).isEqualTo(0L); - } - - // Find User's Groups - @Test - public void testFindUsersGroupsNoQueryNoFilters() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleUsers(); - - val userId = userService.getByName("FirstUser@domain.com").getId().toString(); - val userTwoId = userService.getByName("SecondUser@domain.com").getId().toString(); - val groupId = groupService.getByName("Group One").getId().toString(); - - userService.addUserToGroups(userId, Arrays.asList(groupId)); - userService.addUserToGroups(userTwoId, Arrays.asList(groupId)); - - val groups = groupService.findUserGroups( - userId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(groups.getTotalElements()).isEqualTo(1L); - assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); - } - - @Test - public void testFindUsersGroupsNoQueryNoFiltersNoGroupsFound() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleUsers(); - - val userId = userService.getByName("FirstUser@domain.com").getId().toString(); - - val groups = groupService.findUserGroups( - userId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(groups.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindUsersGroupsNoQueryNoFiltersEmptyGroupString() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleUsers(); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> groupService.findUserGroups("", Collections.emptyList(), new PageableResolver().getPageable())); - } - - @Test - public void testFindUsersGroupsNoQueryFilters() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleUsers(); - - val userId = userService.getByName("FirstUser@domain.com").getId().toString(); - val groupId = groupService.getByName("Group One").getId().toString(); - val groupTwoId = groupService.getByName("Group Two").getId().toString(); - - userService.addUserToGroups(userId, Arrays.asList(groupId, groupTwoId)); - - val groupsFilters = new SearchFilter("name", "Group One"); - - val groups = groupService.findUserGroups( - userId, - Arrays.asList(groupsFilters), - new PageableResolver().getPageable() - ); - - assertThat(groups.getTotalElements()).isEqualTo(1L); - assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); - } - - @Test - public void testFindUsersGroupsQueryAndFilters() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleUsers(); - - val userId = userService.getByName("FirstUser@domain.com").getId().toString(); - val groupId = groupService.getByName("Group One").getId().toString(); - val groupTwoId = groupService.getByName("Group Two").getId().toString(); - - userService.addUserToGroups(userId, Arrays.asList(groupId, groupTwoId)); - - val groupsFilters = new SearchFilter("name", "Group One"); - - val groups = groupService.findUserGroups( - userId, - "Two", - Arrays.asList(groupsFilters), - new PageableResolver().getPageable() - ); - - assertThat(groups.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindUsersGroupsQueryNoFilters() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleUsers(); - - val userId = userService.getByName("FirstUser@domain.com").getId().toString(); - val groupId = groupService.getByName("Group One").getId().toString(); - val groupTwoId = groupService.getByName("Group Two").getId().toString(); - - userService.addUserToGroups(userId, Arrays.asList(groupId, groupTwoId)); - - val groups = groupService.findUserGroups( - userId, - "Two", - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(groups.getTotalElements()).isEqualTo(1L); - assertThat(groups.getContent().get(0).getName()).isEqualTo("Group Two"); - } - - // Find Application's Groups - @Test - public void testFindApplicationsGroupsNoQueryNoFilters() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - val groupTwoId = groupService.getByName("Group Two").getId().toString(); - val applicationId = applicationService.getByClientId("111111").getId().toString(); - val applicationTwoId = applicationService.getByClientId("222222").getId().toString(); - - groupService.addAppsToGroup(groupId, Arrays.asList(applicationId)); - groupService.addAppsToGroup(groupTwoId, Arrays.asList(applicationTwoId)); - - val groups = groupService.findApplicationGroups( - applicationId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(groups.getTotalElements()).isEqualTo(1L); - assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); - } - - @Test - public void testFindApplicationsGroupsNoQueryNoFiltersNoGroup() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val applicationId = applicationService.getByClientId("111111").getId().toString(); - - val groups = groupService.findApplicationGroups(applicationId, Collections.emptyList(), new PageableResolver().getPageable()); - - assertThat(groups.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindApplicationsGroupsNoQueryNoFiltersEmptyGroupString() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> groupService - .findApplicationGroups( - "", - Collections.emptyList(), - new PageableResolver().getPageable() - ) - ); - } - - @Test - public void testFindApplicationsGroupsNoQueryFilters() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - val groupTwoId = groupService.getByName("Group Two").getId().toString(); - val applicationId = applicationService.getByClientId("111111").getId().toString(); - - groupService.addAppsToGroup(groupId, Arrays.asList(applicationId)); - groupService.addAppsToGroup(groupTwoId, Arrays.asList(applicationId)); - - val groupsFilters = new SearchFilter("name", "Group One"); - - val groups = groupService.findApplicationGroups(applicationId, Arrays.asList(groupsFilters), new PageableResolver().getPageable()); - - assertThat(groups.getTotalElements()).isEqualTo(1L); - assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); - } - - @Test - public void testFindApplicationsGroupsQueryAndFilters() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - val groupTwoId = groupService.getByName("Group Two").getId().toString(); - val applicationId = applicationService.getByClientId("111111").getId().toString(); - - groupService.addAppsToGroup(groupId, Arrays.asList(applicationId)); - groupService.addAppsToGroup(groupTwoId, Arrays.asList(applicationId)); - - val groupsFilters = new SearchFilter("name", "Group One"); - - val groups = groupService.findApplicationGroups(applicationId, "Two", Arrays.asList(groupsFilters), new PageableResolver().getPageable()); - - assertThat(groups.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindApplicationsGroupsQueryNoFilters() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - val groupTwoId = groupService.getByName("Group Two").getId().toString(); - val applicationId = applicationService.getByClientId("111111").getId().toString(); - - groupService.addAppsToGroup(groupId, Arrays.asList(applicationId)); - groupService.addAppsToGroup(groupTwoId, Arrays.asList(applicationId)); - - val groups = groupService.findApplicationGroups(applicationId, "Group One", Collections.emptyList(), new PageableResolver().getPageable()); - - assertThat(groups.getTotalElements()).isEqualTo(1L); - assertThat(groups.getContent().get(0).getName()).isEqualTo("Group One"); - } - - // Update - @Test - public void testUpdate() { - val group = groupService.create(entityGenerator.createOneGroup("Group One")); - group.setDescription("New Description"); - val updated = groupService.update(group); - assertThat(updated.getDescription()).isEqualTo("New Description"); - } - - @Test - public void testUpdateNonexistentEntity() { - groupService.create(entityGenerator.createOneGroup("Group One")); - val nonExistentEntity = entityGenerator.createOneGroup("Group Two"); - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> groupService.update(nonExistentEntity)); - } - - @Test - public void testUpdateIdNotAllowed() { - val group = groupService.create(entityGenerator.createOneGroup("Group One")); - group.setId(new UUID(12312912931L,12312912931L)); - // New id means new non-existent entity or one that exists and is being overwritten - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> groupService.update(group)); - } - - @Test - @Ignore - public void testUpdateNameNotAllowed() { -// entityGenerator.setupSimpleGroups(); -// val group = groupService.getByName("Group One"); -// group.setName("New Name"); -// val updated = groupService.update(group); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - @Test - @Ignore - public void testUpdateStatusNotInAllowedEnum() { -// entityGenerator.setupSimpleGroups(); -// val group = groupService.getByName("Group One"); -// group.setStatus("Junk"); -// val updated = groupService.update(group); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - // Add Apps to Group - @Test - public void addAppsToGroup() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - val application = applicationService.getByClientId("111111"); - val applicationId = application.getId().toString(); - - groupService.addAppsToGroup(groupId, Arrays.asList(applicationId)); - - val group = groupService.get(groupId); - - assertThat(group.getWholeApplications()).contains(applicationService.getByClientId("111111")); - } - - @Test - public void addAppsToGroupNoGroup() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - val applicationId = applicationService.getByClientId("111111").getId().toString(); - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> groupService.addAppsToGroup(UUID.randomUUID().toString(), Arrays.asList(applicationId))); - } - - @Test - public void addAppsToGroupEmptyGroupString() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - val applicationId = applicationService.getByClientId("111111").getId().toString(); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> groupService.addAppsToGroup("", Arrays.asList(applicationId))); - } - - @Test - public void addAppsToGroupNoApp() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> groupService.addAppsToGroup(groupId, Arrays.asList(UUID.randomUUID().toString()))); - } - - @Test - public void addAppsToGroupWithAppListOneEmptyString() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> groupService.addAppsToGroup(groupId, Arrays.asList(""))); - } - - @Test - public void addAppsToGroupEmptyAppList() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val group = groupService.getByName("Group One"); - val groupId = group.getId().toString(); - - groupService.addAppsToGroup(groupId, Collections.emptyList()); - - val nonUpdated = groupService.getByName("Group One"); - assertThat(nonUpdated).isEqualTo(group); - } - - // Delete - @Test - public void testDelete() { - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - - groupService.delete(group.getId().toString()); - - val groups = groupService.listGroups(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(groups.getTotalElements()).isEqualTo(2L); - assertThat(groups.getContent()).doesNotContain(group); - } - - @Test - public void testDeleteNonExisting() { - entityGenerator.setupSimpleGroups(); - assertThatExceptionOfType(EmptyResultDataAccessException.class) - .isThrownBy(() -> groupService.delete(UUID.randomUUID().toString())); - } - - @Test - public void testDeleteEmptyIdString() { - entityGenerator.setupSimpleGroups(); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> groupService.delete("")); - } - - // Delete Apps from Group - @Test - public void testDeleteAppFromGroup() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - val application = applicationService.getByClientId("111111"); - val applicationId = application.getId().toString(); - - groupService.addAppsToGroup(groupId, Arrays.asList(applicationId)); - - val group = groupService.get(groupId); - assertThat(group.getWholeApplications().size()).isEqualTo(1); - - groupService.deleteAppsFromGroup(groupId, Arrays.asList(applicationId)); - - val groupWithDeleteApp = groupService.get(groupId); - assertThat(groupWithDeleteApp.getWholeApplications().size()).isEqualTo(0); - } - - @Test - public void testDeleteAppsFromGroupNoGroup() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - val application = applicationService.getByClientId("111111"); - val applicationId = application.getId().toString(); - - groupService.addAppsToGroup(groupId, Arrays.asList(applicationId)); - - val group = groupService.get(groupId); - assertThat(group.getWholeApplications().size()).isEqualTo(1); - - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> groupService - .deleteAppsFromGroup(UUID.randomUUID().toString(), Arrays.asList(applicationId))); - } - - @Test - public void testDeleteAppsFromGroupEmptyGroupString() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - val application = applicationService.getByClientId("111111"); - val applicationId = application.getId().toString(); - - groupService.addAppsToGroup(groupId, Arrays.asList(applicationId)); - - val group = groupService.get(groupId); - assertThat(group.getWholeApplications().size()).isEqualTo(1); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> groupService.deleteAppsFromGroup("", Arrays.asList(applicationId))); - } - - @Test - public void testDeleteAppsFromGroupEmptyAppsList() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleApplications(); - - val groupId = groupService.getByName("Group One").getId().toString(); - val application = applicationService.getByClientId("111111"); - val applicationId = application.getId().toString(); - - groupService.addAppsToGroup(groupId, Arrays.asList(applicationId)); - - val group = groupService.get(groupId); - assertThat(group.getWholeApplications().size()).isEqualTo(1); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> groupService.deleteAppsFromGroup(groupId, Arrays.asList(""))); - } - - /** - * This test guards against bad cascades against users - */ - @Test - public void testDeleteGroupWithUserRelations() { - val user = userService.create(entityGenerator.createOneUser(Pair.of("foo", "bar"))); - val group = groupService.create(entityGenerator.createOneGroup("testGroup")); - - group.addUser(user); - val updatedGroup = groupService.update(group); - - groupService.delete(updatedGroup.getId().toString()); - assertThat(userService.get(user.getId().toString())).isNotNull(); - } - - /** - * This test guards against bad cascades against applications - */ - @Test - public void testDeleteGroupWithApplicationRelations() { - val app = applicationService.create(entityGenerator.createOneApplication("foobar")); - val group = groupService.create(entityGenerator.createOneGroup("testGroup")); - - group.addApplication(app); - val updatedGroup = groupService.update(group); - - groupService.delete(updatedGroup.getId().toString()); - assertThat(applicationService.get(app.getId().toString())).isNotNull(); - } - - @Test - public void testAddGroupPermissions() { - entityGenerator.setupSimpleGroups(); - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) - .getContent(); - entityGenerator.setupSimpleAclEntities(groups); - - val study001 = policyService.getByName("Study001"); - val study001id = study001.getId().toString(); - - val study002 = policyService.getByName("Study002"); - val study002id = study002.getId().toString(); - - val study003 = policyService.getByName("Study003"); - val study003id = study003.getId().toString(); - - val permissions = Arrays.asList( - new Scope(study001id, "READ"), - new Scope(study002id, "WRITE"), - new Scope(study003id, "DENY") - ); - - val firstGroup = groups.get(0); - - groupService.addGroupPermissions(firstGroup.getId().toString(), permissions); - - assertThat(extractPermissionStrings(firstGroup.getGroupPermissions())) - .containsExactlyInAnyOrder( - "Study001.READ", - "Study002.WRITE", - "Study003.DENY" - ); - } - - @Test - public void testDeleteGroupPermissions() { - entityGenerator.setupSimpleGroups(); - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) - .getContent(); - entityGenerator.setupSimpleAclEntities(groups); - - val firstGroup = groups.get(0); - - val study001 = policyService.getByName("Study001"); - val study001id = study001.getId().toString(); - - val study002 = policyService.getByName("Study002"); - val study002id = study002.getId().toString(); - - val study003 = policyService.getByName("Study003"); - val study003id = study003.getId().toString(); - - val permissions = Arrays.asList( - new Scope(study001id, "READ"), - new Scope(study002id, "WRITE"), - new Scope(study003id, "DENY") - ); - - groupService.addGroupPermissions(firstGroup.getId().toString(), permissions); - - val groupPermissionsToRemove = firstGroup.getGroupPermissions() - .stream() - .filter(p -> !p.getEntity().getName().equals("Study001")) - .map(p -> p.getId().toString()) - .collect(Collectors.toList()); - - groupService.deleteGroupPermissions(firstGroup.getId().toString(), groupPermissionsToRemove); - - assertThat(extractPermissionStrings(firstGroup.getGroupPermissions())) - .containsExactlyInAnyOrder( - "Study001.READ" - ); - } - - @Test - public void testGetGroupPermissions() { - entityGenerator.setupSimpleGroups(); - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) - .getContent(); - entityGenerator.setupSimpleAclEntities(groups); - - val firstGroup = groups.get(0); - - val study001 = policyService.getByName("Study001"); - val study001id = study001.getId().toString(); - - val study002 = policyService.getByName("Study002"); - val study002id = study002.getId().toString(); - - val study003 = policyService.getByName("Study003"); - val study003id = study003.getId().toString(); - - val permissions = Arrays.asList( - new Scope(study001id, "READ"), - new Scope(study002id, "WRITE"), - new Scope(study003id, "DENY") - ); - - groupService.addGroupPermissions(firstGroup.getId().toString(), permissions); - - val pagedGroupPermissions = groupService.getGroupPermissions(firstGroup.getId().toString(), new PageableResolver().getPageable()); - - assertThat(pagedGroupPermissions.getTotalElements()).isEqualTo(3L); - } -} diff --git a/src/test/java/org/overture/ego/service/ScopeServiceTest.java b/src/test/java/org/overture/ego/service/ScopeServiceTest.java deleted file mode 100644 index 6dfad8ec2..000000000 --- a/src/test/java/org/overture/ego/service/ScopeServiceTest.java +++ /dev/null @@ -1,167 +0,0 @@ -package org.overture.ego.service; - -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.overture.ego.controller.resolver.PageableResolver; -import org.overture.ego.model.entity.Group; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.utils.EntityGenerator; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.util.Pair; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.transaction.annotation.Transactional; - -import javax.persistence.EntityNotFoundException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -@Slf4j -@SpringBootTest -@RunWith(SpringRunner.class) -@ActiveProfiles("test") -@Transactional -public class ScopeServiceTest { - - @Autowired - private PolicyService policyService; - - @Autowired - private GroupService groupService; - - @Autowired - private EntityGenerator entityGenerator; - - private List groups; - - @Before - public void setUp() { - // We need groups to be owners of aclEntities - entityGenerator.setupSimpleGroups(); - groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) - .getContent(); - } - - // Create - @Test - public void testCreate() { - val policy = policyService - .create(entityGenerator.createOneAclEntity(Pair.of("Study001", groups.get(0).getId()))); - assertThat(policy.getName()).isEqualTo("Study001"); - } - - @Test - @Ignore - public void testCreateUniqueName() { -// aclEntityService.create(entityGenerator.createOneAclEntity(Pair.of("Study001", groups.get(0).getId()))); -// aclEntityService.create(entityGenerator.createOneAclEntity(Pair.of("Study002", groups.get(0).getId()))); -// assertThatExceptionOfType(DataIntegrityViolationException.class) -// .isThrownBy(() -> aclEntityService.create(entityGenerator.createOneAclEntity(Pair.of("Study001", groups.get(0).getId())))); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - // Read - @Test - public void testGet() { - val policy = policyService.create(entityGenerator.createOneAclEntity(Pair.of("Study001", groups.get(0).getId()))); - val savedPolicy = policyService.get(policy.getId().toString()); - assertThat(savedPolicy.getName()).isEqualTo("Study001"); - } - - @Test - public void testGetEntityNotFoundException() { - assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> policyService.get(UUID.randomUUID().toString())); - } - - @Test - public void testGetByName() { - policyService.create(entityGenerator.createOneAclEntity(Pair.of("Study001", groups.get(0).getId()))); - val savedUser = policyService.getByName("Study001"); - assertThat(savedUser.getName()).isEqualTo("Study001"); - } - - @Test - public void testGetByNameAllCaps() { - policyService.create(entityGenerator.createOneAclEntity(Pair.of("Study001", groups.get(0).getId()))); - val savedUser = policyService.getByName("STUDY001"); - assertThat(savedUser.getName()).isEqualTo("Study001"); - } - - @Test - @Ignore - public void testGetByNameNotFound() { - // TODO Currently returning null, should throw exception (EntityNotFoundException?) - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> policyService.getByName("Study000")); - } - - @Test - public void testListUsersNoFilters() { - entityGenerator.setupSimpleAclEntities(groups); - val aclEntities = policyService - .listAclEntities(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(aclEntities.getTotalElements()).isEqualTo(3L); - } - - @Test - public void testListUsersNoFiltersEmptyResult() { - val aclEntities = policyService - .listAclEntities(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(aclEntities.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testListUsersFiltered() { - entityGenerator.setupSimpleAclEntities(groups); - val userFilter = new SearchFilter("name", "Study001"); - val aclEntities = policyService - .listAclEntities(Arrays.asList(userFilter), new PageableResolver().getPageable()); - assertThat(aclEntities.getTotalElements()).isEqualTo(1L); - } - - @Test - public void testListUsersFilteredEmptyResult() { - entityGenerator.setupSimpleAclEntities(groups); - val userFilter = new SearchFilter("name", "Study004"); - val aclEntities = policyService - .listAclEntities(Arrays.asList(userFilter), new PageableResolver().getPageable()); - assertThat(aclEntities.getTotalElements()).isEqualTo(0L); - } - - - // Update - @Test - public void testUpdate() { - val policy = policyService.create(entityGenerator.createOneAclEntity(Pair.of("Study001", groups.get(0).getId()))); - policy.setName("StudyOne"); - val updated = policyService.update(policy); - assertThat(updated.getName()).isEqualTo("StudyOne"); - } - - // Delete - @Test - public void testDelete() { - entityGenerator.setupSimpleAclEntities(groups); - - val policy = policyService.getByName("Study001"); - - policyService.delete(policy.getId().toString()); - - val remainingAclEntities = policyService.listAclEntities(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(remainingAclEntities.getTotalElements()).isEqualTo(2L); - assertThat(remainingAclEntities.getContent()).doesNotContain(policy); - } - -} diff --git a/src/test/java/org/overture/ego/service/UserServiceTest.java b/src/test/java/org/overture/ego/service/UserServiceTest.java deleted file mode 100644 index 64c822409..000000000 --- a/src/test/java/org/overture/ego/service/UserServiceTest.java +++ /dev/null @@ -1,1000 +0,0 @@ -package org.overture.ego.service; - -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.overture.ego.controller.resolver.PageableResolver; -import org.overture.ego.model.entity.User; -import org.overture.ego.model.params.Scope; -import org.overture.ego.model.search.SearchFilter; -import org.overture.ego.token.IDToken; -import org.overture.ego.utils.EntityGenerator; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.util.Pair; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.transaction.annotation.Transactional; - -import javax.persistence.EntityNotFoundException; -import java.util.Collections; -import java.util.UUID; -import java.util.stream.Collectors; - -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.overture.ego.utils.AclPermissionUtils.extractPermissionStrings; - -@Slf4j -@SpringBootTest -@RunWith(SpringRunner.class) -@ActiveProfiles("test") -@Transactional -public class UserServiceTest { - - private static final String NON_EXISTENT_USER = "827fae28-7fb8-11e8-adc0-fa7ae01bbebc"; - - @Autowired - private ApplicationService applicationService; - - @Autowired - private UserService userService; - - @Autowired - private GroupService groupService; - - @Autowired - private PolicyService policyService; - - @Autowired - private EntityGenerator entityGenerator; - - // Create - @Test - public void testCreate() { - val user = userService.create(entityGenerator.createOneUser(Pair.of("Demo", "User"))); - // UserName == UserEmail - assertThat(user.getName()).isEqualTo("DemoUser@domain.com"); - } - - @Test - public void testCreateUniqueNameAndEmail() { - userService.create(entityGenerator.createOneUser(Pair.of("User", "One"))); - userService.create(entityGenerator.createOneUser(Pair.of("User", "One"))); - assertThatExceptionOfType(DataIntegrityViolationException.class) - .isThrownBy(() -> userService.getByName("UserOne@domain.com")); - } - - @Test - public void testCreateFromIDToken() { - val idToken = IDToken.builder() - .email("UserOne@domain.com") - .given_name("User") - .family_name("User") - .build(); - - val idTokenUser = userService.createFromIDToken(idToken); - - assertThat(idTokenUser.getName()).isEqualTo("UserOne@domain.com"); - assertThat(idTokenUser.getEmail()).isEqualTo("UserOne@domain.com"); - assertThat(idTokenUser.getFirstName()).isEqualTo("User"); - assertThat(idTokenUser.getLastName()).isEqualTo("User"); - assertThat(idTokenUser.getStatus()).isEqualTo("Approved"); - assertThat(idTokenUser.getRole()).isEqualTo("USER"); - } - - @Test - public void testCreateFromIDTokenUniqueNameAndEmail() { - // Note: This test has one strike due to Hibernate Cache. - userService.create(entityGenerator.createOneUser(Pair.of("User", "One"))); - val idToken = IDToken.builder() - .email("UserOne@domain.com") - .given_name("User") - .family_name("One") - .build(); - userService.createFromIDToken(idToken); - - assertThatExceptionOfType(DataIntegrityViolationException.class) - .isThrownBy(() -> userService.getByName("UserOne@domain.com")); - } - - // Get - @Test - public void testGet() { - val user = userService.create(entityGenerator.createOneUser(Pair.of("User", "One"))); - val savedUser = userService.get(user.getId().toString()); - assertThat(savedUser.getName()).isEqualTo("UserOne@domain.com"); - } - - @Test - public void testGetEntityNotFoundException() { - assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> userService.get(NON_EXISTENT_USER)); - } - - @Test - public void testGetByName() { - userService.create(entityGenerator.createOneUser(Pair.of("User", "One"))); - val savedUser = userService.getByName("UserOne@domain.com"); - assertThat(savedUser.getName()).isEqualTo("UserOne@domain.com"); - } - - @Test - public void testGetByNameAllCaps() { - userService.create(entityGenerator.createOneUser(Pair.of("User", "One"))); - val savedUser = userService.getByName("USERONE@DOMAIN.COM"); - assertThat(savedUser.getName()).isEqualTo("UserOne@domain.com"); - } - - @Test - @Ignore - public void testGetByNameNotFound() { - // TODO Currently returning null, should throw exception (EntityNotFoundException?) - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> userService.getByName("UserOne@domain.com")); - } - - @Test - public void testGetOrCreateDemoUser() { - val demoUser = userService.getOrCreateDemoUser(); - assertThat(demoUser.getName()).isEqualTo("Demo.User@example.com"); - assertThat(demoUser.getEmail()).isEqualTo("Demo.User@example.com"); - assertThat(demoUser.getFirstName()).isEqualTo("Demo"); - assertThat(demoUser.getLastName()).isEqualTo("User"); - assertThat(demoUser.getStatus()).isEqualTo("Approved"); - assertThat(demoUser.getRole()).isEqualTo("ADMIN"); - } - - @Test - public void testGetOrCreateDemoUserAlREADyExisting() { - // This should force the demo user to have admin and approved status's - val demoUserObj = User.builder() - .name("Demo.User@example.com") - .email("Demo.User@example.com") - .firstName("Demo") - .lastName("User") - .status("Pending") - .role("USER") - .build(); - - val user = userService.create(demoUserObj); - - assertThat(user.getStatus()).isEqualTo("Pending"); - assertThat(user.getRole()).isEqualTo("USER"); - - val demoUser = userService.getOrCreateDemoUser(); - assertThat(demoUser.getStatus()).isEqualTo("Approved"); - assertThat(demoUser.getRole()).isEqualTo("ADMIN"); - } - - // List Users - @Test - public void testListUsersNoFilters() { - entityGenerator.setupSimpleUsers(); - val users = userService - .listUsers(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(users.getTotalElements()).isEqualTo(3L); - } - - @Test - public void testListUsersNoFiltersEmptyResult() { - val users = userService - .listUsers(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(users.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testListUsersFiltered() { - entityGenerator.setupSimpleUsers(); - val userFilter = new SearchFilter("email", "FirstUser@domain.com"); - val users = userService - .listUsers(singletonList(userFilter), new PageableResolver().getPageable()); - assertThat(users.getTotalElements()).isEqualTo(1L); - } - - @Test - public void testListUsersFilteredEmptyResult() { - entityGenerator.setupSimpleUsers(); - val userFilter = new SearchFilter("email", "FourthUser@domain.com"); - val users = userService - .listUsers(singletonList(userFilter), new PageableResolver().getPageable()); - assertThat(users.getTotalElements()).isEqualTo(0L); - } - - // Find Users - @Test - public void testFindUsersNoFilters() { - entityGenerator.setupSimpleUsers(); - val users = userService - .findUsers("First", Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(users.getTotalElements()).isEqualTo(1L); - assertThat(users.getContent().get(0).getName()).isEqualTo("FirstUser@domain.com"); - } - - @Test - public void testFindUsersFiltered() { - entityGenerator.setupSimpleUsers(); - val userFilter = new SearchFilter("email", "FirstUser@domain.com"); - val users = userService - .findUsers("Second", singletonList(userFilter), new PageableResolver().getPageable()); - // Expect empty list - assertThat(users.getTotalElements()).isEqualTo(0L); - } - - // Find Group Users - @Test - public void testFindGroupUsersNoQueryNoFilters() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val user = userService.getByName("FirstUser@domain.com"); - val userTwo = (userService.getByName("SecondUser@domain.com")); - val groupId = groupService.getByName("Group One").getId().toString(); - - userService.addUserToGroups(user.getId().toString(), singletonList(groupId)); - userService.addUserToGroups(userTwo.getId().toString(), singletonList(groupId)); - - val users = userService.findGroupUsers( - groupId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(users.getTotalElements()).isEqualTo(2L); - assertThat(users.getContent()).contains(user, userTwo); - } - - @Test - public void testFindGroupUsersNoQueryNoFiltersNoUsersFound() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val groupId = groupService.getByName("Group One").getId().toString(); - - val users = userService.findGroupUsers( - groupId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(users.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindGroupUsersNoQueryFiltersEmptyGroupString() { - entityGenerator.setupSimpleGroups(); - entityGenerator.setupSimpleUsers(); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> userService.findGroupUsers("", - Collections.emptyList(), - new PageableResolver().getPageable()) - ); - } - - @Test - public void testFindGroupUsersNoQueryFilters() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val user = userService.getByName("FirstUser@domain.com"); - val userTwo = (userService.getByName("SecondUser@domain.com")); - val groupId = groupService.getByName("Group One").getId().toString(); - - userService.addUserToGroups(user.getId().toString(), singletonList(groupId)); - userService.addUserToGroups(userTwo.getId().toString(), singletonList(groupId)); - - val userFilters = new SearchFilter("name", "First"); - - val users = userService.findGroupUsers( - groupId, - singletonList(userFilters), - new PageableResolver().getPageable() - ); - - assertThat(users.getTotalElements()).isEqualTo(1L); - assertThat(users.getContent()).contains(user); - } - - @Test - public void testFindGroupUsersQueryAndFilters() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val user = userService.getByName("FirstUser@domain.com"); - val userTwo = (userService.getByName("SecondUser@domain.com")); - val groupId = groupService.getByName("Group One").getId().toString(); - - userService.addUserToGroups(user.getId().toString(), singletonList(groupId)); - userService.addUserToGroups(userTwo.getId().toString(), singletonList(groupId)); - - val userFilters = new SearchFilter("name", "First"); - - val users = userService.findGroupUsers( - groupId, - "Second", - singletonList(userFilters), - new PageableResolver().getPageable() - ); - - assertThat(users.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindGroupUsersQueryNoFilters() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val user = userService.getByName("FirstUser@domain.com"); - val userTwo = (userService.getByName("SecondUser@domain.com")); - val groupId = groupService.getByName("Group One").getId().toString(); - - userService.addUserToGroups(user.getId().toString(), singletonList(groupId)); - userService.addUserToGroups(userTwo.getId().toString(), singletonList(groupId)); - - - val users = userService.findGroupUsers( - groupId, - "Second", - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(users.getTotalElements()).isEqualTo(1L); - assertThat(users.getContent()).contains(userTwo); - } - - // Find App Users - - @Test - public void testFindAppUsersNoQueryNoFilters() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val user = userService.getByName("FirstUser@domain.com"); - val userTwo = (userService.getByName("SecondUser@domain.com")); - val appId = applicationService.getByClientId("111111").getId().toString(); - - userService.addUserToApps(user.getId().toString(), singletonList(appId)); - userService.addUserToApps(userTwo.getId().toString(), singletonList(appId)); - - val users = userService.findAppUsers( - appId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(users.getTotalElements()).isEqualTo(2L); - assertThat(users.getContent()).contains(user, userTwo); - } - - @Test - public void testFindAppUsersNoQueryNoFiltersNoUser() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val appId = applicationService.getByClientId("111111").getId().toString(); - - val users = userService.findAppUsers( - appId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(users.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindAppUsersNoQueryNoFiltersEmptyUserString() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> userService - .findAppUsers( - "", - Collections.emptyList(), - new PageableResolver().getPageable() - ) - ); - } - - @Test - public void testFindAppUsersNoQueryFilters() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val user = userService.getByName("FirstUser@domain.com"); - val userTwo = (userService.getByName("SecondUser@domain.com")); - val appId = applicationService.getByClientId("111111").getId().toString(); - - userService.addUserToApps(user.getId().toString(), singletonList(appId)); - userService.addUserToApps(userTwo.getId().toString(), singletonList(appId)); - - val userFilters = new SearchFilter("name", "First"); - - val users = userService.findAppUsers( - appId, - singletonList(userFilters), - new PageableResolver().getPageable() - ); - - assertThat(users.getTotalElements()).isEqualTo(1L); - assertThat(users.getContent()).contains(user); - } - - @Test - public void testFindAppUsersQueryAndFilters() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val user = userService.getByName("FirstUser@domain.com"); - val userTwo = (userService.getByName("SecondUser@domain.com")); - val appId = applicationService.getByClientId("111111").getId().toString(); - - userService.addUserToApps(user.getId().toString(), singletonList(appId)); - userService.addUserToApps(userTwo.getId().toString(), singletonList(appId)); - - val userFilters = new SearchFilter("name", "First"); - - val users = userService.findAppUsers( - appId, - "Second", - singletonList(userFilters), - new PageableResolver().getPageable() - ); - - assertThat(users.getTotalElements()).isEqualTo(0L); - } - - @Test - public void testFindAppUsersQueryNoFilters() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val user = userService.getByName("FirstUser@domain.com"); - val userTwo = (userService.getByName("SecondUser@domain.com")); - val appId = applicationService.getByClientId("111111").getId().toString(); - - userService.addUserToApps(user.getId().toString(), singletonList(appId)); - userService.addUserToApps(userTwo.getId().toString(), singletonList(appId)); - - val users = userService.findAppUsers( - appId, - "First", - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(users.getTotalElements()).isEqualTo(1L); - assertThat(users.getContent()).contains(user); - } - - // Update - @Test - public void testUpdate() { - val user = userService.create(entityGenerator.createOneUser(Pair.of("First", "User"))); - user.setFirstName("NotFirst"); - val updated = userService.update(user); - assertThat(updated.getFirstName()).isEqualTo("NotFirst"); - } - - @Test - public void testUpdateRoleUser() { - val user = userService.create(entityGenerator.createOneUser(Pair.of("First", "User"))); - user.setRole("user"); - val updated = userService.update(user); - assertThat(updated.getRole()).isEqualTo("USER"); - } - - @Test - public void testUpdateRoleAdmin() { - val user = userService.create(entityGenerator.createOneUser(Pair.of("First", "User"))); - user.setRole("admin"); - val updated = userService.update(user); - assertThat(updated.getRole()).isEqualTo("ADMIN"); - } - - @Test - public void testUpdateNonexistentEntity() { - userService.create(entityGenerator.createOneUser(Pair.of("First", "User"))); - val nonExistentEntity = entityGenerator.createOneUser(Pair.of("First", "User")); - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> userService.update(nonExistentEntity)); - } - - @Test - public void testUpdateIdNotAllowed() { - val user = userService.create(entityGenerator.createOneUser(Pair.of("First", "User"))); - user.setId(UUID.fromString("0c1dc4b8-7fb8-11e8-adc0-fa7ae01bbebc")); - // New id means new non-existent entity or one that exists and is being overwritten - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> userService.update(user)); - } - - @Test - @Ignore - public void testUpdateNameNotAllowed() { -// val user = userService.create(entityGenerator.createOneUser(Pair.of("First", "User"))); -// user.setName("NewName"); -// val updated = userService.update(user); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - @Test - @Ignore - public void testUpdateEmailNotAllowed() { -// val user = userService.create(entityGenerator.createOneUser(Pair.of("First", "User"))); -// user.setEmail("NewName@domain.com"); -// val updated = userService.update(user); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - @Test - @Ignore - public void testUpdateStatusNotInAllowedEnum() { -// entityGenerator.setupSimpleUsers(); -// val user = userService.getByName("FirstUser@domain.com"); -// user.setStatus("Junk"); -// val updated = userService.update(user); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - @Test - @Ignore - public void testUpdateLanguageNotInAllowedEnum() { -// entityGenerator.setupSimpleUsers(); -// val user = userService.getByName("FirstUser@domain.com"); -// user.setPreferredLanguage("Klingon"); -// val updated = userService.update(user); - assertThat(1).isEqualTo(2); - // TODO Check for uniqueness in application, currently only SQL - } - - // Add User to Groups - @Test - public void addUserToGroups() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - val groupId = group.getId().toString(); - val groupTwo = groupService.getByName("Group Two"); - val groupTwoId = groupTwo.getId().toString(); - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToGroups(userId, asList(groupId, groupTwoId)); - - val groups = groupService.findUserGroups( - userId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(groups.getContent()).contains(group, groupTwo); - } - - @Test - public void addUserToGroupsNoUser() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - val groupId = group.getId().toString(); - - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> userService.addUserToGroups(NON_EXISTENT_USER, singletonList(groupId))); - } - - @Test - public void addUserToGroupsEmptyUserString() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - val groupId = group.getId().toString(); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> userService.addUserToGroups("", singletonList(groupId))); - } - - @Test - public void addUserToGroupsWithGroupsListOneEmptyString() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> userService.addUserToGroups(userId, singletonList(""))); - } - - @Test - public void addUserToGroupsEmptyGroupsList() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToGroups(userId, Collections.emptyList()); - - val nonUpdated = userService.getByName("FirstUser@domain.com"); - assertThat(nonUpdated).isEqualTo(user); - } - - // Add User to Apps - @Test - public void addUserToApps() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val app = applicationService.getByClientId("111111"); - val appId = app.getId().toString(); - val appTwo = applicationService.getByClientId("222222"); - val appTwoId = appTwo.getId().toString(); - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToApps(userId, asList(appId, appTwoId)); - - val apps = applicationService.findUserApps( - userId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(apps.getContent()).contains(app, appTwo); - } - - @Test - public void addUserToAppsNoUser() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val app = applicationService.getByClientId("111111"); - val appId = app.getId().toString(); - - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> userService.addUserToApps(NON_EXISTENT_USER, singletonList(appId))); - } - - @Test - public void addUserToAppsWithAppsListOneEmptyString() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> userService.addUserToApps(userId, singletonList(""))); - } - - @Test - public void addUserToAppsEmptyAppsList() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToApps(userId, Collections.emptyList()); - - val nonUpdated = userService.getByName("FirstUser@domain.com"); - assertThat(nonUpdated).isEqualTo(user); - } - - // Delete - @Test - public void testDelete() { - entityGenerator.setupSimpleUsers(); - - val user = userService.getByName("FirstUser@domain.com"); - - userService.delete(user.getId().toString()); - - val users = userService.listUsers(Collections.emptyList(), new PageableResolver().getPageable()); - assertThat(users.getTotalElements()).isEqualTo(2L); - assertThat(users.getContent()).doesNotContain(user); - } - - @Test - public void testDeleteNonExisting() { - entityGenerator.setupSimpleUsers(); - assertThatExceptionOfType(EmptyResultDataAccessException.class) - .isThrownBy(() -> userService.delete(NON_EXISTENT_USER)); - } - - @Test - public void testDeleteEmptyIdString() { - entityGenerator.setupSimpleGroups(); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> userService.delete("")); - } - - // Delete User from Group - @Test - public void testDeleteUserFromGroup() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - val groupId = group.getId().toString(); - val groupTwo = groupService.getByName("Group Two"); - val groupTwoId = groupTwo.getId().toString(); - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToGroups(userId, asList(groupId, groupTwoId)); - - userService.deleteUserFromGroups(userId, singletonList(groupId)); - - val groupWithoutUser = groupService.findUserGroups( - userId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(groupWithoutUser.getContent()).containsOnly(groupTwo); - } - - @Test - public void testDeleteUserFromGroupNoUser() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - val groupId = group.getId().toString(); - val groupTwo = groupService.getByName("Group Two"); - val groupTwoId = groupTwo.getId().toString(); - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToGroups(userId, asList(groupId, groupTwoId)); - - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> userService - .deleteUserFromGroups(NON_EXISTENT_USER, singletonList(groupId))); - } - - @Test - public void testDeleteUserFromGroupEmptyUserString() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val group = groupService.getByName("Group One"); - val groupId = group.getId().toString(); - val groupTwo = groupService.getByName("Group Two"); - val groupTwoId = groupTwo.getId().toString(); - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToGroups(userId, asList(groupId, groupTwoId)); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> userService - .deleteUserFromGroups("", singletonList(groupId))); - } - - @Test - public void testDeleteUserFromGroupEmptyGroupsList() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - val group = groupService.getByName("Group One"); - val groupId = group.getId().toString(); - - userService.addUserToGroups(userId, singletonList(groupId)); - assertThat(user.getWholeGroups().size()).isEqualTo(1); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> userService - .deleteUserFromGroups(userId, singletonList(""))); - } - - // Delete User from App - @Test - public void testDeleteUserFromApp() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val app = applicationService.getByClientId("111111"); - val appId = app.getId().toString(); - val appTwo = applicationService.getByClientId("222222"); - val appTwoId = appTwo.getId().toString(); - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToApps(userId, asList(appId, appTwoId)); - - userService.deleteUserFromApps(userId, singletonList(appId)); - - val groupWithoutUser = applicationService.findUserApps( - userId, - Collections.emptyList(), - new PageableResolver().getPageable() - ); - - assertThat(groupWithoutUser.getContent()).containsOnly(appTwo); - } - - @Test - public void testDeleteUserFromAppNoUser() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val app = applicationService.getByClientId("111111"); - val appId = app.getId().toString(); - val appTwo = applicationService.getByClientId("222222"); - val appTwoId = appTwo.getId().toString(); - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToApps(userId, asList(appId, appTwoId)); - - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> userService - .deleteUserFromApps(NON_EXISTENT_USER, singletonList(appId))); - } - - @Test - public void testDeleteUserFromAppEmptyUserString() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val app = applicationService.getByClientId("111111"); - val appId = app.getId().toString(); - val appTwo = applicationService.getByClientId("222222"); - val appTwoId = appTwo.getId().toString(); - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToApps(userId, asList(appId, appTwoId)); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> userService - .deleteUserFromApps("", singletonList(appId))); - } - - @Test - public void testDeleteUserFromAppEmptyAppsList() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleApplications(); - - val app = applicationService.getByClientId("111111"); - val appId = app.getId().toString(); - val appTwo = applicationService.getByClientId("222222"); - val appTwoId = appTwo.getId().toString(); - val user = userService.getByName("FirstUser@domain.com"); - val userId = user.getId().toString(); - - userService.addUserToApps(userId, asList(appId, appTwoId)); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> userService - .deleteUserFromApps(userId, singletonList(""))); - } - - @Test - public void testAddUserPermissions() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) - .getContent(); - entityGenerator.setupSimpleAclEntities(groups); - - val user = userService.getByName("FirstUser@domain.com"); - - val study001 = policyService.getByName("Study001"); - val study001id = study001.getId().toString(); - - val study002 = policyService.getByName("Study002"); - val study002id = study002.getId().toString(); - - val study003 = policyService.getByName("Study003"); - val study003id = study003.getId().toString(); - - val permissions = asList( - new Scope(study001id, "READ"), - new Scope(study002id, "WRITE"), - new Scope(study003id, "DENY") - ); - - userService.addUserPermissions(user.getId().toString(), permissions); - - assertThat(extractPermissionStrings(user.getUserPermissions())) - .containsExactlyInAnyOrder( - "Study001.READ", - "Study002.WRITE", - "Study003.DENY" - ); - } - - @Test - public void testRemoveUserPermissions() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) - .getContent(); - entityGenerator.setupSimpleAclEntities(groups); - - val user = userService.getByName("FirstUser@domain.com"); - - val study001 = policyService.getByName("Study001"); - val study001id = study001.getId().toString(); - - val study002 = policyService.getByName("Study002"); - val study002id = study002.getId().toString(); - - val study003 = policyService.getByName("Study003"); - val study003id = study003.getId().toString(); - - val permissions = asList( - new Scope(study001id, "READ"), - new Scope(study002id, "WRITE"), - new Scope(study003id, "DENY") - ); - - userService.addUserPermissions(user.getId().toString(), permissions); - - val userPermissionsToRemove = user.getUserPermissions() - .stream() - .filter(p -> !p.getEntity().getName().equals("Study001")) - .map(p -> p.getId().toString()) - .collect(Collectors.toList()); - - userService.deleteUserPermissions(user.getId().toString(), userPermissionsToRemove); - - assertThat(extractPermissionStrings(user.getUserPermissions())) - .containsExactlyInAnyOrder( - "Study001.READ" - ); - } - - @Test - public void testGetUserPermissions() { - entityGenerator.setupSimpleUsers(); - entityGenerator.setupSimpleGroups(); - val groups = groupService - .listGroups(Collections.emptyList(), new PageableResolver().getPageable()) - .getContent(); - entityGenerator.setupSimpleAclEntities(groups); - - val user = userService.getByName("FirstUser@domain.com"); - - val study001 = policyService.getByName("Study001"); - val study001id = study001.getId().toString(); - - val study002 = policyService.getByName("Study002"); - val study002id = study002.getId().toString(); - - val study003 = policyService.getByName("Study003"); - val study003id = study003.getId().toString(); - - val permissions = asList( - new Scope(study001id, "READ"), - new Scope(study002id, "WRITE"), - new Scope(study003id, "DENY") - ); - - userService.addUserPermissions(user.getId().toString(), permissions); - - val pagedUserPermissions = userService.getUserPermissions(user.getId().toString(), new PageableResolver().getPageable()); - - assertThat(pagedUserPermissions.getTotalElements()).isEqualTo(3L); - } -} diff --git a/src/test/java/org/overture/ego/token/TokenServiceTest.java b/src/test/java/org/overture/ego/token/TokenServiceTest.java deleted file mode 100644 index 80c85e097..000000000 --- a/src/test/java/org/overture/ego/token/TokenServiceTest.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2018. The Ontario Institute for Cancer Research. 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. - * - */ - -package org.overture.ego.token; - -import com.google.common.collect.Sets; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.overture.ego.service.ApplicationService; -import org.overture.ego.service.GroupService; -import org.overture.ego.service.UserService; -import org.overture.ego.utils.EntityGenerator; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.util.Pair; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -@Slf4j -@SpringBootTest -@RunWith(SpringRunner.class) -@ActiveProfiles("test") -@Ignore -public class TokenServiceTest { - @Autowired - private ApplicationService applicationService; - - @Autowired - private UserService userService; - - @Autowired - private GroupService groupService; - - @Autowired - private EntityGenerator entityGenerator; - - @Autowired - private TokenService tokenService; - - @Test - public void generateUserToken() { - val user = userService.create(entityGenerator.createOneUser(Pair.of("foo", "bar"))); - val group = groupService.create(entityGenerator.createOneGroup("testGroup")); - val app = applicationService.create(entityGenerator.createOneApplication("foo")); - - val group2 = groupService.getByName("testGroup"); - group2.addUser(user); - groupService.update(group2); - - val app2 = applicationService.getByClientId("foo"); - app2.setWholeUsers(Sets.newHashSet(user)); - applicationService.update(app2); - - - val token = tokenService.generateUserToken(userService.get(user.getId().toString())); - } - -} diff --git a/src/test/java/org/overture/ego/utils/EntityGenerator.java b/src/test/java/org/overture/ego/utils/EntityGenerator.java deleted file mode 100644 index 68e033eb7..000000000 --- a/src/test/java/org/overture/ego/utils/EntityGenerator.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.overture.ego.utils; - -import lombok.val; -import org.overture.ego.model.entity.Policy; -import org.overture.ego.model.entity.Application; -import org.overture.ego.model.entity.Group; -import org.overture.ego.model.entity.User; -import org.overture.ego.service.PolicyService; -import org.overture.ego.service.ApplicationService; -import org.overture.ego.service.GroupService; -import org.overture.ego.service.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.util.Pair; -import org.springframework.stereotype.Component; - -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -@Component -public class EntityGenerator { - - @Autowired - private ApplicationService applicationService; - - @Autowired - private UserService userService; - - @Autowired - private GroupService groupService; - - @Autowired - private PolicyService policyService; - - public Application createOneApplication(String clientId) { - return new Application(String.format("Application %s", clientId), clientId, new StringBuilder(clientId).reverse().toString()); - } - - public List createApplicationsFromList(List clientIds) { - return clientIds.stream().map(this::createOneApplication).collect(Collectors.toList()); - } - - public void setupSimpleApplications() { - for (Application application : createApplicationsFromList(Arrays.asList("111111", "222222", "333333", "444444", "555555"))) { - applicationService.create(application); - } - } - - public User createOneUser(Pair user) { - val firstName = user.getFirst(); - val lastName = user.getSecond(); - - return User - .builder() - .email(String.format("%s%s@domain.com", firstName, lastName)) - .name(String.format("%s%s", firstName, lastName)) - .firstName(firstName) - .lastName(lastName) - .status("Approved") - .preferredLanguage("English") - .lastLogin(null) - .role("ADMIN") - .build(); - } - - public List createUsersFromList(List> users) { - return users.stream().map(this::createOneUser).collect(Collectors.toList()); - } - - public void setupSimpleUsers() { - for (User user : createUsersFromList(Arrays.asList(Pair.of("First", "User"), Pair.of("Second", "User"), Pair.of("Third", "User")))) { - userService.create(user); - } - } - - public Group createOneGroup(String name) { - return new Group(name); - } - - public List createGroupsfromList(List groups) { - return groups.stream().map(this::createOneGroup).collect(Collectors.toList()); - } - - public void setupSimpleGroups() { - for (Group group : createGroupsfromList(Arrays.asList("Group One", "Group Two", "Group Three"))) { - groupService.create(group); - } - } - - public Policy createOneAclEntity(Pair aclEntity) { - return Policy.builder() - .name(aclEntity.getFirst()) - .owner(aclEntity.getSecond()) - .build(); - } - - public List createAclEntitiesFromList(List> aclEntities) { - return aclEntities.stream().map(this::createOneAclEntity).collect(Collectors.toList()); - } - - public void setupSimpleAclEntities(List threeGroups) { - - for (Policy policy : createAclEntitiesFromList( - Arrays.asList( - Pair.of("Study001", threeGroups.get(0).getId()), - Pair.of("Study002", threeGroups.get(1).getId()), - Pair.of("Study003", threeGroups.get(2).getId()) - ))) { - policyService.create(policy); - } - } -} diff --git a/src/test/resources/conf/bs.conf.json b/src/test/resources/conf/bs.conf.json new file mode 100644 index 000000000..97d0da917 --- /dev/null +++ b/src/test/resources/conf/bs.conf.json @@ -0,0 +1,19 @@ +{ + "server": "hub-cloud.browserstack.com", + "user": "*", + "key": "*", + + "capabilities": { + "os": "Windows", + "os_version": "10", + "browser": "Chrome", + "browser_version": "62.0", + "browserstack.debug": true, + "browserstack.local": true, + "project": "ego" + }, + + "environments": [{ + "browser": "chrome" + }] +} \ No newline at end of file