keycloak-uncached

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