Merge pull request #3597 from jlpettersson/KEYCLOAK-4018

KEYCLOAK-4018 Client-Based Policy
This commit is contained in:
Pedro Igor 2016-12-09 17:32:19 -02:00 committed by GitHub
commit 588e1711dd
10 changed files with 488 additions and 1 deletions

View file

@ -0,0 +1,47 @@
package org.keycloak.authorization.policy.provider.client;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import static org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory.getClients;
public class ClientPolicyProvider implements PolicyProvider {
private final Policy policy;
private final AuthorizationProvider authorization;
public ClientPolicyProvider(Policy policy, AuthorizationProvider authorization) {
this.policy = policy;
this.authorization = authorization;
}
@Override
public void evaluate(Evaluation evaluation) {
EvaluationContext context = evaluation.getContext();
String[] clients = getClients(this.policy);
if (clients.length > 0) {
for (String client : clients) {
ClientModel clientModel = getCurrentRealm().getClientById(client);
if (context.getAttributes().containsValue("kc.client.id", clientModel.getClientId())) {
evaluation.grant();
return;
}
}
}
}
@Override
public void close() {
}
private RealmModel getCurrentRealm() {
return this.authorization.getKeycloakSession().getContext().getRealm();
}
}

View file

@ -0,0 +1,111 @@
package org.keycloak.authorization.policy.provider.client;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel.ClientRemovedEvent;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ClientPolicyProviderFactory implements PolicyProviderFactory {
@Override
public String getName() {
return "Client";
}
@Override
public String getGroup() {
return "Identity Based";
}
@Override
public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
return new ClientPolicyProvider(policy, authorization);
}
@Override
public PolicyProviderAdminService getAdminResource(ResourceServer resourceServer) {
return null;
}
@Override
public PolicyProvider create(KeycloakSession session) {
return null;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
factory.register(event -> {
if (event instanceof ClientRemovedEvent) {
KeycloakSession keycloakSession = ((ClientRemovedEvent) event).getKeycloakSession();
AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class);
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
ClientModel removedClient = ((ClientRemovedEvent) event).getClient();
policyStore.findByType(getId()).forEach(policy -> {
List<String> clients = new ArrayList<>();
for (String clientId : getClients(policy)) {
if (!clientId.equals(removedClient.getId())) {
clients.add(clientId);
}
}
try {
if (clients.isEmpty()) {
policyStore.findDependentPolicies(policy.getId()).forEach(dependentPolicy -> {
dependentPolicy.removeAssociatedPolicy(policy);
});
policyStore.delete(policy.getId());
} else {
policy.getConfig().put("clients", JsonSerialization.writeValueAsString(clients));
}
} catch (IOException e) {
throw new RuntimeException("Error while synchronizing clients with policy [" + policy.getName() + "].", e);
}
});
}
});
}
@Override
public void close() {
}
@Override
public String getId() {
return "client";
}
static String[] getClients(Policy policy) {
String clients = policy.getConfig().get("clients");
if (clients != null) {
try {
return JsonSerialization.readValue(clients.getBytes(), String[].class);
} catch (IOException e) {
throw new RuntimeException("Could not parse clients [" + clients + "] from policy config [" + policy.getName() + "].", e);
}
}
return new String[]{};
}
}

View file

@ -41,3 +41,4 @@ org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory
org.keycloak.authorization.policy.provider.scope.ScopePolicyProviderFactory
org.keycloak.authorization.policy.provider.time.TimePolicyProviderFactory
org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory
org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory

View file

@ -25,6 +25,10 @@ import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
@ -34,6 +38,7 @@ import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -327,6 +332,93 @@ public class ResourcePermissionManagementTest extends AbstractPhotozAdminTest {
assertEquals(0, evaluationsUserRole.size());
}
@Test
public void testResourceAccessWithClientBasedPolicy() throws Exception {
ClientModel testClient1 = getClientByClientId("test-client-1");
ClientModel testClient2 = getClientByClientId("test-client-2");
Policy clientPolicy = createClientPolicy(Collections.singletonList(testClient1));
PolicyRepresentation newPermission = new PolicyRepresentation();
newPermission.setName("Client Permission");
newPermission.setType("resource");
HashedMap config = new HashedMap();
config.put("defaultResourceType", "http://photoz.com/admin");
config.put("applyPolicies", JsonSerialization.writeValueAsString(new String[] {clientPolicy.getId()}));
newPermission.setConfig(config);
Response response = newPermissionRequest().post(Entity.entity(newPermission, MediaType.APPLICATION_JSON_TYPE));
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
PolicyRepresentation permission = response.readEntity(PolicyRepresentation.class);
onAuthorizationSession(authorizationProvider -> {
Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId());
assertNotNull(policyModel);
assertEquals(permission.getId(), policyModel.getId());
assertEquals(newPermission.getName(), policyModel.getName());
assertEquals(resourceServer.getId(), policyModel.getResourceServer().getId());
});
Map<String, DefaultEvaluation> evaluations = performEvaluation(
Collections.singletonList(new ResourcePermission(adminResource, Collections.emptyList(), resourceServer)),
createAccessTokenForClient(testClient1),
createClientConnection("127.0.0.1"));
assertEquals(1, evaluations.size());
assertTrue(evaluations.containsKey(clientPolicy.getId()));
assertEquals(Effect.PERMIT, evaluations.get(clientPolicy.getId()).getEffect());
Map<String, DefaultEvaluation> evaluations2 = performEvaluation(
Collections.singletonList(new ResourcePermission(adminResource, Collections.emptyList(), resourceServer)),
createAccessTokenForClient(testClient2),
createClientConnection("127.0.0.1"));
assertEquals(1, evaluations2.size());
assertTrue(evaluations2.containsKey(clientPolicy.getId()));
assertEquals(Effect.DENY, evaluations2.get(clientPolicy.getId()).getEffect());
}
private Policy createClientPolicy(List<ClientModel> allowedClients) {
return onAuthorizationSession(authorizationProvider -> {
StoreFactory storeFactory = authorizationProvider.getStoreFactory();
PolicyStore policyStore = storeFactory.getPolicyStore();
Policy policy = policyStore.create("Client-Based Policy", "client", resourceServer);
List<String> clientIds = new ArrayList<>();
for (ClientModel client : allowedClients) {
clientIds.add(client.getId());
}
String[] clients = clientIds.toArray(new String[clientIds.size()]);
HashedMap config = new HashedMap();
try {
config.put("clients", JsonSerialization.writeValueAsString(clients));
} catch (IOException e) {
throw new RuntimeException(e);
}
policy.setConfig(config);
return policy;
});
}
private AccessToken createAccessTokenForClient(ClientModel client) {
AccessToken accessToken = new AccessToken();
accessToken.setRealmAccess(new AccessToken.Access());
accessToken.issuedFor = client.getClientId();
return accessToken;
}
private PolicyRepresentation createAlbumResourceTypePermission() throws Exception {
PolicyRepresentation newPermission = new PolicyRepresentation();

View file

@ -153,6 +153,26 @@
"/confidential-no-service-account/*"
],
"secret": "secret"
},
{
"clientId": "test-client-1",
"secret": "secret",
"enabled": true,
"baseUrl": "test-client-1",
"redirectUris": [
"/test-client-1/*"
],
"webOrigins" : ["*"]
},
{
"clientId": "test-client-2",
"secret": "secret",
"enabled": true,
"baseUrl": "test-client-2",
"redirectUris": [
"/test-client-2/*"
],
"webOrigins" : ["*"]
}
]
}

View file

@ -1101,6 +1101,11 @@ authz-add-user-policy=Add User Policy
authz-no-users-assigned=No users assigned.
authz-policy-user-users.tooltip=Specifies which user(s) are allowed by this policy.
# Authz Client Policy Detail
authz-add-client-policy=Add Client Policy
authz-no-clients-assigned=No clients assigned.
authz-policy-client-clients.tooltip=Specifies which client(s) are allowed by this policy.
# Authz Time Policy Detail
authz-add-time-policy=Add Time Policy
authz-policy-time-not-before.tooltip=Defines the time before which the policy MUST NOT be granted. Only granted if current date/time is after or equal to this value.

View file

@ -1047,6 +1047,11 @@ authz-add-user-policy=Legg til policy for bruker
authz-no-users-assigned=Ingen tildelte brukere.
authz-policy-user-users.tooltip=Spesifiser bruker(e) som tillates av denne policien.
# Authz Client Policy Detail
authz-add-client-policy=Legg til policy for klient
authz-no-clients-assigned=Ingen tildelte klienter.
authz-policy-client-clients.tooltip=Spesifiser klient(er) som tillates av denne policien.
# Authz Time Policy Detail
authz-add-time-policy=Legg til policy for tid
authz-policy-time-not-before.tooltip=Definerer tiden f\u00F8r policien M\u00C5 IKKE innvilges. Denne innvilges kun om gjeldende dato/tid er f\u00F8r eller lik denne verdien.

View file

@ -263,6 +263,28 @@ module.config(['$routeProvider', function ($routeProvider) {
}
},
controller: 'ResourceServerPolicyUserDetailCtrl'
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/client/create', {
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-client-detail.html',
resolve: {
realm: function (RealmLoader) {
return RealmLoader();
},
client : function(ClientLoader) {
return ClientLoader();
}
},
controller: 'ResourceServerPolicyClientDetailCtrl'
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/client/:id', {
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-client-detail.html',
resolve: {
realm: function (RealmLoader) {
return RealmLoader();
},
client : function(ClientLoader) {
return ClientLoader();
}
},
controller: 'ResourceServerPolicyClientDetailCtrl'
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/role/create', {
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-role-detail.html',
resolve: {

View file

@ -883,6 +883,99 @@ module.controller('ResourceServerPolicyUserDetailCtrl', function($scope, $route,
}, realm, client, $scope);
});
module.controller('ResourceServerPolicyClientDetailCtrl', function($scope, $route, realm, client, PolicyController, Client) {
PolicyController.onInit({
getPolicyType : function() {
return "client";
},
onInit : function() {
$scope.clientsUiSelect = {
minimumInputLength: 1,
delay: 500,
allowClear: true,
query: function (query) {
var data = {results: []};
if ('' == query.term.trim()) {
query.callback(data);
return;
}
Client.query({realm: $route.current.params.realm, search: query.term.trim(), max: 20}, function(response) {
data.results = response;
query.callback(data);
});
},
formatResult: function(object, container, query) {
return object.clientId;
}
};
$scope.selectedClients = [];
$scope.selectClient = function(client) {
if (!client || !client.id) {
return;
}
$scope.selectedClient = null;
for (var i = 0; i < $scope.selectedClients.length; i++) {
if ($scope.selectedClients[i].id == client.id) {
return;
}
}
$scope.selectedClients.push(client);
}
$scope.removeFromList = function(list, index) {
list.splice(index, 1);
}
},
onInitUpdate : function(policy) {
var selectedClients = [];
if (policy.config.clients) {
var clients = eval(policy.config.clients);
for (var i = 0; i < clients.length; i++) {
Client.get({realm: $route.current.params.realm, client: clients[i]}, function(data) {
selectedClients.push(data);
$scope.selectedClients = angular.copy(selectedClients);
});
}
}
$scope.$watch('selectedClients', function() {
if (!angular.equals($scope.selectedClients, selectedClients)) {
$scope.changed = true;
}
}, true);
},
onUpdate : function() {
var clients = [];
for (var i = 0; i < $scope.selectedClients.length; i++) {
clients.push($scope.selectedClients[i].id);
}
$scope.policy.config.clients = JSON.stringify(clients);
},
onCreate : function() {
var clients = [];
for (var i = 0; i < $scope.selectedClients.length; i++) {
clients.push($scope.selectedClients[i].id);
}
$scope.policy.config.clients = JSON.stringify(clients);
}
}, realm, client, $scope);
});
module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route, realm, client, Client, ClientRole, PolicyController, Role, RoleById) {
PolicyController.onInit({
getPolicyType : function() {

View file

@ -0,0 +1,91 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' | translate}}</a></li>
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">{{:: 'authz-policies' | translate}}</a></li>
<li data-ng-show="create">{{:: 'authz-add-client-policy' | translate}}</li>
<li data-ng-hide="create">{{:: 'client' | translate}}</li>
<li data-ng-hide="create">{{originalPolicy.name}}</li>
</ol>
<h1 data-ng-show="create">{{:: 'authz-add-client-policy' | translate}}</h1>
<h1 data-ng-hide="create">{{originalPolicy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
data-ng-click="remove()"></i></h1>
<form class="form-horizontal" name="clientForm" novalidate>
<fieldset class="border-top">
<div class="form-group">
<label class="col-md-2 control-label" for="name">{{:: 'name' | translate}} <span class="required">*</span></label>
<div class="col-sm-6">
<input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required data-ng-blur="checkNewNameAvailability()">
</div>
<kc-tooltip>{{:: 'authz-policy-name.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="description">{{:: 'description' | translate}} </label>
<div class="col-sm-6">
<input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
</div>
<kc-tooltip>{{:: 'authz-policy-description.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="clients">{{:: 'clients' | translate}} <span class="required">*</span></label>
<div class="col-md-6">
<input type="hidden" ui-select2="clientsUiSelect" id="clients" data-ng-model="selectedClient" data-ng-change="selectClient(selectedClient);" data-placeholder="Select an client..." data-ng-required="selectedClients.length == 0">
</input>
</div>
<kc-tooltip>{{:: 'authz-policy-client-clients.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" style="margin-top: -15px;">
<label class="col-md-2 control-label"></label>
<div class="col-sm-3">
<table class="table table-striped table-bordered">
<thead>
<tr data-ng-hide="!selectedClients.length">
<th>{{:: 'clientId' | translate}}</th>
<th>{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="client in selectedClients | orderBy:'clientId'">
<td>{{client.clientId}}</td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" ng-click="removeFromList(selectedClients, $index);">{{:: 'remove' | translate}}</button>
</td>
</tr>
<tr data-ng-show="!selectedClients.length">
<td class="text-muted" colspan="3">{{:: 'authz-no-clients-assigned' | translate}}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="policy.logic">{{:: 'authz-policy-logic' | translate}}</label>
<div class="col-sm-1">
<select class="form-control" id="policy.logic"
data-ng-model="policy.logic">
<option value="POSITIVE">{{:: 'authz-policy-logic-positive' | translate}}</option>
<option value="NEGATIVE">{{:: 'authz-policy-logic-negative' | translate}}</option>
</select>
</div>
<kc-tooltip>{{:: 'authz-policy-logic.tooltip' | translate}}</kc-tooltip>
</div>
<input type="hidden" data-ng-model="policy.type"/>
</fieldset>
<div class="form-group" data-ng-show="access.manageAuthorization">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>