Skip to content

Commit

Permalink
feat: Add support for optional client scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
Nilsbakken authored and SergK committed Sep 16, 2024
1 parent e8760b2 commit 4bc7be0
Show file tree
Hide file tree
Showing 12 changed files with 383 additions and 4 deletions.
5 changes: 5 additions & 0 deletions api/v1/keycloakclient_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ type KeycloakClientSpec struct {
// +optional
DefaultClientScopes []string `json:"defaultClientScopes,omitempty"`

// OptionalClientScopes is a list of optional client scopes assigned to client.
// +nullable
// +optional
OptionalClientScopes []string `json:"optionalClientScopes,omitempty"`

// RedirectUris is a list of valid URI pattern a browser can redirect to after a successful login.
// Simple wildcards are allowed such as 'https://example.com/*'.
// Relative path can be specified too, such as /my/relative/path/*. Relative paths are relative to the client root URL.
Expand Down
5 changes: 5 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions config/crd/bases/v1.edp.epam.com_keycloakclients.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,13 @@ spec:
name:
description: Name is a client name.
type: string
optionalClientScopes:
description: OptionalClientScopes is a list of optional client scopes
assigned to client.
items:
type: string
nullable: true
type: array
protocol:
description: Protocol is a client protocol.
nullable: true
Expand Down
37 changes: 35 additions & 2 deletions controllers/keycloakclient/chain/put_client_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,50 @@ func (el *PutClientScope) Serve(ctx context.Context, keycloakClient *keycloakApi
}

func (el *PutClientScope) putClientScope(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) error {
if err := el.putDefaultClientScope(ctx, keycloakClient, realmName); err != nil {
return err
}

if err := el.putOptionalClientScope(ctx, keycloakClient, realmName); err != nil {
return err
}

return nil
}

func (el *PutClientScope) putDefaultClientScope(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) error {
kCloakSpec := keycloakClient.Spec

if len(kCloakSpec.DefaultClientScopes) == 0 {
return nil
}

scopes, err := el.keycloakApiClient.GetClientScopesByNames(ctx, realmName, kCloakSpec.DefaultClientScopes)
defaultScopes, err := el.keycloakApiClient.GetClientScopesByNames(ctx, realmName, kCloakSpec.DefaultClientScopes)
if err != nil {
return errors.Wrap(err, "error during GetClientScope")
}

err = el.keycloakApiClient.AddDefaultScopeToClient(ctx, realmName, kCloakSpec.ClientId, defaultScopes)
if err != nil {
return fmt.Errorf("failed to add default scope to client %s: %w", keycloakClient.Name, err)
}

return nil
}

func (el *PutClientScope) putOptionalClientScope(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) error {
kCloakSpec := keycloakClient.Spec

if len(kCloakSpec.OptionalClientScopes) == 0 {
return nil
}

optionalScopes, err := el.keycloakApiClient.GetClientScopesByNames(ctx, realmName, kCloakSpec.OptionalClientScopes)
if err != nil {
return errors.Wrap(err, "error during GetClientScope")
}

err = el.keycloakApiClient.AddDefaultScopeToClient(ctx, realmName, kCloakSpec.ClientId, scopes)
err = el.keycloakApiClient.AddOptionalScopeToClient(ctx, realmName, kCloakSpec.ClientId, optionalScopes)
if err != nil {
return fmt.Errorf("failed to add default scope to client %s: %w", keycloakClient.Name, err)
}
Expand Down
114 changes: 114 additions & 0 deletions controllers/keycloakclient/chain/put_client_scope_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package chain

import (
"context"
keycloakApi "github.com/epam/edp-keycloak-operator/api/v1"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/mocks"
"github.com/go-logr/logr"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"testing"
)

func TestPutClientScope_Serve(t *testing.T) {
t.Parallel()

tests := []struct {
name string
client func(t *testing.T) client.Client
keycloakClient client.ObjectKey
keycloakApiClient func(t *testing.T) *mocks.MockClient
wantErr require.ErrorAssertionFunc
}{
{
name: "with default scopes",
client: func(t *testing.T) client.Client {
s := runtime.NewScheme()
require.NoError(t, keycloakApi.AddToScheme(s))
require.NoError(t, corev1.AddToScheme(s))

return fake.NewClientBuilder().WithScheme(s).WithObjects(
&keycloakApi.KeycloakClient{
ObjectMeta: metav1.ObjectMeta{
Name: "test-client",
Namespace: "default",
},
Spec: keycloakApi.KeycloakClientSpec{
ClientId: "test-client-id",
DefaultClientScopes: []string{"default-scope"},
},
}).Build()
},
keycloakClient: client.ObjectKey{
Name: "test-client",
Namespace: "default",
},
keycloakApiClient: func(t *testing.T) *mocks.MockClient {
m := mocks.NewMockClient(t)

m.On("GetClientScopesByNames", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
m.On("AddDefaultScopeToClient", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

return m
},
wantErr: require.NoError,
},
{
name: "with optional scopes",
client: func(t *testing.T) client.Client {
s := runtime.NewScheme()
require.NoError(t, keycloakApi.AddToScheme(s))
require.NoError(t, corev1.AddToScheme(s))

return fake.NewClientBuilder().WithScheme(s).WithObjects(
&keycloakApi.KeycloakClient{
ObjectMeta: metav1.ObjectMeta{
Name: "test-client",
Namespace: "default",
},
Spec: keycloakApi.KeycloakClientSpec{
ClientId: "test-client-id",
OptionalClientScopes: []string{"optional-scope"},
},
}).Build()
},
keycloakClient: client.ObjectKey{
Name: "test-client",
Namespace: "default",
},
keycloakApiClient: func(t *testing.T) *mocks.MockClient {
m := mocks.NewMockClient(t)

m.On("GetClientScopesByNames", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
m.On("AddOptionalScopeToClient", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

return m
},
wantErr: require.NoError,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

cl := &keycloakApi.KeycloakClient{}
require.NoError(t, tt.client(t).Get(context.Background(), tt.keycloakClient, cl))

el := NewPutClientScope(tt.keycloakApiClient(t))
err := el.Serve(
ctrl.LoggerInto(context.Background(), logr.Discard()),
cl,
"realm",
)
tt.wantErr(t, err)
})
}
}
7 changes: 7 additions & 0 deletions deploy-templates/crds/v1.edp.epam.com_keycloakclients.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,13 @@ spec:
name:
description: Name is a client name.
type: string
optionalClientScopes:
description: OptionalClientScopes is a list of optional client scopes
assigned to client.
items:
type: string
nullable: true
type: array
protocol:
description: Protocol is a client protocol.
nullable: true
Expand Down
7 changes: 7 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,13 @@ KeycloakClientSpec defines the desired state of KeycloakClient.
Name is a client name.<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>optionalClientScopes</b></td>
<td>[]string</td>
<td>
OptionalClientScopes is a list of optional client scopes assigned to client.<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>protocol</b></td>
<td>string</td>
Expand Down
2 changes: 2 additions & 0 deletions pkg/client/keycloak/adapter/gocloak.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type GoCloakClients interface {
GetClientScope(ctx context.Context, token, realm, scopeID string) (*gocloak.ClientScope, error)
GetClientsDefaultScopes(ctx context.Context, token, realm, clientID string) ([]*gocloak.ClientScope, error)
AddDefaultScopeToClient(ctx context.Context, token, realm, clientID, scopeID string) error
GetClientsOptionalScopes(ctx context.Context, token, realm, clientID string) ([]*gocloak.ClientScope, error)
AddOptionalScopeToClient(ctx context.Context, token, realm, clientID, scopeID string) error
GetClientScopes(ctx context.Context, token, realm string) ([]*gocloak.ClientScope, error)

GetScopes(ctx context.Context, token, realm, idOfClient string, params gocloak.GetScopeParams) ([]*gocloak.ScopeRepresentation, error)
Expand Down
42 changes: 40 additions & 2 deletions pkg/client/keycloak/adapter/gocloak_adapter_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const defaultMax = 100

func (a GoCloakAdapter) AddDefaultScopeToClient(ctx context.Context, realmName, clientName string, scopes []ClientScope) error {
log := a.log.WithValues("clientName", clientName, logKeyRealm, realmName)
log.Info("Start add Client Scopes to client...")
log.Info("Start add Default Client Scopes to client...")

clientID, err := a.GetClientID(clientName, realmName)
if err != nil {
Expand Down Expand Up @@ -43,7 +43,45 @@ func (a GoCloakAdapter) AddDefaultScopeToClient(ctx context.Context, realmName,
}
}

log.Info("End add Client Scopes to client...")
log.Info("End add Default Client Scopes to client...")

return nil
}

func (a GoCloakAdapter) AddOptionalScopeToClient(ctx context.Context, realmName, clientName string, scopes []ClientScope) error {
log := a.log.WithValues("clientName", clientName, logKeyRealm, realmName)
log.Info("Start add Optional Client Scopes to client...")

clientID, err := a.GetClientID(clientName, realmName)
if err != nil {
return errors.Wrap(err, "error during GetClientId")
}

existingScopes, err := a.client.GetClientsOptionalScopes(ctx, a.token.AccessToken, realmName, clientID)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to get existing client scope for client %s", clientName))
}

existingScopesMap := make(map[string]*gocloak.ClientScope)

for _, s := range existingScopes {
if s != nil {
existingScopesMap[*s.ID] = s
}
}

for _, scope := range scopes {
if _, ok := existingScopesMap[scope.ID]; ok {
continue
}

err := a.client.AddOptionalScopeToClient(ctx, a.token.AccessToken, realmName, clientID, scope.ID)
if err != nil {
a.log.Error(err, fmt.Sprintf("failed link scope %s to client %s", scope.Name, clientName))
}
}

log.Info("End add Optional Client Scopes to client...")

return nil
}
Expand Down
Loading

0 comments on commit 4bc7be0

Please sign in to comment.