Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading