keycloak-uncached
Changes
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java 47(+47 -0)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java 111(+111 -0)
authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 3(+2 -1)
Details
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java
new file mode 100644
index 0000000..ec84bbc
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java
@@ -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();
+ }
+}
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java
new file mode 100644
index 0000000..e800a5b
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java
@@ -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[]{};
+ }
+}
diff --git a/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
index 1e8dfd2..e4588f8 100644
--- a/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
+++ b/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -40,4 +40,5 @@ org.keycloak.authorization.policy.provider.resource.ResourcePolicyProviderFactor
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
\ No newline at end of file
+org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory
+org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
\ No newline at end of file
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
index 195835c..9ecbc3d 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
@@ -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();
diff --git a/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json b/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json
index 340576c..4e300e4 100644
--- a/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json
+++ b/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json
@@ -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" : ["*"]
}
]
}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 8cf09f3..173bcfb 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -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.
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_no.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_no.properties
index 9b8a6cb..da1127f 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_no.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_no.properties
@@ -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.
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
index 467038c..f201fdc 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
@@ -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: {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
index 72b6c43..ea039c1 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
@@ -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() {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-client-detail.html
new file mode 100644
index 0000000..634b836
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-client-detail.html
@@ -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>
\ No newline at end of file