Repository: Nerzal/gocloak
Pull request #432 (2023-06-20 07:35:11)

Added APIs to support fine-grained authorization
See issue #431 

dmartinol: Hi, could anyone have a look at this PR?

Nerzal: The tests fail cause of a linter warning. 
Could you fix that?
Nerzal: Thank you for the contribution! 
Could you also add 1 test case per new function? 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/Nerzal/gocloak/pull/432?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) Report
> Merging [#432](https://app.codecov.io/gh/Nerzal/gocloak/pull/432?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) (d7c3b29) into [main](https://app.codecov.io/gh/Nerzal/gocloak/commit/166f4421c779f1ff62256fcbbc0f6cea2c4a169b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) (166f442) will **decrease** coverage by `1.66%`.
> The diff coverage is `0.00%`.

```diff
@@            Coverage Diff             @@
##             main     #432      +/-   ##
==========================================
- Coverage   77.25%   75.60%   -1.66%     
==========================================
  Files           4        4              
  Lines        2198     2246      +48     
==========================================
  Hits         1698     1698              
- Misses        338      386      +48     
  Partials      162      162              
```


| [Impacted Files](https://app.codecov.io/gh/Nerzal/gocloak/pull/432?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) | Coverage Δ | |
|---|---|---|
| [client.go](https://app.codecov.io/gh/Nerzal/gocloak/pull/432?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal#diff-Y2xpZW50Lmdv) | `73.99% <0.00%> (-1.79%)` | :arrow_down: |
| [models.go](https://app.codecov.io/gh/Nerzal/gocloak/pull/432?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal#diff-bW9kZWxzLmdv) | `90.32% <ø> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal)

dmartinol: > Thank you for the contribution! Could you also add 1 test case per new function?

I added 2 and the coverage should increase, can you try?
Nerzal: LGTM, thank you for the contribution!

Patch:
diff --git a/Dockerfile b/Dockerfile
index 9b88331..1590f82 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,6 +6,6 @@ ENV KEYCLOAK_USER=admin
 ENV KEYCLOAK_PASSWORD=secret
 ENV KEYCLOAK_ADMIN=admin
 ENV KEYCLOAK_ADMIN_PASSWORD=secret
-ENV KC_FEATURES=account-api,account2,authorization,client-policies,impersonation,docker,scripts,upload_scripts
+ENV KC_FEATURES=account-api,account2,authorization,client-policies,impersonation,docker,scripts,upload_scripts,admin-fine-grained-authz
 RUN /opt/keycloak/bin/kc.sh import --file /data/import/gocloak-realm.json
 ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
diff --git a/client.go b/client.go
index 1a94c3d..c1853ed 100644
--- a/client.go
+++ b/client.go
@@ -869,6 +869,24 @@ func (g *GoCloak) UpdateGroup(ctx context.Context, token, realm string, updatedG
 	return checkForError(resp, err, errMessage)
 }
 
+// UpdateGroupManagementPermissions updates the given group management permissions
+func (g *GoCloak) UpdateGroupManagementPermissions(ctx context.Context, accessToken, realm string, idOfGroup string, managementPermissions ManagementPermissionRepresentation) (*ManagementPermissionRepresentation, error) {
+	const errMessage = "could not update group management permissions"
+
+	var result ManagementPermissionRepresentation
+
+	resp, err := g.GetRequestWithBearerAuth(ctx, accessToken).
+		SetResult(&result).
+		SetBody(managementPermissions).
+		Put(g.getAdminRealmURL(realm, "groups", idOfGroup, "management", "permissions"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
 // UpdateClient updates the given Client
 func (g *GoCloak) UpdateClient(ctx context.Context, token, realm string, updatedClient Client) error {
 	const errMessage = "could not update client"
@@ -906,6 +924,24 @@ func (g *GoCloak) UpdateClientRepresentation(ctx context.Context, accessToken, r
 	return &result, nil
 }
 
+// UpdateClientManagementPermissions updates the given client management permissions
+func (g *GoCloak) UpdateClientManagementPermissions(ctx context.Context, accessToken, realm string, idOfClient string, managementPermissions ManagementPermissionRepresentation) (*ManagementPermissionRepresentation, error) {
+	const errMessage = "could not update client management permissions"
+
+	var result ManagementPermissionRepresentation
+
+	resp, err := g.GetRequestWithBearerAuth(ctx, accessToken).
+		SetResult(&result).
+		SetBody(managementPermissions).
+		Put(g.getAdminRealmURL(realm, "clients", idOfClient, "management", "permissions"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
 // UpdateRole updates the given role.
 func (g *GoCloak) UpdateRole(ctx context.Context, token, realm, idOfClient string, role Role) error {
 	const errMessage = "could not update role"
@@ -1682,6 +1718,23 @@ func (g *GoCloak) GetGroups(ctx context.Context, token, realm string, params Get
 	return result, nil
 }
 
+// GetGroupManagementPermissions returns whether group Authorization permissions have been initialized or not and a reference
+// to the managed permissions
+func (g *GoCloak) GetGroupManagementPermissions(ctx context.Context, token, realm string, idOfGroup string) (*ManagementPermissionRepresentation, error) {
+	const errMessage = "could not get management permissions"
+
+	var result ManagementPermissionRepresentation
+	resp, err := g.GetRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(g.getAdminRealmURL(realm, "groups", idOfGroup, "management", "permissions"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
 // GetGroupsByRole gets groups assigned with a specific role of a realm
 func (g *GoCloak) GetGroupsByRole(ctx context.Context, token, realm string, roleName string) ([]*Group, error) {
 	const errMessage = "could not get groups"
@@ -1944,6 +1997,23 @@ func (g *GoCloak) GetClients(ctx context.Context, token, realm string, params Ge
 	return result, nil
 }
 
+// GetClientManagementPermissions returns whether client Authorization permissions have been initialized or not and a reference
+// to the managed permissions
+func (g *GoCloak) GetClientManagementPermissions(ctx context.Context, token, realm string, idOfClient string) (*ManagementPermissionRepresentation, error) {
+	const errMessage = "could not get management permissions"
+
+	var result ManagementPermissionRepresentation
+	resp, err := g.GetRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(g.getAdminRealmURL(realm, "clients", idOfClient, "management", "permissions"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
 // UserAttributeContains checks if the given attribute value is set
 func UserAttributeContains(attributes map[string][]string, attribute, value string) bool {
 	for _, item := range attributes[attribute] {
@@ -3316,6 +3386,34 @@ func (g *GoCloak) CreateScope(ctx context.Context, token, realm, idOfClient stri
 	return &result, nil
 }
 
+// GetPermissionScope gets the permission scope associated with the client
+func (g *GoCloak) GetPermissionScope(ctx context.Context, token, realm, idOfClient string, idOfScope string) (*PolicyRepresentation, error) {
+	const errMessage = "could not get permission scope"
+
+	var result PolicyRepresentation
+	resp, err := g.GetRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		SetBody(result).
+		Get(g.getAdminRealmURL(realm, "clients", idOfClient, "authz", "resource-server", "permission", "scope", idOfScope))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// UpdatePermissionScope updates a permission scope associated with the client
+func (g *GoCloak) UpdatePermissionScope(ctx context.Context, token, realm, idOfClient string, idOfScope string, policy PolicyRepresentation) error {
+	const errMessage = "could not create permission scope"
+
+	resp, err := g.GetRequestWithBearerAuth(ctx, token).
+		SetBody(policy).
+		Put(g.getAdminRealmURL(realm, "clients", idOfClient, "authz", "resource-server", "permission", "scope", idOfScope))
+
+	return checkForError(resp, err, errMessage)
+}
+
 // UpdateScope updates a scope associated with the client
 func (g *GoCloak) UpdateScope(ctx context.Context, token, realm, idOfClient string, scope ScopeRepresentation) error {
 	const errMessage = "could not update scope"
diff --git a/models.go b/models.go
index 070d8a5..afd2e65 100644
--- a/models.go
+++ b/models.go
@@ -1408,6 +1408,14 @@ type RequiredActionProviderRepresentation struct {
 	ProviderID    *string            `json:"providerId,omitempty"`
 }
 
+// ManagementPermissionRepresentation is a representation of management permissions
+// v18: https://www.keycloak.org/docs-api/18.0/rest-api/#_managementpermissionreference
+type ManagementPermissionRepresentation struct {
+	Enabled          *bool              `json:"enabled,omitempty"`
+	Resource         *string            `json:"resource,omitempty"`
+	ScopePermissions *map[string]string `json:"scopePermissions,omitempty"`
+}
+
 // prettyStringStruct returns struct formatted into pretty string
 func prettyStringStruct(t interface{}) string {
 	json, err := json.MarshalIndent(t, "", "\t")


Test patch:
diff --git a/client_test.go b/client_test.go
index 441313b..3693cf7 100644
--- a/client_test.go
+++ b/client_test.go
@@ -1196,6 +1196,103 @@ func Test_CreateListGetUpdateDeleteGetChildGroup(t *testing.T) {
 	require.NoError(t, err, "GetGroup failed")
 }
 
+func Test_GroupPermissions(t *testing.T) {
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetAdminToken(t, client)
+
+	// Create
+	tearDown, groupID := CreateGroup(t, client)
+	// Delete
+	defer tearDown()
+
+	groupPermission, err := client.GetGroupManagementPermissions(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		groupID,
+	)
+	require.NoError(t, err, "GetGroupManagementPermissions failed")
+	require.Equal(t, false, *groupPermission.Enabled)
+
+	groupPermission.Enabled = gocloak.BoolP(true)
+	updatedGroupPermission, err := client.UpdateGroupManagementPermissions(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		groupID,
+		*groupPermission,
+	)
+	require.NoError(t, err, "UpdateGroupManagementPermissions failed")
+	require.Equal(t, true, *updatedGroupPermission.Enabled)
+
+	clients, err := client.GetClients(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloak.GetClientsParams{
+			ClientID: gocloak.StringP("realm-management"),
+		},
+	)
+	require.NoError(t, err, "GetClients failed")
+	require.Equal(t, 1, len(clients))
+	realManagementClient := clients[0]
+
+	_, policyID := CreatePolicy(t, client, gocloakClientID, gocloak.PolicyRepresentation{
+		Name:        GetRandomNameP("PolicyName"),
+		Description: gocloak.StringP("Policy Description"),
+		Type:        gocloak.StringP("client"),
+		Logic:       gocloak.POSITIVE,
+		ClientPolicyRepresentation: gocloak.ClientPolicyRepresentation{
+			Clients: &[]string{
+				gocloakClientID,
+			},
+		},
+	})
+
+	for _, scopeID := range *updatedGroupPermission.ScopePermissions {
+		permissionScope, err := client.GetPermissionScope(
+			context.Background(),
+			token.AccessToken,
+			cfg.GoCloak.Realm,
+			*realManagementClient.ID,
+			scopeID)
+		require.NoError(t, err, "GetPermissionScope failed for %s", scopeID)
+
+		scopePolicies, err := client.GetAuthorizationPolicyScopes(
+			context.Background(),
+			token.AccessToken,
+			cfg.GoCloak.Realm,
+			*realManagementClient.ID,
+			scopeID)
+		require.NoError(t, err, "GetAuthorizationPolicyScopes failed for %s", scopeID)
+		require.Equal(t, 1, len(scopePolicies), "GetAuthorizationPolicyScopes found more than 1 policies")
+		scopePolicy := scopePolicies[0]
+
+		policyResources, err := client.GetAuthorizationPolicyResources(
+			context.Background(),
+			token.AccessToken,
+			cfg.GoCloak.Realm,
+			*realManagementClient.ID,
+			scopeID)
+		require.NoError(t, err, "GetAuthorizationPolicyResources failed for %s", scopeID)
+		require.Equal(t, 1, len(policyResources), "GetAuthorizationPolicyResources found more than 1 policies")
+		policyResource := policyResources[0]
+
+		permissionScope.Policies = &[]string{policyID}
+		permissionScope.Resources = &[]string{*policyResource.ID}
+		permissionScope.Scopes = &[]string{*scopePolicy.ID}
+		err = client.UpdatePermissionScope(
+			context.Background(),
+			token.AccessToken,
+			cfg.GoCloak.Realm,
+			*realManagementClient.ID,
+			scopeID,
+			*permissionScope)
+		require.NoError(t, err, "UpdatePermissionScope failed for %s", scopeID)
+	}
+}
+
 func CreateClientRole(t *testing.T, client *gocloak.GoCloak) (func(), string) {
 	cfg := GetConfig(t)
 	token := GetAdminToken(t, client)
@@ -1226,6 +1323,42 @@ func CreateClientRole(t *testing.T, client *gocloak.GoCloak) (func(), string) {
 	return tearDown, roleName
 }
 
+func Test_ClientPermissions(t *testing.T) {
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetAdminToken(t, client)
+
+	t.Logf("Checking Client Permission")
+	testClient := gocloak.Client{
+		ClientID:         GetRandomNameP("ClientID"),
+		BaseURL:          gocloak.StringP("https://example.com"),
+		FullScopeAllowed: gocloak.BoolP(false),
+	}
+	// Creating client
+	tearDownClient, idOfClient := CreateClient(t, client, &testClient)
+	defer tearDownClient()
+
+	clientPermissions, err := client.GetClientManagementPermissions(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		idOfClient,
+	)
+	require.NoError(t, err, "GetClientManagementPermissions failed")
+	require.Equal(t, false, *clientPermissions.Enabled)
+
+	clientPermissions.Enabled = gocloak.BoolP(true)
+	updatedClientPermissions, err := client.UpdateClientManagementPermissions(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		idOfClient,
+		*clientPermissions,
+	)
+	require.NoError(t, err, "UpdateClientManagementPermissions failed")
+	require.Equal(t, true, *updatedClientPermissions.Enabled)
+}
+
 func Test_CreateClientRole(t *testing.T) {
 	t.Parallel()
 	client := NewClientWithDebug(t)
diff --git a/run-tests.sh b/run-tests.sh
index ed2de6c..57875f8 100755
--- a/run-tests.sh
+++ b/run-tests.sh
@@ -3,7 +3,17 @@
 docker-compose down
 docker-compose up -d
 
-sleep 10
+keycloakServer=http://localhost:8080
+url="${keycloakServer}/health"
+echo "Checking service availability at $url (CTRL+C to exit)"
+while true; do
+    response=$(curl -s -o /dev/null -w "%{http_code}" $url)
+    if [ $response -eq 200 ]; then
+        break
+    fi
+    sleep 1
+done
+echo "Service is now available at ${keycloakServer}"
 
 ARGS=()
 if [ $# -gt 0 ]; then
@@ -13,4 +23,4 @@ fi
 
 go test -failfast -race -cover -coverprofile=coverage.out -covermode=atomic -p 10 -cpu 1,2 -bench . -benchmem ${ARGS[@]}
 
-docker-compose down
\ No newline at end of file
+docker-compose down