Skip to content

Commit

Permalink
feat: Enable secret reference support in KeycloakClient resource (#21)
Browse files Browse the repository at this point in the history
Change-Id: I35ff2b0d20e624c5bb6d38deacfd68609efec56e
  • Loading branch information
zmotso committed Nov 14, 2023
1 parent 4ac64f4 commit 820bc0f
Show file tree
Hide file tree
Showing 27 changed files with 1,129 additions and 1,021 deletions.
9 changes: 5 additions & 4 deletions api/v1/keycloakclient_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ type KeycloakClientSpec struct {
// +optional
RealmRef common.RealmRef `json:"realmRef"`

// Secret is a client secret used for authentication. If not provided, it will be generated.
// Secret is kubernetes secret name where the client's secret will be stored.
// Secret should have the following format: $secretName:secretKey.
// If not specified, a client secret will be generated and stored in a secret with the name keycloak-client-{metadata.name}-secret.
// If keycloak client is public, secret property will be ignored.
// +optional
// +kubebuilder:example="$keycloak-secret:client_secret"
Secret string `json:"secret,omitempty"`

// RealmRoles is a list of realm roles assigned to client.
Expand Down Expand Up @@ -173,9 +177,6 @@ type KeycloakClientStatus struct {

// +optional
FailureCount int64 `json:"failureCount,omitempty"`

// +optional
ClientSecretName string `json:"clientSecretName,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
10 changes: 6 additions & 4 deletions config/crd/bases/v1.edp.epam.com_keycloakclients.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,12 @@ spec:
nullable: true
type: array
secret:
description: Secret is a client secret used for authentication. If
not provided, it will be generated.
description: 'Secret is kubernetes secret name where the client''s
secret will be stored. Secret should have the following format:
$secretName:secretKey. If not specified, a client secret will be
generated and stored in a secret with the name keycloak-client-{metadata.name}-secret.
If keycloak client is public, secret property will be ignored.'
example: $keycloak-secret:client_secret
type: string
serviceAccount:
description: ServiceAccount is a service account configuration.
Expand Down Expand Up @@ -212,8 +216,6 @@ spec:
properties:
clientId:
type: string
clientSecretName:
type: string
failureCount:
format: int64
type: integer
Expand Down
2 changes: 1 addition & 1 deletion config/samples/v1_v1_keycloakclient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ spec:
clientId: agocd
directAccess: true
public: false
secret: ''
secret: $client-secret-name:client-secret-key
realmRef:
name: keycloakrealm-sample
kind: KeycloakRealm
Expand Down
3 changes: 3 additions & 0 deletions controllers/keycloakclient/chain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/epam/edp-keycloak-operator/pkg/secretref"
)

func Make(scheme *runtime.Scheme, client client.Client, logger logr.Logger) Element {
Expand All @@ -15,6 +17,7 @@ func Make(scheme *runtime.Scheme, client client.Client, logger logr.Logger) Elem

return &PutClient{
BaseElement: baseElement,
SecretRef: secretref.NewSecretRef(client),
next: &PutClientRole{
BaseElement: baseElement,
next: &PutRealmRole{
Expand Down
172 changes: 4 additions & 168 deletions controllers/keycloakclient/chain/chain_test.go
Original file line number Diff line number Diff line change
@@ -1,179 +1,15 @@
package chain

import (
"context"
"testing"
"time"

"github.com/Nerzal/gocloak/v12"
"github.com/stretchr/testify/assert"
testifyMock "github.com/stretchr/testify/mock"
"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

"github.com/epam/edp-keycloak-operator/api/common"
keycloakApi "github.com/epam/edp-keycloak-operator/api/v1"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/adapter"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/dto"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/mock"
)

func TestPrivateClientSecret(t *testing.T) {
kc := keycloakApi.KeycloakClient{ObjectMeta: metav1.ObjectMeta{Name: "main", Namespace: "namespace"},
Spec: keycloakApi.KeycloakClientSpec{TargetRealm: "namespace.main", Secret: "keycloak-secret",
RealmRoles: &[]keycloakApi.RealmRole{{Name: "fake-client-administrators", Composite: "administrator"},
{Name: "fake-client-users", Composite: "developer"},
}, Public: false, ClientId: "fake-client", WebUrl: "fake-url", DirectAccess: false,
AdvancedProtocolMappers: true, ClientRoles: nil, ProtocolMappers: &[]keycloakApi.ProtocolMapper{
{Name: "bar", Config: map[string]string{"bar": "1"}},
{Name: "foo", Config: map[string]string{"foo": "2"}},
},
},
}

secret := corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "keycloak-secret", Namespace: "namespace"},
Data: map[string][]byte{"username": []byte("user"), "password": []byte("pass")}}

s := scheme.Scheme
s.AddKnownTypes(v1.SchemeGroupVersion, &kc)
client := fake.NewClientBuilder().WithRuntimeObjects(&kc, &secret).Build()
clientDTO := dto.ConvertSpecToClient(&kc.Spec, "", "")

kClient := new(adapter.Mock)
kClient.On("ExistClient", clientDTO.ClientId, clientDTO.RealmName).Return(true, nil)
kClient.On("GetClientID", clientDTO.ClientId, clientDTO.RealmName).Return("3333", nil)
kClient.On("UpdateClient", testifyMock.Anything).Return(nil)

baseElement := BaseElement{
scheme: s,
Client: client,
Logger: mock.NewLogr(),
}
putCl := PutClient{
BaseElement: baseElement,
}

ctx := context.Background()

if err := putCl.Serve(ctx, &kc, kClient, ""); err != nil {
t.Fatalf("%+v", err)
}

kc.Spec.Secret = ""

if err := putCl.Serve(ctx, &kc, kClient, ""); err != nil {
t.Fatalf("%+v", err)
}

var (
checkSecret corev1.Secret
checkClient keycloakApi.KeycloakClient
)

err := client.Get(context.Background(), types.NamespacedName{Name: kc.Name, Namespace: kc.Namespace},
&checkClient)
require.NoError(t, err)

if kc.Spec.Secret == "" || kc.Status.ClientSecretName == "" {
t.Fatal("client secret not updated")
}

err = client.Get(context.Background(), types.NamespacedName{Namespace: checkClient.Namespace,
Name: checkClient.Spec.Secret}, &checkSecret)
require.NoError(t, err)

if _, ok := checkSecret.Data[keycloakApi.ClientSecretKey]; !ok {
t.Fatal("client secret key not found in secret")
}
}

func TestMake(t *testing.T) {
k := keycloakApi.Keycloak{ObjectMeta: metav1.ObjectMeta{Name: "test-keycloak", Namespace: "namespace"},
Spec: keycloakApi.KeycloakSpec{Url: "https://some", Secret: "keycloak-secret"},
Status: keycloakApi.KeycloakStatus{Connected: true}}
secret := corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "keycloak-secret", Namespace: "namespace"},
Data: map[string][]byte{"username": []byte("user"), "password": []byte("pass")}}
kr := keycloakApi.KeycloakRealm{ObjectMeta: metav1.ObjectMeta{Name: "main", Namespace: "namespace",
OwnerReferences: []metav1.OwnerReference{{Name: "test-keycloak", Kind: "Keycloak"}}},
Spec: keycloakApi.KeycloakRealmSpec{RealmName: "namespace.main"},
}
delTime := metav1.Time{Time: time.Now()}
kc := keycloakApi.KeycloakClient{ObjectMeta: metav1.ObjectMeta{Name: "main", Namespace: "namespace",
DeletionTimestamp: &delTime},
Spec: keycloakApi.KeycloakClientSpec{
RealmRef: common.RealmRef{
Kind: keycloakApi.KeycloakRealmKind,
Name: kr.Name,
},
Secret: "keycloak-secret",
RealmRoles: &[]keycloakApi.RealmRole{{Name: "fake-client-administrators", Composite: "administrator"},
{Name: "fake-client-users", Composite: "developer"},
}, Public: true, ClientId: "fake-client", WebUrl: "fake-url", DirectAccess: false,
AdvancedProtocolMappers: true, ClientRoles: nil, ProtocolMappers: &[]keycloakApi.ProtocolMapper{
{Name: "bar", Config: map[string]string{"bar": "1"}},
{Name: "foo", Config: map[string]string{"foo": "2"}},
},
},
}

s := scheme.Scheme
s.AddKnownTypes(v1.SchemeGroupVersion, &k, &kr, &kc, &keycloakApi.KeycloakRealm{}, &keycloakApi.KeycloakRealmList{})
client := fake.NewClientBuilder().WithRuntimeObjects(&secret, &k, &kr, &kc).Build()

kClient := new(adapter.Mock)
chain := Make(s, client, mock.NewLogr())

clientDTO := dto.ConvertSpecToClient(&kc.Spec, "", kr.Spec.RealmName)
kClient.On("ExistClient", clientDTO.ClientId, clientDTO.RealmName).
Return(false, nil)
kClient.On("CreateClient", clientDTO).Return(nil)
kClient.On("GetClientID", clientDTO.ClientId, clientDTO.RealmName).Return("3333", nil)
kClient.On("UpdateClient", testifyMock.Anything).Return(nil)
kClient.On("ExistRealmRole", kr.Spec.RealmName, "fake-client-users").
Return(true, nil)
kClient.On("ExistRealmRole", kr.Spec.RealmName, "fake-client-administrators").
Return(false, nil)
kClient.On("SyncClientProtocolMapper", clientDTO, []gocloak.ProtocolMapperRepresentation{
{Name: gocloak.StringP("bar"), Protocol: gocloak.StringP(""), Config: &map[string]string{"bar": "1"},
ProtocolMapper: gocloak.StringP("")},
{Name: gocloak.StringP("foo"), Protocol: gocloak.StringP(""), Config: &map[string]string{"foo": "2"},
ProtocolMapper: gocloak.StringP("")},
}, false).Return(nil)

role1DTO := dto.IncludedRealmRole{Name: "fake-client-administrators", Composite: "administrator"}
kClient.On("CreateIncludedRealmRole", kr.Spec.RealmName, &role1DTO).Return(nil)

err := chain.Serve(context.Background(), &kc, kClient, kr.Spec.RealmName)
require.NoError(t, err)

if kc.Status.ClientID != "3333" {
t.Fatal("keycloak client status not changed")
}
}

func TestPutClientScope_Serve(t *testing.T) {
pcs := PutClientScope{}
kc := keycloakApi.KeycloakClient{
Spec: keycloakApi.KeycloakClientSpec{
ClientId: "clid1",
RealmRef: common.RealmRef{
Kind: keycloakApi.KeycloakRealmKind,
Name: "realm",
}}}
kClient := new(adapter.Mock)
adapterScope := adapter.ClientScope{ID: "scope-id1"}

ctx := context.Background()

kClient.On("GetClientScopesByNames", ctx, adapterScope.ID, "realm").Return([]adapter.ClientScope{adapterScope}, nil)
kClient.On("AddDefaultScopeToClient", ctx, "realm", adapterScope.ID).
Return(nil)

err := pcs.putClientScope(ctx, &kc, kClient, "realm")
assert.NoError(t, err)
chain := Make(runtime.NewScheme(), fake.NewClientBuilder().Build(), logr.Discard())
require.NotNil(t, chain)
}
Loading

0 comments on commit 820bc0f

Please sign in to comment.