Assume you've understood how Spring-based applications can be basically secured as explained here, this sample project goes one step further, demonstrating instance-based access control using access control lists (ACL) in applications based on Spring Boot and deployed on SAP Business Technology Platform, Cloud Foundry (SAP BTP CF).
This Spring Boot 2.0 demo application 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 SAP Container Security Library (Java), which is available on maven central.
Instances, you want to protect, could be business objects (e.g. procurement order), data records on the database (e.g. leave request) or any other resource.
The microservice is adapted from the code developed in the openSAP course: Cloud-Native Development with SAP CP and runs in the Cloud Foundry environment within SAP Business Technology Platform.
This document is divided into the following sections
- Understanding Access Control Lists (ACL) - understand when and how ACL helps (in comparison to Attribute-Based Access Control (ABAC))
- Use Cases
- Download and Installation - a description of how to use this project
- Steps to Deploy and Test on Cloud Foundry - explains how to deploy and test the application on Cloud Foundry
- Implementation Details - dig into details of its implementation, configuration, notable features
- Further References - references and further learning material
Assume you have a cloud-native application that exposes REST APIs to access (read, write,...) instances that are worth protecting. An instance could be a business object having the type of a procurement order, a leave request or in our case an advertisement. With Access Control Lists (ACL) you can control the access to dedicated instances.
An Access Control List (ACL) is a list of permissions attached to dedicated instances. An ACL specifies for an instance which operations (e.g. read, write, publish, ...) are granted to an identity. An identity can be a user principal or match to a user role/attribute.
The ACL approach can be useful when:
- The access to instances cannot be solely protected by roles (scopes and attributes), and in addition there is no "static" criteria (like cost center, confidentiality level, ...) that could be used as a filter. Then the decision whether a user is authorized for a certain instance can be defined independently per instance just by maintaining the ACL.
- The instance-based access shall change dynamically during the instance lifecycle without changing the instance and without consulting the administrator. For example, the instance owner wants to delegate permissions temporarily to dedicated users/substitutes or user groups.
- You want to model ACL hierarchies.
- For example, a facilitator should get edit-permissions to some buildings within a location. Furthermore, he should have the same permissions to most of the rooms, that are linked to the buildings.
- Same could be the case for organizational hierarchies, like cost center.
- You like to manage the ACL as part of your application and close to the instances, you want to protect without delegating the task of authorization management to a dedicated user administrator.
- Simplicity, and lower development effort as no additional infrastructure is needed.
Attribute-Based Access Control is explained and implemented with this basis sample project.
- The authorization decision is better understandable as missing authorizations can be clearly identified as such and not just as a filtered result list of an operation, which can have different reasons.
- Simplifies the implementation of audit logs and GDPR compliance as all relevant data is stored in central authorization tables.
- Not limited to the attributes of an instance that needs somehow to match the attributes attached to a Role.
- The ACL Approach can be combined with the Filter approach (with the cost of an additional table JOIN).
The user with log-on name advertiser
creates a new advertisement (id = 55
). You can test with POST-request /api/v1/ads/acl/
that the following entries are created in the postgreSql test
database.
-
For the user principal
advertiser
an entry gets created (if not yet there) in theACL_SID
table:id principal sid 7 true advertiser Note: the
sid
value is in fact notadvertiser
but contains also the origin (IdP) of the JWT tokenuser/{origin}/advertiser
. -
For the new advertisement instance with id
55
an entry gets created in theACL_OBJECT_IDENTITY
table:id object_id_class object_id_identity parent_object owner_sid entries_inheriting ... ... ... ... ... ... 3 1 55 1200 1 true The
ACL_OBJECT_IDENTITY
table refers to ancom.sap.cp.appsec.domain.Advertisement
instance. The advertisement instance id is specified in theobject_id_identity
column and its type is classified in theACL_CLASS
table. -
For each permission (read, write, admin, ...) the user (sid =
7
) gets for the instance (acl_object_identity =3
) anACL_ENTRY
table entry:id acl_object_identity ace_order sid mask granting audit_success audit_failure 26 3 0 7 1 true true true 27 3 1 7 2 true true true 28 3 2 7 16 true true true It's possible to insert both granting and revoking entries in
ACL_ENTRY
. Theace_order
matters meaning it's possible to revoke access in line 0 and that will take precedence over a grant on line 1.
Any user with "admin" permission (e.g. instance owner) for an advertisement should be able to grant permissions to other individuals (e.g. myfriend
) without involvement of an administrator. You can test with PUT-request to /api/v1/ads/acl/grantPermissionsToUser/{id}
.
-
Again, for the user principal
myfriend
an entry gets created (if not yet there) in theACL_SID
table:id principal sid 7 true advertiser 8 true myfriend -
For each permission (read, write, admin, ...) the user (sid =
8
) gets for the instance (acl_object_identity =3
) anACL_ENTRY
table entry:id acl_object_identity ace_order sid mask granting audit_success audit_failure ... ... ... ... ... ... ... ... 29 3 3 8 1 true true true 30 3 4 8 2 true true true 31 3 5 8 16 true true true
Any user with "admin" permission for an advertisement should be able to grant permission to all users of a dedicated user group. You can test with PUT-request to /api/v1/ads/acl/grantPermissionsToUserGroup/{id}
.
First question might be, how to model / specify a user group? How we've done it: any user must provide its group assignnments as xs.user.attributes
as part of its JWT token:
{
...
"scope": [],
"xs.user.attributes": {
"group": [
"UG_MY_TEAM"
]
}
}
These groups e.g. UG_MY_TEAM
must be exposed in the ACL_SID
table:
id | principal | sid |
---|---|---|
7 | true | advertiser |
8 | true | myfriend |
9 | false | ATTR:GROUP=UG_MY_TEAM |
10 | false | ATTR:GROUP=UG_OTHER_TEAM |
For each permission (read, write,...) the user group "UG_MY_TEAM" (sid = 9
) gets for the instance (acl_object_identity = 3
) an ACL_ENTRY
table entry as we've learnt in the examples above. With that the number of ACL_ENTRY
table entries can be reduced by the number of individuals a user group consists of.
Technical note: when grouping individuals by non-principal SIDs (e.g.
ATTR:GROUP=UG_MY_TEAM
) the number of entries in theACL_ENTRY
table can be reduced.
Now the advertisement
(id = 55
) is in a final state and should be published to a bulletinboard that is watched by many other people.
Here the user acts as "publisher", who requires "admin" permission of the advertisement to be published as well as access to the target bulletinboard:
{
...
"scope": [],
"xs.user.attributes": {
"bulletinboard": [
"DE_WDF03_Board"
]
}
}
Note that in our case the bulletinboard is not a core domain object that is managed by our Advertisement
application.
So we'd only like to refer to it by a key of type java.lang.String
.
-
"Static" entries in
ACL_CLASS
table:id class class_id_type 2.000 location java.lang.String 2.002 bulletinboard java.lang.String 1 com.sap.cp.appsec.domain.Advertisement java.lang.Long Note that the target parent instance - in our case the
bulletinboard
- must not necessarily be an instance of a Java Class with an identifier of typejava.lang.Long
. With Spring security version5.2.0
you can also specify a unique identifier such asDE_WDF03_Board
of typejava.lang.String
orjava.util.UUID
in theACL_OBJECT_IDENTITY
table. -
Entries in
ACL_OBJECT_IDENTITY
before publishing:id object_id_class object_id_identity parent_object owner_sid entries_inheriting 1.100 2.000 DE [NULL] 3.000 false 1.101 2.000 IL [NULL] 3.000 false 1.200 2.002 DE_WDF03_Board 1.100 3.005 true 1.201 2.002 DE_WDF04_Board 1.100 3.005 true 1.202 2.002 IL_RAA03_Board 1.101 3.005 true 3 1 55 [NULL] 1 true -
After successful publishing the column
parent_object
of the advertisementsACL_OBJECT_IDENTITY
entry has changed:id object_id_class object_id_identity parent_object owner_sid entries_inheriting ... ... ... ... ... ... 3 1 55 1200 1 true Technical note: with hierachical permissions the number of entries in the
ACL_OBJECT_IDENTITY
table can be reduced.
User with permissions for a bulletinboard, has the same permissions for its (published) advertisement instances. In this case the advertisement instances inherits the permissions of the parent instance, i.e. the bulletinboard.
User with permissions for a location, has the same permissions for its bulletinboards and as well its (published) advertisement instances. In this case the advertisement instances inherits the permissions of its parent instances, i.e. the bulletinboard, location.
User with "read" permission can paginate advertisement instances, he has "read" authorizations for. You can test with GET-request to http://localhost:8080/api/v1/ads/acl/published
.
User with "admin" permission for an advertisement can remove permissions to it from users (user groups). You can test with PUT-request to /api/v1/ads/acl/removePermissionsFromUser/{id}
.
After this the related entries for the principal user (sid) should disappear from the ACL_ENTRY
table.
The JdbcMutableAclService
Spring ACL class supports the following methods:
deleteAcl(ObjectIdentity objectIdentity, boolean deleteChildren)
This should be called whenever an object instance, for which acl entries are mapped, is deleted.
But this will not remove all / some permissions from a dedicated user principal. This needs to be implemented in the ACL Service wrapper class, namely AclSupport.
See also the Spring forum here.
Setup your development environment according to the description here.
We need to make sure that a PostgreSQL database is running on the local machine, as referenced in application-localdb.properties
.
The docker-compose.yml
specifies all required docker containers.
In order to start a fresh database container with PostgreSQL, execute
docker-compose up -d
To tear down all containers, execute:
docker-compose down
To run the application locally you have two options: start it directly via Maven on the command line or within your IDE (Eclipse, IntelliJ).
In both cases, your application will be deployed to an embedded Tomcat web server and is visible at the address http://localhost:8080/api/v1/ads/acl
.
The provided localEnvironmentSetup
shell script can be used to set the necessary values for local execution. Within your development IDE (Eclipse, IntelliJ), you need to define the following environment variables: VCAP_APPLICATION
and SPRING_PROFILES_ACTIVE
- as done in the script.
Execute in terminal (within project root, which contains thepom.xml
):
source localEnvironmentSetup.sh
mvn spring-boot:run
Or on Windows command line:
localEnvironmentSetup.bat
mvn spring-boot:run
In Eclipse Spring Tool Suite (STS) you can import the project as an existing Maven project. There you can start the main method in com.sap.cp.appsec.Application
.
You can also right-click on the class in the Package Explorer, and select Run As
- Spring Boot App
. Make sure that you have set in the same environment variables in the Run Configuration as specified in the localEnvironmentSetup script
.
Now you are ready to test the application manually using the Postman
chrome plugin.
The service endpoints are secured, that means no unauthorized user can access the endpoint. The application expects a so called JWT
(JSON Web Token) as part of the Authorization
header of the service that also contains the scope, the user is assigned to.
You can import the Postman collection, as well as the Postman environment that provides different JWT tokens for the Authorization
headers to do some sample requests.
Note: For all requests make sure, that you provide a header namely Authorization
with a JWT token as value e.g. Bearer eyJhbGciOiJSUzI1NiIs...
.
For reference look up Postman documentation.
Build the Advertisement Service which is a Java web application running in a Java VM. Maven build tool compiles the code and packages it in its distributable format, such as a JAR
(Java Archive). With this the maven dependencies are downloaded from the Maven central into the ~/.m2/repository
directory. Furthermore the JUnit tests are executed and the target/demo-application-security-acl.jar
is created.
Execute in the command line (within project directory, which contains thepom.xml
):
mvn package
Make sure your are logged in to Cloud Foundry and you target your trial space. The following commands will setup your environment to use the provided Cloud Foundry instance.
cf api <<Your API endpoint>>
(API endpoints are listed here)cf login -u <<your user id>>
- In case you are assigned to multiple orgs, select the
trial
organisation.
Create the (backing) services that are specified in the manifest.yml
.
Execute in terminal (within project directory, which contains the security
folder):
cf create-service postgresql v9.6-dev postgres-bulletinboard-ads
cf create-service xsuaa application uaa-bulletinboard -c security/xs-security.json
Using the marketplace (
cf m
) you can see the backing services and its plans that are available on SAP BTP and (!) you are entitled to use.
As a prerequisite step open the ../vars.yml file locally and replace the ID
for example by your SAP account user name, to make the routes unique. You might want to adapt the LANDSCAPE_APPS_DOMAIN
as well.
The application can be built and pushed using these commands (within root directory, which contains themanifest.yml
):
cf push --vars-file ../vars.yml
The application will be pushed using the settings provided in the
manifest.yml
and../vars.yml
. You can get the exact urls/routes of your deployed application withcf apps
.
We make use of the trial
subaccount. As you can see in the SAP BTP Cockpit subaccounts have properties (see Subaccount Details) which of the most important one is the Subdomain. The Subdomain serves as the value for the technical property Tenant ID.
The Tenant ID is encoded in the url, for example https://<<your tenant>>-approuter-<<ID>>.<<LANDSCAPE_APPS_DOMAIN>>
.
That's why we need to specify an approuter route per Tenant ID (subdomain name), e.g. p012345trial
. For example:
cf map-route approuter <<LANDSCAPE_APPS_DOMAIN e.g. cfapps.eu10.hana.ondemand.com>> -n <<your tenant e.g. p0123456trial>>-approuter-<<ID e.g. p0123456>>
And cf app approuter
shows another tenant-specific approuter route, which is hereinafter also called "approuterUri
".
Go to the SAP BTP Cockpit
- Navigate to your bulletinboard-ads application. Create some Role, e.g.
ROLE_MY_TEAM_MEMBER
based on theGroupMember
role template. And specify its attributegroup
=UG_MY_TEAM
:
Note: Alternatively, in case your IdP supports SAML user attributes, you can also map to a dedicated SAML user attribute e.g.
Groups
. This gives you the advantage to inherit all group values, that are managed as part of the IdP.
- Then navigate to your Subaccount and create a Role Collection e.g.
RC_GroupMember_MY_TEAM
and add the created Role. - Finally, as part of your Identity Provider, e.g. SAP ID Service, assign the created Role Collection to your user.
Further up-to-date information you can get on sap.help.com:
- Maintain Roles for Applications.
- Maintain Role Collections
- Assign Role Collections to Business Users
Open a browser to test whether your microservice runs in the cloud. Call the tenant-specific approuterUri
URL as created previously. This will bring you the login page. Note: You have to enter here your SAP Cloud Identity credentials. After successful login you get redirected to the advertisement service that returns you the status of the application.
Note: This
xs-app.json
file specifies how the approuter routes are mapped to the advertisement routes. E.g.<<approuterUri>>/ads/actuator/health
maps to<<bulletinboardAdsUri>>/actuator/health
.
Test the deployed REST Service on Cloud Foundry via the approuter URI using the Postman
chrome plugin together with the Postman Interceptor
chrome plugin. You can import the Postman collection and create an environment, which specifies the key-value pair approuterUri
=<<your tenant-specific approuterUri>>
.
Find a more detailed description on how to test using Postman
in the basis sample.
- Setup Spring ACL database table (using liquibase): database changelog, database population
- Configure Spring ACL: AclConfig and AclAuditLogger
- Convenience wrapper for Spring
AclService
implementation: AclSupport - Assign user's attributes to Spring Security Context: CustomTokenAuthorizationsExtractor.
- Assign and validate instance permissions: AdvertisementService.
- Support of pagination is implemented with Spring Data and SQL CE functions: AdvertisementAclRepository
- The application should support values-request for the application-specific
xs.user.attributes
, e.g./api/v1/attribute/group
: AttributeFinderController. - Application security model consists of role-templates with references to attributes only: xs-security.json
By default, Spring ACL refers to BasePermission
class for all available permissions (actions).
You may subclass BasePermission
in order to define application specific permissions. In this case you also need to replace as part of the lookupStrategy
bean the DefaultPermissionFactory
by your custom PermissionFactory
implementation.
By default the DefaultPermissionGrantingStrategy
does only support exact mask matching, even when using a composite mask e.g. "RW". This behaviour was discussed here on Spring ACL github project:
With Spring ACL you can reject unauthorized access of instances on method level using @PreAuthorize("hasPermission(<object>,<permission>)")
or @PreAuthorize("hasPermission(<id>, <type>, <permission>)")
. Furthermore you can filter the result set by making use of @PostAuthorize("hasPermission(<object>, <permission>)")
, after the result set has been retrieved from underlying layers (often database). This is obviously not the best approach in terms of performance, too many useless objects are retrieved but evicted by the filter. Let's consider pagination and assume a page size of 20 items. Spring Data repository returns up to 20 items, but all of them need to be filtered, as the caller is not granted to access those...
As consequence we have implemented as part of our Spring Data repository our own SQL CE function to fetch only these instances from the database, the user has granted access to.
Example: Spring Data repository implementation.
You can implement an audit logger that is able to write audit-relevant logs in case of granted / un-granted access to an AuditableAcl (ACL) or more precisely to an AuditableAccessControlEntry (ACE).
Note: by default, an ACE is not configured to audit successful or failed access. You need to specify that in context of the AuditableAcl
object with updateAuditing(aceIndex, auditSuccess, auditFailure)
.
By default AuditLogger.logIfNeeded(isGranted, ace)
is only called in a few cases, e.g. in case of owner change, change of audit log settings or general change (see AclAuthorizationStrategy
).
Additionlly note that AuditLogger.logIfNeeded(isGranted, ace)
with isGranted=false
is only called when ACE explicitly specifies "granted=false" and not when a permission (ACE) is missing.
Therefore we suggest to enhance your audit logger implementation in order to handle other audit relevant *events".
Example: Implementation of AuditLogger.
DEBUG 16360 --- o.s.s.w.a.i.FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /api/v1/ads/acl; Attributes: [authenticated]
DEBUG 16360 --- o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated: com.sap.cloud.security.xsuaa.token.AuthenticationToken@e69e23c2: Principal: user/userIdp/adOwner; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ATTR:BULLETINBOARD=IL_RAA03_Board, ATTR:LOCATION=DE
DEBUG 16360 --- o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@63e17053, returned: 1
DEBUG 16360 --- o.s.s.w.a.i.FilterSecurityInterceptor : Authorization successful
INFO 16360 --- com.sap.cp.appsec.config.AclAuditLogger : CREATED ACE: AccessControlEntryImpl[id: 100000005024; granting: true; sid: PrincipalSid[user/userIdp/adOwner]; permission: BasePermission[...............................R=1]; auditSuccess: true; auditFailure: true]
INFO 16360 --- com.sap.cp.appsec.config.AclAuditLogger : CREATED ACE: AccessControlEntryImpl[id: 100000005025; granting: true; sid: PrincipalSid[user/userIdp/adOwner]; permission: BasePermission[..............................W.=2]; auditSuccess: true; auditFailure: true]
INFO 16360 --- com.sap.cp.appsec.config.AclAuditLogger : CREATED ACE: AccessControlEntryImpl[id: 100000005026; granting: true; sid: PrincipalSid[user/userIdp/adOwner]; permission: BasePermission[...........................A....=16]; auditSuccess: true; auditFailure: true]
DEBUG 16360 --- s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
For the POST-request the user must only be authenticated (see WebSecurityConfig), additionally we log the creation of the three ACEs that provides the owner of the created advertisement these access permissions: read, write and admin.
DEBUG 16360 --- o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/api/v1/ads/acl/grantPermissionsToUser/1'; against '/api/v1/ads/acl/**'
DEBUG 16360 --- o.s.s.w.a.i.FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /api/v1/ads/acl/grantPermissionsToUser/1; Attributes: [authenticated]
DEBUG 16360 --- o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated: com.sap.cloud.security.xsuaa.token.AuthenticationToken@810322a0: Principal: user/userIdp/adOwner; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ATTR:BULLETINBOARD=IL_RAA03_Board, ATTR:LOCATION=DE
...
DEBUG 16360 --- o.s.s.a.i.a.MethodSecurityInterceptor : Secure object: ReflectiveMethodInvocation: public void com.sap.cp.appsec.services.AdvertisementService.grantPermissions(java.lang.Long,java.lang.String,org.springframework.security.acls.model.Permission[]); target is of class [com.sap.cp.appsec.services.AdvertisementService]; Attributes: [[authorize: 'hasPermission(#id, 'com.sap.cp.appsec.domain.Advertisement', 'administration')', filter: 'null', filterTarget: 'null']]
DEBUG 16360 --- o.s.s.a.i.a.MethodSecurityInterceptor : Previously Authenticated: com.sap.cloud.security.xsuaa.token.AuthenticationToken@e774a5e7: Principal: user/userIdp/adOwner; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ATTR:BULLETINBOARD=IL_RAA03_Board, ATTR:LOCATION=DE
DEBUG 16360 --- o.s.s.acls.AclPermissionEvaluator : Checking permission 'administration' for object 'org.springframework.security.acls.domain.ObjectIdentityImpl[Type: com.sap.cp.appsec.domain.Advertisement; Identifier: 1]'
INFO 16360 --- com.sap.cp.appsec.config.AclAuditLogger : GRANTED due to ACE: AccessControlEntryImpl[id: 100000005026; granting: true; sid: PrincipalSid[user/userIdp/adOwner]; permission: BasePermission[...........................A....=16]; auditSuccess: true; auditFailure: true]
DEBUG 16360 --- o.s.s.acls.AclPermissionEvaluator : Access is granted
DEBUG 16360 --- o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter@4f3891db, returned: 1
DEBUG 16360 --- o.s.s.a.i.a.MethodSecurityInterceptor : Authorization successful
...
INFO 16360 --- com.sap.cp.appsec.config.AclAuditLogger : GRANTED due to ACE: AccessControlEntryImpl[id: 100000005026; granting: true; sid: PrincipalSid[user/userIdp/adOwner]; permission: BasePermission[...........................A....=16]; auditSuccess: true; auditFailure: true]
INFO 16360 --- com.sap.cp.appsec.config.AclAuditLogger : GRANTED due to ACE: AccessControlEntryImpl[id: 100000005026; granting: true; sid: PrincipalSid[user/userIdp/adOwner]; permission: BasePermission[...........................A....=16]; auditSuccess: true; auditFailure: true]
INFO 16360 --- com.sap.cp.appsec.config.AclAuditLogger : CREATED ACE: AccessControlEntryImpl[id: 100000005030; granting: true; sid: PrincipalSid[user/userIdp/otherOne]; permission: BasePermission[...........................A....=16]; auditSuccess: true; auditFailure: true]
INFO 16360 --- com.sap.cp.appsec.config.AclAuditLogger : CREATED ACE: AccessControlEntryImpl[id: 100000005031; granting: true; sid: PrincipalSid[user/userIdp/otherOne]; permission: BasePermission[...............................R=1]; auditSuccess: true; auditFailure: true]
DEBUG 16360 --- s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
For the PUT-request /api/v1/ads/acl/grantPermissionsToUser/{advertisementId}
the user must be authenticated (see WebSecurityConfig). Then the @PreAuthorize
method security expression checks, whether the user has admin permissions. Finally, as the given ACL for an existing advertisment is changed the admin permission is again checked before the new ACEs are persisted. Additionally we log the creation of the ACEs that provides another user (here: otherOne
) these access permissions: admin and read.
- AclPermissionEvaluator.hasPermission
- AclImpl.isGranted
- Dual control principle. Example: an advertisement can only be published, when another user (with a dedicated role) has approved it.
- Time-based access control. Example: A user has only access granted in a dedicated time-period, like a representative. This person should not be able to access instances that were created/updated in a time period before or after time period.
- REST endpoints using Spring Web MVC (
@RestController
) - Tests (Servlet with
RestTemplate
,MockMvc
, JUnit, Mockito, Hamcrest) - JPA Implementation: Hibernate
- Spring Data (repository)
- Security with Spring Security and Xsuaa
- Spring Security ACL, setup database schema using Liquibase