Repository: Nerzal/gocloak
Pull request #285 (2021-06-22 10:58:17)

Add and update API calls, v9
Hey @Nerzal! I want to bring a couple of changes from our fork back to upstream :) Let me know if something is missing. The tests are failing on what I think is an unrelated change (`TestGocloak_CreateGetUpdateDeleteResourcePolicy`: Keycloak returns a `500` with error message `unknown_error`) — maybe you can have a look as well?

Changes:

bump version to v9.0.0

updated function signatures, backwards incompatible:
- GetRealmRoles(ctx context.Context, accessToken, realm string, params GetRolesParams) ([]*Role, error)
- GetClientRoles(ctx context.Context, accessToken, realm, clientID string, params GetRolesParams) ([]*Role, error)
- CreateIdentityProviderMapper(ctx context.Context, token, realm, alias string, mapper IdentityProviderMapper) (string, error)

added new functions:
- GetRealmRoleByID(ctx context.Context, token, realm, roleID string) (*Role, error)
- UpdateRealmRoleByID(ctx context.Context, token, realm, roleID string, role Role) error
- GetCompositeRealmRoles(ctx context.Context, token, realm, roleName string) ([]*Role, error)
- GetIdentityProviderMapper(ctx context.Context, token string, realm string, alias string, mapperID string) (*IdentityProviderMapper, error)

Nerzal: thank you for the contribution :) 
Can you please resolve the merge conflicts?

I'm going to merge after and then i'll search for a solution for failing test pipelines (before submitting a new tag)

Patch:
diff --git a/README.md b/README.md
index 65c9c0f..63c5c36 100644
--- a/README.md
+++ b/README.md
@@ -258,7 +258,7 @@ type GoCloak interface {
  GetGroupMembers(ctx context.Context, accessToken, realm, groupID string, params GetGroupsParams) ([]*User, error)
  GetRoleMappingByGroupID(ctx context.Context, accessToken, realm, groupID string) (*MappingsRepresentation, error)
  GetRoleMappingByUserID(ctx context.Context, accessToken, realm, userID string) (*MappingsRepresentation, error)
- GetClientRoles(ctx context.Context, accessToken, realm, idOfClient string) ([]*Role, error)
+ GetClientRoles(ctx context.Context, accessToken, realm, idOfClient string, params GetRoleParams) ([]*Role, error)
  GetClientRole(ctx context.Context, token, realm, idOfClient, roleName string) (*Role, error)
  GetClientRoleByID(ctx context.Context, accessToken, realm, roleID string) (*Role, error)
  GetClients(ctx context.Context, accessToken, realm string, params GetClientsParams) ([]*Client, error)
@@ -274,10 +274,12 @@ type GoCloak interface {
 
  CreateRealmRole(ctx context.Context, token, realm string, role Role) (string, error)
  GetRealmRole(ctx context.Context, token, realm, roleName string) (*Role, error)
- GetRealmRoles(ctx context.Context, accessToken, realm string) ([]*Role, error)
+ GetRealmRoles(ctx context.Context, accessToken, realm string, params GetRoleParams) ([]*Role, error)
+ GetRealmRoleByID(ctx context.Context, token, realm, roleID string) (*Role, error)
  GetRealmRolesByUserID(ctx context.Context, accessToken, realm, userID string) ([]*Role, error)
  GetRealmRolesByGroupID(ctx context.Context, accessToken, realm, groupID string) ([]*Role, error)
  UpdateRealmRole(ctx context.Context, token, realm, roleName string, role Role) error
+ UpdateRealmRoleByID(ctx context.Context, token, realm, roleID string, role Role) error
  DeleteRealmRole(ctx context.Context, token, realm, roleName string) error
  AddRealmRoleToUser(ctx context.Context, token, realm, userID string, roles []Role) error
  DeleteRealmRoleFromUser(ctx context.Context, token, realm, userID string, roles []Role) error
@@ -285,6 +287,7 @@ type GoCloak interface {
  DeleteRealmRoleFromGroup(ctx context.Context, token, realm, groupID string, roles []Role) error
  AddRealmRoleComposite(ctx context.Context, token, realm, roleName string, roles []Role) error
  DeleteRealmRoleComposite(ctx context.Context, token, realm, roleName string, roles []Role) error
+ GetCompositeRealmRoles(ctx context.Context, token, realm, roleName string) ([]*Role, error)
  GetCompositeRealmRolesByRoleID(ctx context.Context, token, realm, roleID string) ([]*Role, error)
  GetCompositeRealmRolesByUserID(ctx context.Context, token, realm, userID string) ([]*Role, error)
  GetCompositeRealmRolesByGroupID(ctx context.Context, token, realm, groupID string) ([]*Role, error)
@@ -385,6 +388,8 @@ type GoCloak interface {
  UpdateIdentityProvider(ctx context.Context, token, realm, alias string, providerRep IdentityProviderRepresentation) error
  DeleteIdentityProvider(ctx context.Context, token, realm, alias string) error
 
+ CreateIdentityProviderMapper(ctx context.Context, token, realm, alias string, mapper IdentityProviderMapper) (string, error)
+ GetIdentityProviderMapper(ctx context.Context, token string, realm string, alias string, mapperID string) (*IdentityProviderMapper, error)
  CreateUserFederatedIdentity(ctx context.Context, token, realm, userID, providerID string, federatedIdentityRep FederatedIdentityRepresentation) error
  GetUserFederatedIdentities(ctx context.Context, token, realm, userID string) ([]*FederatedIdentityRepresentation, error)
  DeleteUserFederatedIdentity(ctx context.Context, token, realm, userID, providerID string) error
diff --git a/client.go b/client.go
index 3dba3e5..08f92d0 100644
--- a/client.go
+++ b/client.go
@@ -1411,12 +1411,18 @@ func (client *gocloak) GetGroupMembers(ctx context.Context, token, realm, groupI
 }
 
 // GetClientRoles get all roles for the given client in realm
-func (client *gocloak) GetClientRoles(ctx context.Context, token, realm, idOfClient string) ([]*Role, error) {
+func (client *gocloak) GetClientRoles(ctx context.Context, token, realm, idOfClient string, params GetRoleParams) ([]*Role, error) {
 	const errMessage = "could not get client roles"
 
 	var result []*Role
+	queryParams, err := GetQueryParams(params)
+	if err != nil {
+		return nil, errors.Wrap(err, errMessage)
+	}
+
 	resp, err := client.getRequestWithBearerAuth(ctx, token).
 		SetResult(&result).
+		SetQueryParams(queryParams).
 		Get(client.getAdminRealmURL(realm, "clients", idOfClient, "roles"))
 
 	if err := checkForError(resp, err, errMessage); err != nil {
@@ -1637,6 +1643,22 @@ func (client *gocloak) GetRealmRole(ctx context.Context, token, realm, roleName
 	return &result, nil
 }
 
+// GetRealmRoleByID returns a role from a realm by role's ID
+func (client *gocloak) GetRealmRoleByID(ctx context.Context, token, realm, roleID string) (*Role, error) {
+	const errMessage = "could not get realm role"
+
+	var result Role
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(client.getAdminRealmURL(realm, "roles-by-id", roleID))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
 // GetRealmRoles get all roles of the given realm.
 func (client *gocloak) GetRealmRoles(ctx context.Context, token, realm string, params GetRoleParams) ([]*Role, error) {
 	const errMessage = "could not get realm roles"
@@ -1702,6 +1724,17 @@ func (client *gocloak) UpdateRealmRole(ctx context.Context, token, realm, roleNa
 	return checkForError(resp, err, errMessage)
 }
 
+// UpdateRealmRoleByID updates a role in a realm by role's ID
+func (client *gocloak) UpdateRealmRoleByID(ctx context.Context, token, realm, roleID string, role Role) error {
+	const errMessage = "could not update realm role"
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetBody(role).
+		Put(client.getAdminRealmURL(realm, "roles-by-id", roleID))
+
+	return checkForError(resp, err, errMessage)
+}
+
 // DeleteRealmRole deletes a role in a realm by role's name
 func (client *gocloak) DeleteRealmRole(ctx context.Context, token, realm, roleName string) error {
 	const errMessage = "could not delete realm role"
@@ -1776,6 +1809,22 @@ func (client *gocloak) DeleteRealmRoleComposite(ctx context.Context, token, real
 	return checkForError(resp, err, errMessage)
 }
 
+// GetCompositeRealmRoles returns all realm composite roles associated with the given realm role
+func (client *gocloak) GetCompositeRealmRoles(ctx context.Context, token, realm, roleName string) ([]*Role, error) {
+	const errMessage = "could not get composite realm roles by role"
+
+	var result []*Role
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(client.getAdminRealmURL(realm, "roles", roleName, "composites"))
+
+	if err = checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
 // GetCompositeRealmRolesByRoleID returns all realm composite roles associated with the given client role
 func (client *gocloak) GetCompositeRealmRolesByRoleID(ctx context.Context, token, realm, roleID string) ([]*Role, error) {
 	const errMessage = "could not get composite client roles by role id"
@@ -2416,14 +2465,34 @@ func (client *gocloak) ImportIdentityProviderConfigFromFile(ctx context.Context,
 }
 
 // CreateIdentityProviderMapper creates an instance of an identity provider mapper associated with the given alias
-func (client *gocloak) CreateIdentityProviderMapper(ctx context.Context, token, realm, alias string, mapper IdentityProviderMapper) error {
+func (client *gocloak) CreateIdentityProviderMapper(ctx context.Context, token, realm, alias string, mapper IdentityProviderMapper) (string, error) {
 	const errMessage = "could not create mapper for identity provider"
 
 	resp, err := client.getRequestWithBearerAuth(ctx, token).
 		SetBody(mapper).
 		Post(client.getAdminRealmURL(realm, "identity-provider", "instances", alias, "mappers"))
 
-	return checkForError(resp, err, errMessage)
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return "", err
+	}
+
+	return getID(resp), nil
+}
+
+// GetIdentityProviderMapper gets the mapper by id for the given identity provider alias in a realm
+func (client *gocloak) GetIdentityProviderMapper(ctx context.Context, token string, realm string, alias string, mapperID string) (*IdentityProviderMapper, error) {
+	const errMessage = "could not get identity provider mapper"
+
+	result := IdentityProviderMapper{}
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(client.getAdminRealmURL(realm, "identity-provider", "instances", alias, "mappers", mapperID))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
 }
 
 // DeleteIdentityProviderMapper deletes an instance of an identity provider mapper associated with the given alias and mapper ID
@@ -3368,5 +3437,3 @@ func (client *gocloak) CreateClientScopesScopeMappingsRealmRoles(ctx context.Con
 
 	return checkForError(resp, err, errMessage)
 }
-
-
diff --git a/gocloak.go b/gocloak.go
index 9c388f1..8f0d223 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -188,6 +188,8 @@ type GoCloak interface {
 	CreateRealmRole(ctx context.Context, token, realm string, role Role) (string, error)
 	// GetRealmRole returns a role from a realm by role's name
 	GetRealmRole(ctx context.Context, token, realm, roleName string) (*Role, error)
+	// GetRealmRoleByID returns a role from a realm by role's ID
+	GetRealmRoleByID(ctx context.Context, token, realm, roleID string) (*Role, error)
 	// GetRealmRoles get all roles of the given realm. It's an alias for the GetRoles function
 	GetRealmRoles(ctx context.Context, accessToken, realm string, params GetRoleParams) ([]*Role, error)
 	// GetRealmRolesByUserID returns all roles assigned to the given user
@@ -196,6 +198,8 @@ type GoCloak interface {
 	GetRealmRolesByGroupID(ctx context.Context, accessToken, realm, groupID string) ([]*Role, error)
 	// UpdateRealmRole updates a role in a realm
 	UpdateRealmRole(ctx context.Context, token, realm, roleName string, role Role) error
+	// UpdateRealmRoleByID updates a role in a realm by role's ID
+	UpdateRealmRoleByID(ctx context.Context, token, realm, roleID string, role Role) error
 	// DeleteRealmRole deletes a role in a realm by role's name
 	DeleteRealmRole(ctx context.Context, token, realm, roleName string) error
 	// AddRealmRoleToUser adds realm-level role mappings
@@ -210,6 +214,8 @@ type GoCloak interface {
 	AddRealmRoleComposite(ctx context.Context, token, realm, roleName string, roles []Role) error
 	// AddRealmRoleComposite adds roles as composite
 	DeleteRealmRoleComposite(ctx context.Context, token, realm, roleName string, roles []Role) error
+	// GetCompositeRealmRoles returns all realm composite roles associated with the given realm role
+	GetCompositeRealmRoles(ctx context.Context, token, realm, roleName string) ([]*Role, error)
 	// GetCompositeRealmRolesByRoleID returns all realm composite roles associated with the given client role
 	GetCompositeRealmRolesByRoleID(ctx context.Context, token, realm, roleID string) ([]*Role, error)
 	// GetCompositeRealmRolesByUserID returns all realm roles and composite roles assigned to the given user
@@ -236,7 +242,7 @@ type GoCloak interface {
 	// DeleteClientRoleFromGroup removes a client role from from the group
 	DeleteClientRoleFromGroup(ctx context.Context, token, realm, idOfClient, groupID string, roles []Role) error
 	// GetClientRoles gets roles for the given client
-	GetClientRoles(ctx context.Context, accessToken, realm, idOfClient string) ([]*Role, error)
+	GetClientRoles(ctx context.Context, accessToken, realm, idOfClient string, params GetRoleParams) ([]*Role, error)
 	// GetClientRoleById gets role for the given client using role id
 	GetClientRoleByID(ctx context.Context, accessToken, realm, roleID string) (*Role, error)
 	// GetRealmRolesByUserID returns all client roles assigned to the given user
@@ -334,7 +340,7 @@ type GoCloak interface {
 	// ExportIDPPublicBrokerConfig exports the broker config for a given alias
 	ExportIDPPublicBrokerConfig(ctx context.Context, token, realm, alias string) (*string, error)
 	// CreateIdentityProviderMapper creates an instance of an identity provider mapper associated with the given alias
-	CreateIdentityProviderMapper(ctx context.Context, token, realm, alias string, mapper IdentityProviderMapper) error
+	CreateIdentityProviderMapper(ctx context.Context, token, realm, alias string, mapper IdentityProviderMapper) (string, error)
 	// GetIdentityProviderMapperByID gets the mapper of an identity provider
 	GetIdentityProviderMapperByID(ctx context.Context, token, realm, alias, mapperID string) (*IdentityProviderMapper, error)
 	// UpdateIdentityProviderMapper updates mapper of an identity provider
diff --git a/models.go b/models.go
index 5029777..63e8c66 100644
--- a/models.go
+++ b/models.go
@@ -342,9 +342,10 @@ type Role struct {
 
 // GetRoleParams represents the optional parameters for getting roles
 type GetRoleParams struct {
-	First  *int    `json:"first,string,omitempty"`
-	Max    *int    `json:"max,string,omitempty"`
-	Search *string `json:"search,omitempty"`
+	First               *int    `json:"first,string,omitempty"`
+	Max                 *int    `json:"max,string,omitempty"`
+	Search              *string `json:"search,omitempty"`
+	BriefRepresentation *bool   `json:"briefRepresentation,string,omitempty"`
 }
 
 // ClientMappingsRepresentation is a client role mappings


Test patch:
diff --git a/client_test.go b/client_test.go
index e8bac44..e790398 100644
--- a/client_test.go
+++ b/client_test.go
@@ -1609,6 +1609,7 @@ func TestGocloak_ClientScopeMappingsClientRoles(t *testing.T) {
 		token.AccessToken,
 		cfg.GoCloak.Realm,
 		gocloakClientID,
+		gocloak.GetRoleParams{},
 	)
 	require.NoError(t, err, "GetClientRoles failed")
 
@@ -1991,7 +1992,8 @@ func TestGocloak_GetClientRoles(t *testing.T) {
 		context.Background(),
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		*testClient.ID)
+		*testClient.ID,
+		gocloak.GetRoleParams{})
 	require.NoError(t, err, "GetClientRoles failed")
 }
 
@@ -3215,7 +3217,8 @@ func TestGoCloak_ClientSecret(t *testing.T) {
 	defer tearDown()
 	require.Equal(t, *testClient.ID, idOfClient)
 
-	oldCreds, err := client.GetClientSecret(
+	// Keycloak does not support setting the secret while creating the client
+	_, err := client.GetClientSecret(
 		context.Background(),
 		token.AccessToken,
 		cfg.GoCloak.Realm,
@@ -3230,7 +3233,8 @@ func TestGoCloak_ClientSecret(t *testing.T) {
 		idOfClient,
 	)
 	require.NoError(t, err, "RegenerateClientSecret failed")
-	require.NotEqual(t, *oldCreds.Value, *regeneratedCreds.Value)
+	require.NotNil(t, regeneratedCreds.Value, "RegenerateClientSecret value is nil")
+	require.NotEmpty(t, *regeneratedCreds.Value, "RegenerateClientSecret value is empty")
 
 	err = client.DeleteClient(
 		context.Background(),
@@ -3916,7 +3920,7 @@ func TestGocloak_CreateGetDeleteUserFederatedIdentity(t *testing.T) {
 		},
 	}
 
-	err = client.CreateIdentityProviderMapper(
+	mapperPID, err := client.CreateIdentityProviderMapper(
 		context.Background(),
 		token.AccessToken,
 		cfg.GoCloak.Realm,
@@ -3924,6 +3928,7 @@ func TestGocloak_CreateGetDeleteUserFederatedIdentity(t *testing.T) {
 		mapperP,
 	)
 	require.NoError(t, err)
+	require.NotEmpty(t, mapperPID)
 
 	mappers, err := client.GetIdentityProviderMappers(
 		context.Background(),
@@ -3934,6 +3939,7 @@ func TestGocloak_CreateGetDeleteUserFederatedIdentity(t *testing.T) {
 	require.NoError(t, err)
 	require.Len(t, mappers, 1)
 	mapperID := mappers[0].ID
+	require.Equal(t, mapperPID, gocloak.PString(mapperID))
 
 	mapperP.ID = mapperID
 	// get single mapper
@@ -5594,6 +5600,8 @@ E8go1LcvbfHNyknHu2sptnRq55fHZSHr18vVsQRfDYMG</ds:X509Certificate>
 		"nameIDPolicyFormat":              "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
 		"wantAuthnRequestsSigned":         "false",
 		"addExtensionsElementWithKeyInfo": "false",
+		"loginHint":                       "false",
+		"enabledFromMetadata":             "true",
 	}
 
 	require.Len(