├── .gitignore ├── .reuse └── dep5 ├── .travis.yml ├── CREDITS ├── LICENSE ├── LICENSES └── Apache-2.0.txt ├── README.md ├── etc └── eclipse-code-formatter.xml ├── images ├── Figure_OAuth2.0_SAP_CP_Components.png └── some.txt ├── prerequisites └── README.md ├── spring-security-acl ├── README.md ├── docker-compose.yml ├── documentation │ ├── images │ │ ├── ACL_DatabaseTables.png │ │ └── CreateRole.jpg │ └── testing │ │ ├── spring-acl-cloudfoundry.postman_collection.json │ │ ├── spring-acl-local.postman_collection.json │ │ └── spring-acl-local.postman_environment.json ├── localEnvironmentSetup.bat ├── localEnvironmentSetup.sh ├── manifest.yml ├── pom.xml ├── security │ └── xs-security.json └── src │ ├── main │ ├── approuter │ │ ├── .npmrc │ │ ├── package.json │ │ ├── resources │ │ │ └── index.html │ │ └── xs-app.json │ ├── java │ │ └── com │ │ │ └── sap │ │ │ └── cp │ │ │ └── appsec │ │ │ ├── Application.java │ │ │ ├── config │ │ │ ├── AclAuditLogger.java │ │ │ ├── AclConfig.java │ │ │ ├── MethodSecurityConfig.java │ │ │ ├── PersistenceConfig.java │ │ │ └── WebSecurityConfig.java │ │ │ ├── controllers │ │ │ ├── AdvertisementAclController.java │ │ │ ├── AttributeFinderController.java │ │ │ └── CustomExceptionMapper.java │ │ │ ├── domain │ │ │ ├── AclAttribute.java │ │ │ ├── Advertisement.java │ │ │ ├── AdvertisementAclRepository.java │ │ │ └── BaseEntity.java │ │ │ ├── dto │ │ │ ├── AdvertisementDto.java │ │ │ ├── AdvertisementListDto.java │ │ │ ├── BulletinboardDto.java │ │ │ ├── ErrorDto.java │ │ │ ├── PageHeaderBuilder.java │ │ │ └── PermissionDto.java │ │ │ ├── exceptions │ │ │ ├── BadRequestException.java │ │ │ ├── NotAuthorizedException.java │ │ │ └── NotFoundException.java │ │ │ ├── mock │ │ │ └── XsuaaMockPostProcessor.java │ │ │ ├── security │ │ │ ├── AclSupport.java │ │ │ └── CustomTokenAuthorizationsExtractor.java │ │ │ └── services │ │ │ └── AdvertisementService.java │ └── resources │ │ ├── META-INF │ │ └── spring.factories │ │ ├── application-localdb.properties │ │ ├── application-uaamock.properties │ │ ├── application.properties │ │ ├── db.population │ │ ├── acl_class.csv │ │ ├── acl_entry.csv │ │ ├── acl_object_identity.csv │ │ └── acl_sid.csv │ │ └── db │ │ └── changelog │ │ ├── db.changelog-main.yaml │ │ └── v0.1 │ │ ├── create-acl-tables.yaml │ │ ├── create-application-tables.yaml │ │ ├── main.yaml │ │ └── populate-acl-tables.yaml │ └── test │ ├── java │ └── com │ │ └── sap │ │ └── cp │ │ └── appsec │ │ ├── ApplicationTest.java │ │ ├── controllers │ │ ├── AdvertisementACLControllerTest.java │ │ └── AttributeFinderControllerTest.java │ │ ├── domain │ │ └── AdvertisementAclRepositoryTest.java │ │ └── services │ │ └── AdvertisementServiceTest.java │ └── resources │ ├── application.properties │ └── db │ └── data │ ├── acl_test_data.sql │ ├── acl_test_data_hierarchy.sql │ └── acl_test_data_mass.sql ├── spring-security-basis ├── .gitignore ├── README.md ├── documentation │ ├── Prerequisites.md │ ├── images │ │ ├── Figure_OAuth2.0_SAP_CP_Components.png │ │ └── SAP_CP_Cockpit_AssignRoleCollectionToUser.png │ └── testing │ │ ├── spring-security-cloudfoundry.postman_collection.json │ │ ├── spring-security-local.postman_collection.json │ │ └── spring-security-local.postman_environment.json ├── localEnvironmentSetup.bat ├── localEnvironmentSetup.sh ├── manifest.yml ├── pom.xml ├── security │ └── xs-security.json └── src │ ├── main │ ├── approuter │ │ ├── .npmrc │ │ ├── package.json │ │ ├── resources │ │ │ └── index.html │ │ └── xs-app.json │ ├── java │ │ └── com │ │ │ └── sap │ │ │ └── cp │ │ │ └── appsec │ │ │ ├── Application.java │ │ │ ├── config │ │ │ ├── PersistenceConfig.java │ │ │ └── WebSecurityConfig.java │ │ │ ├── controllers │ │ │ ├── AdvertisementController.java │ │ │ ├── AttributeFinder.java │ │ │ └── CustomExceptionMapper.java │ │ │ ├── domain │ │ │ ├── Advertisement.java │ │ │ ├── AdvertisementRepository.java │ │ │ ├── BaseEntity.java │ │ │ └── ConfidentialityLevel.java │ │ │ ├── dto │ │ │ ├── AdvertisementDto.java │ │ │ ├── AdvertisementListDto.java │ │ │ ├── ErrorDto.java │ │ │ └── PageHeaderBuilder.java │ │ │ ├── exceptions │ │ │ ├── BadRequestException.java │ │ │ ├── NotAuthorizedException.java │ │ │ └── NotFoundException.java │ │ │ ├── mock │ │ │ └── XsuaaMockPostProcessor.java │ │ │ └── security │ │ │ ├── AdvertisementSpecificationBuilder.java │ │ │ └── WebSecurityExpressions.java │ └── resources │ │ ├── META-INF │ │ └── spring.factories │ │ ├── application-uaamock.properties │ │ └── application.properties │ └── test │ ├── java │ └── com │ │ └── sap │ │ └── cp │ │ └── appsec │ │ ├── ApplicationTest.java │ │ ├── controllers │ │ ├── AdvertisementControllerTest.java │ │ └── AttributeFinderTest.java │ │ └── domain │ │ └── AdvertisementRepositoryTest.java │ └── resources │ └── application.properties └── vars.yml /.gitignore: -------------------------------------------------------------------------------- 1 | */target/ 2 | 3 | manifest-* 4 | 5 | ### Ignore npm files/folders ### 6 | node_modules 7 | npm-debug.log 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | 19 | ### IntelliJ IDEA ### 20 | .idea 21 | *.iws 22 | *.iml 23 | *.ipr 24 | 25 | ### NetBeans ### 26 | /nbproject/private/ 27 | /build/ 28 | /nbbuild/ 29 | /dist/ 30 | /nbdist/ 31 | /.nb-gradle/ 32 | 33 | ### Others 34 | vim.* -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: cloud-application-security-sample 3 | Upstream-Contact: Anett Lippert 4 | Source: https://github.com/SAP-samples/cloud-application-security-sample 5 | Disclaimer: The code in this project may include calls to APIs (“API Calls”) of 6 | SAP or third-party products or services developed outside of this project 7 | (“External Products”). 8 | “APIs” means application programming interfaces, as well as their respective 9 | specifications and implementing code that allows software to communicate with 10 | other software. 11 | API Calls to External Products are not licensed under the open source license 12 | that governs this project. The use of such API Calls and related External 13 | Products are subject to applicable additional agreements with the relevant 14 | provider of the External Products. In no event shall the open source license 15 | that governs this project grant any rights in or to any External Products,or 16 | alter, expand or supersede any terms of the applicable additional agreements. 17 | If you have a valid license agreement with SAP for the use of a particular SAP 18 | External Product, then you may make use of any API Calls included in this 19 | project’s code for that SAP External Product, subject to the terms of such 20 | license agreement. If you do not have a valid license agreement for the use of 21 | a particular SAP External Product, then you may only make use of any API Calls 22 | in this project for that SAP External Product for your internal, non-productive 23 | and non-commercial test and evaluation of such API Calls. Nothing herein grants 24 | you any rights to use or access any SAP External Product, or provide any third 25 | parties the right to use of access any SAP External Product, through API Calls. 26 | 27 | Files: * 28 | Copyright: 2019-2020 SAP SE or an SAP affiliate company and cloud-application-security-sample 29 | License: Apache-2.0 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - openjdk8 5 | 6 | script: 7 | - cd spring-security-acl 8 | - mvn clean verify 9 | - cd ../spring-security-basis 10 | - mvn clean verify 11 | 12 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | This program references the following third party open source or other free download components. 2 | The third party licensors of these components may provide additional license rights, 3 | terms and conditions and/or require certain notices as described below. 4 | 5 | Spring Boot Framework (https://github.com/spring-projects/spring-boot) 6 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Spring Security (https://github.com/spring-projects/spring-security) 9 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | SAP Java Security Library (https://github.com/SAP/cloud-security-xsuaa-integration) 12 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Hibernate Core ORM (JPA) (https://github.com/hibernate/hibernate-orm/tree/master/hibernate-core) 15 | Licensed under GNU Lesser General Public License v2.1 - https://github.com/hibernate/hibernate-orm/blob/master/lgpl.txt 16 | 17 | PostgreSQL JDBC Driver (https://jdbc.postgresql.org/) 18 | Licensed under BSD 2-Clause "Simplified" License - https://github.com/pgjdbc/pgjdbc/blob/master/LICENSE 19 | 20 | HikariCP JDBC Connection Pool (https://github.com/brettwooldridge/HikariCP) 21 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 22 | 23 | Spring Data JPA (https://github.com/spring-projects/spring-data-jpa) 24 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 25 | 26 | Liquibase (http://github.com/liquibase/liquibase/) 27 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 28 | 29 | EHCache (http://ehcache.org/) 30 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 31 | 32 | Spring Cloud Connector (https://github.com/spring-cloud/spring-cloud-connectors) 33 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 34 | 35 | SAP Java Logging Support for Cloud Foundry (https://github.com/SAP/cf-java-logging-support) 36 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 37 | 38 | Jackson Project (https://github.com/FasterXML/jackson) 39 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 40 | 41 | Hibernate Validator (http://hibernate.org/validator/) 42 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 43 | 44 | 45 | JUnit (https://junit.org/junit4/) 46 | Licensed under Eclipse Public License 1.0 - http://www.eclipse.org/legal/epl-v10.html 47 | 48 | Java Hamcrest (http://hamcrest.org/JavaHamcrest/) 49 | Licensed under BSD-3-Clause License - https://opensource.org/licenses/BSD-3-Clause 50 | 51 | Mockito (http://site.mockito.org/) 52 | Licensed under MIT License - https://github.com/mockito/mockito/blob/master/LICENSE 53 | 54 | Jayway JsonPath (https://github.com/json-path/JsonPath/) 55 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 56 | 57 | H2 Database (http://www.h2database.com/html/main.html) 58 | Licensed under Mozilla Public License Version 2.0 - https://www.mozilla.org/en-US/MPL/2.0/ 59 | 60 | 61 | Tomcat Maven Plugin (https://tomcat.apache.org/maven-plugin-trunk/tomcat7-maven-plugin/) 62 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 63 | 64 | JaCoCo Java Code Coverage Library (http://www.eclemma.org/jacoco/) 65 | Licensed under Eclipse Public License v1.0 - http://www.eclipse.org/legal/epl-v10.html 66 | 67 | FindBugs Maven Plugin (https://gleclaire.github.io/findbugs-maven-plugin/) 68 | Licensed under Apache License, Version 2.0 - https://gleclaire.github.io/findbugs-maven-plugin/license.html 69 | 70 | Apache Maven PMD Plugin (https://maven.apache.org/plugins/maven-pmd-plugin/) 71 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 72 | 73 | Maven Surefire Plugin (http://maven.apache.org/components/surefire/maven-surefire-plugin/) 74 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 75 | 76 | Maven Failsafe Plugin (http://maven.apache.org/components/surefire/maven-failsafe-plugin/) 77 | Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 78 | 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![REUSE status](https://api.reuse.software/badge/github.com/SAP-samples/cloud-application-security-sample)](https://api.reuse.software/info/github.com/SAP-samples/cloud-application-security-sample) 2 | [![Not Maintained](https://img.shields.io/badge/Maintenance%20Level-Not%20Maintained-yellow.svg)](https://gist.github.com/cheerfulstoic/d107229326a01ff0f333a1d3476e068d) 3 | 4 | 5 | # Cloud Application Security Samples 6 | 7 | ## Description 8 | Implementing security in SAP BTP Applications? 9 | The SAP BTP offers specific support to implement authentication and authorization for business users that access SAP BTP applications. As a developer of such applications you have different options how to leverage the possibilities that SAP BTP offers. In this repository we want to showcase how this can be done. These samples demonstrate several different technologies to implement security authentication and authorization. Specifically, they integrate to SAP BTP XSUAA service using one of SAP's Container Security Libraries. 10 | 11 | ## Overview of Samples 12 | Each sample is provided in a separate module. 13 | 14 | Module | Description | Availability | Prerequisite 15 | ---- | -------- | ---- | --- 16 | [spring-security-basis](spring-security-basis) | This module shows how to implement basic access control in Spring-based SAP BTP applications. It leverages Spring Security 5.x and integrates to SAP BTP XSUAA service (OAuth Resource Server) using the SAP Container Security Library (Java), which is available on maven central. Furthermore the sample demonstrates a multi-tenancy setup and how to implement JUnit tests. | 2019-06 | [openSAP course: Cloud-Native Development with SAP CP](https://open.sap.com/courses/cp5) - Week 5 17 | [spring-security-acl](spring-security-acl) | This module shows the usage of Spring Security ACL to implement instance-based access control in Spring-based SAP BTP applications. Furthermore the sample demonstrates a multi-tenancy setup and how to implement JUnit tests. | 2019-01 | Module: [spring-security-basis](spring-security-basis) 18 | 19 | 20 | 21 | ## Understanding OAuth 2.0 Components 22 | To better understand the content of this repository, you should gain a rough understanding about the SAP BTP OAuth 2.0 components, which are depicted in figure below. 23 | 24 | ![](images/Figure_OAuth2.0_SAP_CP_Components.png) 25 | 26 | #### OAuth Resource Server 27 | First, we still have a **microservice** or CF application that we want to secure. In OAuth terminology this is the **Resource Server** that protects the resources by checking the existence and validity of an OAuth2 access token before allowing a request from the Client to succeed. 28 | 29 | #### OAuth Access Token (JWT) 30 | Access and refresh tokens in the form of **JSON Web Token (JWT)** represent the user’s identity and authorization claims. If the access token is compromised, it can be revoked, which forces the generation of a new access token via the user’s refresh token. 31 | 32 | Example JWT 33 | ```json 34 | { 35 | "client_id": "sb-xsapplication!t895", 36 | "cid": "sb-xsapplication!t895", 37 | "exp": 2147483647, 38 | "user_name": "John Doe", 39 | "user_id": "P0123456", 40 | "email": "johndoe@test.org", 41 | "zid": "1e505bb1-2fa9-4d2b-8c15-8c3e6e6279c6", 42 | "grant_type": "urn:ietf:params:oauth:grant-type:saml2-bearer", 43 | "scope": [ "xsapplication!t895.Display" ], 44 | "xs.user.attributes": { 45 | "country": [ 46 | "DE" 47 | ] 48 | } 49 | } 50 | ``` 51 | 52 | #### OAuth Authorization Server 53 | Furthermore we have the **Extended Services for User Account and Authentication (XSUAA)** that acts as **OAuth Authorization Server** and issues authorization codes and JWT tokens after the user was successfully authenticated by an identity provider. Technically the XSUAA is a SAP-specific extension of CloudFoundry’s UAA service to deal with authentication and authorization. 54 | 55 | #### OAuth Client 56 | The **Application Router (approuter)** is an edge service that provides a single entry point to a business application that consists of several backend microservices. It acts as reverse proxy that routes incoming HTTP requests to the configured target microservice, which allows handling Cross-origin resource sharing (CORS) between the microservices. It plays a central role in the OAuth flow. 57 | 58 | Just like HTTP, token-based authentication is stateless, and therefore for scalability reasons an OAuth Resource Server must not store a JWT. The consequence would be that the JWT is stored client side as it must be provided with every request. Here, the Application Router takes over this responsibility and acts an **OAuth Client** and is mainly responsible for managing authentication flows. 59 | 60 | The Application Router takes incoming, unauthenticated requests from users and initiates an OAuth2 flow with the XSUAA. After the user has successfully logged on the Identity Provider the XSUAA considers this request as authenticated and uses the information of the Bearer Assertion to finally create a JWT containing the authenticated user as well as all scopes that he or she has been granted. Furthermore the Application Router enriches each subsequent request with the JWT, before the request is routed to a dedicated microservice (instance), so that they are freed up from this task. 61 | 62 | > In this flow it is important to notice that the JWT never appears in the browser as the Application Router acts as OAuth client where the user “authorizes” the approuter to obtain the authorizations - the JWT - from the XSUAA component. 63 | 64 | #### Conclusion 65 | 66 | You need to configure the Application Router for your business application as explained in the free [openSAP course](https://open.sap.com/courses/cp5) exercises: 67 | 68 | - [Exercise 22: Deploy Application Router and Set Up Authentication](https://github.com/SAP/cloud-bulletinboard-ads/blob/Documentation/Security/Exercise_22_DeployApplicationRouter.md) 69 | - [[Optional] Exercise 23: Setup Generic Authorization](https://github.com/SAP/cloud-bulletinboard-ads/blob/Documentation/Security/Exercise_23_SetupGenericAuthorization.md) 70 | 71 | Note that the Application Router can be bypassed and the microservice can directly be accessed. So the backend microservices must protect all their endpoints by validating the JWT access token and implementing proper scope checks. 72 | 73 | In order to validate an access token, the JWT must be decoded and its signature must be verified with one of the JSON Web Keys (JWK) such as public RSA keys. Furthermore the claims found inside the access token must be validated. For example, the client id (`cid`), the issuer (`iss`), the audience (`aud`), and the expiry time (`exp`). 74 | Hence, every microservice has to maintain a service binding to the XSUAA that provides the XSUAA url as part of `VCAP_SERVICES` to get the current JWKs and has to configure the XSUAA as OAuth 2.0 Resource Server with its XSUAA access token validators by making use of one of SAP's Container Security Libraries. 75 | 76 | ## How to obtain support 77 | For any question please [open an issue](https://github.com/SAP/cloud-application-security-sample/issues/new) in GitHub and make use of the [labels](https://github.com/SAP/cloud-application-security-sample/labels) in order to refer to the sample and to categorize the kind of the issue. 78 | 79 | # License 80 | Copyright (c) 2019-2020 SAP SE or an SAP affiliate company. All rights reserved. 81 | This project is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSES/Apache-2.0.txt) file. 82 | -------------------------------------------------------------------------------- /images/Figure_OAuth2.0_SAP_CP_Components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/cloud-application-security-sample/d6d1727ec2736f025655ed78d4249adfe5010bdb/images/Figure_OAuth2.0_SAP_CP_Components.png -------------------------------------------------------------------------------- /images/some.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /prerequisites/README.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | ## Table of Contents 4 | - [Local Setup](#headline-2) 5 | - [Java 8 JDK](#headline-2.2) 6 | - [Maven](#headline-2.3) 7 | - [Eclipse IDE](#headline-2.4) 8 | - [Cloud Foundry Client](#headline-2.6) 9 | - [Docker](#headline-2.7) 10 | - [Project Setup](#headline-3) 11 | - [Clone Git Repository](#headline-3.1) 12 | - [Import Maven project into Eclipse](#headline-3.2) 13 | - [Rest API Testing Tools](#headline-4) 14 | 15 | 16 | ## Local Setup 17 | In case you like to run the example locally, you need to prepare your local system environment. Like the sample code this comes with no warranty and we can not provide support here. For further details see also [LICENSE](/LICENSE) file. 18 | 19 | 20 | ### Java 8 JDK 21 | - Install the latest version of the [Java JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html) on your machine (at least Java 8). 22 | - Windows: 23 | - Run the console `cmd` and enter `setx JAVA_HOME "C:\Program Files\Java\jdk1.8.X"` (replace the path with the path leading to your JDK installation) 24 | - To test your installation, open a new console and run both `"%JAVA_HOME%\bin\java" -version` and `java -version`. Both should return "java version 1.8.X". 25 | - MacOSX: nothing to do, env variables should be adjusted automatically. 26 | 27 | 28 | ### Maven 29 | The builds of the individual microservices are managed using Apache Maven. 30 | 31 | Install and configure Maven as documented [here](https://maven.apache.org/users/index.html). 32 | 33 | To test your installation, open a new console and run `mvn --version`. 34 | 35 | 36 | 37 | ### Eclipse IDE 38 | An integrated development environment (IDE) is useful for development and experimenting with the code. 39 | We recommend to use Eclipse as the following descriptions are tailored for it. 40 | 41 | - Eclipse IDE 42 | - [Download Eclipse](https://spring.io/tools/eclipse) (select Eclipse IDE for Java EE Developers) 43 | - Unpack the ZIP file to a suitable location on your computer, e.g. `C:\dev\eclipse` 44 | - Assign installed Java JRE to Eclipse: `Window` - `Preferences`, type `jre` in filter, in `Installed JREs`, select `Add...`->`Standard VM` and enter the path to your Java installation. 45 | - Optionally you can configure your proxies within Eclipse as explained [here](https://help.eclipse.org/mars/index.jsp?topic=%2Forg.eclipse.platform.doc.user%2Freference%2Fref-net-preferences.htm). 46 | 47 | > Note: the Community edition of [IntelliJ IDEA](https://www.jetbrains.com/idea/) is an alternative IDE. 48 | 49 | 50 | 51 | ### Cloud Foundry Client 52 | The developed microservices will run on the Cloud Foundry platform. 53 | 54 | - Install the Cloud Foundry Command Line Interface (CLI) following [this](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html) guide. 55 | - Create a account as explained in this tutorial: [SAP Cloud Platform: Get a Free Trial Account in Cloud Foundry environment](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/e3d82674bd68448eb85198619aa99b6d.html#42e7e54590424e65969fced1acd47694.html). 56 | 57 | 58 | 59 | ### Docker 60 | Docker is needed to conveniently start services like PostgreSQL on your local machine. 61 | 62 | Download and install the latest [**Docker for Windows**](https://www.docker.com/docker-windows) or [**Docker for Mac**](https://www.docker.com/docker-mac) release. 63 | See the [Getting Started](https://docs.docker.com/get-started/) documentation if you're not yet familiar with Docker. 64 | 65 | > In case you experience problems with the latest version, you can try to install older versions that are linked in the release notes: [Windows](https://docs.docker.com/docker-for-windows/release-notes/), [Mac](https://docs.docker.com/docker-for-mac/release-notes/). 66 | 67 | > Note: the docker installer automatically enables Hyper-V if you have not done so yet. 68 | This requires a restart that may take several minutes to complete. 69 | 70 | > Note: Hyper-V interferes with VirtualBox (Hyper-V must to be enabled for Docker, but this [crashes VirtualBox](https://www.virtualbox.org/ticket/16801)) 71 | 72 | To start all docker containers required for a sample module, execute `docker-compose up -d` in the directory of the module. 73 | This will run all containers as defined in the `docker-compose.yml` file located at the root of the module. To tear down all containers, execute `docker-compose down`. 74 | 75 | Execute `docker ps` to view all running docker images. 76 | 77 | 78 | 79 | ## Project Setup 80 | Each module is a separate Java project in a separate folder as part of this Git repository. 81 | 82 | 83 | ### Clone Git Repository 84 | The project can be cloned using this the following URL: `git@github.com:SAP/cloud-application-security-sample.git`. 85 | Either use the command line and type `git clone https://github.com/SAP/cloud-application-security-sample.git` or use the Git perspective in Eclipse and choose `Clone a Git Repository`. 86 | 87 | > Note: In case SSH is not working make use of the HTTPS link when cloning the repository. 88 | 89 | 90 | ### Import Maven project into Eclipse 91 | Within Eclipse you need to import the source code. 92 | 93 | 1. Select `File - Import` in the main menu 94 | 2. Select `Maven - Existing Maven Projects` in the dialog 95 | 3. Import the module you want to work on, e.g. `spring-security-acl` by selecting the respective directory and clicking `OK` 96 | 4. Finally, update the Maven Settings of the project by presssing `ALT+F5` and then `OK`. 97 | 98 | 99 | ## REST API Testing Tools 100 | - [Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop) is a Chrome Plugin that helps to create and test custom HTTP requests. 101 | - You might need to install another [`Postman Interceptor` Chrome Plugin](https://chrome.google.com/webstore/detail/postman-interceptor/aicmkgpgakddgnaphhhpliifpcfhicfo), which will help you to send requests which uses browser cookies through the `Postman` app. 102 | -------------------------------------------------------------------------------- /spring-security-acl/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | postgres: #https://hub.docker.com/_/postgres/ 5 | restart: always 6 | image: postgres:9.6 7 | ports: 8 | - "5432:5432" 9 | environment: 10 | - POSTGRES_USER=testuser 11 | - POSTGRES_DB=test 12 | - POSTGRES_PASSWORD=test123! -------------------------------------------------------------------------------- /spring-security-acl/documentation/images/ACL_DatabaseTables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/cloud-application-security-sample/d6d1727ec2736f025655ed78d4249adfe5010bdb/spring-security-acl/documentation/images/ACL_DatabaseTables.png -------------------------------------------------------------------------------- /spring-security-acl/documentation/images/CreateRole.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/cloud-application-security-sample/d6d1727ec2736f025655ed78d4249adfe5010bdb/spring-security-acl/documentation/images/CreateRole.jpg -------------------------------------------------------------------------------- /spring-security-acl/documentation/testing/spring-acl-cloudfoundry.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": [], 3 | "info": { 4 | "name": "CPApplicationSecurity-CF", 5 | "_postman_id": "8e7e3ccb-8a80-3f64-035f-1adfafa489a7", 6 | "description": "", 7 | "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" 8 | }, 9 | "item": [ 10 | { 11 | "name": "/ads/actuator/health", 12 | "request": { 13 | "url": "{{approuterUri}}/ads/actuator/health", 14 | "method": "GET", 15 | "header": [], 16 | "body": { 17 | "mode": "raw", 18 | "raw": "{\n\t\"title\":\"test\",\n\t\"price\": \"40\",\n\t\"contact\": \"myemail\"\n}" 19 | }, 20 | "description": "" 21 | }, 22 | "response": [] 23 | }, 24 | { 25 | "name": "/ads/api/v1/ads/acl/my", 26 | "request": { 27 | "url": "{{approuterUri}}/ads/api/v1/ads/acl/my", 28 | "method": "GET", 29 | "header": [], 30 | "body": {}, 31 | "description": "" 32 | }, 33 | "response": [] 34 | }, 35 | { 36 | "name": "Fetch X-Csrf-Token for POST / PUT", 37 | "request": { 38 | "url": "{{approuterUri}}", 39 | "method": "GET", 40 | "header": [ 41 | { 42 | "key": "x-csrf-token", 43 | "value": "fetch", 44 | "description": "" 45 | } 46 | ], 47 | "body": {}, 48 | "description": "Prerequisite: Activate Postman Interceptor\n\nEnter the received JSESSIONID in the \"Cookie\" header" 49 | }, 50 | "response": [] 51 | }, 52 | { 53 | "name": "/ads/api/v1/ads/acl", 54 | "request": { 55 | "url": "{{approuterUri}}/ads/api/v1/ads/acl", 56 | "method": "POST", 57 | "header": [ 58 | { 59 | "key": "Content-Type", 60 | "value": "application/json", 61 | "description": "" 62 | }, 63 | { 64 | "key": "x-csrf-token", 65 | "value": "", 66 | "description": "", 67 | "disabled": true 68 | } 69 | ], 70 | "body": { 71 | "mode": "raw", 72 | "raw": "{\n\t\"title\":\"test\",\n\t\"price\": \"40\",\n\t\"contact\": \"myemail\"\n}" 73 | }, 74 | "description": "" 75 | }, 76 | "response": [] 77 | }, 78 | { 79 | "name": "/.../acl/grantPermissionsToUserGroup/{id}", 80 | "request": { 81 | "url": "{{approuterUri}}/ads/api/v1/ads/acl/grantPermissionsToUserGroup/{id}", 82 | "method": "PUT", 83 | "header": [ 84 | { 85 | "key": "Content-Type", 86 | "value": "application/json", 87 | "description": "" 88 | }, 89 | { 90 | "key": "x-csrf-token", 91 | "value": "csrfProtection", 92 | "description": "", 93 | "disabled": true 94 | } 95 | ], 96 | "body": { 97 | "mode": "raw", 98 | "raw": "{\n\t\"name\":\"UG_MY_TEAM\",\n\t\"permissionCodes\":[\"R\"]\n}" 99 | }, 100 | "description": "" 101 | }, 102 | "response": [] 103 | }, 104 | { 105 | "name": "/.../acl/grantPermissionsToUser/{id}", 106 | "request": { 107 | "url": "{{approuterUri}}/ads/api/v1/ads/acl/grantPermissionsToUser/{id}", 108 | "method": "PUT", 109 | "header": [ 110 | { 111 | "key": "Content-Type", 112 | "value": "application/json", 113 | "description": "" 114 | }, 115 | { 116 | "key": "x-csrf-token", 117 | "value": "csrfProtection", 118 | "description": "", 119 | "disabled": true 120 | } 121 | ], 122 | "body": { 123 | "mode": "raw", 124 | "raw": "{\n\t\"name\":\"myfriend\",\n\t\"permissionCodes\":[\"R\", \"A\"]\n}" 125 | }, 126 | "description": "Publish to Group, specify permission(s)" 127 | }, 128 | "response": [] 129 | }, 130 | { 131 | "name": "/.../acl/removePermissionsFromUser/{id}", 132 | "request": { 133 | "url": "{{approuterUri}}/ads/acl/removePermissionsFromUser/{id}", 134 | "method": "PUT", 135 | "header": [ 136 | { 137 | "key": "Content-Type", 138 | "value": "application/json", 139 | "description": "" 140 | }, 141 | { 142 | "key": "x-csrf-token", 143 | "value": "", 144 | "description": "", 145 | "disabled": true 146 | } 147 | ], 148 | "body": { 149 | "mode": "raw", 150 | "raw": "{\n\t\"name\":\"myfriend\",\n\t\"permissionCodes\":[\"R\"]\n}" 151 | }, 152 | "description": "Publish to Group, specify permission(s)" 153 | }, 154 | "response": [] 155 | } 156 | ] 157 | } -------------------------------------------------------------------------------- /spring-security-acl/documentation/testing/spring-acl-local.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "9ec1dfbb-daee-713e-37be-0e46052581d0", 3 | "name": "spring-acl-local", 4 | "values": [ 5 | { 6 | "enabled": true, 7 | "key": "AUTH_myfriend", 8 | "value": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzMxOTUvdG9rZW5fa2V5cyIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkifQ.eyJleHRfYXR0ciI6eyJ6ZG4iOiIifSwiemlkIjoidWFhIiwiemRuIjoiIiwiZ3JhbnRfdHlwZSI6InVybjppZXRmOnBhcmFtczpvYXV0aDpncmFudC10eXBlOnNhbWwyLWJlYXJlciIsInhzLnVzZXIuYXR0cmlidXRlcyI6eyJidWxsZXRpbmJvYXJkIjpbIkRFX1dERjAzX0JvYXJkIl19LCJ1c2VyX25hbWUiOiJteWZyaWVuZCIsIm9yaWdpbiI6InVzZXJJZHAiLCJleHAiOjY5NzQwMzE2MDAsImlhdCI6MTU2MjMzODEzNCwiZW1haWwiOiJteWZyaWVuZEB0ZXN0Lm9yZyIsImNpZCI6InNiLWJ1bGxldGluYm9hcmQhdDQwMCJ9.b50Qa0-aXk9Uxj7J2GfVKOpu6kqwfxf-yc6DDgYVFXSvpASDQ74iPNEhLR4njxdBngLtirJZ0v5TaO9pYycK25ZARGK4sihmbzvMXm65BpBUBi7YTnRi293hsd-n72PWwo1Z7lji45WArvZ_5UF4BublRwAWpvQ9_mTr8CVlPw41d96ubuknxvjGhtNntJBv2KN4VV4PZV2K20WDw1ZchvtHyxDblFFS3k6xqxsYOWCbwv2mTfrOka_PADB4jyCcqzxEfx3Gp57SXdQ1HSD4Z5iEIdqRmx6pGOWRNMMbLNOJbCw_Fsi-EyKSaW2IxOTApkwc-UcQPebwdc4dNWEuCA", 9 | "type": "text" 10 | }, 11 | { 12 | "enabled": true, 13 | "key": "AUTH_advertiser", 14 | "value": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzMxOTUvdG9rZW5fa2V5cyIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkifQ.eyJleHRfYXR0ciI6eyJ6ZG4iOiIifSwiemlkIjoidWFhIiwiemRuIjoiIiwiZ3JhbnRfdHlwZSI6InVybjppZXRmOnBhcmFtczpvYXV0aDpncmFudC10eXBlOnNhbWwyLWJlYXJlciIsInhzLnVzZXIuYXR0cmlidXRlcyI6eyJsb2NhdGlvbiI6WyJERSJdLCJidWxsZXRpbmJvYXJkIjpbIklMX1JBQTAzX0JvYXJkIl19LCJ1c2VyX25hbWUiOiJhZE93bmVyIiwib3JpZ2luIjoidXNlcklkcCIsImV4cCI6Njk3NDAzMTYwMCwiaWF0IjoxNTYyMzM4MDM5LCJlbWFpbCI6ImFkT3duZXJAdGVzdC5vcmciLCJjaWQiOiJzYi1idWxsZXRpbmJvYXJkIXQ0MDAifQ.AhLzE1GXeIIZVC0XIg7dGQWItYXIIqJThGAUJDp24xsIyfMk8uOUOCGvYRQgdcY9gAEIUgMQjLTVBTvfU0lQWt78_D0cR3dhvUD9vT-MRkSjA08kjl5NTuaXTCInYmeCpbQ1fuIpZV5rhAvNyhaJalwLSnq90ANJV8_dnLjx950dhOQ5dvqJJ8oKu-U-pB7qmutFz8QiDVuiYKLblaDOlvfrKJxYmKPUozR2_nuGIHVHSs6vjEiVk_mJLcPH5UajTg5CDFmiq_2DcZUSMWoUzBUvRRdal7guCJYmAv-fR40Nya9Gh2HQ1E9vccMVYZTccLXaHf6EBOhlsafD1rvPLQ", 15 | "type": "text" 16 | }, 17 | { 18 | "enabled": true, 19 | "key": "AUTH_myTeamMember", 20 | "value": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzMxOTUvdG9rZW5fa2V5cyIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkifQ.eyJleHRfYXR0ciI6eyJ6ZG4iOiIifSwiemlkIjoidWFhIiwiemRuIjoiIiwiZ3JhbnRfdHlwZSI6InVybjppZXRmOnBhcmFtczpvYXV0aDpncmFudC10eXBlOnNhbWwyLWJlYXJlciIsInhzLnVzZXIuYXR0cmlidXRlcyI6eyJncm91cCI6WyJVR19NWV9URUFNIl19LCJ1c2VyX25hbWUiOiJncm91cE1lbWJlciIsIm9yaWdpbiI6InVzZXJJZHAiLCJleHAiOjY5NzQwMzE2MDAsImlhdCI6MTU2MjMzODA3NywiZW1haWwiOiJncm91cE1lbWJlckB0ZXN0Lm9yZyIsImNpZCI6InNiLWJ1bGxldGluYm9hcmQhdDQwMCJ9.tXQku3SFF9_S4_FgFy_D-a8eLrkM2mWfPpvZ2IHO5eKs4goO23p6ppsnz5Uz0uCMfK9wGZyUcyc00RsdF27hGLWPCaYmR-K9DOzd1Qj-bZWO4kUlTFNUYvxNP1NSwK7sI5XiDsQ4bmVoQWFaeY-V1xjy-aOGaxVZBQv8GVvMIfLhejaP0RpiQfgpALT3ZFiQiX_zGuDytS3h4ePSdPiD_Z9oA9jSWBN7Ea9MW6UIozKDF-Z32fVTfq8fDrK_OxxNDr8r1AHJmtUaRymydty9_v_WLoPCuqSbhPSCH6qGFb_cLqL4-4eeuRcmm8fH_7ej5bEb-Tp6S3ACAnoT_2Tzxw", 21 | "type": "text" 22 | }, 23 | { 24 | "enabled": true, 25 | "key": "AUTH_viewer_DE_WDF03", 26 | "value": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzMxOTUvdG9rZW5fa2V5cyIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkifQ.eyJleHRfYXR0ciI6eyJ6ZG4iOiIifSwiemlkIjoidWFhIiwiemRuIjoiIiwiZ3JhbnRfdHlwZSI6InVybjppZXRmOnBhcmFtczpvYXV0aDpncmFudC10eXBlOnNhbWwyLWJlYXJlciIsInhzLnVzZXIuYXR0cmlidXRlcyI6eyJidWxsZXRpbmJvYXJkIjpbIkRFX1dERjAzX0JvYXJkIl19LCJ1c2VyX25hbWUiOiJib2FyZFZpZXdlciIsIm9yaWdpbiI6InVzZXJJZHAiLCJleHAiOjY5NzQwMzE2MDAsImlhdCI6MTU2MjMzODE1MSwiZW1haWwiOiJib2FyZFZpZXdlckB0ZXN0Lm9yZyIsImNpZCI6InNiLWJ1bGxldGluYm9hcmQhdDQwMCJ9.JOQH7eLvOyzHPxa_bRMw3ibiPAI7P5TdLDcd6RqgHtkCTnu_NCMYVZBBIYI_IUwpykKZd78fc085Lt2sADKH1J7GZo3DGwrxYd0TgJ34vjJcHd9JFeCRvWMwQyUO_uZWKQJJgPTwy4FK6vC1GPktIsDjRbHBVg-9xbhA_z8jpOkL00K4SWv5n8angCBqurWsLVz-E104Vni4WgUGSRLCuUHEkF00vzp0JwuRoCoTbK7EA-Fbv6ZaQJ9n-zkCO4R-xAVhZNJlglQwYeuvC4LEjZ3qKLE8G1d7Uj4o069nAdY53vfF4fBM91YTqRacUsVxzDrTlrh8csyzC4JNb_QD9g", 27 | "type": "text" 28 | } 29 | ], 30 | "timestamp": 1562338254879, 31 | "_postman_variable_scope": "environment", 32 | "_postman_exported_at": "2019-07-05T15:00:18.874Z", 33 | "_postman_exported_using": "Postman/5.5.4" 34 | } -------------------------------------------------------------------------------- /spring-security-acl/localEnvironmentSetup.bat: -------------------------------------------------------------------------------- 1 | REM This script prepares the current shell's environment variables (not permanently) 2 | 3 | SET SPRING_PROFILES_ACTIVE=cloud,uaamock,localdb 4 | SET VCAP_APPLICATION={} 5 | 6 | -------------------------------------------------------------------------------- /spring-security-acl/localEnvironmentSetup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Hint: run script with 'source localEnvironmentSetup.sh'" 3 | echo "This script prepares the current shell's environment variables (not permanently)" 4 | 5 | export SPRING_PROFILES_ACTIVE='cloud,uaamock,localdb' 6 | export VCAP_APPLICATION='{}' 7 | 8 | 9 | -------------------------------------------------------------------------------- /spring-security-acl/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Configuration: 3 | # configured for EU10. For other landscapes, please adopt LANDSCAPE_APPS_DOMAIN in ../vars.yml 4 | # If the route is occupied, you might need to change ID in ../vars.yml as well 5 | applications: 6 | - name: bulletinboard-ads 7 | instances: 1 8 | memory: 1G 9 | timeout: 360 10 | routes: 11 | - route: bulletinboard-ads-((ID)).((LANDSCAPE_APPS_DOMAIN)) 12 | path: target/demo-application-security-acl.jar 13 | health-check-type: http 14 | health-check-http-endpoint: /actuator/health 15 | env: 16 | # Disable Spring Auto Reconfiguration 17 | JBP_CONFIG_SPRING_AUTO_RECONFIGURATION: '{enabled: false}' 18 | # Use the non-blocking /dev/urandom instead of the default to generate random numbers. 19 | # When using Java community buildpack, increase startup times, especially when using Spring Boot. 20 | JAVA_OPTS: -Djava.security.egd=file:///dev/./urandom 21 | services: 22 | - postgres-bulletinboard-ads 23 | - uaa-bulletinboard 24 | # Application Router as web server 25 | - name: approuter 26 | routes: 27 | - route: approuter-((ID)).((LANDSCAPE_APPS_DOMAIN)) 28 | path: src/main/approuter 29 | memory: 128M 30 | timeout: 360 31 | env: 32 | TENANT_HOST_PATTERN: "^(.*)-approuter-((ID)).((LANDSCAPE_APPS_DOMAIN))" 33 | destinations: > 34 | [{ 35 | "name":"ads-destination", 36 | "url" :"https://bulletinboard-ads-((ID)).((LANDSCAPE_APPS_DOMAIN))", 37 | "forwardAuthToken": true} 38 | ] 39 | services: 40 | - uaa-bulletinboard 41 | -------------------------------------------------------------------------------- /spring-security-acl/security/xs-security.json: -------------------------------------------------------------------------------- 1 | { 2 | "xsappname": "bulletinboard", 3 | "description": "Bulletinboard application", 4 | "tenant-mode": "shared", 5 | "attributes": [ 6 | { 7 | "name": "group", 8 | "description": "Assignment to User groups, the user collaborates with", 9 | "valueType" : "string" 10 | },{ 11 | "name": "bulletinboard", 12 | "description": "Bulletinboards, advertisements are published to", 13 | "valueType" : "string" 14 | },{ 15 | "name": "location", 16 | "description": "Locations, advertisements are published to", 17 | "valueType" : "string" 18 | } 19 | ], 20 | "role-templates": [ 21 | { 22 | "name": "BulletinboardAccessor", 23 | "description": "View and add advertisements to a bulletinboard", 24 | "attribute-references" : [ "bulletinboard" ] 25 | }, 26 | { 27 | "name": "BulletinboardInLocationAccessor", 28 | "description": "View and add advertisements to all bulletinboards that belong to location", 29 | "attribute-references" : [ "location" ] 30 | }, 31 | { 32 | "name": "GroupMember", 33 | "description": "Groups people that likes to collaborate", 34 | "attribute-references" : [ "group" ] 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /spring-security-acl/src/main/approuter/.npmrc: -------------------------------------------------------------------------------- 1 | @sap:registry=https://npm.sap.com -------------------------------------------------------------------------------- /spring-security-acl/src/main/approuter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "approuter", 3 | "dependencies": { 4 | "@sap/approuter": "5.15.0" 5 | }, 6 | "scripts": { 7 | "start": "node node_modules/@sap/approuter/approuter.js" 8 | }, 9 | "engines": { 10 | "node": "10" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/approuter/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Welcome to our Spring Boot sample application! 4 | 5 | It shows how to implement instance-based access control in Spring based SAP Business Technology Platform applications. It leverages Spring Security ACL and integrates to SAP Business Technology Platform XSUAA service using the Java Client Security Library offered by SAP. 6 | 7 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/approuter/xs-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcomeFile": "index.html", 3 | "routes": [{ 4 | "source": "^/ads", 5 | "target": "/", 6 | "destination": "ads-destination", 7 | "csrfProtection": false 8 | }] 9 | } 10 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/Application.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec; 2 | 3 | import com.sap.cloud.security.xsuaa.XsuaaServiceConfigurationDefault; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | 8 | @SpringBootApplication 9 | public class Application { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(Application.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/config/AclAuditLogger.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.security.acls.domain.AclAuthorizationStrategy; 6 | import org.springframework.security.acls.domain.AuditLogger; 7 | import org.springframework.security.acls.model.AccessControlEntry; 8 | import org.springframework.security.acls.model.AuditableAccessControlEntry; 9 | import org.springframework.security.acls.model.AuditableAcl; 10 | import org.springframework.util.Assert; 11 | 12 | /** 13 | * You can specify an audit logger that is able to write audit-relevant logs in case of granted / un-granted access 14 | * to an access control list (ACL) or more precisely to an {@link AuditableAccessControlEntry} (ACE). 15 | * 16 | * Note: by default, an ACL is not configured to audit successful or failed access. 17 | * You need to specify that in context of the {@link AuditableAcl} using updateAuditing method . 18 | * 19 | * By default #logIfNeeded is only called in a few cases, e.g. in case of owner change, 20 | * change of audit log settings or general change ({@link AclAuthorizationStrategy}. 21 | * 22 | * Additionlly note that #logIfNeeded with `isGranted=false` is only called when ACE explicitly specifies "granted=false" 23 | * and not when a permission (ACE) is missing. 24 | * 25 | */ 26 | public class AclAuditLogger implements AuditLogger { 27 | private Logger logger = LoggerFactory.getLogger(getClass()); 28 | 29 | @Override 30 | public void logIfNeeded(boolean isGranted, AccessControlEntry ace) { 31 | Assert.notNull(ace, "AccessControlEntry required"); 32 | 33 | if (ace instanceof AuditableAccessControlEntry) { 34 | AuditableAccessControlEntry auditableAce = (AuditableAccessControlEntry) ace; 35 | 36 | // log only in case ACE configures auditSuccess = true 37 | if (isGranted && auditableAce.isAuditSuccess()) { 38 | logger.info("GRANTED due to ACE: " + ace); 39 | } 40 | // log only in case ACE configures auditFailure = true 41 | if (!isGranted && auditableAce.isAuditFailure()) { 42 | logger.warn("DENIED due to ACE: " + ace); 43 | } 44 | } 45 | } 46 | 47 | public void logGrantPermission(AccessControlEntry ace) { 48 | Assert.notNull(ace, "AccessControlEntry required"); 49 | logger.info("CREATED ACE: " + ace); 50 | } 51 | 52 | public void logRemovePermission(AccessControlEntry ace) { 53 | Assert.notNull(ace, "AccessControlEntry required"); 54 | logger.info("REMOVED ACE: " + ace); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/config/AclConfig.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.config; 2 | 3 | import com.zaxxer.hikari.HikariDataSource; 4 | import org.springframework.cache.ehcache.EhCacheFactoryBean; 5 | import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.security.access.PermissionEvaluator; 9 | import org.springframework.security.acls.AclPermissionEvaluator; 10 | import org.springframework.security.acls.domain.*; 11 | import org.springframework.security.acls.jdbc.BasicLookupStrategy; 12 | import org.springframework.security.acls.jdbc.JdbcMutableAclService; 13 | import org.springframework.security.acls.jdbc.LookupStrategy; 14 | import org.springframework.security.acls.model.AclCache; 15 | import org.springframework.security.acls.model.MutableAclService; 16 | import org.springframework.security.acls.model.PermissionGrantingStrategy; 17 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 18 | 19 | import javax.sql.DataSource; 20 | import java.util.List; 21 | 22 | @Configuration 23 | public class AclConfig { 24 | 25 | @Bean 26 | public MutableAclService aclService(HikariDataSource dataSource, LookupStrategy lookupStrategy, AclCache aclCache) { 27 | JdbcMutableAclService jdbcAclService = new PostgresJdbcMutableAclService( 28 | dataSource, lookupStrategy, aclCache); 29 | 30 | jdbcAclService.setAclClassIdSupported(true); 31 | 32 | if (dataSource.getDriverClassName().equals("org.postgresql.Driver")) { 33 | // because of PostgreSQL as documented here: 34 | // https://docs.spring.io/spring-security/site/docs/current/reference/html5/#postgresql 35 | jdbcAclService.setClassIdentityQuery("select currval(pg_get_serial_sequence('acl_class', 'id'))"); 36 | jdbcAclService.setSidIdentityQuery("select currval(pg_get_serial_sequence('acl_sid', 'id'))"); 37 | } 38 | return jdbcAclService; 39 | } 40 | 41 | @Bean //implements hasPermission annotations 42 | public PermissionEvaluator permissionEvaluator(MutableAclService aclService) { 43 | return new AclPermissionEvaluator(aclService); 44 | } 45 | 46 | @Bean 47 | public LookupStrategy lookupStrategy(DataSource dataSource) { 48 | BasicLookupStrategy strategy = new BasicLookupStrategy( 49 | dataSource, 50 | aclCache(), 51 | aclAuthorizationStrategy(), 52 | new AclAuditLogger() 53 | ); 54 | 55 | strategy.setAclClassIdSupported(true); 56 | strategy.setPermissionFactory(new DefaultPermissionFactory(BasePermission.class)); 57 | return strategy; 58 | } 59 | 60 | @Bean 61 | public AclAuthorizationStrategy aclAuthorizationStrategy() { 62 | return new AclAuthorizationStrategyImpl( 63 | new SimpleGrantedAuthority("ROLE_ACL_ADMIN")); 64 | } 65 | 66 | @Bean 67 | public PermissionGrantingStrategy permissionGrantingStrategy() { 68 | return new DefaultPermissionGrantingStrategy( 69 | new AclAuditLogger()); 70 | } 71 | 72 | // Cache Setup 73 | 74 | @Bean 75 | public AclCache aclCache() { 76 | return new EhCacheBasedAclCache( 77 | aclEhCacheFactoryBean().getObject(), 78 | permissionGrantingStrategy(), 79 | aclAuthorizationStrategy() 80 | ); 81 | } 82 | 83 | @Bean 84 | public EhCacheFactoryBean aclEhCacheFactoryBean() { 85 | EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean(); 86 | ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject()); 87 | ehCacheFactoryBean.setCacheName("acl_cache"); 88 | return ehCacheFactoryBean; 89 | } 90 | 91 | @Bean 92 | public EhCacheManagerFactoryBean aclCacheManager() { 93 | EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean(); 94 | cacheManagerFactoryBean.setShared(true); 95 | return cacheManagerFactoryBean; 96 | } 97 | 98 | 99 | public static class PostgresJdbcMutableAclService extends JdbcMutableAclService { 100 | private String selectSidsWithPrefix = "select acl_sid.sid from acl_sid " 101 | + "where acl_sid.sid like ? and " 102 | + " acl_sid.principal = false"; 103 | 104 | public PostgresJdbcMutableAclService(DataSource dataSource, LookupStrategy lookupStrategy, AclCache aclCache) { 105 | super(dataSource, lookupStrategy, aclCache); 106 | } 107 | 108 | public List getAllSidsWithPrefix(String prefix) { 109 | return jdbcOperations.queryForList(selectSidsWithPrefix, String.class, prefix + "_%"); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/config/MethodSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.config; 2 | 3 | import org.springframework.context.ApplicationContext; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.access.PermissionEvaluator; 6 | import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; 7 | import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; 8 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 9 | import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; 10 | 11 | @Configuration 12 | @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) 13 | class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { 14 | 15 | private ApplicationContext context; 16 | private PermissionEvaluator permissionEvaluator; 17 | 18 | MethodSecurityConfig(ApplicationContext context, PermissionEvaluator permissionEvaluator) { 19 | this.context = context; 20 | this.permissionEvaluator = permissionEvaluator; 21 | } 22 | 23 | @Override 24 | protected MethodSecurityExpressionHandler createExpressionHandler() { 25 | DefaultMethodSecurityExpressionHandler expressionHandler = 26 | new DefaultMethodSecurityExpressionHandler(); 27 | // You can also implement a custom one as explained here 28 | // https://www.baeldung.com/spring-security-create-new-custom-security-expression 29 | expressionHandler.setPermissionEvaluator(permissionEvaluator); 30 | expressionHandler.setApplicationContext(context); 31 | 32 | return expressionHandler; 33 | } 34 | 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/config/PersistenceConfig.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.config; 2 | 3 | import com.sap.cloud.security.xsuaa.token.SpringSecurityContext; 4 | import com.sap.cloud.security.xsuaa.token.Token; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.data.domain.AuditorAware; 10 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 11 | 12 | import java.util.Optional; 13 | 14 | @Configuration 15 | @EnableJpaAuditing 16 | public class PersistenceConfig { 17 | @Bean 18 | AuditorAware auditorProvider() { 19 | return new AuditorAwareImpl(); 20 | } 21 | 22 | private static class AuditorAwareImpl implements AuditorAware { 23 | 24 | @Override 25 | public Optional getCurrentAuditor() { 26 | String user; 27 | Logger logger = LoggerFactory.getLogger(getClass()); 28 | 29 | Token token = SpringSecurityContext.getToken(); 30 | user = token.getLogonName(); 31 | logger.info("token for user {} initialized", user); 32 | return Optional.ofNullable(user); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.config; 2 | 3 | import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration; 4 | import com.sap.cloud.security.xsuaa.token.TokenAuthenticationConverter; 5 | import com.sap.cp.appsec.domain.AclAttribute; 6 | import com.sap.cp.appsec.security.CustomTokenAuthorizationsExtractor; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.core.convert.converter.Converter; 10 | import org.springframework.security.authentication.AbstractAuthenticationToken; 11 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 14 | import org.springframework.security.config.http.SessionCreationPolicy; 15 | import org.springframework.security.oauth2.jwt.Jwt; 16 | 17 | @Configuration 18 | @EnableWebSecurity 19 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 20 | 21 | @Autowired 22 | private XsuaaServiceConfiguration xsuaaServiceConfiguration; 23 | 24 | // configure Spring Security, demand authentication and specific scopes 25 | @Override 26 | public void configure(HttpSecurity http) throws Exception { 27 | 28 | // @formatter:off 29 | http 30 | .sessionManagement() 31 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 32 | .and() 33 | .authorizeRequests() 34 | // enable OAuth2 checks 35 | .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/v2/api-docs", "/webjars/**", "/api-docs/**").permitAll() // this should allow swagger to operate 36 | .antMatchers("/**/*.js", "/**/*.json", "/**/*.xml", "/**/*.html").permitAll() 37 | // acl endpoints 38 | .antMatchers("/api/v1/ads/acl/**").authenticated() 39 | // public endpoints 40 | .antMatchers("/actuator/**").permitAll() 41 | .antMatchers("/api/v1/attribute/**").permitAll() 42 | .anyRequest().denyAll() // deny anything not configured above 43 | .and() 44 | .oauth2ResourceServer() 45 | .jwt() 46 | .jwtAuthenticationConverter(getJwtAuthoritiesConverter()); // customizes how GrantedAuthority s are derived from a Jwt 47 | // @formatter:on 48 | } 49 | 50 | /** 51 | * Customizes how GrantedAuthority are derived from a Jwt 52 | */ 53 | Converter getJwtAuthoritiesConverter() { 54 | return new TokenAuthenticationConverter(new CustomTokenAuthorizationsExtractor(xsuaaServiceConfiguration.getAppId(), AclAttribute.values())); 55 | } 56 | } -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/controllers/AdvertisementAclController.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.controllers; 2 | 3 | import com.sap.cloud.security.xsuaa.token.Token; 4 | import com.sap.cp.appsec.domain.Advertisement; 5 | import com.sap.cp.appsec.dto.*; 6 | import com.sap.cp.appsec.exceptions.BadRequestException; 7 | import com.sap.cp.appsec.services.AdvertisementService; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.data.domain.Page; 12 | import org.springframework.data.domain.Sort; 13 | import org.springframework.http.HttpHeaders; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 17 | import org.springframework.validation.annotation.Validated; 18 | import org.springframework.web.bind.annotation.*; 19 | import org.springframework.web.util.UriComponents; 20 | import org.springframework.web.util.UriComponentsBuilder; 21 | 22 | import javax.validation.Valid; 23 | import javax.validation.constraints.Min; 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | 27 | import static org.springframework.http.HttpStatus.NO_CONTENT; 28 | 29 | @RestController 30 | @RequestMapping(AdvertisementAclController.PATH) 31 | @Validated 32 | public class AdvertisementAclController { 33 | static final String PATH = "/api/v1/ads/acl"; 34 | private final AdvertisementService service; 35 | 36 | private static final String PATH_PAGES = PATH + "/my/pages/"; 37 | public static final int FIRST_PAGE_ID = 0; 38 | public static final int DEFAULT_PAGE_SIZE = 20; // allows server side optimization e.g. via caching 39 | 40 | private final Logger logger = LoggerFactory.getLogger(getClass()); 41 | 42 | 43 | @Autowired 44 | public AdvertisementAclController(AdvertisementService adsService) { 45 | this.service = adsService; 46 | } 47 | 48 | @PostMapping 49 | public ResponseEntity create(@RequestBody @Valid AdvertisementDto advertisement, 50 | UriComponentsBuilder uriComponentsBuilder) { 51 | Advertisement savedAds = service.create(advertisement.toEntity()); 52 | 53 | UriComponents uriComponents = uriComponentsBuilder.path(PATH + "/{id}") 54 | .buildAndExpand(savedAds.getId()); 55 | HttpHeaders headers = new HttpHeaders(); 56 | headers.setLocation(uriComponents.toUri()); 57 | 58 | return new ResponseEntity<>(new AdvertisementDto(savedAds), headers, HttpStatus.CREATED); 59 | } 60 | 61 | @GetMapping("/hello-token") 62 | public Map message(@AuthenticationPrincipal Token token) { 63 | Map result = new HashMap<>(); 64 | result.put("grant type", token.getGrantType()); 65 | result.put("client id", token.getClientId()); 66 | result.put("tenant id", token.getSubaccountId()); 67 | result.put("logon name", token.getLogonName()); 68 | result.put("family name", token.getFamilyName()); 69 | result.put("given name", token.getGivenName()); 70 | result.put("email", token.getEmail()); 71 | result.put("token", token.getAppToken()); 72 | result.put("authorizations", token.getAuthorities().toString()); 73 | 74 | return result; 75 | } 76 | 77 | @PutMapping("/{id}") 78 | public AdvertisementDto update(@RequestBody AdvertisementDto updatedAdvertisement, @PathVariable("id") Long id) { 79 | throwIfInconsistent(id, updatedAdvertisement.getId()); 80 | 81 | Advertisement updatedAds = service.update(updatedAdvertisement.toEntity()); 82 | logger.trace("updated ad with version {}", updatedAdvertisement.metadata.version); 83 | return new AdvertisementDto(updatedAds); 84 | } 85 | 86 | @GetMapping("/{id}") 87 | public AdvertisementDto read(@PathVariable("id") @Min(0) Long id) { 88 | AdvertisementDto advertisement = new AdvertisementDto(service.findById(id)); 89 | logger.trace("returning: {}", advertisement); 90 | return advertisement; 91 | } 92 | 93 | /** 94 | * Read all my advertisements, I'm directly authorized to as owner, delegate, admin. 95 | */ 96 | @GetMapping("/my") 97 | public ResponseEntity readAll() { 98 | return readMyAdvertisementsPage(FIRST_PAGE_ID, DEFAULT_PAGE_SIZE); 99 | } 100 | 101 | @GetMapping("/my/pages/{pageId}") 102 | public ResponseEntity readPage(@PathVariable("pageId") int pageId) { 103 | return readMyAdvertisementsPage(pageId, DEFAULT_PAGE_SIZE); 104 | } 105 | 106 | @PutMapping("/grantPermissionsToUser/{id}") 107 | public void grantPermissionsToUser(@PathVariable("id") @Min(0) Long id, @RequestBody PermissionDto userPermission) { 108 | service.grantPermissions(id, userPermission.name, userPermission.getPermissions()); 109 | } 110 | 111 | @PutMapping("/removePermissionsFromUser/{id}") 112 | public void removePermissionsFromUser(@PathVariable("id") @Min(0) Long id, @RequestBody PermissionDto userPermission) { 113 | service.removePermissions(id, userPermission.name, userPermission.getPermissions()); 114 | } 115 | 116 | @PutMapping("/grantPermissionsToUserGroup/{id}") 117 | public void grantPermissionsToUserGroup(@PathVariable("id") @Min(0) Long id, @RequestBody PermissionDto groupPermission) { 118 | service.grantPermissionsToUserGroup(id, groupPermission.name, groupPermission.getPermissions()); 119 | } 120 | 121 | 122 | /** 123 | * Publishes advertisement to a board, 124 | * so that all that have access to board or to all boards in location can read it. 125 | */ 126 | @PutMapping("/publish/{id}") 127 | public void publishToBoard(@PathVariable("id") @Min(0) Long id, @RequestBody BulletinboardDto bulletinboard) { 128 | service.publishToBulletinboard(id, bulletinboard.name); 129 | } 130 | 131 | /** 132 | * Read all published advertisements, I have directly (owner, delegate) 133 | * and indirectly read access rights to (attribute: location, board,...). 134 | */ 135 | @GetMapping("/published") 136 | public ResponseEntity readAllPublished() { 137 | return readPublishedAdvertisementsPage(FIRST_PAGE_ID, DEFAULT_PAGE_SIZE); 138 | } 139 | 140 | @DeleteMapping("{id}") 141 | @ResponseStatus(NO_CONTENT) 142 | public void deleteById(@PathVariable("id") Long id) { 143 | service.deleteById(id); 144 | } 145 | 146 | private ResponseEntity readMyAdvertisementsPage(int pageId, int pageSize) { 147 | Page page = service.findAll(pageId, pageSize, Sort.Direction.DESC, new String[]{"ads.id"}); 148 | 149 | return new ResponseEntity<>(new AdvertisementListDto(page.getContent()), 150 | PageHeaderBuilder.createLinkHeader(page, PATH_PAGES), HttpStatus.OK); 151 | } 152 | 153 | private ResponseEntity readPublishedAdvertisementsPage(int pageId, int pageSize) { 154 | Page page = service.findAllPublished(pageId, pageSize, Sort.Direction.DESC, new String[]{"ads.id"}); 155 | 156 | return new ResponseEntity<>(new AdvertisementListDto(page.getContent()), 157 | PageHeaderBuilder.createLinkHeader(page, PATH_PAGES), HttpStatus.OK); 158 | } 159 | 160 | private void throwIfInconsistent(Long expected, Long actual) { 161 | if (!expected.equals(actual)) { 162 | String message = String.format( 163 | "bad request, inconsistent IDs between request and object: request id = %d, object id = %d", 164 | expected, actual); 165 | throw new BadRequestException(message); 166 | } 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/controllers/AttributeFinderController.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.controllers; 2 | 3 | import com.sap.cp.appsec.domain.AclAttribute; 4 | import com.sap.cp.appsec.security.AclSupport; 5 | import org.springframework.validation.annotation.Validated; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | @RestController 15 | @Validated 16 | @RequestMapping(AttributeFinderController.PATH) 17 | public class AttributeFinderController { 18 | 19 | static final String PATH = "/api/v1/attribute"; 20 | public static final String ATTRIBUTE_GROUP = "group"; 21 | public static final String ATTRIBUTE_BULLETINBOARD = "bulletinboard"; 22 | public static final String ATTRIBUTE_LOCATION = "location"; 23 | 24 | private final AclSupport aclSupport; 25 | 26 | public AttributeFinderController(AclSupport aclSupport) { 27 | this.aclSupport = aclSupport; 28 | } 29 | 30 | @GetMapping("/{ATTRIBUTE_NAME}") 31 | public List getAllValuesForAttribute(@PathVariable("ATTRIBUTE_NAME") String attributeName) { 32 | List attributeValues = new ArrayList<>(); 33 | switch (attributeName) { 34 | case ATTRIBUTE_GROUP: 35 | attributeValues = getAclAttributeValues(AclAttribute.GROUP); 36 | break; 37 | case ATTRIBUTE_BULLETINBOARD: 38 | attributeValues = getAclAttributeValues(AclAttribute.BULLETINBOARD); 39 | break; 40 | case ATTRIBUTE_LOCATION: 41 | attributeValues = getAclAttributeValues(AclAttribute.LOCATION); 42 | break; 43 | default: 44 | break; 45 | } 46 | return attributeValues; 47 | } 48 | 49 | private List getAclAttributeValues(AclAttribute aclAttribute) { 50 | List attributeValues = new ArrayList<>(); 51 | 52 | for (String sid : aclSupport.getAllSidsWithPrefix(aclAttribute.getSidPrefix())) { 53 | String attributeValue = sid.replace(aclAttribute.getSidPrefix(), ""); 54 | attributeValues.add(attributeValue); 55 | } 56 | return attributeValues; 57 | } 58 | 59 | @GetMapping 60 | public List getAllAttributes() { 61 | List attributes = new ArrayList<>(); 62 | attributes.add(ATTRIBUTE_GROUP); 63 | attributes.add(ATTRIBUTE_BULLETINBOARD); 64 | attributes.add(ATTRIBUTE_LOCATION); 65 | return attributes; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/controllers/CustomExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.controllers; 2 | 3 | import com.sap.cp.appsec.dto.ErrorDto; 4 | import com.sap.cp.appsec.exceptions.BadRequestException; 5 | import com.sap.cp.appsec.exceptions.NotAuthorizedException; 6 | import com.sap.cp.appsec.exceptions.NotFoundException; 7 | import org.springframework.http.HttpHeaders; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.security.access.AccessDeniedException; 11 | import org.springframework.transaction.TransactionSystemException; 12 | import org.springframework.web.bind.MethodArgumentNotValidException; 13 | import org.springframework.web.bind.annotation.ExceptionHandler; 14 | import org.springframework.web.bind.annotation.RestControllerAdvice; 15 | import org.springframework.web.context.request.WebRequest; 16 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 17 | 18 | import javax.validation.ConstraintViolation; 19 | import javax.validation.ConstraintViolationException; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | import static com.sap.cp.appsec.dto.ErrorDto.DetailError; 24 | 25 | 26 | /** 27 | * A simple exception mapper for exceptions that also provides the error messages as part of the response. Gathers 28 | * all @ExceptionHandler methods in a single class so that exceptions from all controllers are handled consistently in 29 | * one place. 30 | */ 31 | @RestControllerAdvice 32 | public class CustomExceptionMapper extends ResponseEntityExceptionHandler { 33 | 34 | @ExceptionHandler 35 | public ResponseEntity handleConstraintViolationException(ConstraintViolationException exception, 36 | WebRequest request) { 37 | List errors = new ArrayList<>(); 38 | for (ConstraintViolation violation : exception.getConstraintViolations()) { 39 | String msg = violation.getRootBeanClass().getSimpleName() + " " + violation.getPropertyPath() + ": " 40 | + violation.getMessage() + " [current value = " + violation.getInvalidValue() + "]"; 41 | 42 | errors.add(new DetailError(msg)); 43 | } 44 | 45 | ErrorDto apiError = new ErrorDto(HttpStatus.BAD_REQUEST, exception.getLocalizedMessage(), request, 46 | errors.toArray(new DetailError[0])); 47 | 48 | return new ResponseEntity<>(apiError, new HttpHeaders(), HttpStatus.BAD_REQUEST); 49 | } 50 | 51 | @ExceptionHandler 52 | public ResponseEntity handleWrappedConstraintViolationException(TransactionSystemException exception, 53 | WebRequest request) throws Exception { 54 | if (exception.getRootCause() instanceof ConstraintViolationException) { 55 | ConstraintViolationException constraintException = (ConstraintViolationException) exception.getRootCause(); 56 | return handleConstraintViolationException(constraintException, request); 57 | } else { 58 | return handleException(exception, request); 59 | } 60 | } 61 | 62 | /** 63 | * Here we have to override implementation of ResponseEntityExceptionHandler. 64 | */ 65 | @Override 66 | public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException exception, 67 | HttpHeaders headers, HttpStatus status, WebRequest request) { 68 | return convertToResponseEntity(exception, HttpStatus.BAD_REQUEST, request); 69 | } 70 | 71 | @ExceptionHandler 72 | public ResponseEntity handleBadRequestException(BadRequestException exception, WebRequest request) { 73 | return convertToResponseEntity(exception, HttpStatus.BAD_REQUEST, request); 74 | } 75 | 76 | @ExceptionHandler 77 | public ResponseEntity handleNotFoundException(NotFoundException exception, WebRequest request) { 78 | return convertToResponseEntity(exception, HttpStatus.NOT_FOUND, request); 79 | } 80 | 81 | @ExceptionHandler 82 | public ResponseEntity handleNotAuthorizedException(NotAuthorizedException exception, WebRequest request) { 83 | return convertToResponseEntity(exception, HttpStatus.FORBIDDEN, request); 84 | } 85 | 86 | @ExceptionHandler 87 | public ResponseEntity handleNotAuthorizedException(AccessDeniedException exception, WebRequest request) { 88 | return convertToResponseEntity(exception, HttpStatus.FORBIDDEN, request); 89 | } 90 | 91 | @ExceptionHandler 92 | public ResponseEntity handleAll(Exception exception, WebRequest request) { 93 | return convertToResponseEntity(exception, HttpStatus.INTERNAL_SERVER_ERROR, request); 94 | } 95 | 96 | private ResponseEntity convertToResponseEntity(Exception exception, HttpStatus status, WebRequest request) { 97 | ErrorDto apiError = new ErrorDto(status, exception.getLocalizedMessage(), request, 98 | new DetailError(exception.getClass().getSimpleName() + ": error occurred")); 99 | 100 | return new ResponseEntity<>(apiError, new HttpHeaders(), status); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/domain/AclAttribute.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.domain; 2 | 3 | import com.sap.cp.appsec.controllers.AttributeFinderController; 4 | 5 | public enum AclAttribute { 6 | 7 | GROUP(AttributeFinderController.ATTRIBUTE_GROUP), 8 | BULLETINBOARD(AttributeFinderController.ATTRIBUTE_BULLETINBOARD), 9 | LOCATION(AttributeFinderController.ATTRIBUTE_LOCATION); 10 | 11 | private String attributeName; 12 | 13 | AclAttribute(String attributeName) { 14 | this.attributeName = attributeName; 15 | } 16 | 17 | public String getXSUserAttributeName() { 18 | return this.attributeName; 19 | } 20 | 21 | public String getSidForAttributeValue(String value) { 22 | return this.getSidPrefix() + value; 23 | } 24 | 25 | public String getSidPrefix() { 26 | return "ATTR:" + this.attributeName.toUpperCase() + "="; 27 | } 28 | 29 | public String getAttributeName() { 30 | return this.attributeName; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/domain/Advertisement.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Table; 6 | import javax.validation.constraints.NotBlank; 7 | import javax.validation.constraints.NotNull; 8 | 9 | 10 | @Entity 11 | @Table(name = "advertisement") 12 | public class Advertisement extends BaseEntity { 13 | 14 | /** 15 | * mandatory fields 16 | **/ 17 | @NotBlank 18 | @Column(name = "title") 19 | private String title; 20 | 21 | @NotBlank 22 | @Column(name = "contact") 23 | private String contact; 24 | 25 | @NotNull 26 | @Column(name = "is_published") 27 | private boolean isPublished; 28 | 29 | /** 30 | * Any JPA Entity needs a default constructor. 31 | */ 32 | public Advertisement() { 33 | } 34 | 35 | public Advertisement(String title, String contact) { 36 | this.title = title; 37 | this.contact = contact; 38 | } 39 | 40 | public String getTitle() { 41 | return title; 42 | } 43 | 44 | public void setTitle(String title) { 45 | this.title = title; 46 | } 47 | 48 | public String getContact() { 49 | return contact; 50 | } 51 | 52 | public boolean isPublished() { 53 | return isPublished; 54 | } 55 | 56 | public void setPublished(boolean isPublished) { 57 | this.isPublished = isPublished; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return "Advertisement [id=" + id + ", title=" + title + "]"; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/domain/AdvertisementAclRepository.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.domain; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.PagingAndSortingRepository; 7 | import org.springframework.data.repository.query.Param; 8 | 9 | import java.util.List; 10 | 11 | public interface AdvertisementAclRepository extends PagingAndSortingRepository { 12 | 13 | String FIND_ADS_FOR_SID_SUBQUERY = 14 | "FROM ACL_OBJECT_IDENTITY obj " + 15 | "INNER JOIN ACL_ENTRY entry ON entry.acl_object_identity = obj.id " + 16 | "INNER JOIN ACL_SID sid ON entry.sid = sid.id " + 17 | "INNER JOIN ADVERTISEMENT ads ON CAST(obj.object_id_identity as bigint) = ads.id " + 18 | "WHERE sid.sid IN :sid " + 19 | "AND (entry.mask = :mask) " + 20 | "AND entry.granting = true " + 21 | "AND obj.object_id_class = (SELECT id FROM ACL_CLASS WHERE acl_class.class = 'com.sap.cp.appsec.domain.Advertisement')"; 22 | 23 | String GET_ALL_ACCESSIBLE_OBJECTS_RECURSIVE_CTE = 24 | "WITH RECURSIVE accessibleObjects (id, object_id_class, object_id_identity, parent_object) AS " + 25 | "( SELECT id, " + 26 | "object_id_class, " + 27 | "object_id_identity, " + 28 | "parent_object " + 29 | " FROM ACL_OBJECT_IDENTITY " + 30 | " WHERE id IN (SELECT DISTINCT acl_object_identity " + 31 | "FROM ACL_ENTRY entry " + 32 | "INNER JOIN ACL_SID sid ON entry.sid = sid.id " + 33 | "INNER JOIN ACL_OBJECT_IDENTITY obj ON entry.acl_object_identity = obj.id " + 34 | "WHERE " + 35 | "entry.granting = true " + // AND ENTRIES_INHERITING = true to exclude leafs 36 | "AND entry.mask = :mask " + 37 | "AND sid.sid IN :sid " + 38 | ") " + 39 | " UNION ALL " + 40 | "SELECT parent.id, " + 41 | "parent.object_id_class, " + 42 | "parent.object_id_identity, " + 43 | "parent.parent_object " + 44 | "FROM ACL_OBJECT_IDENTITY parent, accessibleObjects " + 45 | "WHERE parent.parent_object = accessibleObjects.id " + 46 | ") "; 47 | 48 | String FIND_ADS_FOR_SID_IN_HIERARCHY_SUBQUERY = 49 | "FROM accessibleObjects " + 50 | "INNER JOIN ADVERTISEMENT ads ON CAST(object_id_identity AS bigint) = ads.id " + 51 | "WHERE ads.is_published = TRUE " + 52 | "AND object_id_class = ( " + 53 | "SELECT id FROM ACL_CLASS WHERE acl_class.class = 'com.sap.cp.appsec.domain.Advertisement' ) " + 54 | "ORDER BY ads.id DESC "; 55 | 56 | 57 | String SELECT_ADS_FOR_SID_QUERY = "SELECT DISTINCT ads.*" 58 | + FIND_ADS_FOR_SID_SUBQUERY; 59 | 60 | String COUNT_ADS_FOR_SID_QUERY = "SELECT COUNT( DISTINCT ads.id) " + FIND_ADS_FOR_SID_SUBQUERY; 61 | 62 | String SELECT_ADS_FOR_SID_IN_HIERARCHY_QUERY = GET_ALL_ACCESSIBLE_OBJECTS_RECURSIVE_CTE + 63 | "SELECT DISTINCT ads.* " + FIND_ADS_FOR_SID_IN_HIERARCHY_SUBQUERY; 64 | 65 | String COUNT_ADS_FOR_SID_IN_HIERARCHY_QUERY = GET_ALL_ACCESSIBLE_OBJECTS_RECURSIVE_CTE + 66 | "SELECT COUNT(DISTINCT ads.id) " + FIND_ADS_FOR_SID_IN_HIERARCHY_SUBQUERY; 67 | 68 | 69 | @Query(value = SELECT_ADS_FOR_SID_QUERY, countQuery = COUNT_ADS_FOR_SID_QUERY, nativeQuery = true) 70 | Page findAllByPermission(@Param("mask") int permissionCode, @Param("sid") String[] sid, Pageable pageable); 71 | 72 | @Query(value = SELECT_ADS_FOR_SID_IN_HIERARCHY_QUERY, countQuery = COUNT_ADS_FOR_SID_IN_HIERARCHY_QUERY, nativeQuery = true) 73 | Page findAllPublishedByHierarchicalPermission( 74 | @Param("mask") int permissionCode, @Param("sid") String[] sid, Pageable pageable); 75 | 76 | List findByTitle(String title); 77 | } 78 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/domain/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.domain; 2 | 3 | import org.springframework.data.annotation.CreatedBy; 4 | import org.springframework.data.annotation.CreatedDate; 5 | import org.springframework.data.annotation.LastModifiedBy; 6 | import org.springframework.data.annotation.LastModifiedDate; 7 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 8 | 9 | import javax.persistence.*; 10 | import java.sql.Timestamp; 11 | 12 | @MappedSuperclass 13 | @EntityListeners(AuditingEntityListener.class) 14 | public abstract class BaseEntity { 15 | /** 16 | * * technical fields 17 | **/ 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.AUTO) 20 | protected Long id; 21 | 22 | @Version 23 | @Column(name = "version") 24 | protected long version; 25 | 26 | @Column(name = "created_at", nullable = false, updatable = false) 27 | @CreatedDate 28 | protected Timestamp createdAt; 29 | 30 | @Column(name = "modified_at", insertable = false) 31 | @LastModifiedDate 32 | protected Timestamp modifiedAt; 33 | 34 | @Column(name = "created_by", nullable = false, updatable = false) 35 | @CreatedBy 36 | protected String createdBy; 37 | 38 | @Column(name = "modified_by", insertable = false) 39 | @LastModifiedBy 40 | protected String modifiedBy; 41 | 42 | 43 | public Long getId() { 44 | return id; 45 | } 46 | 47 | public long getVersion() { 48 | return version; 49 | } 50 | 51 | public Timestamp getCreatedAt() { 52 | if (createdAt != null) { 53 | return new Timestamp(createdAt.getTime()); 54 | } 55 | return null; 56 | } 57 | 58 | public Timestamp getModifiedAt() { 59 | if (modifiedAt != null) { 60 | return new Timestamp(modifiedAt.getTime()); 61 | } 62 | return null; 63 | } 64 | 65 | public String getCreatedBy() { 66 | return createdBy; 67 | } 68 | 69 | public String getModifiedBy() { 70 | return modifiedBy; 71 | } 72 | 73 | // use only in tests or when you need to map DTO to Entity 74 | public void setId(Long id) { 75 | this.id = id; 76 | } 77 | 78 | // use only in tests or when you need to map DTO to Entity 79 | public void setVersion(long version) { 80 | this.version = version; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/dto/AdvertisementDto.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.dto; 2 | 3 | import com.sap.cp.appsec.domain.Advertisement; 4 | 5 | import javax.validation.constraints.NotBlank; 6 | import javax.validation.constraints.NotNull; 7 | import java.sql.Timestamp; 8 | import java.time.ZoneId; 9 | import java.time.ZonedDateTime; 10 | import java.time.format.DateTimeFormatter; 11 | 12 | 13 | /** 14 | * A Data Transfer Object (DTO) is a data structure without logic. 15 | *

16 | * Note: This class implements also the mapping between DTO and Entity and vice versa 17 | */ 18 | //@SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") 19 | public class AdvertisementDto { 20 | /** 21 | * id is null in case of a new advertisement 22 | **/ 23 | private Long id; 24 | 25 | @NotBlank 26 | @NotNull 27 | public String title; 28 | 29 | @NotBlank 30 | @NotNull 31 | public String contact; 32 | 33 | public boolean isPublished; 34 | 35 | public MetaData metadata = new MetaData(); 36 | 37 | /** 38 | * Default constructor required by Jackson JSON Converter 39 | */ 40 | public AdvertisementDto() { 41 | } 42 | 43 | /** 44 | * Transforms Advertisement entity to DTO 45 | */ 46 | public AdvertisementDto(Advertisement ad) { 47 | this.id = ad.getId(); 48 | this.title = ad.getTitle(); 49 | this.contact = ad.getContact(); 50 | 51 | this.metadata.createdAt = convertToDateTime(ad.getCreatedAt()); 52 | this.metadata.modifiedAt = convertToDateTime(ad.getModifiedAt()); 53 | this.metadata.createdBy = "" + ad.getCreatedBy(); 54 | this.metadata.modifiedBy = "" + ad.getModifiedBy(); 55 | this.metadata.version = ad.getVersion(); 56 | this.isPublished = ad.isPublished(); 57 | } 58 | 59 | public void setId(long id) { 60 | this.id = id; 61 | } 62 | 63 | public Long getId() { 64 | return id; 65 | } 66 | 67 | public Advertisement toEntity() { 68 | // does not map "read-only" attributes 69 | Advertisement ad = new Advertisement(title, contact); 70 | ad.setId(id); 71 | ad.setVersion(metadata.version); 72 | return ad; 73 | } 74 | 75 | private String convertToDateTime(Timestamp timestamp) { 76 | if (timestamp == null) { 77 | return null; 78 | } 79 | ZonedDateTime dateTime = ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneId.systemDefault()); 80 | return dateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); // ISO 8601 81 | } 82 | 83 | public static class MetaData { 84 | public String createdAt; 85 | public String modifiedAt; 86 | public String createdBy; 87 | public String modifiedBy; 88 | 89 | public long version = 0L; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/dto/AdvertisementListDto.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.sap.cp.appsec.domain.Advertisement; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.StreamSupport; 9 | 10 | //@SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") 11 | public class AdvertisementListDto { 12 | @JsonProperty("value") 13 | public List advertisements; 14 | 15 | public AdvertisementListDto(Iterable ads) { 16 | this.advertisements = StreamSupport.stream(ads.spliterator(), false).map(AdvertisementDto::new) 17 | .collect(Collectors.toList()); 18 | } 19 | } -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/dto/BulletinboardDto.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.dto; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.NotNull; 5 | 6 | public class BulletinboardDto { 7 | 8 | @NotBlank 9 | @NotNull 10 | public String name; 11 | 12 | /** 13 | * Default constructor required by Jackson JSON Converter 14 | */ 15 | public BulletinboardDto() { 16 | } 17 | 18 | public BulletinboardDto(String name) { 19 | this.name = name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/dto/ErrorDto.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 4 | import com.fasterxml.jackson.annotation.JsonTypeName; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.context.request.WebRequest; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | /** 12 | * Common structure for an Error Response. 13 | */ 14 | @JsonTypeName("error") 15 | @JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME) 16 | public class ErrorDto { 17 | private HttpStatus status; 18 | private String message; // user-facing (localizable) message, describing the error 19 | private String target; // endpoint of origin request 20 | private List details; 21 | 22 | public ErrorDto(HttpStatus status, String message, WebRequest request, DetailError... errors) { 23 | this.status = status; 24 | this.message = message; 25 | if (message == null) { 26 | this.message = status.getReasonPhrase(); 27 | } 28 | this.details = Arrays.asList(errors); 29 | this.target = request.getDescription(false).substring(4); 30 | } 31 | 32 | public int getStatus() { 33 | return status.value(); 34 | } 35 | 36 | public String getTarget() { 37 | return target; 38 | } 39 | 40 | public String getMessage() { 41 | return message; 42 | } 43 | 44 | public List getDetails() { 45 | return details; 46 | } 47 | 48 | public static class DetailError { 49 | private final String message; // user-facing (localizable) message, describing the error 50 | 51 | public DetailError(String message) { 52 | this.message = message; 53 | } 54 | 55 | public String getMessage() { 56 | return message; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/dto/PageHeaderBuilder.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.dto; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.http.HttpHeaders; 5 | 6 | public class PageHeaderBuilder { 7 | 8 | public static HttpHeaders createLinkHeader(Page page, String path) { 9 | StringBuilder linkHeader = new StringBuilder(); 10 | if (page.hasPrevious()) { 11 | int prevNumber = page.getNumber() - 1; 12 | linkHeader.append("<").append(path).append(prevNumber).append(">; rel=\"previous\""); 13 | if (!page.isLast()) { 14 | linkHeader.append(", "); 15 | } 16 | } 17 | if (page.hasNext()) { 18 | int nextNumber = page.getNumber() + 1; 19 | linkHeader.append("<").append(path).append(nextNumber).append(">; rel=\"next\""); 20 | } 21 | HttpHeaders headers = new HttpHeaders(); 22 | headers.add(HttpHeaders.LINK, linkHeader.toString()); 23 | return headers; 24 | } 25 | } -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/dto/PermissionDto.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import org.springframework.security.acls.domain.BasePermission; 5 | import org.springframework.security.acls.model.Permission; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.util.HashMap; 10 | import java.util.HashSet; 11 | import java.util.Map; 12 | import java.util.Set; 13 | 14 | public class PermissionDto { 15 | 16 | @NotBlank 17 | @NotNull 18 | public String name; 19 | 20 | @NotNull 21 | public Character[] permissionCodes; 22 | 23 | @JsonIgnore 24 | public Map getMappedPermissions() { 25 | Map permissions = new HashMap<>(); 26 | 27 | for (Character permissionCode : permissionCodes) { 28 | switch (permissionCode) { 29 | case 'R': 30 | permissions.put(permissionCode, BasePermission.READ); 31 | break; 32 | case 'W': 33 | permissions.put(permissionCode, BasePermission.WRITE); 34 | break; 35 | case 'D': 36 | permissions.put(permissionCode, BasePermission.DELETE); 37 | break; 38 | case 'A': 39 | permissions.put(permissionCode, BasePermission.ADMINISTRATION); 40 | break; 41 | default: 42 | break; 43 | } 44 | } 45 | return permissions; 46 | } 47 | 48 | @JsonIgnore 49 | public Permission[] getPermissions() { 50 | Set permissions = new HashSet<>(); 51 | 52 | for (Character permissionCode : permissionCodes) { 53 | switch (permissionCode) { 54 | case 'R': 55 | permissions.add(BasePermission.READ); 56 | break; 57 | case 'W': 58 | permissions.add(BasePermission.WRITE); 59 | break; 60 | case 'D': 61 | permissions.add(BasePermission.DELETE); 62 | break; 63 | case 'A': 64 | permissions.add(BasePermission.ADMINISTRATION); 65 | break; 66 | default: 67 | break; 68 | } 69 | } 70 | return permissions.toArray(new Permission[0]); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/exceptions/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @SuppressWarnings("serial") 7 | @ResponseStatus(HttpStatus.BAD_REQUEST) 8 | public class BadRequestException extends RuntimeException { 9 | public BadRequestException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/exceptions/NotAuthorizedException.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @SuppressWarnings("serial") 7 | @ResponseStatus(HttpStatus.FORBIDDEN) // set status code "403" 8 | public class NotAuthorizedException extends RuntimeException { 9 | public NotAuthorizedException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/exceptions/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | // need to define exceptions with response status, not predefined 7 | @SuppressWarnings("serial") 8 | @ResponseStatus(HttpStatus.NOT_FOUND) 9 | public class NotFoundException extends RuntimeException { 10 | public NotFoundException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/mock/XsuaaMockPostProcessor.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.mock; 2 | 3 | import com.sap.cloud.security.xsuaa.mock.XsuaaMockWebServer; 4 | import org.springframework.beans.factory.DisposableBean; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.env.EnvironmentPostProcessor; 7 | import org.springframework.core.env.ConfigurableEnvironment; 8 | import org.springframework.core.env.Profiles; 9 | 10 | public class XsuaaMockPostProcessor implements EnvironmentPostProcessor, DisposableBean { 11 | 12 | private final XsuaaMockWebServer mockAuthorizationServer = new XsuaaMockWebServer(); 13 | 14 | @Override 15 | public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { 16 | if (environment.acceptsProfiles(Profiles.of("uaamock"))) { 17 | environment.getPropertySources().addFirst(this.mockAuthorizationServer); 18 | } 19 | } 20 | 21 | @Override 22 | public void destroy() throws Exception { 23 | this.mockAuthorizationServer.destroy(); 24 | } 25 | } -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/security/AclSupport.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.security; 2 | 3 | import com.sap.cp.appsec.config.AclAuditLogger; 4 | import com.sap.cp.appsec.config.AclConfig.PostgresJdbcMutableAclService; 5 | import org.springframework.security.acls.domain.GrantedAuthoritySid; 6 | import org.springframework.security.acls.domain.ObjectIdentityImpl; 7 | import org.springframework.security.acls.domain.PrincipalSid; 8 | import org.springframework.security.acls.model.*; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.util.Assert; 11 | 12 | import java.io.Serializable; 13 | import java.util.*; 14 | 15 | @Service 16 | public class AclSupport { 17 | 18 | private final PermissionGrantingStrategy permissionGrantingStrategy; 19 | private PostgresJdbcMutableAclService aclService; 20 | private AclAuditLogger aclAuditLogger = new AclAuditLogger(); 21 | 22 | public AclSupport(PostgresJdbcMutableAclService aclService, PermissionGrantingStrategy permissionGrantingStrategy) { 23 | this.aclService = aclService; 24 | this.permissionGrantingStrategy = permissionGrantingStrategy; 25 | } 26 | 27 | public AuditableAcl removePermissionFromUser(String type, Long id, String principal, Permission[] permissions) { 28 | return removePermissions(type, id, new PrincipalSid(principal), permissions); 29 | } 30 | 31 | private AuditableAcl removePermissions(String type, Long id, PrincipalSid principalSid, Permission[] permissions) { 32 | Assert.notEmpty(permissions, "Permission must be not empty"); 33 | AuditableAcl acl = get(type, id); 34 | int index = 0; 35 | for (AccessControlEntry entry : acl.getEntries()) { 36 | boolean deletedEntry = false; 37 | for (Permission permission : permissions) { 38 | if (entry.isGranting() 39 | && entry.getSid().equals(principalSid) 40 | && entry.getPermission().equals(permission)) { 41 | acl.deleteAce(index); 42 | deletedEntry = true; 43 | } 44 | } 45 | index = deletedEntry ? index : index + 1; 46 | if (deletedEntry) { 47 | aclAuditLogger.logRemovePermission(entry); 48 | } 49 | } 50 | return (AuditableAcl) aclService.updateAcl(acl); 51 | } 52 | 53 | 54 | public AuditableAcl grantPermissionsToUser(String type, Long id, String principal, Permission[] permissions) { 55 | return grantPermissions(type, id, new PrincipalSid(principal), permissions); 56 | } 57 | 58 | public AuditableAcl grantPermissionsToSid(String type, Long id, String sidName, Permission[] permissions) { 59 | return grantPermissions(type, id, new GrantedAuthoritySid(sidName), permissions); 60 | } 61 | 62 | private AuditableAcl grantPermissions(String type, Long id, Sid sid, Permission[] permissions) { 63 | Assert.notEmpty(permissions, "Permission must be not empty"); 64 | AuditableAcl acl = getOrCreate(type, id); 65 | Set indices = new HashSet<>(); 66 | for (Permission permission : permissions) { 67 | int index = acl.getEntries().size(); 68 | boolean granting = true; 69 | acl.insertAce(index, permission, sid, granting); 70 | indices.add(index); 71 | } 72 | 73 | for (Integer index : indices) { 74 | acl.updateAuditing(index, true, true); 75 | } 76 | 77 | acl = (AuditableAcl) aclService.updateAcl(acl); 78 | 79 | for (Integer index : indices) { 80 | aclAuditLogger.logGrantPermission(acl.getEntries().get(index)); 81 | } 82 | return acl; 83 | } 84 | 85 | public void setParent(String type, Long id, String parentType, Serializable parentId) { 86 | MutableAcl acl = get(type, id); 87 | Assert.notNull(acl, "Acl (type =" + type + ", id =" + id + ") could not be retrieved"); 88 | 89 | MutableAcl aclParent = get(parentType, parentId); 90 | Assert.notNull(aclParent, "Acl of parent (type =" + parentType + ", id =" + parentId + ") could not be retrieved"); 91 | 92 | acl.setParent(aclParent); 93 | aclService.updateAcl(acl); 94 | } 95 | 96 | private AuditableAcl getOrCreate(String type, Long id) { 97 | AuditableAcl acl = get(type, id); 98 | if (acl == null) { 99 | acl = create(type, id); 100 | } 101 | return acl; 102 | } 103 | 104 | private AuditableAcl create(String type, Long id) { 105 | AuditableAcl acl = (AuditableAcl) aclService.createAcl(new ObjectIdentityImpl(type, id)); 106 | Assert.notNull(acl, "Acl could not be retrieved or created"); 107 | return acl; 108 | } 109 | 110 | private AuditableAcl get(String type, Serializable id) { 111 | try { 112 | return (AuditableAcl) aclService.readAclById(new ObjectIdentityImpl(type, id)); 113 | } catch (NotFoundException exception) { 114 | return null; 115 | } 116 | } 117 | 118 | public boolean hasUserPermission(String type, Long id, String principal, Permission[] permissions) { 119 | try { 120 | Acl acl = aclService.readAclById(new ObjectIdentityImpl(type, id)); 121 | if (acl != null) { 122 | return permissionGrantingStrategy 123 | .isGranted( 124 | acl, 125 | Arrays.asList(permissions), 126 | Collections.singletonList(new PrincipalSid(principal)), 127 | true); 128 | } else { 129 | return false; 130 | } 131 | } catch (NotFoundException exception) { 132 | return false; 133 | } 134 | } 135 | 136 | public List getAllSidsWithPrefix(String sidPrefix) { 137 | return aclService.getAllSidsWithPrefix(sidPrefix); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/security/CustomTokenAuthorizationsExtractor.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.security; 2 | 3 | import java.util.Collection; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | import com.sap.cloud.security.xsuaa.extractor.AuthoritiesExtractor; 8 | import com.sap.cloud.security.xsuaa.extractor.LocalAuthoritiesExtractor; 9 | import com.sap.cloud.security.xsuaa.token.Token; 10 | import com.sap.cloud.security.xsuaa.token.XsuaaToken; 11 | import com.sap.cp.appsec.domain.AclAttribute; 12 | import org.springframework.security.core.GrantedAuthority; 13 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 14 | 15 | public class CustomTokenAuthorizationsExtractor implements AuthoritiesExtractor { 16 | private AuthoritiesExtractor authoritiesExtractor; 17 | protected AclAttribute[] aclAttributes; 18 | 19 | public CustomTokenAuthorizationsExtractor(String appId, AclAttribute... aclAttributes) { 20 | authoritiesExtractor = new LocalAuthoritiesExtractor(appId); 21 | this.aclAttributes = aclAttributes; 22 | } 23 | 24 | public Collection getAuthorities(XsuaaToken jwt) { 25 | Collection authorities = authoritiesExtractor.getAuthorities(jwt); 26 | authorities.addAll(getCustomAuthorities(jwt)); 27 | return authorities; 28 | } 29 | 30 | private Collection getCustomAuthorities(Token token) { 31 | Set newAuthorities = new HashSet<>(); 32 | for (AclAttribute aclAttribute : aclAttributes) { 33 | String[] xsUserAttributeValues = token.getXSUserAttribute(aclAttribute.getXSUserAttributeName()); 34 | if (xsUserAttributeValues != null) { 35 | for (String xsUserAttributeValue : xsUserAttributeValues) { 36 | newAuthorities.add(new SimpleGrantedAuthority(aclAttribute.getSidForAttributeValue(xsUserAttributeValue))); 37 | } 38 | } 39 | } 40 | return newAuthorities; 41 | } 42 | 43 | private static String getSidForAttributeValue(String attributeName, String attributeValue) { 44 | return "ATTR:" + attributeName.toUpperCase() + "=" + attributeValue; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/java/com/sap/cp/appsec/services/AdvertisementService.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.services; 2 | 3 | import com.sap.cloud.security.xsuaa.token.XsuaaToken; 4 | import com.sap.cp.appsec.domain.AclAttribute; 5 | import com.sap.cp.appsec.domain.Advertisement; 6 | import com.sap.cp.appsec.domain.AdvertisementAclRepository; 7 | import com.sap.cp.appsec.exceptions.NotFoundException; 8 | import com.sap.cp.appsec.security.AclSupport; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.domain.Page; 13 | import org.springframework.data.domain.PageRequest; 14 | import org.springframework.data.domain.Sort; 15 | import org.springframework.security.access.prepost.PreAuthorize; 16 | import org.springframework.security.acls.domain.BasePermission; 17 | import org.springframework.security.acls.domain.GrantedAuthoritySid; 18 | import org.springframework.security.acls.domain.PrincipalSid; 19 | import org.springframework.security.acls.domain.SidRetrievalStrategyImpl; 20 | import org.springframework.security.acls.model.Permission; 21 | import org.springframework.security.acls.model.Sid; 22 | import org.springframework.security.core.Authentication; 23 | import org.springframework.security.core.context.SecurityContextHolder; 24 | import org.springframework.stereotype.Service; 25 | import org.springframework.web.bind.annotation.PathVariable; 26 | 27 | import javax.transaction.Transactional; 28 | import javax.validation.constraints.Min; 29 | import java.util.HashSet; 30 | import java.util.List; 31 | import java.util.Optional; 32 | import java.util.Set; 33 | 34 | 35 | @Service 36 | public class AdvertisementService { 37 | 38 | private final AdvertisementAclRepository repository; 39 | 40 | private final AclSupport aclService; 41 | 42 | private final Logger logger = LoggerFactory.getLogger(getClass()); 43 | 44 | 45 | @Autowired 46 | public AdvertisementService(AdvertisementAclRepository repository, 47 | AclSupport aclService) { 48 | this.repository = repository; 49 | this.aclService = aclService; 50 | } 51 | 52 | @Transactional 53 | public Advertisement create(Advertisement newAds) { 54 | Advertisement savedAds = repository.save(newAds); 55 | 56 | String userName = getUniqueCurrentUserName(); 57 | 58 | aclService.grantPermissionsToUser( 59 | Advertisement.class.getName(), 60 | savedAds.getId(), 61 | userName, 62 | new Permission[]{BasePermission.READ, BasePermission.WRITE, BasePermission.ADMINISTRATION}); 63 | 64 | return savedAds; 65 | } 66 | 67 | @PreAuthorize("hasPermission(#id, 'com.sap.cp.appsec.domain.Advertisement', 'read')") 68 | public Advertisement findById(Long id) throws NotFoundException { 69 | Optional advertisement = repository.findById(id); 70 | if (!advertisement.isPresent()) { 71 | NotFoundException notFoundException = new NotFoundException("no Advertisement with id " + id); 72 | logger.warn("request failed", notFoundException); 73 | throw notFoundException; 74 | } 75 | return advertisement.get(); 76 | } 77 | 78 | public Page findAll(int pageNumber, int pageSize, Sort.Direction sortDirection, String[] properties) throws NotFoundException { 79 | MySidRetrievalStrategyImpl sidRetrievalStrategy = new MySidRetrievalStrategyImpl(); 80 | Set sids = sidRetrievalStrategy.getGrantedAuthorities(SecurityContextHolder.getContext().getAuthentication()); 81 | sids.add(sidRetrievalStrategy.getPrincipalSid(SecurityContextHolder.getContext().getAuthentication())); 82 | 83 | PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, new Sort(sortDirection, properties)); 84 | 85 | return repository.findAllByPermission(BasePermission.READ.getMask(), sids.toArray(new String[0]), pageRequest); 86 | } 87 | 88 | public Page findAllPublished(int pageNumber, int pageSize, Sort.Direction sortDirection, String[] properties) throws NotFoundException { 89 | MySidRetrievalStrategyImpl sidRetrievalStrategy = new MySidRetrievalStrategyImpl(); 90 | Set sids = sidRetrievalStrategy.getGrantedAuthorities(SecurityContextHolder.getContext().getAuthentication()); 91 | sids.add(sidRetrievalStrategy.getPrincipalSid(SecurityContextHolder.getContext().getAuthentication())); 92 | 93 | PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, new Sort(sortDirection, properties)); 94 | 95 | return repository.findAllPublishedByHierarchicalPermission(BasePermission.READ.getMask(), sids.toArray(new String[0]), pageRequest); 96 | } 97 | 98 | @PreAuthorize("hasPermission(#updatedAds, 'write')") 99 | public Advertisement update(Advertisement updatedAds) { 100 | assert repository.existsById(updatedAds.getId()); 101 | 102 | return repository.save(updatedAds); 103 | } 104 | 105 | @PreAuthorize("hasPermission(#id, 'com.sap.cp.appsec.domain.Advertisement', 'administration')") 106 | @Transactional 107 | public void grantPermissions(Long id, String principal, Permission[] permissions) { 108 | 109 | aclService.grantPermissionsToUser( 110 | Advertisement.class.getName(), 111 | id, 112 | getUniqueUserName(principal), 113 | permissions); 114 | } 115 | 116 | @PreAuthorize("hasPermission(#id, 'com.sap.cp.appsec.domain.Advertisement', 'administration')") 117 | @Transactional 118 | public void removePermissions(Long id, String principal, Permission[] permissions) { 119 | aclService.removePermissionFromUser( 120 | Advertisement.class.getName(), 121 | id, 122 | getUniqueUserName(principal), 123 | permissions); 124 | } 125 | 126 | @PreAuthorize("hasPermission(#id, 'com.sap.cp.appsec.domain.Advertisement', 'administration')") 127 | @Transactional 128 | public void grantPermissionsToUserGroup(Long id, String groupName, Permission[] permissions) { 129 | aclService.grantPermissionsToSid( 130 | Advertisement.class.getName(), 131 | id, 132 | AclAttribute.GROUP.getSidForAttributeValue(groupName), 133 | permissions); 134 | } 135 | 136 | @PreAuthorize("hasPermission(#id, 'com.sap.cp.appsec.domain.Advertisement', 'administration')" 137 | + " and hasPermission(#boardName, 'bulletinboard', 'read')") 138 | @Transactional 139 | public void publishToBulletinboard(@PathVariable("id") @Min(0) Long id, String boardName) { 140 | 141 | Advertisement ads = findById(id); 142 | 143 | aclService.setParent( 144 | Advertisement.class.getName(), 145 | ads.getId(), 146 | AclAttribute.BULLETINBOARD.getAttributeName(), 147 | boardName); 148 | 149 | ads.setPublished(true); 150 | repository.save(ads); 151 | } 152 | 153 | @PreAuthorize("hasPermission(#id, 'com.sap.cp.appsec.domain.Advertisement', 'administration')") 154 | @Transactional 155 | public void deleteById(Long id) { 156 | } 157 | 158 | 159 | private String getUniqueCurrentUserName() { 160 | Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 161 | return new PrincipalSid(auth).getPrincipal(); 162 | } 163 | 164 | private String getUniqueUserName(String userName) { 165 | Object currentUsersPrincipal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 166 | String origin = currentUsersPrincipal instanceof XsuaaToken ? ((XsuaaToken) currentUsersPrincipal).getOrigin() : null; 167 | return origin == null ? userName : XsuaaToken.getUniquePrincipalName(origin, userName); 168 | } 169 | 170 | // TODO: contribute?? maybe this can be implemented by SidRetrievalStrategyImpl 171 | private class MySidRetrievalStrategyImpl extends SidRetrievalStrategyImpl { 172 | 173 | Set getGrantedAuthorities(Authentication authentication) { 174 | List sids = super.getSids(authentication); 175 | 176 | Set grantedAuthorities = new HashSet<>(); 177 | for (Sid sid : sids) { 178 | if (sid instanceof GrantedAuthoritySid) { 179 | grantedAuthorities.add(((GrantedAuthoritySid) sid).getGrantedAuthority()); 180 | } 181 | } 182 | return grantedAuthorities; 183 | } 184 | 185 | String getPrincipalSid(Authentication authentication) { 186 | List sids = super.getSids(authentication); 187 | 188 | for (Sid sid : sids) { 189 | if (sid instanceof PrincipalSid) { 190 | return ((PrincipalSid) sid).getPrincipal(); 191 | } 192 | } 193 | logger.error("Unexpected error: there is no Principal Sid for user"); 194 | return null; //Should never happen 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | # https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/env/EnvironmentPostProcessor.html 2 | org.springframework.boot.env.EnvironmentPostProcessor=com.sap.cp.appsec.mock.XsuaaMockPostProcessor 3 | 4 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/application-localdb.properties: -------------------------------------------------------------------------------- 1 | # setup for local postgreSQL database (localhost) 2 | # You need to start the docker container on the terminal via "docker-compose up -d" in order to setup the database 3 | spring.datasource.url = jdbc:postgresql://localhost:5432/test 4 | spring.datasource.username = testuser 5 | spring.datasource.password = test123! 6 | spring.datasource.driverClassName = org.postgresql.Driver -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/application-uaamock.properties: -------------------------------------------------------------------------------- 1 | xsuaa.clientid = sb-bulletinboard!t400 2 | xsuaa.xsappname = bulletinboard!t400 3 | 4 | # Setting Log Levels - set to ERROR for production setups. 5 | logging.level.com.sap: DEBUG 6 | logging.level.org.springframework: ERROR 7 | logging.level.org.springframework.security: DEBUG 8 | logging.level.com.sap.cp.appsec: DEBUG -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=cp-application-security-acl 2 | spring.profiles.active=cloud 3 | 4 | management.endpoints.web.exposure.include=health, metrics, mappings 5 | 6 | # validate schema when the application is launched. 7 | spring.jpa.hibernate.ddl-auto = update 8 | spring.jpa.hibernate.use-new-id-generator-mappings = true 9 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 10 | spring.jpa.properties.javax.persistence.schema-generation.database.action = none 11 | spring.jpa.open-in-view = false 12 | 13 | # debugging log levels 14 | logging.level.org.springframework.security.*=DEBUG 15 | logging.level.org.springframework.jdbc.*=DEBUG 16 | logging.level.org.springframework.security.acls.*=DEBUG 17 | logging.level.com.sap.cp.appsec.*=DEBUG 18 | logging.level.javax.persistence.*=DEBUG 19 | 20 | # show sql statements in log - only recommended for testing 21 | spring.jpa.show-sql = true 22 | 23 | # setup for database connection pool size - default value is 100 24 | # spring.datasource.tomcat.max-active = 100 -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/db.population/acl_class.csv: -------------------------------------------------------------------------------- 1 | id,class,class_id_type 2 | 100000002000,location,java.lang.String 3 | 100000002002,bulletinboard,java.lang.String 4 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/db.population/acl_entry.csv: -------------------------------------------------------------------------------- 1 | id,acl_object_identity,ace_order,sid,mask,granting,audit_success,audit_failure 2 | 100000005001,100000001100,0,100000003000,1,true,false,false 3 | 100000005002,100000001100,1,100000003000,2,true,false,false 4 | 100000005003,100000001100,2,100000003000,16,true,false,false 5 | 100000005004,100000001100,3,100000003060,1,true,false,false 6 | 100000005005,100000001101,0,100000003000,1,true,false,false 7 | 100000005006,100000001101,1,100000003000,2,true,false,false 8 | 100000005007,100000001101,2,100000003000,16,true,false,false 9 | 100000005008,100000001101,3,100000003070,1,true,false,false 10 | 100000005009,100000001101,4,100000003070,16,true,false,false 11 | 100000005010,100000001200,0,100000003005,1,true,false,false 12 | 100000005011,100000001200,1,100000003005,2,true,false,false 13 | 100000005012,100000001200,2,100000003005,16,true,false,false 14 | 100000005013,100000001200,3,100000003020,1,true,false,false 15 | 100000005014,100000001201,0,100000003005,1,true,false,false 16 | 100000005015,100000001201,1,100000003005,2,true,false,false 17 | 100000005016,100000001201,2,100000003005,1,true,false,false 18 | 100000005017,100000001201,3,100000003030,1,true,false,false 19 | 100000005018,100000001201,4,100000003030,16,true,false,false 20 | 100000005019,100000001202,0,100000003005,1,true,false,false 21 | 100000005020,100000001202,1,100000003005,2,true,false,false 22 | 100000005021,100000001202,2,100000003005,16,true,false,false 23 | 100000005022,100000001202,3,100000003040,1,true,false,false 24 | 100000005023,100000001202,4,100000003040,16,true,false,false -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/db.population/acl_object_identity.csv: -------------------------------------------------------------------------------- 1 | id,object_id_class,object_id_identity,parent_object,owner_sid,entries_inheriting 2 | 100000001100,100000002000,DE,NULL,100000003000,false 3 | 100000001101,100000002000,IL,NULL,100000003000,false 4 | 100000001200,100000002002,DE_WDF03_Board,100000001100,100000003005,true 5 | 100000001201,100000002002,DE_WDF04_Board,100000001100,100000003005,true 6 | 100000001202,100000002002,IL_RAA03_Board,100000001101,100000003005,true -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/db.population/acl_sid.csv: -------------------------------------------------------------------------------- 1 | id,principal,sid 2 | 100000003000,true,user/userIdp/locationAdmin 3 | 100000003005,true,user/userIdp/boardAdmin 4 | 100000003020,false,ATTR:BULLETINBOARD=DE_WDF03_Board 5 | 100000003030,false,ATTR:BULLETINBOARD=DE_WDF04_Board 6 | 100000003040,false,ATTR:BULLETINBOARD=IL_RAA03_Board 7 | 100000003060,false,ATTR:LOCATION=DE 8 | 100000003070,false,ATTR:LOCATION=IL 9 | 100000003080,false,ATTR:GROUP=UG_MY_TEAM -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/db/changelog/db.changelog-main.yaml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - include: 3 | file: v0.1/main.yaml 4 | relativeToChangelogFile: true -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/db/changelog/v0.1/create-acl-tables.yaml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - changeSet: 3 | id: 1 4 | author: sap 5 | changes: 6 | - createTable: 7 | tableName: acl_sid 8 | columns: 9 | - column: 10 | name: id 11 | type: bigserial 12 | autoIncrement: true 13 | constraints: 14 | primaryKey: true 15 | nullable: false 16 | - column: 17 | name: principal 18 | type: boolean 19 | constraints: 20 | nullable: false 21 | - column: 22 | name: sid 23 | type: varchar(300) 24 | constraints: 25 | nullable: false 26 | - addUniqueConstraint: 27 | columnNames: sid, principal 28 | constraintName: unique_uk_1 29 | tableName: acl_sid 30 | - createIndex: 31 | tableName: acl_sid 32 | columns: 33 | - column: 34 | name: sid 35 | type: varchar(100) 36 | indexName: idx_sid 37 | unique: true 38 | - createTable: 39 | tableName: acl_class 40 | columns: 41 | - column: 42 | name: id 43 | type: bigserial 44 | autoIncrement: true 45 | constraints: 46 | primaryKey: true 47 | nullable: false 48 | - column: 49 | name: class 50 | type: varchar(100) 51 | constraints: 52 | nullable: false 53 | unique: true 54 | uniqueConstraintName: unique_uk_2 55 | - column: 56 | name: class_id_type 57 | type: varchar(100) 58 | constraints: 59 | nullable: true 60 | - createTable: 61 | tableName: acl_object_identity 62 | columns: 63 | - column: 64 | name: id 65 | type: bigserial 66 | autoIncrement: true 67 | constraints: 68 | primaryKey: true 69 | nullable: false 70 | - column: 71 | name: object_id_class 72 | type: bigint 73 | constraints: 74 | nullable: false 75 | - column: 76 | name: object_id_identity 77 | type: varchar(36) 78 | constraints: 79 | nullable: false 80 | - column: 81 | name: parent_object 82 | type: bigint 83 | - column: 84 | name: owner_sid 85 | type: bigint 86 | - column: 87 | name: entries_inheriting 88 | type: boolean 89 | constraints: 90 | nullable: false 91 | - addUniqueConstraint: 92 | columnNames: object_id_class, object_id_identity 93 | constraintName: unique_uk_3 94 | tableName: acl_object_identity 95 | - addForeignKeyConstraint: 96 | baseColumnNames: parent_object 97 | baseTableName: acl_object_identity 98 | constraintName: foreign_fk_1 99 | referencedColumnNames: id 100 | referencedTableName: acl_object_identity 101 | - addForeignKeyConstraint: 102 | baseColumnNames: object_id_class 103 | baseTableName: acl_object_identity 104 | constraintName: foreign_fk_2 105 | referencedColumnNames: id 106 | referencedTableName: acl_class 107 | - addForeignKeyConstraint: 108 | baseColumnNames: owner_sid 109 | baseTableName: acl_object_identity 110 | constraintName: foreign_fk_3 111 | referencedColumnNames: id 112 | referencedTableName: acl_sid 113 | - createTable: 114 | tableName: acl_entry 115 | columns: 116 | - column: 117 | name: id 118 | type: bigserial 119 | autoIncrement: true 120 | constraints: 121 | primaryKey: true 122 | nullable: false 123 | - column: 124 | name: acl_object_identity 125 | type: bigint 126 | constraints: 127 | nullable: false 128 | - column: 129 | name: ace_order 130 | type: int 131 | constraints: 132 | nullable: false 133 | - column: 134 | name: sid 135 | type: bigint 136 | constraints: 137 | nullable: false 138 | - column: 139 | name: mask 140 | type: integer 141 | constraints: 142 | nullable: false 143 | - column: 144 | name: granting 145 | type: boolean 146 | constraints: 147 | nullable: false 148 | - column: 149 | name: audit_success 150 | type: boolean 151 | constraints: 152 | nullable: false 153 | - column: 154 | name: audit_failure 155 | type: boolean 156 | constraints: 157 | nullable: false 158 | - addUniqueConstraint: 159 | columnNames: acl_object_identity, ace_order 160 | constraintName: unique_uk_4 161 | tableName: acl_entry 162 | - addForeignKeyConstraint: 163 | baseColumnNames: acl_object_identity 164 | baseTableName: acl_entry 165 | constraintName: foreign_fk_4 166 | referencedColumnNames: id 167 | referencedTableName: acl_object_identity 168 | - addForeignKeyConstraint: 169 | baseColumnNames: sid 170 | baseTableName: acl_entry 171 | constraintName: foreign_fk_5 172 | referencedColumnNames: id 173 | referencedTableName: acl_sid 174 | 175 | 176 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/db/changelog/v0.1/create-application-tables.yaml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - changeSet: 3 | id: 1 4 | author: sap 5 | changes: 6 | - createTable: 7 | tableName: advertisement 8 | columns: 9 | - column: 10 | name: id 11 | type: bigserial 12 | autoIncrement: true 13 | constraints: 14 | primaryKey: true 15 | nullable: false 16 | - column: 17 | name: version 18 | type: bigint 19 | constraints: 20 | nullable: true 21 | - column: 22 | name: created_at 23 | type: timestamp 24 | constraints: 25 | nullable: false 26 | - column: 27 | name: created_by 28 | type: varchar(100) 29 | constraints: 30 | nullable: false 31 | - column: 32 | name: modified_at 33 | type: timestamp 34 | constraints: 35 | nullable: true 36 | - column: 37 | name: modified_by 38 | type: varchar(100) 39 | constraints: 40 | nullable: true 41 | - column: 42 | name: title 43 | type: varchar(255) 44 | constraints: 45 | nullable: false 46 | - column: 47 | name: is_published 48 | type: boolean 49 | constraints: 50 | nullable: false 51 | - column: 52 | name: contact 53 | type: varchar(100) 54 | constraints: 55 | nullable: false 56 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/db/changelog/v0.1/main.yaml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - include: 3 | file: create-application-tables.yaml 4 | relativeToChangelogFile: true 5 | - include: 6 | file: create-acl-tables.yaml 7 | relativeToChangelogFile: true 8 | - include: 9 | file: populate-acl-tables.yaml 10 | relativeToChangelogFile: true 11 | -------------------------------------------------------------------------------- /spring-security-acl/src/main/resources/db/changelog/v0.1/populate-acl-tables.yaml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - changeSet: 3 | id: 1 4 | author: sap 5 | changes: 6 | - loadData: 7 | encoding: UTF-8 8 | file: db.population/acl_class.csv 9 | tableName: acl_class 10 | - loadData: 11 | encoding: UTF-8 12 | file: db.population/acl_sid.csv 13 | tableName: acl_sid 14 | - loadData: 15 | encoding: UTF-8 16 | file: db.population/acl_object_identity.csv 17 | tableName: acl_object_identity 18 | - loadData: 19 | encoding: UTF-8 20 | file: db.population/acl_entry.csv 21 | tableName: acl_entry -------------------------------------------------------------------------------- /spring-security-acl/src/test/java/com/sap/cp/appsec/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ApplicationTest { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /spring-security-acl/src/test/java/com/sap/cp/appsec/controllers/AttributeFinderControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.controllers; 2 | 3 | import net.minidev.json.JSONArray; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.jdbc.Sql; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | 13 | import static org.hamcrest.Matchers.*; 14 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 16 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 17 | 18 | @RunWith(SpringRunner.class) 19 | @SpringBootTest 20 | @AutoConfigureMockMvc 21 | public class AttributeFinderControllerTest { 22 | private static final String ACL_SID_INSERT_STMT = "INSERT INTO ACL_SID (ID, PRINCIPAL, SID) VALUES " + 23 | "(90000, true, 'owner')," + 24 | "(90001, false, 'ATTR:GROUP=GROUP')," + 25 | "(90002, false, 'ATTR:GROUP_=GROUP')," + 26 | "(90003, false, 'ATTR:GROUP_:GROUP')," + 27 | "(90004, false, 'ATTR:GROUP=ADMIN');"; 28 | 29 | @Autowired 30 | private MockMvc mockMvc; 31 | 32 | @Test 33 | public void getAll() throws Exception { 34 | // check that the returned location is correct 35 | mockMvc.perform(get(AttributeFinderController.PATH)) 36 | .andExpect(status().isOk()) 37 | .andExpect(jsonPath("$", isA(JSONArray.class))) 38 | .andExpect(jsonPath("$.length()", is(3))); 39 | } 40 | 41 | @Test 42 | @Sql(statements = ACL_SID_INSERT_STMT) 43 | public void createAndGetByAclAttribute() throws Exception { 44 | // check that the returned location is correct 45 | mockMvc.perform(get(AttributeFinderController.PATH + "/" + AttributeFinderController.ATTRIBUTE_GROUP)) 46 | .andExpect(status().isOk()) 47 | .andExpect(jsonPath("$", isA(JSONArray.class))) 48 | .andExpect(jsonPath("$.length()", greaterThanOrEqualTo(2))) 49 | .andExpect(jsonPath("$", hasItem("GROUP"))) 50 | .andExpect(jsonPath("$", hasItem("ADMIN"))); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spring-security-acl/src/test/java/com/sap/cp/appsec/domain/AdvertisementAclRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.domain; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.mock.mockito.MockBean; 10 | import org.springframework.data.domain.AuditorAware; 11 | import org.springframework.orm.ObjectOptimisticLockingFailureException; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | 14 | import java.sql.Timestamp; 15 | import java.util.Optional; 16 | 17 | import static org.hamcrest.MatcherAssert.assertThat; 18 | import static org.hamcrest.Matchers.is; 19 | import static org.hamcrest.Matchers.not; 20 | import static org.hamcrest.core.IsNull.notNullValue; 21 | import static org.mockito.Mockito.when; 22 | 23 | 24 | @RunWith(SpringRunner.class) 25 | @SpringBootTest 26 | public class AdvertisementAclRepositoryTest { 27 | @Autowired 28 | private AdvertisementAclRepository repo; 29 | private Advertisement entity; 30 | 31 | @MockBean 32 | private AuditorAware auditorAware; 33 | 34 | @Before 35 | public void setUp() { 36 | when(auditorAware.getCurrentAuditor()) 37 | .thenReturn(Optional.of("Auditor_1")) 38 | .thenReturn(Optional.of("Auditor_2")) 39 | .thenReturn(Optional.of("Auditor_3")); 40 | entity = new Advertisement("SOME title", "contact@email.de"); 41 | } 42 | 43 | @After 44 | public void tearDown() { 45 | repo.deleteAll(); 46 | assertThat(repo.count(), is(0L)); 47 | } 48 | 49 | @Test 50 | public void shouldSetIdOnFirstSave() { 51 | entity = repo.save(entity); 52 | assertThat(entity.getId(), is(notNullValue())); 53 | } 54 | 55 | @Test 56 | public void shouldSetCreatedTimestampOnFirstSaveOnly() throws InterruptedException { 57 | entity = repo.save(entity); 58 | Timestamp timestampAfterCreation = entity.getCreatedAt(); 59 | assertThat(timestampAfterCreation, is(notNullValue())); 60 | 61 | entity.setTitle("Updated Title"); 62 | Thread.sleep(5); // Better: mock time! 63 | 64 | entity = repo.save(entity); 65 | Timestamp timestampAfterUpdate = entity.getCreatedAt(); 66 | assertThat(timestampAfterUpdate, is(timestampAfterCreation)); 67 | } 68 | 69 | @Test 70 | public void shouldSetCreatedByOnFirstSaveOnly() { 71 | entity = repo.save(entity); 72 | String userAfterCreation = entity.getCreatedBy(); 73 | assertThat(userAfterCreation, is("Auditor_1")); 74 | 75 | entity.setTitle("Updated Title"); 76 | 77 | entity = repo.save(entity); 78 | String userAfterUpdate = entity.getCreatedBy(); 79 | assertThat(userAfterUpdate, is(userAfterCreation)); 80 | } 81 | 82 | @Test 83 | public void shouldSetModifiedTimestampOnEveryUpdate() throws InterruptedException { 84 | entity = repo.save(entity); 85 | 86 | entity.setTitle("Updated Title"); 87 | entity = repo.save(entity); 88 | 89 | Timestamp timestampAfterFirstUpdate = entity.getModifiedAt(); 90 | assertThat(timestampAfterFirstUpdate, is(notNullValue())); 91 | 92 | Thread.sleep(5); // Better: mock time! 93 | 94 | entity.setTitle("Updated Title 2"); 95 | entity = repo.save(entity); 96 | Timestamp timestampAfterSecondUpdate = entity.getModifiedAt(); 97 | assertThat(timestampAfterSecondUpdate, is(not(timestampAfterFirstUpdate))); 98 | } 99 | 100 | @Test 101 | public void shouldSetModifiedByOnEveryUpdate() throws InterruptedException { 102 | entity = repo.save(entity); 103 | 104 | entity.setTitle("Updated Title"); 105 | entity = repo.save(entity); 106 | 107 | String userAfterFirstUpdate = entity.getModifiedBy(); 108 | assertThat(userAfterFirstUpdate, is("Auditor_2")); 109 | 110 | Thread.sleep(5); // Better: mock time! 111 | 112 | entity.setTitle("Updated Title 2"); 113 | entity = repo.save(entity); 114 | String userAfterSecondUpdate = entity.getModifiedBy(); 115 | assertThat(userAfterSecondUpdate, is(not(userAfterFirstUpdate))); 116 | } 117 | 118 | 119 | @Test(expected = ObjectOptimisticLockingFailureException.class) 120 | public void shouldUseVersionForConflicts() { 121 | // persists entity and sets initial version 122 | entity = repo.save(entity); 123 | 124 | entity.setTitle("entity instance 1"); 125 | repo.save(entity); // returns instance with updated version 126 | 127 | repo.save(entity); // tries to persist entity with outdated version 128 | } 129 | 130 | @Test 131 | public void shouldFindByTitle() { 132 | String title = "Find me"; 133 | 134 | entity.setTitle(title); 135 | repo.save(entity); 136 | 137 | Advertisement foundEntity = repo.findByTitle(title).get(0); 138 | assertThat(foundEntity.getTitle(), is(title)); 139 | } 140 | } -------------------------------------------------------------------------------- /spring-security-acl/src/test/java/com/sap/cp/appsec/services/AdvertisementServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.services; 2 | 3 | import com.sap.cp.appsec.domain.Advertisement; 4 | import com.sap.cp.appsec.security.AclSupport; 5 | import org.junit.Before; 6 | import org.junit.Ignore; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.mockito.Mockito; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.boot.test.mock.mockito.MockBean; 13 | import org.springframework.data.domain.AuditorAware; 14 | import org.springframework.security.access.AccessDeniedException; 15 | import org.springframework.security.acls.domain.BasePermission; 16 | import org.springframework.security.acls.model.Permission; 17 | import org.springframework.security.core.context.SecurityContextHolder; 18 | import org.springframework.security.core.userdetails.User; 19 | import org.springframework.security.test.context.support.WithMockUser; 20 | import org.springframework.test.context.jdbc.Sql; 21 | import org.springframework.test.context.junit4.SpringRunner; 22 | 23 | import javax.transaction.Transactional; 24 | import java.util.Optional; 25 | 26 | import static org.hamcrest.Matchers.*; 27 | import static org.junit.Assert.assertThat; 28 | 29 | 30 | @RunWith(SpringRunner.class) 31 | @SpringBootTest 32 | @Transactional 33 | @Sql({"/db/data/acl_test_data.sql"}) 34 | public class AdvertisementServiceTest { 35 | 36 | @Autowired 37 | private AdvertisementService service; 38 | 39 | @Autowired 40 | private AclSupport aclService; 41 | 42 | private Long advertisementId = 777L; 43 | 44 | private static final String OWNER = "ownerAndAdmin"; 45 | private static final String ADMIN = "noOwnerButAdmin"; 46 | private static final String READER = "noOwnerButReader"; 47 | private static final String WRITER = "noOwnerButWriter"; 48 | private static final String ANYONE = "anyone"; 49 | private static final String OTHERUSER = "otherUser"; 50 | 51 | @MockBean 52 | private AuditorAware auditorAware; 53 | 54 | 55 | @Before 56 | public void setUp() { 57 | Mockito.doAnswer(invocation -> { 58 | User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 59 | return Optional.of(user.getUsername()); 60 | }).when(auditorAware).getCurrentAuditor(); 61 | } 62 | 63 | @Test 64 | @WithMockUser(username = ANYONE) //can be realized w/o ACL 65 | public void anyOne_canCreateAndRead() { 66 | String title = "my new advertisement"; 67 | Advertisement adsCreated = service.create(buildAdvertisement(title)); 68 | 69 | assertThat(adsCreated, is(notNullValue())); 70 | assertThat(adsCreated.getTitle(), is(title)); 71 | 72 | Advertisement adsRead = service.findById(adsCreated.getId()); 73 | assertThat(adsRead.getTitle(), is(title)); 74 | } 75 | 76 | @Test 77 | @WithMockUser(username = OWNER) //can be realized w/o ACL 78 | public void owner_canRead() { 79 | Advertisement adsRead = service.findById(advertisementId); 80 | assertThat(adsRead, is(notNullValue())); 81 | } 82 | 83 | @Test 84 | @WithMockUser(username = OWNER) //can be realized w/o ACL 85 | public void owner_canModify() { 86 | String newTitle = "Ads updated"; 87 | Advertisement adsCreatedByOwner = service.findById(advertisementId); 88 | adsCreatedByOwner.setTitle(newTitle); 89 | 90 | Advertisement adsUpdated = service.update(adsCreatedByOwner); 91 | assertThat(adsUpdated.getTitle(), is(newTitle)); 92 | } 93 | 94 | @Test 95 | @WithMockUser(username = OWNER) 96 | public void owner_canGrantAdminPermissionsToOtherUser() { 97 | service.grantPermissions(advertisementId, OTHERUSER, new Permission[]{BasePermission.ADMINISTRATION}); 98 | boolean otherUserIsAdmin = hasUserPermission(OTHERUSER, new Permission[]{BasePermission.ADMINISTRATION}); 99 | assertThat(otherUserIsAdmin, is(true)); 100 | } 101 | 102 | @Test 103 | @WithMockUser(username = READER) 104 | public void reader_canRead() { 105 | boolean readerHasReadPermission = hasUserPermission(READER, new Permission[]{BasePermission.READ}); 106 | assertThat(readerHasReadPermission, is(true)); 107 | 108 | Advertisement adsRead = service.findById(advertisementId); 109 | assertThat(adsRead, is(notNullValue())); 110 | } 111 | 112 | @WithMockUser(username = OWNER) 113 | public void owner_canRemoveReadPermission() { 114 | service.removePermissions(advertisementId, READER, new Permission[]{BasePermission.READ}); 115 | 116 | boolean readerHasReadPermission = hasUserPermission(READER, new Permission[]{BasePermission.READ}); 117 | assertThat(readerHasReadPermission, is(false)); 118 | } 119 | 120 | @Test(expected = AccessDeniedException.class) 121 | @WithMockUser(username = READER) 122 | public void reader_cannotModify() { 123 | boolean readerHasWritePermission = hasUserPermission(READER, new Permission[]{BasePermission.WRITE}); 124 | assertThat(readerHasWritePermission, is(false)); 125 | 126 | Advertisement adsCreatedByOwner = service.findById(advertisementId); 127 | adsCreatedByOwner.setTitle("Ads updated"); 128 | service.update(adsCreatedByOwner); 129 | } 130 | 131 | @Test(expected = AccessDeniedException.class) 132 | @WithMockUser(username = READER) 133 | public void reader_cannotGrantPermissions() { 134 | service.grantPermissions(advertisementId, OTHERUSER, new Permission[]{BasePermission.READ}); 135 | } 136 | 137 | @Test 138 | @WithMockUser(username = ADMIN) 139 | public void admin_canGrantReadWritePermissions() { 140 | service.grantPermissions(advertisementId, OTHERUSER, new Permission[]{BasePermission.READ, BasePermission.WRITE}); 141 | 142 | boolean otherUserCanReadAndWrite = hasUserPermission(OTHERUSER, new Permission[]{BasePermission.READ, BasePermission.WRITE}); 143 | assertThat(otherUserCanReadAndWrite, is(true)); 144 | } 145 | 146 | @Test(expected = AccessDeniedException.class) 147 | @WithMockUser(username = WRITER) 148 | public void writer_cannotGrantAdminPermissions() { 149 | service.grantPermissions(advertisementId, OTHERUSER, new Permission[]{BasePermission.ADMINISTRATION}); 150 | } 151 | 152 | @Test(expected = AccessDeniedException.class) 153 | @WithMockUser(username = WRITER) 154 | public void writer_cannotDelete() { 155 | service.deleteById(advertisementId); 156 | } 157 | 158 | @Test 159 | @WithMockUser(username = OWNER) 160 | @Ignore 161 | public void owner_canDelete() { 162 | assertThat(service.findById(advertisementId), notNullValue()); 163 | service.deleteById(advertisementId); 164 | assertThat(service.findById(advertisementId), nullValue()); 165 | assertThat(hasUserPermission(OWNER, new Permission[]{BasePermission.ADMINISTRATION}), is(false)); 166 | } 167 | 168 | @Test 169 | @WithMockUser(username = ADMIN) 170 | @Ignore 171 | public void admin_canDelete() { 172 | service.deleteById(advertisementId); 173 | assertThat(hasUserPermission(ADMIN, new Permission[]{BasePermission.ADMINISTRATION}), is(false)); 174 | } 175 | 176 | private boolean hasUserPermission(String principal, Permission[] permissions) { 177 | return aclService.hasUserPermission(Advertisement.class.getName(), advertisementId, principal, permissions); 178 | } 179 | 180 | private Advertisement buildAdvertisement(String title) { 181 | return new Advertisement(title, "tester@test.com"); 182 | } 183 | } 184 | 185 | -------------------------------------------------------------------------------- /spring-security-acl/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.profiles.active=test,uaamock 2 | 3 | spring.jpa.generate-ddl=true 4 | 5 | 6 | logging.level.org.springframework.security.*=DEBUG 7 | logging.level.org.springframework.jdbc.*=DEBUG 8 | logging.level.org.springframework.security.acls.*=DEBUG 9 | logging.level.com.sap.cp.appsec.*=DEBUG 10 | logging.level.javax.persistence.*=DEBUG 11 | -------------------------------------------------------------------------------- /spring-security-acl/src/test/resources/db/data/acl_test_data.sql: -------------------------------------------------------------------------------- 1 | --- Business Data 2 | 3 | INSERT INTO ADVERTISEMENT (ID, TITLE, CONTACT, CREATED_AT, CREATED_BY, VERSION, IS_PUBLISHED) VALUES 4 | (777, 'Ads 1','tester@email.com','2018-08-17 10:06:54.742','ownerAndAdmin', 0, false); 5 | 6 | --- ACL Data 7 | 8 | INSERT INTO ACL_CLASS (ID, CLASS,CLASS_ID_TYPE) VALUES 9 | (200, 'com.sap.cp.appsec.domain.Advertisement','java.lang.Long'); 10 | 11 | INSERT INTO ACL_SID (ID, PRINCIPAL, SID) VALUES 12 | (300, true, 'ownerAndAdmin'), 13 | (301, true, 'noOwnerButReader'), 14 | (302, true, 'noOwnerButAdmin'); 15 | 16 | INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING) VALUES 17 | (400, 200, '777', NULL, 300, true); 18 | 19 | INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE) VALUES 20 | (500, 400, 0, 300, 16, true, false, false), 21 | (501, 400, 1, 300, 1, true, false, false), 22 | (502, 400, 2, 300, 2, true, false, false), 23 | (503, 400, 3, 301, 1, true, false, false), 24 | (504, 400, 4, 302, 16, true, false, false); 25 | -------------------------------------------------------------------------------- /spring-security-acl/src/test/resources/db/data/acl_test_data_hierarchy.sql: -------------------------------------------------------------------------------- 1 | --- Business Data 2 | INSERT INTO ADVERTISEMENT (ID, TITLE, CONTACT, CREATED_AT, CREATED_BY, VERSION, IS_PUBLISHED) VALUES 3 | (301, 'Ads 1', 'tester@email.com','2018-08-17 17:00:00.000','user/userIdp/adOwner', 0, true), 4 | (302, 'Ads 2', 'tester@email.com','2018-08-17 17:00:00.000','user/userIdp/adOwner', 0, true), 5 | (303, 'Ads 3', 'tester@email.com','2018-08-17 17:00:00.000','user/userIdp/adOwner', 0, true), 6 | (304, 'Ads 4', 'tester@email.com','2018-08-17 17:00:00.000','user/userIdp/adOwner', 0, true), 7 | (305, 'Ads 5', 'tester@email.com','2018-08-17 17:00:00.000','user/userIdp/adOwner', 0, true), 8 | (306, 'Ads 6', 'tester@email.com','2018-08-17 17:00:00.000','user/userIdp/adOwner', 0, true), 9 | (307, 'Ads 7', 'tester@email.com','2018-08-17 17:00:00.000','user/userIdp/adOwner', 0, true); 10 | 11 | --- ACL Data 12 | INSERT INTO ACL_CLASS (ID, CLASS, CLASS_ID_TYPE) VALUES 13 | (2010, 'com.sap.cp.appsec.domain.Advertisement', ''); 14 | 15 | INSERT INTO ACL_SID (ID, PRINCIPAL, SID) VALUES 16 | (3010, true, 'user/userIdp/adOwner'); 17 | 18 | INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING) VALUES 19 | (1301, 2010, '301', NULL, 3010, true), 20 | (1302, 2010, '302', 100000001200, 3010, true), 21 | (1303, 2010, '303', 100000001201, 3010, true), 22 | (1304, 2010, '304', 100000001201, 3010, true), 23 | (1305, 2010, '305', 100000001202, 3010, true), 24 | (1306, 2010, '306', 100000001100, 3010, true), 25 | (1307, 2010, '307', 100000001101, 3010, true); 26 | 27 | INSERT INTO ACL_ENTRY (id, acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) VALUES 28 | (5000, 1301, 0, 3010, 16, true, false, false), 29 | (5001, 1301, 1, 3010, 1, true, false, false), 30 | (5002, 1301, 2, 3010, 2, true, false, false); 31 | 32 | -------------------------------------------------------------------------------- /spring-security-basis/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | manifest-* 5 | 6 | ### Ignore npm files/folders ### 7 | node_modules 8 | npm-debug.log 9 | 10 | ### STS ### 11 | .apt_generated 12 | .classpath 13 | .factorypath 14 | .project 15 | .settings 16 | .springBeans 17 | .sts4-cache 18 | 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | 26 | ### NetBeans ### 27 | /nbproject/private/ 28 | /build/ 29 | /nbbuild/ 30 | /dist/ 31 | /nbdist/ 32 | /.nb-gradle/ 33 | 34 | vars.yml -------------------------------------------------------------------------------- /spring-security-basis/documentation/Prerequisites.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | ## Table of Contents 4 | - [Local Setup](#headline-2) 5 | - [Proxy Settings](#headline-2.1) 6 | - [Java 8 JDK](#headline-2.2) 7 | - [Maven](#headline-2.3) 8 | - [Eclipse IDE](#headline-2.4) 9 | - [JMeter](#headline-2.5) 10 | - [Cloud Foundry Client](#headline-2.6) 11 | - [Docker](#headline-2.7) 12 | - [Project Setup](#headline-3) 13 | - [Clone Git Repository](#headline-3.1) 14 | - [Import Maven project into Eclipse](#headline-3.2) 15 | 16 | 17 | ## Local Setup 18 | 19 | 20 | ### Proxy Settings 21 | - Windows: Download and run [proxyEnv.cmd](https://github.wdf.sap.corp/cc-java-dev/cc-coursematerial/tree/main/CoursePrerequisites/localEnvSetup/proxyEnv.cmd) to permanently set the proxy settings in your environment. 22 | - MacOSX: Add the lines inside [proxyEnv.bash_profile](https://github.wdf.sap.corp/cc-java-dev/cc-coursematerial/tree/main/CoursePrerequisites/localEnvSetup/proxyEnv.bash_profile) to your .bash_profile to permanently set the proxy settings in your environment. After setting this, you need to re-login (or reboot). 23 | 24 | 25 | ### Java 8 JDK 26 | - Install the latest version of the [Java JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html) on your machine (at least Java 8). 27 | - Windows: 28 | - Run the console `cmd` and enter `setx JAVA_HOME "C:\Program Files\Java\jdk1.8.X"` (replace the path with the path leading to your JDK installation) 29 | - To test your installation, open a new console and run both `"%JAVA_HOME%\bin\java" -version` and `java -version`. Both should return "java version 1.8.X". 30 | - MacOSX: nothing to do, env variables should be adjusted automatically. 31 | 32 | 33 | ### Maven 34 | The builds of the individual microservices are managed using Apache Maven. 35 | 36 | We make use of [Maven wrapper](https://github.com/takari/maven-wrapper), which is located at `.mvn\wrapper` in each module. 37 | In the `.mvn/wrapper/maven-wrapper.properties` file, it can be defined which Maven version should be used. 38 | The wrapper can be executed via the `mvnw` script located at the root of a module (one `bat` file for Windows and one shell script for Linux/Git Bash). 39 | It takes care of installing the correct Maven version, so no manual local installation of Maven is required. 40 | 41 | However, you still need to provide Maven configuration on your computer: 42 | - Create the directory `~/.m2/`, where `~` is your home directory, e.g. `C:\Users\D012345`. 43 | > Note: Windows explorer does not allow you create a directory ".name" - you have to add a dot at the end, i.e. ".name.", which will then be removed. 44 | - Download the [settings.xml](https://github.wdf.sap.corp/cc-java-dev/cc-coursematerial/blob/main/CoursePrerequisites/localEnvSetup/settings.xml) configuration file and save it in the `.m2` directory created in the previous step. 45 | 46 | To use the maven wrapper start a spring boot application, execute 47 | - **`./mvnw spring-boot:run` (Linux/Git bash)** or 48 | - **`mvnw spring-boot:run` (Windows)**. 49 | 50 | 51 | ### Eclipse IDE 52 | An integrated development environment (IDE) is useful for development and experimenting with the code. 53 | We recommend to use Eclipse as the following descriptions are tailored for it. 54 | 55 | - Eclipse Oxygen 56 | - [Download Eclipse](https://spring.io/tools/eclipse) (select Eclipse Oxygen - Eclipse IDE for Java EE Developers) 57 | - Unpack the ZIP file to a suitable location on your computer, e.g. `C:\dev\eclipse` 58 | - Assign installed Java JRE to Eclipse: `Window` - `Preferences`, type `jre` in filter, in `Installed JREs`, select `Add...`->`Standard VM` and enter the path to your Java installation. 59 | - Set proxies within Eclipse: `Window` - `Preferences`, type `network` in filter, in `network connections`, select `manual` and add the following values 60 | - For http and https: `proxy.wdf.sap.corp`, port `8080` 61 | - bypass: `*.sap.corp` 62 | 63 | > Note: the Community edition of [IntelliJ IDEA](https://www.jetbrains.com/idea/) is an alternative IDE, but you have to figure out on your own how to import the projects properly. 64 | 65 | 66 | 67 | ### Cloud Foundry Client 68 | The developed microservices will run on the Cloud Foundry platform. 69 | 70 | - Install the Cloud Foundry Command Line Interface (CLI) following [this](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html) guide 71 | - Create an account using this tutorial: [CF @SAP BTP](https://help.cf.sap.hana.ondemand.com/). Don't forget to request your own trial space as explained there. 72 | 73 | 74 | ### Docker 75 | Docker is needed to conveniently start services like PostgreSQL, Redis or RabbitMQ on your local machine. 76 | 77 | Download and install the latest [**Docker for Windows**](https://www.docker.com/docker-windows) or [**Docker for Mac**](https://www.docker.com/docker-mac) release. 78 | See the [Getting Started](https://docs.docker.com/get-started/) documentation if you're not yet familiar with Docker. 79 | 80 | > In case you experience problems with the latest version, you can try to install older versions that are linked in the release notes: [Windows](https://docs.docker.com/docker-for-windows/release-notes/), [Mac](https://docs.docker.com/docker-for-mac/release-notes/). 81 | 82 | > Note: the docker installer automatically enables Hyper-V if you have not done so yet. 83 | This requires a restart that may take several minutes to complete. 84 | 85 | > Note: Hyper-V interferes with VirtualBox (Hyper-V must to be enabled for Docker, but this [crashes VirtualBox](https://www.virtualbox.org/ticket/16801)) 86 | 87 | To start all docker containers required for a module, execute `docker-compose up -d` in the directory of the module. 88 | This will run all containers as defined in the `docker-compose.yml` file located at the root of the module. To tear down all containers, execute `docker-compose down`. 89 | 90 | Execute `docker ps` to view all running docker images. 91 | 92 | 93 | ## Project Setup 94 | Each module is a separate Java project in a separate folder, but all projects are stored in the same Git repository. 95 | 96 | 97 | ### Clone Git Repository 98 | The project can be cloned using this the following URL: `git@github.wdf.sap.corp:CPSecurity/cp-application-security.git`. 99 | Either use the command line and type `git clone https://github.wdf.sap.corp/CPSecurity/cp-application-security` or use the Git perspective in Eclipse and choose `Clone a Git Repostory`. 100 | 101 | > Note: In case SSH is not working make use of the HTTPS link when cloning the repository. 102 | 103 | 104 | ### Import Maven project into Eclipse 105 | Within Eclipse you need to import the source code. 106 | 107 | 1. Select `File - Import` in the main menu 108 | 2. Select `Maven - Existing Maven Projects` in the dialog 109 | 3. Import the module you want to work on, e.g. `spring-security-acl` by selecting the respective directory and clicking `OK` 110 | 4. Finally, update the Maven Settings of the project by presssing `ALT+F5` and then `OK`. 111 | -------------------------------------------------------------------------------- /spring-security-basis/documentation/images/Figure_OAuth2.0_SAP_CP_Components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/cloud-application-security-sample/d6d1727ec2736f025655ed78d4249adfe5010bdb/spring-security-basis/documentation/images/Figure_OAuth2.0_SAP_CP_Components.png -------------------------------------------------------------------------------- /spring-security-basis/documentation/images/SAP_CP_Cockpit_AssignRoleCollectionToUser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/cloud-application-security-sample/d6d1727ec2736f025655ed78d4249adfe5010bdb/spring-security-basis/documentation/images/SAP_CP_Cockpit_AssignRoleCollectionToUser.png -------------------------------------------------------------------------------- /spring-security-basis/documentation/testing/spring-security-cloudfoundry.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": [], 3 | "info": { 4 | "name": "Basis-CPApplicationSecurity-CF", 5 | "_postman_id": "f8198ae8-d9b4-3851-7be7-e9f267606df6", 6 | "description": "", 7 | "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" 8 | }, 9 | "item": [ 10 | { 11 | "name": "/ads/actuator/health", 12 | "request": { 13 | "url": "{{approuterUri}}/ads/actuator/health", 14 | "method": "GET", 15 | "header": [], 16 | "body": { 17 | "mode": "raw", 18 | "raw": "{\n\t\"title\":\"test\",\n\t\"price\": \"40\",\n\t\"contact\": \"myemail\",\n\t\"currency\" : \"EUR\"\n}" 19 | }, 20 | "description": "" 21 | }, 22 | "response": [] 23 | }, 24 | { 25 | "name": "/ads/api/v1/ads/", 26 | "request": { 27 | "url": "{{approuterUri}}/ads/api/v1/ads/", 28 | "method": "GET", 29 | "header": [], 30 | "body": {}, 31 | "description": "" 32 | }, 33 | "response": [] 34 | }, 35 | { 36 | "name": "Fetch X-Csrf-Token for POST / PUT", 37 | "request": { 38 | "url": "{{approuterUri}}", 39 | "method": "GET", 40 | "header": [ 41 | { 42 | "key": "x-csrf-token", 43 | "value": "fetch", 44 | "description": "" 45 | } 46 | ], 47 | "body": {}, 48 | "description": "Prerequisite: Activate Postman Interceptor\n\nEnter the received JSESSIONID in the \"Cookie\" header" 49 | }, 50 | "response": [] 51 | }, 52 | { 53 | "name": "/ads/api/v1/ads/", 54 | "request": { 55 | "url": "{{approuterUri}}/ads/api/v1/ads/", 56 | "method": "POST", 57 | "header": [ 58 | { 59 | "key": "Content-Type", 60 | "value": "application/json", 61 | "description": "" 62 | }, 63 | { 64 | "key": "x-csrf-token", 65 | "value": "", 66 | "description": "", 67 | "disabled": true 68 | } 69 | ], 70 | "body": { 71 | "mode": "raw", 72 | "raw": "{\n\t\"title\":\"test\",\n\t\"price\": \"40\",\n\t\"contact\": \"myemail\",\n\t\"currency\" : \"EUR\",\n\t\"confidentialityLevel\" : \"PUBLIC\"\n}" 73 | }, 74 | "description": "" 75 | }, 76 | "response": [] 77 | }, 78 | { 79 | "name": "/ads/api/v1/ads/{id}", 80 | "request": { 81 | "url": "{{approuterUri}}/ads/api/v1/ads/{id}", 82 | "method": "DELETE", 83 | "header": [], 84 | "body": {}, 85 | "description": "" 86 | }, 87 | "response": [] 88 | } 89 | ] 90 | } -------------------------------------------------------------------------------- /spring-security-basis/documentation/testing/spring-security-local.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": [], 3 | "info": { 4 | "name": "Basis-Local-CPApplicationSecurity", 5 | "_postman_id": "121b1c05-0cd4-9f83-2f60-3d4ca2202e3b", 6 | "description": "", 7 | "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" 8 | }, 9 | "item": [ 10 | { 11 | "name": "F4 /api/v1/attribute/confidentiality_level", 12 | "request": { 13 | "url": "http://localhost:8080/api/v1/attribute/confidentiality_level", 14 | "method": "GET", 15 | "header": [], 16 | "body": { 17 | "mode": "raw", 18 | "raw": "" 19 | }, 20 | "description": "" 21 | }, 22 | "response": [] 23 | }, 24 | { 25 | "name": "/api/v1/ads/", 26 | "request": { 27 | "url": "http://localhost:8080/api/v1/ads/", 28 | "method": "POST", 29 | "header": [ 30 | { 31 | "key": "Content-Type", 32 | "value": "application/json" 33 | }, 34 | { 35 | "key": "Authorization", 36 | "value": "{{AUTH_advertiser}}", 37 | "description": "Advertiser role with confidentiality_level = PUBLIC" 38 | } 39 | ], 40 | "body": { 41 | "mode": "raw", 42 | "raw": "{\n\t\"title\":\"hi\",\n\t\"price\": \"50\",\n\t\"contact\": \"myemail\",\n\t\"currency\" : \"EUR\",\n\t\"confidentialityLevel\": \"CONFIDENTIAL\"\n}" 43 | }, 44 | "description": "" 45 | }, 46 | "response": [] 47 | }, 48 | { 49 | "name": "/api/v1/ads/", 50 | "request": { 51 | "url": "http://localhost:8080/api/v1/ads/{id}", 52 | "method": "GET", 53 | "header": [ 54 | { 55 | "key": "Content-Type", 56 | "value": "application/json", 57 | "description": "" 58 | }, 59 | { 60 | "key": "Authorization", 61 | "value": "{{AUTH_advertiser}}", 62 | "description": "Advertiser role with confidentiality_level = PUBLIC" 63 | } 64 | ], 65 | "body": { 66 | "mode": "raw", 67 | "raw": "{\n\t\"title\":\"hi\",\n\t\"price\": \"50\",\n\t\"contact\": \"myemail\",\n\t\"currency\" : \"EUR\",\n\t\"confidentialityLevel\": \"CONFIDENTIAL\"\n}" 68 | }, 69 | "description": "" 70 | }, 71 | "response": [] 72 | }, 73 | { 74 | "name": "/api/v1/ads/confidentiality/{confidentiality_level}", 75 | "request": { 76 | "url": "http://localhost:8080/api/v1/ads/confidentiality/{confidentiality_level}", 77 | "method": "GET", 78 | "header": [ 79 | { 80 | "key": "Content-Type", 81 | "value": "application/json" 82 | }, 83 | { 84 | "key": "Authorization", 85 | "value": "{{AUTH_viewer_public}}", 86 | "description": "Viewer role with confidentiality_level = PUBLIC" 87 | }, 88 | { 89 | "key": "Authorization", 90 | "value": "{{AUTH_viewer_confidential}}", 91 | "description": "Viewer role with confidentiality_level = STRICTLY_CONFIDENTIAL", 92 | "disabled": true 93 | } 94 | ], 95 | "body": { 96 | "mode": "raw", 97 | "raw": "" 98 | }, 99 | "description": "" 100 | }, 101 | "response": [] 102 | }, 103 | { 104 | "name": "/api/v1/ads/{id}", 105 | "request": { 106 | "url": "http://localhost:8080/api/v1/ads/{id}", 107 | "method": "PUT", 108 | "header": [ 109 | { 110 | "key": "Content-Type", 111 | "value": "application/json" 112 | }, 113 | { 114 | "key": "Authorization", 115 | "value": "{{AUTH_advertiser}}", 116 | "description": "Advertiser role with confidentiality_level = PUBLIC" 117 | } 118 | ], 119 | "body": { 120 | "mode": "raw", 121 | "raw": "{\n \"id\": {id},\n \"title\": \"some update\",\n \"price\": 50,\n \"contact\": \"myemail\",\n \"currency\": \"EUR\",\n \"category\": null,\n \"purchasedOn\": null,\n \"metadata\": {\n \"version\": 0\n }\n}\n" 122 | }, 123 | "description": "" 124 | }, 125 | "response": [] 126 | }, 127 | { 128 | "name": "/api/v1/ads/{id}", 129 | "request": { 130 | "url": "http://localhost:8080/api/v1/ads/{id}", 131 | "method": "DELETE", 132 | "header": [ 133 | { 134 | "key": "Authorization", 135 | "value": "{{AUTH_advertiser}}", 136 | "description": "Advertiser role with confidentiality_level = PUBLIC" 137 | } 138 | ], 139 | "body": { 140 | "mode": "raw", 141 | "raw": "" 142 | }, 143 | "description": "" 144 | }, 145 | "response": [] 146 | } 147 | ] 148 | } -------------------------------------------------------------------------------- /spring-security-basis/documentation/testing/spring-security-local.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3b5fabae-c6ce-929a-2a19-15340b38bc66", 3 | "name": "spring-sec-basis-local", 4 | "values": [ 5 | { 6 | "enabled": true, 7 | "key": "AUTH_advertiser", 8 | "value": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzMxOTUvdG9rZW5fa2V5cyIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkifQ.eyJleHRfYXR0ciI6eyJ6ZG4iOiIifSwiemlkIjoidWFhIiwiemRuIjoiIiwiZ3JhbnRfdHlwZSI6InVybjppZXRmOnBhcmFtczpvYXV0aDpncmFudC10eXBlOnNhbWwyLWJlYXJlciIsInVzZXJfbmFtZSI6Im93bmVyIiwib3JpZ2luIjoidXNlcklkcCIsInNjb3BlIjpbImJ1bGxldGluYm9hcmQhdDQwMC5EaXNwbGF5IiwiYnVsbGV0aW5ib2FyZCF0NDAwLlVwZGF0ZSJdLCJleHAiOjY5NzQwMzE2MDAsImlhdCI6MTU2MjMzNjUwOSwiZW1haWwiOiJvd25lckB0ZXN0Lm9yZyIsImNpZCI6InNiLXhzYXBwbGljYXRpb24hdDg5NSJ9.MF92W9CdPmQYL8Yz2Za4mJWbSDoFR0NdgZUwo5BLXMPmqyJkG8efcXiAcbR69_UDWMaljPQYwadQsgAV0U1jFG04BQwkctZAlYNCmb8-JmgpRNiXWrHs44CSLhpEIwmSvAWnDlLIO-Ricc50zIIr80ae2-5lKoGqaMYVoozLpzfE0_czmcHv6WLX6vIHInGxN7m8GFMB2RmvUJZiJEW5nlspbWahyWNHK-I9dlIePQE2GoDwMedRgwZ1Gub7zbX_4WX93_kg1BKDvBPnEcVCCbVweTMcBqN5YgrC-y9IRI2rxucYqBcCHwdM8kTTrGa9NZeNIa81SQnLwcQ0mSm0lQ", 9 | "type": "text" 10 | }, 11 | { 12 | "enabled": true, 13 | "key": "AUTH_viewer_public", 14 | "value": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzMxOTUvdG9rZW5fa2V5cyIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkifQ.eyJleHRfYXR0ciI6eyJ6ZG4iOiIifSwiemlkIjoidWFhIiwiemRuIjoiIiwiZ3JhbnRfdHlwZSI6InVybjppZXRmOnBhcmFtczpvYXV0aDpncmFudC10eXBlOnNhbWwyLWJlYXJlciIsInhzLnVzZXIuYXR0cmlidXRlcyI6eyJjb25maWRlbnRpYWxpdHlfbGV2ZWwiOlsiUFVCTElDIl19LCJ1c2VyX25hbWUiOiJ2aWV3ZXIiLCJvcmlnaW4iOiJ1c2VySWRwIiwic2NvcGUiOlsiYnVsbGV0aW5ib2FyZCF0NDAwLkRpc3BsYXkiXSwiZXhwIjo2OTc0MDMxNjAwLCJpYXQiOjE1NjIzMzY1NTcsImVtYWlsIjoidmlld2VyQHRlc3Qub3JnIiwiY2lkIjoic2IteHNhcHBsaWNhdGlvbiF0ODk1In0.xWCwlAdOLFH76gZiLoKsnFH48pof5A7Em2k9oZ9CHzbMQZK26D66BD76bavwqnAhIn1toig3e6E01mtnuBwCymbMU1lUNBq5dfkxSBDWFsbQ-btjzb9ktiax5_joGY1xH5R87CFIs87grntZdP5Cw3XfvfOaTX_-WHobRVR59K6EkRP9K1QzqVOjeSm2j_iAqwwAR8QhoUtgDcJTtTEbXfZj1Ri2MSOJyrPpeRXd_ZT7ku7phFII4M7Szz8RFSQFWBaOiC_uyxWjYx7zr1mLw1buSNZUg-TW8bHdkZ_cWZDYSQBjulU5gadLKyzXkZaZHkAEn2KNKsc-azF58mrbuw", 15 | "type": "text" 16 | }, 17 | { 18 | "enabled": true, 19 | "key": "AUTH_viewer_confidential", 20 | "value": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzMxOTUvdG9rZW5fa2V5cyIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkifQ.eyJleHRfYXR0ciI6eyJ6ZG4iOiIifSwiemlkIjoidWFhIiwiemRuIjoiIiwiZ3JhbnRfdHlwZSI6InVybjppZXRmOnBhcmFtczpvYXV0aDpncmFudC10eXBlOnNhbWwyLWJlYXJlciIsInhzLnVzZXIuYXR0cmlidXRlcyI6eyJjb25maWRlbnRpYWxpdHlfbGV2ZWwiOlsiQ09ORklERU5USUFMIl19LCJ1c2VyX25hbWUiOiJ2aWV3ZXIiLCJvcmlnaW4iOiJ1c2VySWRwIiwic2NvcGUiOlsiYnVsbGV0aW5ib2FyZCF0NDAwLkRpc3BsYXkiXSwiZXhwIjo2OTc0MDMxNjAwLCJpYXQiOjE1NjIzMzY1ODgsImVtYWlsIjoidmlld2VyQHRlc3Qub3JnIiwiY2lkIjoic2IteHNhcHBsaWNhdGlvbiF0ODk1In0.y7XG-r-SQ5M_zrvwYZhy9ms2sWFOEMnMpO29TzjXzIwqlojTcqGswoYGDOyl_enFItQ67WVjR31Rmefq84pZsFaKxGOh3KOBu_vC3uM8_EV3MpgioOnZ2DDHkylABlTBOVffFI2omBpqk_eJYePcuW84WSdI3H-pveF8KgqBPZ4FhWtH617s44BxD0Rar9Bc37Hp8nFTfK4HOrVQQr8CkUHaYBovVo9YsS7UA8iHussQFGxCh5skvY3q9qaTJ9q-Z7UWuVoCe6_MH1h0EZbyfSAApEjsghGY49xysGCUOsde5E0_rsPOeXj0BGRud1sf-nqLdGnEKHgjjMVyT3dZPA", 21 | "type": "text" 22 | } 23 | ], 24 | "timestamp": 1562336636231, 25 | "_postman_variable_scope": "environment", 26 | "_postman_exported_at": "2019-07-05T14:26:27.084Z", 27 | "_postman_exported_using": "Postman/5.5.4" 28 | } -------------------------------------------------------------------------------- /spring-security-basis/localEnvironmentSetup.bat: -------------------------------------------------------------------------------- 1 | REM This script prepares the current shell's environment variables (not permanently) 2 | REM VCAP_APPLICATION is required when cloud profile is active 3 | 4 | SET VCAP_APPLICATION={} 5 | SET SPRING_PROFILES_ACTIVE=cloud,uaamock 6 | 7 | -------------------------------------------------------------------------------- /spring-security-basis/localEnvironmentSetup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Hint: run script with 'source localEnvironmentSetup.sh'" 3 | echo "This script prepares the current shell's environment variables (not permanently)" 4 | 5 | export SPRING_PROFILES_ACTIVE='cloud,uaamock' 6 | export VCAP_APPLICATION='{}' # required when cloud profile is active 7 | 8 | 9 | -------------------------------------------------------------------------------- /spring-security-basis/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Configuration: 3 | # configured for EU10. For other landscapes, please adopt LANDSCAPE_APPS_DOMAIN in ../vars.yml 4 | # If the route is occupied, you might need to change ID in ../vars.yml as well 5 | applications: 6 | - name: bulletinboard-ads 7 | instances: 1 8 | memory: 1G 9 | routes: 10 | - route: bulletinboard-ads-((ID)).((LANDSCAPE_APPS_DOMAIN)) 11 | path: target/demo-application-security-basis.jar 12 | health-check-type: http 13 | health-check-http-endpoint: /actuator/health 14 | env: 15 | # Disable Spring Auto Reconfiguration 16 | JBP_CONFIG_SPRING_AUTO_RECONFIGURATION: '{enabled: false}' 17 | # Use the non-blocking /dev/urandom instead of the default to generate random numbers. 18 | # When using Java community buildpack, increase startup times, especially when using Spring Boot. 19 | JAVA_OPTS: -Djava.security.egd=file:///dev/./urandom 20 | services: 21 | - uaa-bulletinboard 22 | # Application Router as web server 23 | - name: approuter 24 | routes: 25 | - route: approuter-((ID)).((LANDSCAPE_APPS_DOMAIN)) 26 | path: src/main/approuter 27 | memory: 128M 28 | env: 29 | TENANT_HOST_PATTERN: "^(.*)-approuter-((ID)).((LANDSCAPE_APPS_DOMAIN))" 30 | destinations: > 31 | [{ 32 | "name":"ads-destination", 33 | "url" :"https://bulletinboard-ads-((ID)).((LANDSCAPE_APPS_DOMAIN))", 34 | "forwardAuthToken": true} 35 | ] 36 | services: 37 | - uaa-bulletinboard 38 | -------------------------------------------------------------------------------- /spring-security-basis/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.sap.cloud.security.xsuaa 5 | demo-application-security-basis 6 | 2.1.0 7 | jar 8 | 9 | demo-application-security-basis 10 | Demo project for Spring Boot using Spring Security. It integrates to SAP BTP XSUAA service 11 | using the Java Client Security Library offered by SAP. 12 | 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.1.9.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 5.2.0.RELEASE 26 | 2.1.0 27 | 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-actuator 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-cloud-connectors 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-data-jpa 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-web 46 | 47 | 48 | 49 | 50 | com.h2database 51 | h2 52 | runtime 53 | 54 | 55 | 56 | 57 | com.sap.cloud.security.xsuaa 58 | xsuaa-spring-boot-starter 59 | ${sap.cloud.security.version} 60 | 61 | 62 | com.sap.cloud.security.xsuaa 63 | spring-xsuaa-mock 64 | ${sap.cloud.security.version} 65 | 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-test 70 | test 71 | 72 | 73 | com.sap.cloud.security.xsuaa 74 | spring-xsuaa-test 75 | ${sap.cloud.security.version} 76 | test 77 | 78 | 79 | 80 | 81 | ${project.artifactId} 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-surefire-plugin 90 | 2.18.1 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /spring-security-basis/security/xs-security.json: -------------------------------------------------------------------------------- 1 | { 2 | "xsappname": "bulletinboard", 3 | "description": "Enabled bulletinboard for multi tenants", 4 | "tenant-mode": "shared", 5 | "scopes": [ 6 | { 7 | "name": "$XSAPPNAME.Display", 8 | "description": "Display Advertisements" 9 | }, 10 | { 11 | "name": "$XSAPPNAME.Update", 12 | "description": "Update Advertisements" 13 | } 14 | ], 15 | "attributes": [ 16 | { 17 | "name": "confidentiality_level", 18 | "description": "Level of Confidentiality", 19 | "valueType" : "string", 20 | "valueRequired" : true 21 | 22 | } 23 | ], 24 | "role-templates": [ 25 | { 26 | "name": "ViewerPUBLIC", 27 | "description": "View Advertisements", 28 | "scope-references": [ 29 | "$XSAPPNAME.Display" 30 | ], 31 | "attribute-references" : [ 32 | { 33 | "name" : "confidentiality_level", 34 | "default-values" : ["PUBLIC"] 35 | } 36 | ] 37 | }, 38 | { 39 | "name": "AdvertiserPUBLIC", 40 | "description": "Maintain Advertisements", 41 | "scope-references": [ 42 | "$XSAPPNAME.Display", 43 | "$XSAPPNAME.Update" 44 | ], 45 | "attribute-references" : [ 46 | { 47 | "name" : "confidentiality_level", 48 | "default-values" : ["PUBLIC"] 49 | } 50 | ] 51 | }, 52 | { 53 | "name": "AdvertiserALL", 54 | "description": "Maintain Advertisements", 55 | "scope-references": [ 56 | "$XSAPPNAME.Display", 57 | "$XSAPPNAME.Update" 58 | ], 59 | "attribute-references" : [ 60 | { 61 | "name" : "confidentiality_level", 62 | "default-values" : ["PUBLIC", "INTERNAL", "CONFIDENTIAL", "STRICTLY_CONFIDENTIAL"] 63 | } 64 | ] 65 | } 66 | ], 67 | "role-collections": [ 68 | { 69 | "name": "RC_ViewerPUBLIC", 70 | "description": "Viewer (public)", 71 | "role-template-references": [ 72 | "$XSAPPNAME.ViewerPUBLIC" 73 | ] 74 | }, 75 | { 76 | "name": "RC_AdvertiserPUBLIC", 77 | "description": "Advertiser (CRUD, public)", 78 | "role-template-references": [ 79 | "$XSAPPNAME.AdvertiserPUBLIC" 80 | ] 81 | }, 82 | { 83 | "name": "RC_AdvertiserALL", 84 | "description": "Advertiser (CRUD, no restriction)", 85 | "role-template-references": [ 86 | "$XSAPPNAME.AdvertiserALL" 87 | ] 88 | } 89 | ] 90 | } -------------------------------------------------------------------------------- /spring-security-basis/src/main/approuter/.npmrc: -------------------------------------------------------------------------------- 1 | @sap:registry=https://npm.sap.com -------------------------------------------------------------------------------- /spring-security-basis/src/main/approuter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "approuter", 3 | "dependencies": { 4 | "@sap/approuter": "5.15.0" 5 | }, 6 | "scripts": { 7 | "start": "node node_modules/@sap/approuter/approuter.js" 8 | }, 9 | "engines": { 10 | "node": "10" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/approuter/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to our Index Page 6 | 7 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/approuter/xs-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcomeFile": "index.html", 3 | "routes": [{ 4 | "source": "^/ads/api/", 5 | "target": "/api/", 6 | "destination": "ads-destination", 7 | "scope": "$XSAPPNAME.Display", 8 | "csrfProtection": false 9 | }, { 10 | "source": "^/ads", 11 | "target": "/", 12 | "destination": "ads-destination" 13 | }] 14 | } 15 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/Application.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.ComponentScan; 6 | 7 | @SpringBootApplication 8 | @ComponentScan 9 | public class Application { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(Application.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/config/PersistenceConfig.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.config; 2 | 3 | import com.sap.cloud.security.xsuaa.token.SpringSecurityContext; 4 | import com.sap.cloud.security.xsuaa.token.Token; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.data.domain.AuditorAware; 8 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 9 | 10 | import java.util.Optional; 11 | 12 | @Configuration 13 | @EnableJpaAuditing 14 | public class PersistenceConfig { 15 | 16 | @Bean 17 | AuditorAware auditorProvider() { 18 | return new AuditorAwareImpl(); 19 | } 20 | 21 | private static class AuditorAwareImpl implements AuditorAware { 22 | 23 | @Override 24 | public Optional getCurrentAuditor() { 25 | Token token = SpringSecurityContext.getToken(); 26 | return Optional.ofNullable(token.getLogonName()); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.config; 2 | 3 | import static org.springframework.http.HttpMethod.GET; 4 | import static org.springframework.http.HttpMethod.POST; 5 | import static org.springframework.http.HttpMethod.PUT; 6 | 7 | import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration; 8 | import com.sap.cloud.security.xsuaa.token.TokenAuthenticationConverter; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.core.convert.converter.Converter; 12 | import org.springframework.security.authentication.AbstractAuthenticationToken; 13 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 14 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 15 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 16 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 17 | import org.springframework.security.config.http.SessionCreationPolicy; 18 | import org.springframework.security.oauth2.jwt.Jwt; 19 | 20 | @Configuration 21 | @EnableWebSecurity 22 | @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) 23 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 24 | 25 | @Autowired 26 | XsuaaServiceConfiguration xsuaaServiceConfiguration; 27 | 28 | // configure Spring Security, demand authentication and specific scopes 29 | @Override 30 | public void configure(HttpSecurity http) throws Exception { 31 | 32 | http 33 | .sessionManagement() 34 | // session is created by approuter 35 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 36 | .and() 37 | // demand specific scopes depending on intended request 38 | .authorizeRequests() 39 | // enable OAuth2 checks 40 | .antMatchers("/**/*.js", "/**/*.json", "/**/*.xml", "/**/*.html").permitAll() 41 | .antMatchers(GET, "/api/v1/ads/**").hasAuthority("Display") 42 | .antMatchers(POST, "/api/v1/ads/**").hasAuthority("Update") 43 | .antMatchers(PUT, "/api/v1/ads/**").hasAuthority("Update") 44 | .antMatchers(GET, "/api/v1/attribute/**").permitAll() 45 | .antMatchers("/api/v1/**").authenticated() 46 | .antMatchers("/").authenticated() 47 | .antMatchers("/actuator/**").permitAll() 48 | .antMatchers("/hystrix.stream").permitAll() 49 | .anyRequest().denyAll() // deny anything not configured above 50 | .and() 51 | .oauth2ResourceServer().jwt() 52 | .jwtAuthenticationConverter(getJwtAuthoritiesConverter()); 53 | } 54 | 55 | /** 56 | * Customizes how GrantedAuthority are derived from a Jwt 57 | * 58 | * @returns jwt converter 59 | */ 60 | Converter getJwtAuthoritiesConverter() { 61 | TokenAuthenticationConverter converter = new TokenAuthenticationConverter(xsuaaServiceConfiguration); 62 | converter.setLocalScopeAsAuthorities(true); 63 | return converter; 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/controllers/AdvertisementController.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.controllers; 2 | 3 | import static com.sap.cp.appsec.security.AdvertisementSpecificationBuilder.confidentialityIsEqualOrLess; 4 | import static com.sap.cp.appsec.security.AdvertisementSpecificationBuilder.hasId; 5 | import static com.sap.cp.appsec.security.AdvertisementSpecificationBuilder.isCreatedBy; 6 | import static org.springframework.data.jpa.domain.Specification.where; 7 | import static org.springframework.http.HttpStatus.NO_CONTENT; 8 | 9 | import javax.validation.Valid; 10 | import javax.validation.constraints.Min; 11 | 12 | import java.util.Optional; 13 | 14 | import com.sap.cloud.security.xsuaa.token.SpringSecurityContext; 15 | import com.sap.cloud.security.xsuaa.token.Token; 16 | import com.sap.cp.appsec.domain.Advertisement; 17 | import com.sap.cp.appsec.domain.AdvertisementRepository; 18 | import com.sap.cp.appsec.domain.ConfidentialityLevel; 19 | import com.sap.cp.appsec.dto.AdvertisementDto; 20 | import com.sap.cp.appsec.dto.AdvertisementListDto; 21 | import com.sap.cp.appsec.dto.PageHeaderBuilder; 22 | import com.sap.cp.appsec.exceptions.BadRequestException; 23 | import com.sap.cp.appsec.exceptions.NotAuthorizedException; 24 | import com.sap.cp.appsec.exceptions.NotFoundException; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | import org.slf4j.MDC; 28 | import org.springframework.data.domain.Page; 29 | import org.springframework.data.domain.PageRequest; 30 | import org.springframework.http.HttpHeaders; 31 | import org.springframework.http.HttpStatus; 32 | import org.springframework.http.ResponseEntity; 33 | import org.springframework.security.access.prepost.PreAuthorize; 34 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 35 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 36 | import org.springframework.validation.annotation.Validated; 37 | import org.springframework.web.bind.annotation.DeleteMapping; 38 | import org.springframework.web.bind.annotation.GetMapping; 39 | import org.springframework.web.bind.annotation.PathVariable; 40 | import org.springframework.web.bind.annotation.PostMapping; 41 | import org.springframework.web.bind.annotation.PutMapping; 42 | import org.springframework.web.bind.annotation.RequestBody; 43 | import org.springframework.web.bind.annotation.RequestMapping; 44 | import org.springframework.web.bind.annotation.ResponseStatus; 45 | import org.springframework.web.bind.annotation.RestController; 46 | import org.springframework.web.context.annotation.RequestScope; 47 | import org.springframework.web.util.UriComponents; 48 | import org.springframework.web.util.UriComponentsBuilder; 49 | 50 | @RequestScope 51 | @RestController 52 | @Validated 53 | /* 54 | * Use a path which does not end with a slash! Otherwise the controller is not reachable when not using the trailing 55 | * slash in the URL 56 | */ 57 | @RequestMapping(AdvertisementController.PATH) 58 | public class AdvertisementController { 59 | static final String PATH = "/api/v1/ads"; 60 | private final AdvertisementRepository adsRepo; 61 | 62 | public static final String PATH_PAGES = PATH + "/pages/"; 63 | public static final int FIRST_PAGE_ID = 0; 64 | public static final int DEFAULT_PAGE_SIZE = 20; 65 | 66 | private final Logger logger = LoggerFactory.getLogger(getClass()); 67 | 68 | public AdvertisementController(AdvertisementRepository adsRepo) { 69 | this.adsRepo = adsRepo; 70 | } 71 | 72 | @PostMapping 73 | public ResponseEntity create(@RequestBody @Valid AdvertisementDto advertisement, 74 | UriComponentsBuilder uriComponentsBuilder) { 75 | 76 | AdvertisementDto savedAdvertisement = new AdvertisementDto(adsRepo.save(advertisement.toEntity())); 77 | logger.trace("created ad with version {}", savedAdvertisement.metadata.version); 78 | UriComponents uriComponents = uriComponentsBuilder.path(PATH + "/{id}") 79 | .buildAndExpand(savedAdvertisement.getId()); 80 | HttpHeaders headers = new HttpHeaders(); 81 | headers.setLocation(uriComponents.toUri()); 82 | return new ResponseEntity<>(savedAdvertisement, headers, HttpStatus.CREATED); 83 | } 84 | 85 | @GetMapping 86 | public ResponseEntity readAll(@AuthenticationPrincipal Token token) { 87 | if (!token.getAuthorities().contains(new SimpleGrantedAuthority("Display"))) { 88 | throw new NotAuthorizedException("This operation requires \"Display\" scope"); 89 | } 90 | 91 | return readPage(FIRST_PAGE_ID); 92 | } 93 | 94 | @GetMapping("/confidentiality/{confidentialityLevel}") 95 | @PreAuthorize("hasAuthority('Display') and @webSecurity.hasAttributeValue('confidentiality_level', #confidentialityLevel)") 96 | public ResponseEntity readByConfidentiality( 97 | @PathVariable("confidentialityLevel") String confidentialityLevel) { 98 | Page page = adsRepo 99 | .findAllByConfidentialityLevel(ConfidentialityLevel.valueOf(confidentialityLevel), 100 | PageRequest.of(FIRST_PAGE_ID, DEFAULT_PAGE_SIZE)); 101 | 102 | return new ResponseEntity<>(new AdvertisementListDto(page.getContent()), 103 | PageHeaderBuilder.createLinkHeader(page, PATH_PAGES), HttpStatus.OK); 104 | } 105 | 106 | @GetMapping("/pages/{pageId}") 107 | public ResponseEntity readPage(@PathVariable("pageId") int pageId) { 108 | Token jwtToken = SpringSecurityContext.getToken(); 109 | Page page = adsRepo 110 | .findAll(where(isCreatedBy(jwtToken.getLogonName()).or(confidentialityIsEqualOrLess( 111 | jwtToken.getXSUserAttribute(ConfidentialityLevel.ATTRIBUTE_NAME)))), 112 | PageRequest.of(pageId, DEFAULT_PAGE_SIZE)); 113 | 114 | return new ResponseEntity<>(new AdvertisementListDto(page.getContent()), 115 | PageHeaderBuilder.createLinkHeader(page, PATH_PAGES), HttpStatus.OK); 116 | } 117 | 118 | @GetMapping("/{id}") 119 | public AdvertisementDto readById(@PathVariable("id") @Min(0) Long id) { 120 | MDC.put("endpoint", "GET: " + PATH + "/" + id); 121 | Token jwtToken = SpringSecurityContext.getToken(); 122 | 123 | // here we apply a filter on database leveraging Spring Data JPA: isCreatedBy or hasAttributeValue 124 | // find further info here: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/ 125 | Optional advertisement = adsRepo 126 | .findOne(where(hasId(id).and(isCreatedBy(jwtToken.getLogonName())).or( 127 | confidentialityIsEqualOrLess( 128 | jwtToken.getXSUserAttribute(ConfidentialityLevel.ATTRIBUTE_NAME))))); 129 | 130 | if (advertisement.isPresent()) { 131 | logger.trace("returning: {}", advertisement.get()); 132 | return new AdvertisementDto(advertisement.get()); 133 | } 134 | throwNonexisting(id); 135 | return null; 136 | } 137 | 138 | @PutMapping("/{id}") 139 | @PreAuthorize("@webSecurity.isCreatedBy(#id)") 140 | public AdvertisementDto update(@RequestBody AdvertisementDto updatedAdvertisement, @PathVariable("id") Long id) { 141 | throwIfInconsistent(id, updatedAdvertisement.getId()); 142 | throwIfNonexisting(id); 143 | logger.trace("updated ad with version {}", updatedAdvertisement.metadata.version); 144 | return new AdvertisementDto(adsRepo.save(updatedAdvertisement.toEntity())); 145 | } 146 | 147 | @DeleteMapping("{id}") 148 | @ResponseStatus(NO_CONTENT) 149 | @PreAuthorize("hasAuthority('Update') and @webSecurity.isCreatedBy(#id)") 150 | public void deleteById(@PathVariable("id") Long id) { 151 | throwIfNonexisting(id); 152 | adsRepo.deleteById(id); 153 | } 154 | 155 | private void throwIfNonexisting(@PathVariable("id") Long id) { 156 | if (!adsRepo.existsById(id)) { 157 | throwNonexisting(id); 158 | } 159 | } 160 | 161 | private void throwNonexisting(@PathVariable("id") Long id) { 162 | NotFoundException notFoundException = new NotFoundException("no Advertisement with id " + id); 163 | logger.warn("request failed", notFoundException); 164 | throw notFoundException; 165 | } 166 | 167 | private void throwIfInconsistent(Long expected, Long actual) { 168 | if (!expected.equals(actual)) { 169 | String message = String.format( 170 | "bad request, inconsistent IDs between request and object: request id = %d, object id = %d", 171 | expected, actual); 172 | throw new BadRequestException(message); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/controllers/AttributeFinder.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.controllers; 2 | 3 | import com.sap.cp.appsec.domain.ConfidentialityLevel; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | @RestController 14 | @RequestMapping(AttributeFinder.PATH) 15 | public class AttributeFinder { 16 | 17 | public static final String ATTRIBUTE_CONFIDENTIALITY_LEVEL = ConfidentialityLevel.ATTRIBUTE_NAME; 18 | static final String PATH = "/api/v1/attribute"; 19 | 20 | @GetMapping("/{ATTRIBUTE_NAME}") 21 | public List getAllValuesForAttribute(@PathVariable("ATTRIBUTE_NAME") String attributeName) { 22 | switch (attributeName) { 23 | case ATTRIBUTE_CONFIDENTIALITY_LEVEL: 24 | return ConfidentialityLevel.getValues(); 25 | default: 26 | return Collections.emptyList(); 27 | } 28 | } 29 | 30 | @GetMapping 31 | public List getAllAttributes() { 32 | List attributes = new ArrayList<>(); 33 | attributes.add(ATTRIBUTE_CONFIDENTIALITY_LEVEL); 34 | return attributes; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/controllers/CustomExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.controllers; 2 | 3 | import com.sap.cp.appsec.dto.ErrorDto; 4 | import com.sap.cp.appsec.exceptions.BadRequestException; 5 | import com.sap.cp.appsec.exceptions.NotAuthorizedException; 6 | import com.sap.cp.appsec.exceptions.NotFoundException; 7 | import org.springframework.http.HttpHeaders; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.security.access.AccessDeniedException; 11 | import org.springframework.transaction.TransactionSystemException; 12 | import org.springframework.web.bind.MethodArgumentNotValidException; 13 | import org.springframework.web.bind.annotation.ExceptionHandler; 14 | import org.springframework.web.bind.annotation.RestControllerAdvice; 15 | import org.springframework.web.context.request.WebRequest; 16 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 17 | 18 | import javax.validation.ConstraintViolation; 19 | import javax.validation.ConstraintViolationException; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | import static com.sap.cp.appsec.dto.ErrorDto.DetailError; 24 | 25 | 26 | /** 27 | * A simple exception mapper for exceptions that also provides the error messages as part of the response. Gathers 28 | * all @ExceptionHandler methods in a single class so that exceptions from all controllers are handled consistently in 29 | * one place. 30 | */ 31 | @RestControllerAdvice 32 | public class CustomExceptionMapper extends ResponseEntityExceptionHandler { 33 | 34 | @ExceptionHandler 35 | public ResponseEntity handleConstraintViolationException(ConstraintViolationException exception, 36 | WebRequest request) { 37 | List errors = new ArrayList<>(); 38 | for (ConstraintViolation violation : exception.getConstraintViolations()) { 39 | String msg = violation.getRootBeanClass().getSimpleName() + " " + violation.getPropertyPath() + ": " 40 | + violation.getMessage() + " [current value = " + violation.getInvalidValue() + "]"; 41 | 42 | errors.add(new DetailError(msg)); 43 | } 44 | 45 | ErrorDto apiError = new ErrorDto(HttpStatus.BAD_REQUEST, exception.getLocalizedMessage(), request, 46 | errors.toArray(new DetailError[errors.size()])); 47 | 48 | return new ResponseEntity<>(apiError, new HttpHeaders(), HttpStatus.BAD_REQUEST); 49 | } 50 | 51 | @ExceptionHandler 52 | public ResponseEntity handleWrappedConstraintViolationException(TransactionSystemException exception, 53 | WebRequest request) throws Exception { 54 | if (exception.getRootCause() instanceof ConstraintViolationException) { 55 | ConstraintViolationException constraintException = (ConstraintViolationException) exception.getRootCause(); 56 | return handleConstraintViolationException(constraintException, request); 57 | } else { 58 | return handleException(exception, request); 59 | } 60 | } 61 | 62 | /** 63 | * Here we have to override implementation of ResponseEntityExceptionHandler. 64 | */ 65 | @Override 66 | public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException exception, 67 | HttpHeaders headers, HttpStatus status, WebRequest request) { 68 | return convertToResponseEntity(exception, HttpStatus.BAD_REQUEST, request); 69 | } 70 | 71 | @ExceptionHandler 72 | public ResponseEntity handleBadRequestException(BadRequestException exception, WebRequest request) { 73 | return convertToResponseEntity(exception, HttpStatus.BAD_REQUEST, request); 74 | } 75 | 76 | @ExceptionHandler 77 | public ResponseEntity handleNotFoundException(NotFoundException exception, WebRequest request) { 78 | return convertToResponseEntity(exception, HttpStatus.NOT_FOUND, request); 79 | } 80 | 81 | @ExceptionHandler 82 | public ResponseEntity handleNotAuthorizedException(NotAuthorizedException exception, WebRequest request) { 83 | return convertToResponseEntity(exception, HttpStatus.FORBIDDEN, request); 84 | } 85 | 86 | @ExceptionHandler 87 | public ResponseEntity handleNotAuthorizedException(AccessDeniedException exception, WebRequest request) { 88 | return convertToResponseEntity(exception, HttpStatus.FORBIDDEN, request); 89 | } 90 | 91 | @ExceptionHandler 92 | public ResponseEntity handleAll(Exception exception, WebRequest request) { 93 | return convertToResponseEntity(exception, HttpStatus.INTERNAL_SERVER_ERROR, request); 94 | } 95 | 96 | private ResponseEntity convertToResponseEntity(Exception exception, HttpStatus status, WebRequest request) { 97 | ErrorDto apiError = new ErrorDto(status, exception.getLocalizedMessage(), request, 98 | new DetailError(exception.getClass().getSimpleName() + ": error occurred")); 99 | 100 | return new ResponseEntity<>(apiError, new HttpHeaders(), status); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/domain/Advertisement.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.domain; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.Table; 9 | import javax.persistence.Transient; 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.NotNull; 12 | 13 | 14 | @Entity 15 | @Table(name = "advertisement") 16 | public class Advertisement extends BaseEntity { 17 | 18 | /** 19 | * mandatory fields 20 | **/ 21 | @NotBlank 22 | @Column(name = "title") 23 | private String title; 24 | 25 | @NotBlank 26 | @Column(name = "contact") 27 | private String contact; 28 | 29 | @NotNull 30 | @Column(name = "confidentiality_level") 31 | private ConfidentialityLevel confidentialityLevel; 32 | 33 | @Transient 34 | private final Logger logger = LoggerFactory.getLogger(getClass()); 35 | 36 | /** 37 | * Any JPA Entity needs a default constructor. 38 | */ 39 | public Advertisement() { 40 | } 41 | 42 | public Advertisement(String title, String contact, ConfidentialityLevel confidentialityLevel) { 43 | this.title = title; 44 | this.contact = contact; 45 | if (confidentialityLevel != null) { 46 | this.confidentialityLevel = confidentialityLevel; 47 | } else { 48 | this.confidentialityLevel = ConfidentialityLevel.STRICTLY_CONFIDENTIAL; 49 | } 50 | } 51 | 52 | public String getTitle() { 53 | return title; 54 | } 55 | 56 | public void setTitle(String title) { 57 | this.title = title; 58 | } 59 | 60 | public ConfidentialityLevel getConfidentialityLevel() { 61 | return confidentialityLevel; 62 | } 63 | 64 | public String getContact() { 65 | return contact; 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return "Advertisement [id=" + id + ", title=" + title + "]"; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/domain/AdvertisementRepository.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.domain; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 8 | import org.springframework.data.repository.PagingAndSortingRepository; 9 | 10 | public interface AdvertisementRepository extends PagingAndSortingRepository , JpaSpecificationExecutor { 11 | List findByTitle(String title); 12 | 13 | Page findAllByConfidentialityLevel(ConfidentialityLevel confidentialityLevel, Pageable pageable); 14 | 15 | boolean existsByIdAndCreatedBy(Long id, String owner); 16 | } -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/domain/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.domain; 2 | 3 | import org.springframework.data.annotation.CreatedBy; 4 | import org.springframework.data.annotation.CreatedDate; 5 | import org.springframework.data.annotation.LastModifiedBy; 6 | import org.springframework.data.annotation.LastModifiedDate; 7 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 8 | 9 | import javax.persistence.*; 10 | import java.sql.Timestamp; 11 | 12 | @MappedSuperclass 13 | @EntityListeners(AuditingEntityListener.class) 14 | public abstract class BaseEntity { 15 | /** 16 | ** technical fields 17 | **/ 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.AUTO) 20 | protected Long id; 21 | 22 | @Version 23 | @Column(name = "version") 24 | protected long version; 25 | 26 | @Column(name = "created_at", nullable = false, updatable = false) 27 | @CreatedDate 28 | protected Timestamp createdAt; 29 | 30 | @Column(name = "modified_at", insertable = false) 31 | @LastModifiedDate 32 | protected Timestamp modifiedAt; 33 | 34 | @Column(name = "created_by", nullable = false, updatable = false) 35 | @CreatedBy 36 | protected String createdBy; 37 | 38 | @Column(name = "modified_by", insertable = false) 39 | @LastModifiedBy 40 | protected String modifiedBy; 41 | 42 | 43 | public Long getId() { 44 | return id; 45 | } 46 | 47 | public long getVersion() { 48 | return version; 49 | } 50 | 51 | public Timestamp getCreatedAt() { 52 | return createdAt; 53 | } 54 | 55 | public Timestamp getModifiedAt() { 56 | return modifiedAt; 57 | } 58 | 59 | public String getCreatedBy() { 60 | return createdBy; 61 | } 62 | 63 | public String getModifiedBy() { 64 | return modifiedBy; 65 | } 66 | 67 | // use only in tests or when you need to map DTO to Entity 68 | public void setId(Long id) { 69 | this.id = id; 70 | } 71 | 72 | // use only in tests or when you need to map DTO to Entity 73 | public void setVersion(long version) { 74 | this.version = version; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/domain/ConfidentialityLevel.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public enum ConfidentialityLevel { 7 | /** 8 | * Please note: ordinal is relevant in AdvertisementSpecificationBuilder 9 | **/ 10 | PUBLIC("Public"), // ordinal 0 11 | INTERNAL("Internal"), 12 | CONFIDENTIAL("Confidential"), 13 | STRICTLY_CONFIDENTIAL("Strictly confidential"); 14 | 15 | public static final String ATTRIBUTE_NAME = "confidentiality_level"; 16 | 17 | private String description; 18 | private int level = super.ordinal(); 19 | 20 | ConfidentialityLevel(String description) { 21 | this.description = description; 22 | } 23 | 24 | public int getLevel() { 25 | return this.level; 26 | } 27 | 28 | public String getDescription() { 29 | return this.description; 30 | } 31 | 32 | public static List getValues() { 33 | ConfidentialityLevel[] values = ConfidentialityLevel.values(); 34 | ArrayList stringValues = new ArrayList<>(); 35 | 36 | for (int i = 0; i < values.length; i++) { 37 | stringValues.add(values[i].toString()); 38 | } 39 | return stringValues; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/dto/AdvertisementDto.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.dto; 2 | 3 | import com.sap.cp.appsec.domain.Advertisement; 4 | import com.sap.cp.appsec.domain.ConfidentialityLevel; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | import javax.validation.constraints.NotNull; 8 | import java.math.BigDecimal; 9 | import java.sql.Timestamp; 10 | import java.time.ZoneId; 11 | import java.time.ZonedDateTime; 12 | import java.time.format.DateTimeFormatter; 13 | 14 | 15 | /** 16 | * A Data Transfer Object (DTO) is a data structure without logic. 17 | *

18 | * Note: This class implements also the mapping between DTO and Entity and vice versa 19 | */ 20 | public class AdvertisementDto { 21 | /** 22 | * id is null in case of a new advertisement 23 | **/ 24 | private Long id; 25 | 26 | @NotBlank 27 | public String title; 28 | 29 | @NotNull 30 | public BigDecimal price; 31 | 32 | @NotBlank 33 | public String contact; 34 | 35 | @NotBlank 36 | public String currency; 37 | 38 | public final MetaData metadata = new MetaData(); 39 | 40 | public ConfidentialityLevel confidentialityLevel; 41 | 42 | /** 43 | * Default constructor required by Jackson JSON Converter 44 | */ 45 | public AdvertisementDto() { 46 | } 47 | 48 | /** 49 | * Transforms Advertisement entity to DTO 50 | */ 51 | public AdvertisementDto(Advertisement ad) { 52 | this.id = ad.getId(); 53 | this.title = ad.getTitle(); 54 | this.contact = ad.getContact(); 55 | if (ad.getConfidentialityLevel() != null) { 56 | this.confidentialityLevel = ad.getConfidentialityLevel(); 57 | } 58 | this.metadata.createdAt = convertToDateTime(ad.getCreatedAt()); 59 | this.metadata.modifiedAt = convertToDateTime(ad.getModifiedAt()); 60 | this.metadata.createdBy = "" + ad.getCreatedBy(); 61 | this.metadata.modifiedBy = "" + ad.getModifiedBy(); 62 | this.metadata.version = ad.getVersion(); 63 | } 64 | 65 | // use only in tests 66 | public void setId(long id) { 67 | this.id = id; 68 | } 69 | 70 | public Long getId() { 71 | return id; 72 | } 73 | 74 | public Advertisement toEntity() { 75 | // does not map "read-only" attributes 76 | Advertisement ad = new Advertisement(title, contact, confidentialityLevel); 77 | ad.setId(id); 78 | ad.setVersion(metadata.version); 79 | return ad; 80 | } 81 | 82 | private String convertToDateTime(Timestamp timestamp) { 83 | if (timestamp == null) { 84 | return null; 85 | } 86 | ZonedDateTime dateTime = ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneId.systemDefault()); 87 | return dateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); // ISO 8601 88 | } 89 | 90 | public static class MetaData { 91 | public String createdAt; 92 | public String modifiedAt; 93 | public String createdBy; 94 | public String modifiedBy; 95 | 96 | public long version = 0L; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/dto/AdvertisementListDto.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.sap.cp.appsec.domain.Advertisement; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.StreamSupport; 9 | 10 | public class AdvertisementListDto { 11 | @JsonProperty("value") 12 | public List advertisements; 13 | 14 | public AdvertisementListDto(Iterable ads) { 15 | this.advertisements = StreamSupport.stream(ads.spliterator(), false).map(AdvertisementDto::new) 16 | .collect(Collectors.toList()); 17 | } 18 | } -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/dto/ErrorDto.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 4 | import com.fasterxml.jackson.annotation.JsonTypeName; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.context.request.WebRequest; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | /** 12 | * Common structure for an Error Response. 13 | */ 14 | @JsonTypeName("error") 15 | @JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME) 16 | public class ErrorDto { 17 | private HttpStatus status; 18 | private String message; // user-facing (localizable) message, describing the error 19 | private String target; // endpoint of origin request 20 | private List details; 21 | 22 | public ErrorDto(HttpStatus status, String message, WebRequest request, DetailError... errors) { 23 | this.status = status; 24 | this.message = message; 25 | if (message == null) { 26 | this.message = status.getReasonPhrase(); 27 | } 28 | this.details = Arrays.asList(errors); 29 | this.target = request.getDescription(false).substring(4); 30 | } 31 | 32 | public int getStatus() { 33 | return status.value(); 34 | } 35 | 36 | public String getTarget() { 37 | return target; 38 | } 39 | 40 | public String getMessage() { 41 | return message; 42 | } 43 | 44 | public List getDetails() { 45 | return details; 46 | } 47 | 48 | public static class DetailError { 49 | private final String message; // user-facing (localizable) message, describing the error 50 | 51 | public DetailError(String message) { 52 | this.message = message; 53 | } 54 | 55 | public String getMessage() { 56 | return message; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/dto/PageHeaderBuilder.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.dto; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.http.HttpHeaders; 5 | 6 | public class PageHeaderBuilder { 7 | 8 | private PageHeaderBuilder() {} 9 | 10 | public static HttpHeaders createLinkHeader(Page page, String path) { 11 | StringBuilder linkHeader = new StringBuilder(); 12 | if (page.hasPrevious()) { 13 | int prevNumber = page.getNumber() - 1; 14 | linkHeader.append("<").append(path).append(prevNumber).append(">; rel=\"previous\""); 15 | if (!page.isLast()) 16 | linkHeader.append(", "); 17 | } 18 | if (page.hasNext()) { 19 | int nextNumber = page.getNumber() + 1; 20 | linkHeader.append("<").append(path).append(nextNumber).append(">; rel=\"next\""); 21 | } 22 | HttpHeaders headers = new HttpHeaders(); 23 | headers.add(HttpHeaders.LINK, linkHeader.toString()); 24 | return headers; 25 | } 26 | } -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/exceptions/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @SuppressWarnings("serial") 7 | @ResponseStatus(HttpStatus.BAD_REQUEST) 8 | public class BadRequestException extends RuntimeException { 9 | public BadRequestException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/exceptions/NotAuthorizedException.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @SuppressWarnings("serial") 7 | @ResponseStatus(HttpStatus.FORBIDDEN) 8 | public class NotAuthorizedException extends RuntimeException { 9 | public NotAuthorizedException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/exceptions/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | // need to define exceptions with response status, not predefined 7 | @SuppressWarnings("serial") 8 | @ResponseStatus(HttpStatus.NOT_FOUND) 9 | public class NotFoundException extends RuntimeException { 10 | public NotFoundException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/mock/XsuaaMockPostProcessor.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.mock; 2 | 3 | import com.sap.cloud.security.xsuaa.mock.XsuaaMockWebServer; 4 | import org.springframework.beans.factory.DisposableBean; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.env.EnvironmentPostProcessor; 7 | import org.springframework.core.env.ConfigurableEnvironment; 8 | import org.springframework.core.env.Profiles; 9 | 10 | public class XsuaaMockPostProcessor implements EnvironmentPostProcessor, DisposableBean { 11 | 12 | private final XsuaaMockWebServer mockAuthorizationServer = new XsuaaMockWebServer(); 13 | 14 | @Override 15 | public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { 16 | if (environment.acceptsProfiles(Profiles.of("uaamock"))) { 17 | environment.getPropertySources().addFirst(this.mockAuthorizationServer); 18 | } 19 | } 20 | 21 | @Override 22 | public void destroy() throws Exception { 23 | this.mockAuthorizationServer.destroy(); 24 | } 25 | } -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/security/AdvertisementSpecificationBuilder.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.security; 2 | 3 | import javax.persistence.criteria.Path; 4 | 5 | import java.util.Comparator; 6 | import java.util.Optional; 7 | import java.util.stream.Stream; 8 | 9 | import com.sap.cp.appsec.domain.Advertisement; 10 | import com.sap.cp.appsec.domain.ConfidentialityLevel; 11 | import org.springframework.data.jpa.domain.Specification; 12 | 13 | public class AdvertisementSpecificationBuilder { 14 | 15 | private AdvertisementSpecificationBuilder() { 16 | } 17 | 18 | public static Specification isCreatedBy(final String createdBy) { 19 | return (root, critQuery, critBuilder) -> { 20 | Path createdByPath = root.get("createdBy"); 21 | return critBuilder.equal(createdByPath, createdBy); 22 | }; 23 | } 24 | 25 | public static Specification confidentialityIsEqualOrLess(String[] confidentialityLevels) { 26 | Optional maxConfidentialityLevel = confidentialityLevels != null ? Stream.of(confidentialityLevels) 27 | .map(ConfidentialityLevel::valueOf) 28 | .max(Comparator.comparing(ConfidentialityLevel::getLevel)) : Optional.of(ConfidentialityLevel.PUBLIC); 29 | 30 | return (root, critQuery, critBuilder) -> { 31 | return critBuilder.lessThanOrEqualTo(root.get("confidentialityLevel"), 32 | maxConfidentialityLevel.get()); // compares enum ordinals 33 | }; 34 | } 35 | 36 | public static Specification hasId(Long id) { 37 | return (root, critQuery, critBuilder) -> { 38 | return critBuilder.equal(root.get("id"), id); 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/java/com/sap/cp/appsec/security/WebSecurityExpressions.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.security; 2 | 3 | import com.sap.cloud.security.xsuaa.token.SpringSecurityContext; 4 | import com.sap.cloud.security.xsuaa.token.Token; 5 | import com.sap.cp.appsec.domain.AdvertisementRepository; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.util.Assert; 11 | 12 | import java.util.Arrays; 13 | 14 | /** 15 | * https://docs.spring.io/spring-security/site/docs/current/reference/html5/#el-access 16 | */ 17 | @Component("webSecurity") // Bean that offers methods that can be used within Spring Expression Language expressions 18 | public class WebSecurityExpressions { 19 | private Logger logger = LoggerFactory.getLogger(getClass()); 20 | 21 | @Autowired 22 | private AdvertisementRepository repo; 23 | 24 | public boolean isCreatedBy(String id) { 25 | Token token = SpringSecurityContext.getToken(); 26 | String currentUser = token.getLogonName(); 27 | return currentUser == null || repo.existsByIdAndCreatedBy(new Long(id), token.getLogonName()); 28 | } 29 | 30 | public boolean hasAttributeValue(String attributeName, String attributeValue) { 31 | Assert.notNull(attributeName, "requires attributeName"); 32 | Assert.notNull(attributeValue, "requires attributeValue"); 33 | 34 | boolean hasAttributeValue = false; 35 | Token token = SpringSecurityContext.getToken(); 36 | String[] userAttributeValues = token.getXSUserAttribute(attributeName); 37 | if (userAttributeValues != null) { 38 | int index = Arrays.binarySearch(userAttributeValues, attributeValue); 39 | hasAttributeValue = index >= 0; 40 | } 41 | logger.info(String.format("Has user attribute %s = %s ? %s", attributeName, attributeValue, hasAttributeValue)); 42 | return hasAttributeValue; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.env.EnvironmentPostProcessor=com.sap.cp.appsec.mock.XsuaaMockPostProcessor 2 | 3 | -------------------------------------------------------------------------------- /spring-security-basis/src/main/resources/application-uaamock.properties: -------------------------------------------------------------------------------- 1 | xsuaa.clientid = sb-bulletinboard!t400 2 | xsuaa.xsappname = bulletinboard!t400 3 | 4 | # Setting Log Levels - set to ERROR for production setups. 5 | logging.level.com.sap: DEBUG 6 | logging.level.org.springframework: ERROR 7 | logging.level.org.springframework.security: DEBUG 8 | logging.level.org.springframework.web: DEBUG 9 | logging.level.com.sap.cp.appsec: DEBUG -------------------------------------------------------------------------------- /spring-security-basis/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=cp-application-security-basis 2 | spring.profiles.active=cloud 3 | 4 | management.endpoints.web.exposure.include=health, metrics, mappings 5 | 6 | # validate schema when the application is launched. 7 | spring.jpa.hibernate.ddl-auto = update 8 | spring.jpa.hibernate.use-new-id-generator-mappings = true 9 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 10 | spring.jpa.properties.javax.persistence.schema-generation.database.action = none 11 | spring.jpa.open-in-view = false 12 | 13 | # show sql statements in log - only recommended for testing 14 | spring.jpa.show-sql = true 15 | 16 | # setup for connection pool size - default value is 100 17 | spring.datasource.tomcat.max-active = 10 -------------------------------------------------------------------------------- /spring-security-basis/src/test/java/com/sap/cp/appsec/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ApplicationTest { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spring-security-basis/src/test/java/com/sap/cp/appsec/controllers/AttributeFinderTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.controllers; 2 | 3 | import net.minidev.json.JSONArray; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | import org.springframework.test.web.servlet.MockMvc; 11 | 12 | import static org.hamcrest.Matchers.*; 13 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 16 | 17 | @RunWith(SpringRunner.class) 18 | @SpringBootTest 19 | @AutoConfigureMockMvc 20 | public class AttributeFinderTest { 21 | 22 | @Autowired 23 | private MockMvc mockMvc; 24 | 25 | @Test 26 | public void getAll() throws Exception { 27 | // check that the returned location is correct 28 | mockMvc.perform(get(AttributeFinder.PATH)) 29 | .andExpect(status().isOk()) 30 | .andExpect(jsonPath("$", isA(JSONArray.class))) 31 | .andExpect(jsonPath("$.length()", is(1))); 32 | } 33 | 34 | @Test 35 | public void getByConfidentiality() throws Exception { 36 | // check that the returned location is correct 37 | mockMvc.perform(get(AttributeFinder.PATH + "/" + AttributeFinder.ATTRIBUTE_CONFIDENTIALITY_LEVEL)) 38 | .andExpect(status().isOk()) 39 | .andExpect(jsonPath("$", isA(JSONArray.class))) 40 | .andExpect(jsonPath("$.length()", is(both(greaterThan(0)).and(lessThan(10))))) 41 | .andExpect(jsonPath("$", hasItem("STRICTLY_CONFIDENTIAL"))); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /spring-security-basis/src/test/java/com/sap/cp/appsec/domain/AdvertisementRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cp.appsec.domain; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.mock.mockito.MockBean; 10 | import org.springframework.data.domain.AuditorAware; 11 | import org.springframework.orm.ObjectOptimisticLockingFailureException; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | 14 | import java.sql.Timestamp; 15 | import java.util.Optional; 16 | 17 | import static org.hamcrest.MatcherAssert.assertThat; 18 | import static org.hamcrest.Matchers.is; 19 | import static org.hamcrest.Matchers.not; 20 | import static org.hamcrest.core.IsNull.notNullValue; 21 | import static org.mockito.Mockito.when; 22 | 23 | 24 | @RunWith(SpringRunner.class) 25 | @SpringBootTest 26 | public class AdvertisementRepositoryTest { 27 | @Autowired 28 | private AdvertisementRepository repo; 29 | private Advertisement entity; 30 | 31 | @MockBean 32 | AuditorAware auditorAware; 33 | 34 | @Before 35 | public void setUp() { 36 | when(auditorAware.getCurrentAuditor()) 37 | .thenReturn(Optional.of("Auditor_1")) 38 | .thenReturn(Optional.of("Auditor_2")) 39 | .thenReturn(Optional.of("Auditor_3")); 40 | entity = new Advertisement("SOME title", "contact@email.de", null); 41 | } 42 | 43 | @After 44 | public void tearDown() { 45 | repo.deleteAll(); 46 | assertThat(repo.count(), is(0L)); 47 | } 48 | 49 | @Test 50 | public void shouldSetIdOnFirstSave() { 51 | entity = repo.save(entity); 52 | assertThat(entity.getId(), is(notNullValue())); 53 | } 54 | 55 | @Test 56 | public void shouldSetCreatedTimestampOnFirstSaveOnly() throws InterruptedException { 57 | entity = repo.save(entity); 58 | Timestamp timestampAfterCreation = entity.getCreatedAt(); 59 | assertThat(timestampAfterCreation, is(notNullValue())); 60 | 61 | entity.setTitle("Updated Title"); 62 | Thread.sleep(5); // Better: mock time! 63 | 64 | entity = repo.save(entity); 65 | Timestamp timestampAfterUpdate = entity.getCreatedAt(); 66 | assertThat(timestampAfterUpdate, is(timestampAfterCreation)); 67 | } 68 | 69 | @Test 70 | public void shouldSetCreatedByOnFirstSaveOnly() throws InterruptedException { 71 | entity = repo.save(entity); 72 | String userAfterCreation = entity.getCreatedBy(); 73 | assertThat(userAfterCreation, is("Auditor_1")); 74 | 75 | entity.setTitle("Updated Title"); 76 | 77 | entity = repo.save(entity); 78 | String userAfterUpdate = entity.getCreatedBy(); 79 | assertThat(userAfterUpdate, is(userAfterCreation)); 80 | } 81 | 82 | @Test 83 | public void shouldSetModifiedTimestampOnEveryUpdate() throws InterruptedException { 84 | entity = repo.save(entity); 85 | 86 | entity.setTitle("Updated Title"); 87 | entity = repo.save(entity); 88 | 89 | Timestamp timestampAfterFirstUpdate = entity.getModifiedAt(); 90 | assertThat(timestampAfterFirstUpdate, is(notNullValue())); 91 | 92 | Thread.sleep(5); // Better: mock time! 93 | 94 | entity.setTitle("Updated Title 2"); 95 | entity = repo.save(entity); 96 | Timestamp timestampAfterSecondUpdate = entity.getModifiedAt(); 97 | assertThat(timestampAfterSecondUpdate, is(not(timestampAfterFirstUpdate))); 98 | } 99 | 100 | @Test 101 | public void shouldSetModifiedByOnEveryUpdate() throws InterruptedException { 102 | entity = repo.save(entity); 103 | 104 | entity.setTitle("Updated Title"); 105 | entity = repo.save(entity); 106 | 107 | String userAfterFirstUpdate = entity.getModifiedBy(); 108 | assertThat(userAfterFirstUpdate, is("Auditor_2")); 109 | 110 | Thread.sleep(5); // Better: mock time! 111 | 112 | entity.setTitle("Updated Title 2"); 113 | entity = repo.save(entity); 114 | String userAfterSecondUpdate = entity.getModifiedBy(); 115 | assertThat(userAfterSecondUpdate, is(not(userAfterFirstUpdate))); 116 | } 117 | 118 | 119 | @Test(expected = ObjectOptimisticLockingFailureException.class) 120 | public void shouldUseVersionForConflicts() { 121 | // persists entity and sets initial version 122 | entity = repo.save(entity); 123 | 124 | entity.setTitle("entity instance 1"); 125 | repo.save(entity); // returns instance with updated version 126 | 127 | repo.save(entity); // tries to persist entity with outdated version 128 | } 129 | 130 | @Test 131 | public void shouldFindByTitle() { 132 | String title = "Find me"; 133 | 134 | entity.setTitle(title); 135 | repo.save(entity); 136 | 137 | Advertisement foundEntity = repo.findByTitle(title).get(0); 138 | assertThat(foundEntity.getTitle(), is(title)); 139 | } 140 | } -------------------------------------------------------------------------------- /spring-security-basis/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.profiles.active=test,uaamock 2 | 3 | spring.jpa.generate-ddl=true -------------------------------------------------------------------------------- /vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # some data to make the urls unique 3 | # change to another value this if the hostname is already taken 4 | # use lowercase characters! 5 | ID: p0123456 6 | 7 | # Choose cfapps.eu10.hana.ondemand.com for the EU10 landscape, cfapps.us10.hana.ondemand.com for US10 8 | LANDSCAPE_APPS_DOMAIN: cfapps.eu10.hana.ondemand.com 9 | 10 | --------------------------------------------------------------------------------