Skip to content

Commit

Permalink
Merge pull request #1 from rnavagamuwa/sample+sdk
Browse files Browse the repository at this point in the history
Sample+sdk
  • Loading branch information
rnavagamuwa committed Mar 21, 2019
2 parents a2ba824 + 8b359a7 commit 1056ae7
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 23 deletions.
74 changes: 73 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,73 @@
# spring-security-abac
# XACML based authorization for Spring security

### Overview

Even though spring security provides role-based access control it doesn’t allow users to perform policy-based authorization. The main goal of this project is to write an agent which can be used to perform attribute-based access control for Spring security.

### Implementation

Spring security provides an annotation for custom authorization evaluations.

As the initial version, I have managed to write a working sample for this use case. This sample talks to WSO2 PDP for authorization.

#### The high-level sequence diagram


![](https://i.imgur.com/CUBbSxB.png)


#### Usage

1. Create a `keystore` and a `trustStore` in *Resources* directory.
2. Create a file named `xacmlConfig.json` in *Resources* directory. This file contains the body of the XACML request.
* This file is a json file and this can have more than one *Target Domain Objects*. In this case let's define our target domain object as **admin_xacml**.
* All the variables should start with **'$'**. For example if **action-id** is the variable it should be defined in the `xacmlConfig.sjon` as **$action-id**.

A sample `xacmlConfig.json` file is as follows.
````
{
"admin_xacml": {
"Request": {
"Action": {
"Attribute": [
{
"AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id",
"Value": "$action-id"
}
]
},
"Resource": {
"Attribute": [
{
"AttributeId": "urn:oasis:names:tc:xacml:1.0:resource:resource-id",
"Value": "$resource-id"
}
]
}
}
}
}
````
3. Extend `GlobalMethodSecurityConfiguration` class and set `AttributeEvaluator` as the new `PermissionEvaluator`
```
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new AttributeEvaluator());
return expressionHandler;
}
}
```
4. Now add the `@PreAuthorize("hasPermission()")` or `@PostAuthorize("hasPermission()")` annotation as required before the correct controller method. *Target Domain Object* and the *Permissions* should be passed to this annotaion as parameters.*Permissions* is a json object which contains the key value pairs. These permission values will be extracted from the *headers*.

```
@PreAuthorize("hasPermission('admin_xacml','{$action-id:action-id,$resource-id:resource-id}')")
```

7 changes: 6 additions & 1 deletion sample/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
spring.thymeleaf.cache=false
#logging.level.web=debug
xacml.pdp.url=https://localhost:9443/api/identity/entitlement/decision/pdp
xacml.pdp.url.authorize=https://localhost:9443/api/identity/entitlement/decision/pdp
xacml.pdp.url.resourceList=https://localhost:9443/api/identity/entitlement/decision/home
xacml.pdp.trustStore=truststore
xacml.pdp.trustStore.password=password
xacml.pdp.keyStore=keystore
xacml.pdp.keyStore.password=password
Binary file added sample/src/main/resources/keystore
Binary file not shown.
Binary file added sample/src/main/resources/truststore
Binary file not shown.
5 changes: 5 additions & 0 deletions sdk/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
<artifactId>ehcache</artifactId>
</dependency>

<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>

</dependencies>

<properties>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package org.wso2.spring.security.abac;

import org.json.JSONObject;

import java.util.Optional;

/**
* @author Randika Navagamuwa
*/
public interface AttributeHandler {

boolean authorize(String policyRequest);

Optional<JSONObject> getApiResourceList();
}
Original file line number Diff line number Diff line change
@@ -1,69 +1,117 @@
package org.wso2.spring.security.abac;

import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.ResourceUtils;
import org.springframework.web.client.RestTemplate;
import org.wso2.spring.security.abac.cache.CacheManager;
import org.wso2.spring.security.abac.cache.EhCacheManager;
import org.wso2.spring.security.abac.exception.AttributeEvaluatorException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.Collections;
import java.util.Optional;
import java.util.Properties;
import javax.net.ssl.SSLContext;

/**
* @author Randika Navagamuwa
*/
@SuppressWarnings("WeakerAccess")
public class XacmlAttributeHandler implements AttributeHandler {

private static String XACML_PDP_URL;
private CacheManager cacheManager;
private static String XACML_PDP_AUTHORIZE_URL;
private static String XACML_PDP_RESOURCE_LIST_URL;
private static String TRUST_STORE;
private static String TRUST_STORE_PASSWORD;
private static String KEY_STORE;
private static String KEY_STORE_PASSWORD;

private CacheManager responseCacheManager;
private SSLContext sslContext;
private HttpHeaders headers;

public XacmlAttributeHandler() {

try {
XACML_PDP_URL = PropertiesLoaderUtils
.loadAllProperties("application.properties")
.getProperty("xacml.pdp.url");
Properties properties = PropertiesLoaderUtils
.loadAllProperties("application.properties");
XACML_PDP_AUTHORIZE_URL = properties.getProperty("xacml.pdp.url.authorize");
XACML_PDP_RESOURCE_LIST_URL = properties.getProperty("xacml.pdp.url.resourceList");
TRUST_STORE = properties.getProperty("xacml.pdp.trustStore");
TRUST_STORE_PASSWORD = properties.getProperty("xacml.pdp.trustStore.password");
KEY_STORE = properties.getProperty("xacml.pdp.keyStore");
KEY_STORE_PASSWORD = properties.getProperty("xacml.pdp.keyStore.password");
} catch (IOException e) {

//todo stop the whole app
throw new AttributeEvaluatorException("Failed to read the XACML PDP Url", e);
}

if (XACML_PDP_URL == null) {
if (XACML_PDP_AUTHORIZE_URL == null) {
//todo stop the whole app
}

this.cacheManager = new EhCacheManager();
try {
this.sslContext = SSLContextBuilder
.create()
.loadKeyMaterial(loadPfx("classpath:".concat(KEY_STORE), KEY_STORE_PASSWORD.toCharArray()),
KEY_STORE_PASSWORD.toCharArray())
.loadTrustMaterial(ResourceUtils.getFile("classpath:".concat(TRUST_STORE)),
TRUST_STORE_PASSWORD.toCharArray())
.build();
} catch (Exception e) {

//todo stop the whole app
throw new AttributeEvaluatorException("Failed to read keystore or truststore", e);
}

this.headers = new HttpHeaders();
this.headers.setContentType(MediaType.APPLICATION_JSON);
this.headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
this.headers.set("WSO2-Identity-User", "admin");

this.responseCacheManager = new EhCacheManager();
}

@Override
public boolean authorize(String authRequest) {

String cachedResponse = this.cacheManager.get(authRequest);
String cachedResponse = this.responseCacheManager.get(authRequest);

if (cachedResponse == null) {
RestTemplate restTemplate = new RestTemplate();

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setBasicAuth("admin", "admin");
HttpClient client = HttpClients.custom()
.setSSLContext(sslContext)
.build();

HttpEntity<String> entity = new HttpEntity<>(authRequest, headers);
RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder().requestFactory(() ->
new HttpComponentsClientHttpRequestFactory(client));
RestTemplate rt = restTemplateBuilder.build();

ResponseEntity response = restTemplate.postForEntity(XACML_PDP_URL, entity, String.class);
if (response.getBody() == null) {
HttpEntity<String> entity = new HttpEntity<>(authRequest, this.headers);

ResponseEntity response = rt.postForEntity(XACML_PDP_AUTHORIZE_URL, entity, String.class);
if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) {
return false;
}
cachedResponse = response.getBody().toString();
this.cacheManager.putIfAbsent(authRequest, cachedResponse);
this.responseCacheManager.putIfAbsent(authRequest, cachedResponse);
}

JSONObject responseObj = new JSONObject(cachedResponse);
Expand All @@ -82,4 +130,43 @@ public boolean authorize(String authRequest) {
return true;
}

@Override
public Optional<JSONObject> getApiResourceList() {

String cachedResponse = this.responseCacheManager.get(XACML_PDP_RESOURCE_LIST_URL);

if (cachedResponse == null) {

HttpClient client = HttpClients.custom()
.setSSLContext(sslContext)
.build();

RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder().requestFactory(() ->
new HttpComponentsClientHttpRequestFactory(client));
RestTemplate rt = restTemplateBuilder.build();

HttpEntity<String> entity = new HttpEntity<>(this.headers);

ResponseEntity response = rt.getForEntity(XACML_PDP_RESOURCE_LIST_URL, String.class, entity);

if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) {

return Optional.empty();
}
cachedResponse = response.getBody().toString();
this.responseCacheManager.putIfAbsent(XACML_PDP_RESOURCE_LIST_URL, cachedResponse);
}
return Optional.of(new JSONObject(cachedResponse));
}

private KeyStore loadPfx(String file, char[] password) throws Exception {

KeyStore keyStore = KeyStore.getInstance("JKS");
File key = ResourceUtils.getFile(file);
try (InputStream in = new FileInputStream(key)) {
keyStore.load(in, password);
}
return keyStore;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,26 @@ public class EhCacheManager implements org.wso2.spring.security.abac.cache.Cache
private static long MAX_ENTRIES = 100;

private CacheManager cacheManager;
private Cache<String, String> responseCache;
private Cache<String, String> cache;

public EhCacheManager() {

cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build();
cacheManager.init();

responseCache = cacheManager.createCache(CACHE_NAME, CacheConfigurationBuilder
cache = cacheManager.createCache(CACHE_NAME, CacheConfigurationBuilder
.newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(MAX_ENTRIES))
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(EXPIRY_IN_MINUTUES)))
.build());
}

public String get(String cahceKey) {

return this.responseCache.get(cahceKey);
return this.cache.get(cahceKey);
}

public String putIfAbsent(String key, String value) {

return this.responseCache.putIfAbsent(key, value);
return this.cache.putIfAbsent(key, value);
}
}

0 comments on commit 1056ae7

Please sign in to comment.