Repository: Nerzal/gocloak (go)

Issue:
Adding and deleting client-level roles to the user role mapping
Hi,

I'm using gocloak to automate user management in keycloak, and I need to manage client roles for my users. I tried to use the AddRealmRoleToUser and DeleteRealmRoleFromUser functions with my client roles in the input parameters, but I get a 404 error (that's pretty predictable). Therefore I didn't find any methods in the gocloak package that handled these two methods.

API endpoints are described here: https://www.keycloak.org/docs-api/2.5/rest-api/index.html#_delete_client_level_roles_from_user_role_mapping_2

Actually, what I need is AddClientRoleToUser and DeleteClientRoleFromUser functions pointing to these methods' endpoints.

I'm thinking of adding theses functions on my own because I don't have much time to make it happen, maybe this is already possible and I didn't find how to do it ? So Before I start coding this on my side, I ask it here :)

Thanks !

Christophe.

Pull request description:
Manage client role assignment
#76 + #91

Fix #74 

Patch:
diff --git a/.codebeatsettings b/.codebeatsettings
index 05b8a61..bc34a05 100644
--- a/.codebeatsettings
+++ b/.codebeatsettings
@@ -1,5 +1,6 @@
 {
   "GOLANG": {
-    "TOO_MANY_IVARS": [1000, 2000, 3000, 4000]
+    "TOO_MANY_IVARS": [1000, 2000, 3000, 4000],
+    "ARITY": [1000, 2000, 3000, 4000]
   }
 }
diff --git a/client.go b/client.go
index 1d6a5e9..7270e12 100644
--- a/client.go
+++ b/client.go
@@ -1197,3 +1197,21 @@ func (client *gocloak) GetUserOfflineSessionsForClient(token, realm, userID, cli
 	}
 	return res, nil
 }
+
+// AddClientRoleToUser adds client-level role mappings
+func (client *gocloak) AddClientRoleToUser(token string, realm string, clientID string, userID string, roles []Role) error {
+	resp, err := client.getRequestWithBearerAuth(token).
+		SetBody(roles).
+		Post(client.getAdminRealmURL(realm, "users", userID, "role-mappings", "clients", clientID))
+
+	return checkForError(resp, err)
+}
+
+// DeleteClientRoleFromUser adds client-level role mappings
+func (client *gocloak) DeleteClientRoleFromUser(token string, realm string, clientID string, userID string, roles []Role) error {
+	resp, err := client.getRequestWithBearerAuth(token).
+		SetBody(roles).
+		Delete(client.getAdminRealmURL(realm, "users", userID, "role-mappings", "clients", clientID))
+
+	return checkForError(resp, err)
+}
diff --git a/gocloak.go b/gocloak.go
index c484e20..bbfafd6 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -46,8 +46,6 @@ type GoCloak interface {
 
 	// CreateGroup creates a new group
 	CreateGroup(accessToken string, realm string, group Group) error
-	// CreateClientRole creates a new role for a client
-	CreateClientRole(accessToken string, realm string, clientID string, role Role) error
 	// CreateClient creates a new client
 	CreateClient(accessToken string, realm string, clientID Client) error
 	// CreateClientScope creates a new clientScope
@@ -68,8 +66,6 @@ type GoCloak interface {
 	DeleteComponent(accessToken string, realm, componentID string) error
 	// DeleteGroup deletes the given group
 	DeleteGroup(accessToken string, realm, groupID string) error
-	// DeleteClientRole deletes the given role
-	DeleteClientRole(accessToken string, realm, clientID, roleName string) error
 	// DeleteClient deletes the given client
 	DeleteClient(accessToken string, realm, clientID string) error
 	// DeleteClientScope
@@ -115,10 +111,6 @@ type GoCloak interface {
 	GetRoleMappingByGroupID(accessToken string, realm string, groupID string) (*MappingsRepresentation, error)
 	// GetRoleMappingByUserID gets the rolemapping for the given user id
 	GetRoleMappingByUserID(accessToken string, realm string, userID string) (*MappingsRepresentation, error)
-	// GetClientRoles gets roles for the given client
-	GetClientRoles(accessToken string, realm string, clientID string) ([]*Role, error)
-	// GetClientRole get a role for the given client in a realm by role name
-	GetClientRole(token string, realm string, clientID string, roleName string) (*Role, error)
 	// GetClients gets the clients in the realm
 	GetClients(accessToken string, realm string, params GetClientsParams) ([]*Client, error)
 	// GetClientOfflineSessions returns offline sessions associated with the client
@@ -158,6 +150,21 @@ type GoCloak interface {
 	// AddRealmRoleComposite adds roles as composite
 	DeleteRealmRoleComposite(token string, realm string, roleName string, roles []Role) error
 
+	// *** Client Roles ***
+
+	// AddClientRoleToUser adds a client role to the user
+	AddClientRoleToUser(token string, realm string, clientID string, userID string, roles []Role) error
+	// CreateClientRole creates a new role for a client
+	CreateClientRole(accessToken string, realm string, clientID string, role Role) error
+	// DeleteClientRole deletes the given role
+	DeleteClientRole(accessToken string, realm, clientID, roleName string) error
+	// DeleteClientRoleFromUser removes a client role from from the user
+	DeleteClientRoleFromUser(token string, realm string, clientID string, userID string, roles []Role) error
+	// GetClientRoles gets roles for the given client
+	GetClientRoles(accessToken string, realm string, clientID string) ([]*Role, error)
+	// GetClientRole get a role for the given client in a realm by role name
+	GetClientRole(token string, realm string, clientID string, roleName string) (*Role, error)
+
 	// *** Realm ***
 
 	// GetRealm returns top-level representation of the realm


Test patch:
diff --git a/client_test.go b/client_test.go
index 2b98827..aebcf90 100644
--- a/client_test.go
+++ b/client_test.go
@@ -137,6 +137,9 @@ func GetConfig(t *testing.T) *Config {
 			FailIfErr(t, err, "incorrect proxy url: "+config.Proxy)
 			http.DefaultTransport.(*http.Transport).Proxy = http.ProxyURL(proxy)
 		}
+		if config.GoCloak.UserName == "" {
+			config.GoCloak.UserName = "test_user"
+		}
 	})
 	return config
 }
@@ -739,54 +742,98 @@ func TestGocloak_CreateListGetUpdateDeleteGroup(t *testing.T) {
 	defer tearDown()
 }
 
-func TestGocloak_CreateClientRole(t *testing.T) {
-	t.Parallel()
+func CreateClientRole(t *testing.T, client GoCloak) (func(), string) {
 	cfg := GetConfig(t)
-	client := NewClientWithDebug(t)
 	token := GetAdminToken(t, client)
 
-	role := Role{
-		Name: GetRandomName("mySuperCoolRole"),
+	roleName := GetRandomName("Role")
+	t.Logf("Creating Client Role: %s", roleName)
+	err := client.CreateClientRole(
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+		Role{
+			Name: roleName,
+		})
+	assert.NoError(t, err, "CreateClientRole failed")
+	tearDown := func() {
+		err := client.DeleteClientRole(
+			token.AccessToken,
+			cfg.GoCloak.Realm,
+			gocloakClientID,
+			roleName)
+		assert.NoError(t, err, "DeleteClientRole failed")
 	}
-	testClient := GetClientByClientID(t, client, cfg.GoCloak.ClientID)
+	return tearDown, roleName
+}
 
-	err := client.CreateClientRole(
+func TestGocloak_CreateClientRole(t *testing.T) {
+	t.Parallel()
+	client := NewClientWithDebug(t)
+	tearDown, _ := CreateClientRole(t, client)
+	tearDown()
+}
+
+func TestGocloak_GetClientRole(t *testing.T) {
+	t.Parallel()
+	client := NewClientWithDebug(t)
+	tearDown, roleName := CreateClientRole(t, client)
+	defer tearDown()
+	cfg := GetConfig(t)
+	token := GetAdminToken(t, client)
+	role, err := client.GetClientRole(
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID,
-		role)
-	FailIfErr(t, err, "CreateClientRole failed")
-	err = client.DeleteClientRole(
+		gocloakClientID,
+		roleName,
+	)
+	assert.NoError(t, err, "GetClientRoleI failed")
+	assert.NotNil(t, role)
+	token = GetAdminToken(t, client)
+	role, err = client.GetClientRole(
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID,
-		role.Name,
+		gocloakClientID,
+		"Fake-Role-Name",
 	)
-	FailIfErr(t, err, "DeleteClientRole failed")
+	assert.Error(t, err)
+	assert.Nil(t, role)
 }
 
-func TestGocloak_CreateClientScope(t *testing.T) {
-	t.Parallel()
+func CreateClientScope(t *testing.T, client GoCloak, scope *ClientScope) (func(), string) {
 	cfg := GetConfig(t)
-	client := NewClientWithDebug(t)
 	token := GetAdminToken(t, client)
-	defer ClearRealmCache(t, client)
 
-	newScope := ClientScope{
-		ID:   GetRandomName("client-scope-id-"),
-		Name: GetRandomName("client-scope-name-"),
+	if scope == nil {
+		scope = &ClientScope{}
 	}
+	scope.ID = GetRandomName("client-scope-id-")
+	scope.Name = GetRandomName("client-scope-name-")
+
+	t.Logf("Creating Client Scope: %+v", scope)
 	err := client.CreateClientScope(
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		newScope)
+		*scope,
+	)
 	assert.NoError(t, err, "CreateClientScope failed")
+	tearDown := func() {
+		err := client.DeleteClientScope(
+			token.AccessToken,
+			cfg.GoCloak.Realm,
+			scope.ID,
+		)
+		assert.NoError(t, err, "DeleteClientScope failed")
+	}
+	return tearDown, scope.ID
+}
 
-	err = client.DeleteClientScope(
-		token.AccessToken,
-		cfg.GoCloak.Realm,
-		newScope.ID)
-	assert.NoError(t, err, "DeleteClientScope failed")
+func TestGocloak_CreateClientScope_DeleteClientScope(t *testing.T) {
+	t.Parallel()
+	client := NewClientWithDebug(t)
+	defer ClearRealmCache(t, client)
+	tearDown, _ := CreateClientScope(t, client, nil)
+	tearDown()
 }
 
 func TestGocloak_ListAddRemoveDefaultClientScopes(t *testing.T) {
@@ -794,39 +841,38 @@ func TestGocloak_ListAddRemoveDefaultClientScopes(t *testing.T) {
 	cfg := GetConfig(t)
 	client := NewClientWithDebug(t)
 	token := GetAdminToken(t, client)
-	testClient := GetClientByClientID(t, client, cfg.GoCloak.ClientID)
 	defer ClearRealmCache(t, client)
 
-	newScope := ClientScope{
-		ID:       GetRandomName("client-scope-id-"),
-		Name:     GetRandomName("client-scope-name-default-"),
+	scope := ClientScope{
 		Protocol: "openid-connect",
 		ClientScopeAttributes: &ClientScopeAttributes{
 			IncludeInTokenScope: "true",
 		},
 	}
 
-	err := client.CreateClientScope(
-		token.AccessToken,
-		cfg.GoCloak.Realm,
-		newScope)
-	assert.NoError(t, err, "CreateClientScope failed")
+	tearDown, scopeID := CreateClientScope(t, client, &scope)
+	defer tearDown()
 
-	scopesBeforeAdding, err := client.GetClientsDefaultScopes(token.AccessToken,
+	scopesBeforeAdding, err := client.GetClientsDefaultScopes(
+		token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID)
+		gocloakClientID,
+	)
 	assert.NoError(t, err, "GetClientsDefaultScopes failed")
 
 	err = client.AddDefaultScopeToClient(
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID,
-		newScope.ID)
+		gocloakClientID,
+		scopeID,
+	)
 	assert.NoError(t, err, "AddDefaultScopeToClient failed")
 
-	scopesAfterAdding, err := client.GetClientsDefaultScopes(token.AccessToken,
+	scopesAfterAdding, err := client.GetClientsDefaultScopes(
+		token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID)
+		gocloakClientID,
+	)
 	assert.NoError(t, err, "GetClientsDefaultScopes failed")
 
 	assert.NotEqual(t, len(scopesBeforeAdding), len(scopesAfterAdding), "scope should have been added")
@@ -834,22 +880,19 @@ func TestGocloak_ListAddRemoveDefaultClientScopes(t *testing.T) {
 	err = client.RemoveDefaultScopeFromClient(
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID,
-		newScope.ID)
+		gocloakClientID,
+		scopeID,
+	)
 	assert.NoError(t, err, "RemoveDefaultScopeFromClient failed")
 
-	scopesAfterRemoving, err := client.GetClientsDefaultScopes(token.AccessToken,
+	scopesAfterRemoving, err := client.GetClientsDefaultScopes(
+		token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID)
+		gocloakClientID,
+	)
 	assert.NoError(t, err, "GetClientsDefaultScopes failed")
 
 	assert.Equal(t, len(scopesAfterRemoving), len(scopesBeforeAdding), "scope should have been removed")
-
-	err = client.DeleteClientScope(
-		token.AccessToken,
-		cfg.GoCloak.Realm,
-		newScope.ID)
-	assert.NoError(t, err, "DeleteClientScope failed")
 }
 
 func TestGocloak_ListAddRemoveOptionalClientScopes(t *testing.T) {
@@ -857,38 +900,32 @@ func TestGocloak_ListAddRemoveOptionalClientScopes(t *testing.T) {
 	cfg := GetConfig(t)
 	client := NewClientWithDebug(t)
 	token := GetAdminToken(t, client)
-	testClient := GetClientByClientID(t, client, cfg.GoCloak.ClientID)
 	defer ClearRealmCache(t, client)
 
-	newScope := ClientScope{
-		ID:       GetRandomName("client-scope-id-"),
-		Name:     GetRandomName("client-scope-name-"),
+	scope := ClientScope{
 		Protocol: "openid-connect",
 		ClientScopeAttributes: &ClientScopeAttributes{
 			IncludeInTokenScope: "true",
 		},
 	}
-	err := client.CreateClientScope(
-		token.AccessToken,
-		cfg.GoCloak.Realm,
-		newScope)
-	assert.NoError(t, err, "CreateClientScope failed")
+	tearDown, scopeID := CreateClientScope(t, client, &scope)
+	defer tearDown()
 
 	scopesBeforeAdding, err := client.GetClientsOptionalScopes(token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID)
+		gocloakClientID)
 	assert.NoError(t, err, "GetClientsOptionalScopes failed")
 
 	err = client.AddOptionalScopeToClient(
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID,
-		newScope.ID)
+		gocloakClientID,
+		scopeID)
 	assert.NoError(t, err, "AddOptionalScopeToClient failed")
 
 	scopesAfterAdding, err := client.GetClientsOptionalScopes(token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID)
+		gocloakClientID)
 	assert.NoError(t, err, "GetClientsOptionalScopes failed")
 
 	assert.NotEqual(t, len(scopesAfterAdding), len(scopesBeforeAdding), "scope should have been added")
@@ -896,22 +933,16 @@ func TestGocloak_ListAddRemoveOptionalClientScopes(t *testing.T) {
 	err = client.RemoveOptionalScopeFromClient(
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID,
-		newScope.ID)
+		gocloakClientID,
+		scopeID)
 	assert.NoError(t, err, "RemoveOptionalScopeFromClient failed")
 
 	scopesAfterRemoving, err := client.GetClientsOptionalScopes(token.AccessToken,
 		cfg.GoCloak.Realm,
-		testClient.ID)
+		gocloakClientID)
 	assert.NoError(t, err, "GetClientsOptionalScopes failed")
 
 	assert.Equal(t, len(scopesBeforeAdding), len(scopesAfterRemoving), "scope should have been removed")
-
-	err = client.DeleteClientScope(
-		token.AccessToken,
-		cfg.GoCloak.Realm,
-		newScope.ID)
-	assert.NoError(t, err, "DeleteClientScope failed")
 }
 
 func TestGocloak_GetDefaultOptionalClientScopes(t *testing.T) {
@@ -924,7 +955,7 @@ func TestGocloak_GetDefaultOptionalClientScopes(t *testing.T) {
 		token.AccessToken,
 		cfg.GoCloak.Realm)
 
-	FailIfErr(t, err, "GetDefaultOptionalClientScopes failed")
+	assert.NoError(t, err, "GetDefaultOptionalClientScopes failed")
 
 	assert.NotEqual(t, 0, len(scopes), "there should be default optional client scopes")
 }
@@ -939,7 +970,7 @@ func TestGocloak_GetDefaultDefaultClientScopes(t *testing.T) {
 		token.AccessToken,
 		cfg.GoCloak.Realm)
 
-	FailIfErr(t, err, "GetDefaultDefaultClientScopes failed")
+	assert.NoError(t, err, "GetDefaultDefaultClientScopes failed")
 
 	assert.NotEqual(t, 0, len(scopes), "there should be default default client scopes")
 }
@@ -949,26 +980,18 @@ func TestGocloak_GetClientScope(t *testing.T) {
 	cfg := GetConfig(t)
 	client := NewClientWithDebug(t)
 	token := GetAdminToken(t, client)
-
-	newScope := ClientScope{
-		ID:   GetRandomName("client-scope-id-"),
-		Name: GetRandomName("client-scope-name-"),
-	}
-	err := client.CreateClientScope(
-		token.AccessToken,
-		cfg.GoCloak.Realm,
-		newScope)
-	FailIfErr(t, err, "CreateClientScope failed")
+	tearDown, scopeID := CreateClientScope(t, client, nil)
+	defer tearDown()
 
 	// Getting exact client scope
 	createdClientScope, err := client.GetClientScope(
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		newScope.ID)
-	FailIfErr(t, err, "GetClientScope failed")
-	t.Logf("Created clientScope : %+v", createdClientScope)
+		scopeID,
+	)
+	assert.NoError(t, err, "GetClientScope failed")
 	// Checking that GetClientScope returns same client scope
-	AssertEquals(t, newScope.ID, createdClientScope.ID)
+	assert.Equal(t, scopeID, createdClientScope.ID)
 }
 
 func TestGocloak_GetClientScopes(t *testing.T) {
@@ -981,7 +1004,7 @@ func TestGocloak_GetClientScopes(t *testing.T) {
 	scopes, err := client.GetClientScopes(
 		token.AccessToken,
 		cfg.GoCloak.Realm)
-	FailIfErr(t, err, "GetClientScopes failed")
+	assert.NoError(t, err, "GetClientScopes failed")
 	// Checking that GetClientScopes returns scopes
 	assert.NotZero(t, len(scopes), "there should be client scopes")
 }
@@ -1362,7 +1385,7 @@ func TestGocloak_DeleteRealmRole(t *testing.T) {
 	FailIfErr(t, err, "DeleteRealmRole failed")
 }
 
-func TestGocloak_AddRealmRoleToUser(t *testing.T) {
+func TestGocloak_AddRealmRoleToUser_DeleteRealmRoleFromUser(t *testing.T) {
 	t.Parallel()
 	cfg := GetConfig(t)
 	client := NewClientWithDebug(t)
@@ -1376,16 +1399,23 @@ func TestGocloak_AddRealmRoleToUser(t *testing.T) {
 		token.AccessToken,
 		cfg.GoCloak.Realm,
 		roleName)
-	FailIfErr(t, err, "GetRealmRole failed")
+	assert.NoError(t, err, "GetRealmRole failed")
 
+	roles := []Role{*role}
 	err = client.AddRealmRoleToUser(
 		token.AccessToken,
 		cfg.GoCloak.Realm,
 		userID,
-		[]Role{
-			*role,
-		})
-	FailIfErr(t, err, "AddRealmRoleToUser failed")
+		roles,
+	)
+	assert.NoError(t, err, "AddRealmRoleToUser failed")
+	err = client.DeleteRealmRoleFromUser(
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		userID,
+		roles,
+	)
+	assert.NoError(t, err, "DeleteRealmRoleFromUser failed")
 }
 
 func TestGocloak_GetRealmRolesByUserID(t *testing.T) {
@@ -1443,52 +1473,6 @@ func TestGocloak_GetRealmRolesByGroupID(t *testing.T) {
 	FailIfErr(t, err, "GetRealmRolesByGroupID failed")
 }
 
-func TestGocloak_DeleteRealmRoleFromUser(t *testing.T) {
-	t.Parallel()
-	cfg := GetConfig(t)
-	client := NewClientWithDebug(t)
-	token := GetAdminToken(t, client)
-
-	tearDownUser, userID := CreateUser(t, client)
-	defer tearDownUser()
-	tearDownRole, roleName := CreateRealmRole(t, client)
-	defer tearDownRole()
-	role, err := client.GetRealmRole(
-		token.AccessToken,
-		cfg.GoCloak.Realm,
-		roleName)
-	FailIfErr(t, err, "GetRealmRole failed")
-
-	err = client.AddRealmRoleToUser(
-		token.AccessToken,
-		cfg.GoCloak.Realm,
-		userID,
-		[]Role{
-			*role,
-		})
-	FailIfErr(t, err, "AddRealmRoleToUser failed")
-	err = client.DeleteRealmRoleFromUser(
-		token.AccessToken,
-		cfg.GoCloak.Realm,
-		userID,
-		[]Role{
-			*role,
-		})
-	FailIfErr(t, err, "DeleteRealmRoleFromUser failed")
-
-	roles, err := client.GetRealmRolesByUserID(
-		token.AccessToken,
-		cfg.GoCloak.Realm,
-		userID)
-	FailIfErr(t, err, "GetRealmRolesByUserID failed")
-	for _, r := range roles {
-		FailIf(
-			t,
-			r.Name == role.Name,
-			"The role has been found in assigned roles. Role: %+v", role)
-	}
-}
-
 func TestGocloak_AddRealmRoleComposite(t *testing.T) {
 	t.Parallel()
 	cfg := GetConfig(t)
@@ -2007,6 +1991,50 @@ func TestGoCloak_ClientServiceAccount(t *testing.T) {
 	AssertEquals(t, "service-account-gocloak", serviceAccount.Username)
 }
 
+func TestGocloak_AddClientRoleToUser_DeleteClientRoleFromUser(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	SetUpTestUser(t, client)
+	tearDown1, roleName1 := CreateClientRole(t, client)
+	defer tearDown1()
+	token := GetAdminToken(t, client)
+	role1, err := client.GetClientRole(
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+		roleName1,
+	)
+	assert.NoError(t, err, "GetClientRole failed")
+	tearDown2, roleName2 := CreateClientRole(t, client)
+	defer tearDown2()
+	role2, err := client.GetClientRole(
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+		roleName2,
+	)
+	assert.NoError(t, err, "GetClientRole failed")
+	roles := []Role{*role1, *role2}
+	err = client.AddClientRoleToUser(
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+		testUserID,
+		roles,
+	)
+	assert.NoError(t, err, "AddClientRoleToUser failed")
+
+	err = client.DeleteClientRoleFromUser(
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+		testUserID,
+		roles,
+	)
+	assert.NoError(t, err, "DeleteClientRoleFromUser failed")
+}
+
 func TestGocloak_CreateDeleteClientScopeWithMappers(t *testing.T) {
 	t.Parallel()
 	cfg := GetConfig(t)