Repository: Nerzal/gocloak
Pull request #351 (2022-05-01 13:49:11)

Added ClientScope ProtocolMappers CRUD
This PR adds ability to add, modify or remove protocol mappers under a client scope in Keycloak.

Nerzal: Hi! 
Thank you for the contribution! 

Could you please try and update your branch using the current main branch? 
That should fix the CI Pipeline :) 
HimanshuM: Thanks for your response @Nerzal I have merged main into my fork.
codecov[bot]: # [Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/351?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) Report
> Merging [#351](https://codecov.io/gh/Nerzal/gocloak/pull/351?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) (1636b01) into [main](https://codecov.io/gh/Nerzal/gocloak/commit/289eed6740763c589dc44bee0a0ba16178371fec?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) (289eed6) will **increase** coverage by `0.07%`.
> The diff coverage is `81.25%`.

```diff
@@            Coverage Diff             @@
##             main     #351      +/-   ##
==========================================
+ Coverage   76.78%   76.85%   +0.07%     
==========================================
  Files           4        4              
  Lines        1973     2005      +32     
==========================================
+ Hits         1515     1541      +26     
- Misses        317      320       +3     
- Partials      141      144       +3     
```


| [Impacted Files](https://codecov.io/gh/Nerzal/gocloak/pull/351?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) | Coverage Δ | |
|---|---|---|
| [client.go](https://codecov.io/gh/Nerzal/gocloak/pull/351/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal#diff-Y2xpZW50Lmdv) | `75.17% <81.25%> (+0.10%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/351?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/351?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal). Last update [289eed6...1636b01](https://codecov.io/gh/Nerzal/gocloak/pull/351?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal).


Patch:
diff --git a/client.go b/client.go
index 0eaa931..98c7c1a 100644
--- a/client.go
+++ b/client.go
@@ -779,6 +779,21 @@ func (client *gocloak) CreateClientScope(ctx context.Context, token, realm strin
 	return getID(resp), nil
 }
 
+// CreateClientScopeProtocolMapper creates a new protocolMapper under the given client scope
+func (client *gocloak) CreateClientScopeProtocolMapper(ctx context.Context, token, realm, scopeID string, protocolMapper ProtocolMappers) (string, error) {
+	const errMessage = "could not create client scope protocol mapper"
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetBody(protocolMapper).
+		Post(client.getAdminRealmURL(realm, "client-scopes", scopeID, "protocol-mappers", "models"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return "", err
+	}
+
+	return getID(resp), nil
+}
+
 func (client *gocloak) UpdateGroup(ctx context.Context, token, realm string, updatedGroup Group) error {
 	const errMessage = "could not update group"
 
@@ -849,6 +864,17 @@ func (client *gocloak) UpdateClientScope(ctx context.Context, token, realm strin
 	return checkForError(resp, err, errMessage)
 }
 
+// UpdateClientScopeProtocolMapper updates the given protocol mapper for a client scope
+func (client *gocloak) UpdateClientScopeProtocolMapper(ctx context.Context, token, realm, scopeID string, protocolMapper ProtocolMappers) error {
+	const errMessage = "could not update client scope"
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetBody(protocolMapper).
+		Put(client.getAdminRealmURL(realm, "client-scopes", scopeID, "protocol-mappers", "models", PString(protocolMapper.ID)))
+
+	return checkForError(resp, err, errMessage)
+}
+
 func (client *gocloak) DeleteGroup(ctx context.Context, token, realm, groupID string) error {
 	const errMessage = "could not delete group"
 
@@ -906,6 +932,16 @@ func (client *gocloak) DeleteClientScope(ctx context.Context, token, realm, scop
 	return checkForError(resp, err, errMessage)
 }
 
+// DeleteClientScopeProtocolMapper deletes the given protocol mapper from the client scope
+func (client *gocloak) DeleteClientScopeProtocolMapper(ctx context.Context, token, realm, scopeID, protocolMapperID string) error {
+	const errMessage = "could not delete client scope"
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		Delete(client.getAdminRealmURL(realm, "client-scopes", scopeID, "protocol-mappers", "models", protocolMapperID))
+
+	return checkForError(resp, err, errMessage)
+}
+
 // GetClient returns a client
 func (client *gocloak) GetClient(ctx context.Context, token, realm, idOfClient string) (*Client, error) {
 	const errMessage = "could not get client"
@@ -1099,6 +1135,40 @@ func (client *gocloak) GetClientScopes(ctx context.Context, token, realm string)
 	return result, nil
 }
 
+// GetClientScopeProtocolMappers returns all protocol mappers of a client scope
+func (client *gocloak) GetClientScopeProtocolMappers(ctx context.Context, token, realm, scopeID string) ([]*ProtocolMappers, error) {
+	const errMessage = "could not get client scope protocol mappers"
+
+	var result []*ProtocolMappers
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(client.getAdminRealmURL(realm, "client-scopes", scopeID, "protocol-mappers", "models"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+// GetClientScopeProtocolMapper returns a protocol mapper of a client scope
+func (client *gocloak) GetClientScopeProtocolMapper(ctx context.Context, token, realm, scopeID, protocolMapperID string) (*ProtocolMappers, error) {
+	const errMessage = "could not get client scope protocol mappers"
+
+	var result *ProtocolMappers
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(client.getAdminRealmURL(realm, "client-scopes", scopeID, "protocol-mappers", "models", protocolMapperID))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
 // GetClientScopeMappings returns all scope mappings for the client
 func (client *gocloak) GetClientScopeMappings(ctx context.Context, token, realm, idOfClient string) (*MappingsRepresentation, error) {
 	const errMessage = "could not get all scope mappings for the client"
diff --git a/gocloak.go b/gocloak.go
index 8afb130..a478854 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -76,6 +76,8 @@ type GoCloak interface {
 	CreateClient(ctx context.Context, accessToken, realm string, newClient Client) (string, error)
 	// CreateClientScope creates a new clientScope
 	CreateClientScope(ctx context.Context, accessToken, realm string, scope ClientScope) (string, error)
+	// CreateClientScopeProtocolMapper creates a new protocolMapper under the given client scope
+	CreateClientScopeProtocolMapper(ctx context.Context, accessToken, realm, scopeID string, protocolMapper ProtocolMappers) (string, error)
 	// CreateComponent creates a new component
 	CreateComponent(ctx context.Context, accessToken, realm string, component Component) (string, error)
 	// CreateClientScopeMappingsRealmRoles creates realm-level roles to the client’s scope
@@ -95,6 +97,8 @@ type GoCloak interface {
 	UpdateClient(ctx context.Context, accessToken, realm string, updatedClient Client) error
 	// UpdateClientScope updates the given clientScope
 	UpdateClientScope(ctx context.Context, accessToken, realm string, scope ClientScope) error
+	// UpdateClientScopeProtocolMapper updates the given protocol mapper for a client scope
+	UpdateClientScopeProtocolMapper(ctx context.Context, accessToken, realm, scopeID string, protocolMapper ProtocolMappers) error
 	// UpdateClientRepresentation updates the given client representation
 	UpdateClientRepresentation(ctx context.Context, accessToken, realm string, updatedClient Client) (*Client, error)
 
@@ -106,6 +110,8 @@ type GoCloak interface {
 	DeleteClient(ctx context.Context, accessToken, realm, idOfClient string) error
 	// DeleteClientScope
 	DeleteClientScope(ctx context.Context, accessToken, realm, scopeID string) error
+	// DeleteClientScopeProtocolMapper deletes the given protocol mapper from the client scope
+	DeleteClientScopeProtocolMapper(ctx context.Context, accessToken, realm, scopeID, protocolMapperID string) error
 	// DeleteClientScopeMappingsRealmRoles deletes realm-level roles from the client’s scope
 	DeleteClientScopeMappingsRealmRoles(ctx context.Context, token, realm, idOfClient string, roles []Role) error
 	// DeleteClientScopeMappingsClientRoles deletes client-level roles from the client’s scope
@@ -137,6 +143,10 @@ type GoCloak interface {
 	GetClientScope(ctx context.Context, token, realm, scopeID string) (*ClientScope, error)
 	// GetClientScopes returns all client scopes
 	GetClientScopes(ctx context.Context, token, realm string) ([]*ClientScope, error)
+	// GetClientScopeProtocolMappers returns all protocol mappers of a client scope
+	GetClientScopeProtocolMappers(ctx context.Context, token, realm, scopeID string) ([]*ProtocolMappers, error)
+	// GetClientScopeProtocolMapper returns a protocol mapper of a client scope
+	GetClientScopeProtocolMapper(ctx context.Context, token, realm, scopeID, protocolMapperID string) (*ProtocolMappers, error)
 	// GetClientScopeMappings returns all scope mappings for the client
 	GetClientScopeMappings(ctx context.Context, token, realm, idOfClient string) (*MappingsRepresentation, error)
 	// GetClientScopeMappingsRealmRoles returns realm-level roles associated with the client’s scope


Test patch:
diff --git a/client_test.go b/client_test.go
index 4477485..6d94ff0 100644
--- a/client_test.go
+++ b/client_test.go
@@ -1309,6 +1309,87 @@ func Test_CreateClientScope_DeleteClientScope(t *testing.T) {
 	tearDown()
 }
 
+func CreateUpdateClientScopeProtocolMapper(t *testing.T, client gocloak.GoCloak, scopeID string, protocolMapper *gocloak.ProtocolMappers) (func(), string) {
+	cfg := GetConfig(t)
+	token := GetAdminToken(t, client)
+
+	if protocolMapper == nil {
+		protocolMapper = &gocloak.ProtocolMappers{
+			ID:             GetRandomNameP("proto-map-"),
+			Name:           GetRandomNameP("proto-map-"),
+			Protocol:       GetRandomNameP("openid-connect"),
+			ProtocolMapper: gocloak.StringP("oidc-usermodel-realm-role-mapper"),
+			ProtocolMappersConfig: &gocloak.ProtocolMappersConfig{
+				UserAttribute:      gocloak.StringP("foo"),
+				IDTokenClaim:       gocloak.StringP("true"),
+				UserinfoTokenClaim: gocloak.StringP("true"),
+				AccessTokenClaim:   gocloak.StringP("true"),
+				ClaimName:          gocloak.StringP("realm.roles"),
+				JSONTypeLabel:      gocloak.StringP("String"),
+				Multivalued:        gocloak.StringP("true"),
+			},
+		}
+	}
+
+	t.Logf("Creating Client Scope Protocol Mapper: %+v", protocolMapper)
+	protocolMapperID, err := client.CreateClientScopeProtocolMapper(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		scopeID,
+		*protocolMapper,
+	)
+	require.NoError(t, err, "CreateClientScopeProtocolMapper failed")
+	if !gocloak.NilOrEmpty(protocolMapper.ID) {
+		require.Equal(t, protocolMapperID, *protocolMapper.ID)
+	}
+
+	protocolMapper.Name = GetRandomNameP("proto-map2-")
+	protocolMapper.ProtocolMappersConfig.AccessTokenClaim = gocloak.StringP("false")
+	err = client.UpdateClientScopeProtocolMapper(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		scopeID,
+		*protocolMapper,
+	)
+	require.NoError(t, err, "UpdateClientScopeProtocolMapper failed")
+
+	tearDown := func() {
+		err := client.DeleteClientScopeProtocolMapper(
+			context.Background(),
+			token.AccessToken,
+			cfg.GoCloak.Realm,
+			scopeID,
+			protocolMapperID,
+		)
+		require.NoError(t, err, "DeleteClientScopeProtocolMapper failed")
+	}
+	return tearDown, protocolMapperID
+}
+
+func Test_CreateClientScopeProtocolMapper_DeleteClientScopeProtocolMapper(t *testing.T) {
+	// t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetAdminToken(t, client)
+
+	tearDown1, scopeID := CreateClientScope(t, client, nil)
+	tearDown2, protocolMapperID := CreateUpdateClientScopeProtocolMapper(t, client, scopeID, nil)
+	protocolMapper, err := client.GetClientScopeProtocolMapper(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		scopeID,
+		protocolMapperID,
+	)
+	require.NoError(t, err)
+	require.NotEmpty(t, protocolMapper)
+	require.Equal(t, protocolMapper.ProtocolMappersConfig.AccessTokenClaim, gocloak.StringP("false"))
+	tearDown2()
+	tearDown1()
+}
+
 func Test_ListAddRemoveDefaultClientScopes(t *testing.T) {
 	// t.Parallel()
 	cfg := GetConfig(t)
@@ -1501,6 +1582,26 @@ func Test_GetClientScopes(t *testing.T) {
 	require.NotZero(t, len(scopes), "there should be client scopes")
 }
 
+func Test_GetClientScopeProtocolMappers(t *testing.T) {
+	// t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetAdminToken(t, client)
+	tearDown, scopeID := CreateClientScope(t, client, nil)
+	defer tearDown()
+
+	// Getting client scope protocol mappers
+	protocolMappers, err := client.GetClientScopeProtocolMappers(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		scopeID,
+	)
+	require.NoError(t, err, "GetClientScopeProtocolMappers failed")
+	// Checking that GetClientScopeProtocolMappers returns something
+	require.NotNil(t, protocolMappers)
+}
+
 func CreateClientScopeMappingsRealmRoles(t *testing.T, client gocloak.GoCloak, idOfClient string, roles []gocloak.Role) func() {
 	token := GetAdminToken(t, client)
 	cfg := GetConfig(t)