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 0000000000..ec84bbcac1 --- /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 0000000000..e800a5bd50 --- /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 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 1e8dfd20cb..e4588f87a6 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 195835c261..9ecbc3d992 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 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 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 allowedClients) { + return onAuthorizationSession(authorizationProvider -> { + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + PolicyStore policyStore = storeFactory.getPolicyStore(); + Policy policy = policyStore.create("Client-Based Policy", "client", resourceServer); + + List 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 340576c8cd..4e300e4e20 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 8cf09f3b7a..173bcfbd91 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 9b8a6cb28f..da1127f358 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 467038c95c..f201fdccff 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 72b6c43c06..ea039c195a 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 0000000000..634b836913 --- /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 @@ +
+ + + +

{{:: 'authz-add-client-policy' | translate}}

+

{{originalPolicy.name|capitalize}}

+ +
+
+
+ +
+ +
+ {{:: 'authz-policy-name.tooltip' | translate}} +
+
+ +
+ +
+ {{:: 'authz-policy-description.tooltip' | translate}} +
+
+ + +
+ + +
+ {{:: 'authz-policy-client-clients.tooltip' | translate}} +
+
+ +
+ + + + + + + + + + + + + + + + +
{{:: 'clientId' | translate}}{{:: 'actions' | translate}}
{{client.clientId}} + +
{{:: 'authz-no-clients-assigned' | translate}}
+
+
+
+ + +
+ +
+ + {{:: 'authz-policy-logic.tooltip' | translate}} +
+ +
+ +
+
+ + +
+
+
+
+ + \ No newline at end of file