Skip to content

Commit

Permalink
feat: Add ClusterKeycloakRealm browserFlow setting (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
zmotso authored and SergK committed Jun 19, 2024
1 parent 4e32717 commit ae76bc9
Show file tree
Hide file tree
Showing 16 changed files with 246 additions and 21 deletions.
12 changes: 12 additions & 0 deletions api/v1alpha1/clusterkeycloakrealm_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ type ClusterKeycloakRealmSpec struct {
// +nullable
// +optional
TokenSettings *common.TokenSettings `json:"tokenSettings,omitempty"`

// AuthenticationFlow is the configuration for authentication flows in the realm.
// +nullable
// +optional
AuthenticationFlow *AuthenticationFlow `json:"authenticationFlows,omitempty"`
}

type AuthenticationFlow struct {
// BrowserFlow specifies the authentication flow to use for the realm's browser clients.
// +optional
// +kubebuilder:example="browser"
BrowserFlow string `json:"browserFlow,omitempty"`
}

type ClusterRealmThemes struct {
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

11 changes: 11 additions & 0 deletions config/crd/bases/v1.edp.epam.com_clusterkeycloakrealms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ spec:
spec:
description: ClusterKeycloakRealmSpec defines the desired state of ClusterKeycloakRealm.
properties:
authenticationFlows:
description: AuthenticationFlow is the configuration for authentication
flows in the realm.
nullable: true
properties:
browserFlow:
description: BrowserFlow specifies the authentication flow to
use for the realm's browser clients.
example: browser
type: string
type: object
browserSecurityHeaders:
additionalProperties:
type: string
Expand Down
2 changes: 2 additions & 0 deletions config/samples/v1_v1alpha1_clusterkeycloakrealm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ metadata:
spec:
clusterKeycloakRef: clusterkeycloak-sample
realmName: realm-sample
authenticationFlows:
browserFlow: browserFlow-sample
36 changes: 36 additions & 0 deletions controllers/clusterkeycloakrealm/chain/auth_flow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package chain

import (
"context"
"fmt"

ctrl "sigs.k8s.io/controller-runtime"

keycloakApi "github.com/epam/edp-keycloak-operator/api/v1alpha1"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak"
)

type AuthFlow struct {
}

func NewAuthFlow() *AuthFlow {
return &AuthFlow{}
}

func (a AuthFlow) ServeRequest(ctx context.Context, realm *keycloakApi.ClusterKeycloakRealm, kClient keycloak.Client) error {
log := ctrl.LoggerFrom(ctx)
log.Info("Start configuring authentication flow")

if realm.Spec.AuthenticationFlow == nil || realm.Spec.AuthenticationFlow.BrowserFlow == "" {
log.Info("Authentication flow is not provided, skip configuring")
return nil
}

if err := kClient.SetRealmBrowserFlow(ctx, realm.Spec.RealmName, realm.Spec.AuthenticationFlow.BrowserFlow); err != nil {
return fmt.Errorf("setting realm browser flow: %w", err)
}

log.Info("Authentication flow has been configured")

return nil
}
94 changes: 94 additions & 0 deletions controllers/clusterkeycloakrealm/chain/auth_flow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package chain

import (
"context"
"errors"
"testing"

"github.com/go-logr/logr"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
ctrl "sigs.k8s.io/controller-runtime"

keycloakApi "github.com/epam/edp-keycloak-operator/api/v1alpha1"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/mocks"
)

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

tests := []struct {
name string
realm *keycloakApi.ClusterKeycloakRealm
kClient func(t *testing.T) keycloak.Client
wantErr require.ErrorAssertionFunc
}{
{
name: "realm browser flow is not provided",
realm: &keycloakApi.ClusterKeycloakRealm{},
kClient: func(t *testing.T) keycloak.Client {
return mocks.NewMockClient(t)
},
wantErr: require.NoError,
},
{
name: "realm browser flow updated successfully",
realm: &keycloakApi.ClusterKeycloakRealm{
Spec: keycloakApi.ClusterKeycloakRealmSpec{
RealmName: "realm1",
AuthenticationFlow: &keycloakApi.AuthenticationFlow{
BrowserFlow: "flow-alias-1",
},
},
},
kClient: func(t *testing.T) keycloak.Client {
kc := mocks.NewMockClient(t)
kc.On("SetRealmBrowserFlow", mock.Anything, "realm1", "flow-alias-1").
Return(nil)

return kc
},
wantErr: require.NoError,
},
{
name: "error on setting realm browser flow",
realm: &keycloakApi.ClusterKeycloakRealm{
Spec: keycloakApi.ClusterKeycloakRealmSpec{
RealmName: "realm1",
AuthenticationFlow: &keycloakApi.AuthenticationFlow{
BrowserFlow: "flow-alias-1",
},
},
},
kClient: func(t *testing.T) keycloak.Client {
kc := mocks.NewMockClient(t)
kc.On("SetRealmBrowserFlow", mock.Anything, "realm1", "flow-alias-1").
Return(errors.New("failed to set realm browser flow"))
return kc
},

wantErr: func(t require.TestingT, err error, i ...interface{}) {
require.Error(t, err)
require.Contains(t, err.Error(), "setting realm browser flow")
},
},
}

for _, tt := range tests {
tt := tt

t.Run(tt.name, func(t *testing.T) {
t.Parallel()

a := NewAuthFlow()
err := a.ServeRequest(
ctrl.LoggerInto(context.Background(), logr.Discard()),
tt.realm,
tt.kClient(t),
)

tt.wantErr(t, err)
})
}
}
2 changes: 1 addition & 1 deletion controllers/keycloakrealm/chain/auth_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (a AuthFlow) ServeRequest(ctx context.Context, realm *keycloakApi.KeycloakR
return nextServeOrNil(ctx, a.next, realm, kClient)
}

if err := kClient.SetRealmBrowserFlow(realm.Spec.RealmName, *realm.Spec.BrowserFlow); err != nil {
if err := kClient.SetRealmBrowserFlow(ctx, realm.Spec.RealmName, *realm.Spec.BrowserFlow); err != nil {
return errors.Wrap(err, "unable to set realm auth flow")
}

Expand Down
5 changes: 3 additions & 2 deletions controllers/keycloakrealm/chain/auth_flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/Nerzal/gocloak/v12"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

keycloakApi "github.com/epam/edp-keycloak-operator/api/v1"
Expand All @@ -28,7 +29,7 @@ func TestAuthFlow_ServeRequest(t *testing.T) {
err := af.ServeRequest(ctx, &realm, kc)
require.NoError(t, err)

kc.On("SetRealmBrowserFlow", "realm1", "flow-alias-1").Return(nil)
kc.On("SetRealmBrowserFlow", mock.Anything, "realm1", "flow-alias-1").Return(nil)

realm.Spec.BrowserFlow = gocloak.StringP("flow-alias-1")

Expand All @@ -48,7 +49,7 @@ func TestAuthFlow_ServeRequest_Failure(t *testing.T) {

mockErr := errors.New("fatal")

kc.On("SetRealmBrowserFlow", "realm1", "flow-alias-1").Return(mockErr)
kc.On("SetRealmBrowserFlow", mock.Anything, "realm1", "flow-alias-1").Return(mockErr)

realm.Spec.BrowserFlow = gocloak.StringP("flow-alias-1")

Expand Down
2 changes: 2 additions & 0 deletions deploy-templates/_crd_examples/clusterkeycloakrealm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ metadata:
spec:
clusterKeycloakRef: clusterkeycloak-sample
realmName: realm-sample1234
authenticationFlows:
browserFlow: browserFlow-sample
11 changes: 11 additions & 0 deletions deploy-templates/crds/v1.edp.epam.com_clusterkeycloakrealms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ spec:
spec:
description: ClusterKeycloakRealmSpec defines the desired state of ClusterKeycloakRealm.
properties:
authenticationFlows:
description: AuthenticationFlow is the configuration for authentication
flows in the realm.
nullable: true
properties:
browserFlow:
description: BrowserFlow specifies the authentication flow to
use for the realm's browser clients.
example: browser
type: string
type: object
browserSecurityHeaders:
additionalProperties:
type: string
Expand Down
34 changes: 34 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ ClusterKeycloakRealmSpec defines the desired state of ClusterKeycloakRealm.
RealmName specifies the name of the realm.<br/>
</td>
<td>true</td>
</tr><tr>
<td><b><a href="#clusterkeycloakrealmspecauthenticationflows">authenticationFlows</a></b></td>
<td>object</td>
<td>
AuthenticationFlow is the configuration for authentication flows in the realm.<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>browserSecurityHeaders</b></td>
<td>map[string]string</td>
Expand Down Expand Up @@ -176,6 +183,33 @@ Use in combination with the default hostname provider to override the base URL f
</table>


### ClusterKeycloakRealm.spec.authenticationFlows
<sup><sup>[↩ Parent](#clusterkeycloakrealmspec)</sup></sup>



AuthenticationFlow is the configuration for authentication flows in the realm.

<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Required</th>
</tr>
</thead>
<tbody><tr>
<td><b>browserFlow</b></td>
<td>string</td>
<td>
BrowserFlow specifies the authentication flow to use for the realm's browser clients.<br/>
</td>
<td>false</td>
</tr></tbody>
</table>


### ClusterKeycloakRealm.spec.localization
<sup><sup>[↩ Parent](#clusterkeycloakrealmspec)</sup></sup>

Expand Down
6 changes: 3 additions & 3 deletions pkg/client/keycloak/adapter/gocloak_adapter_auth_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,14 @@ func (a GoCloakAdapter) adjustChildFlowsPriority(realmName string, flow *Keycloa
return nil
}

func (a GoCloakAdapter) SetRealmBrowserFlow(realmName string, flowAlias string) error {
realm, err := a.client.GetRealm(context.Background(), a.token.AccessToken, realmName)
func (a GoCloakAdapter) SetRealmBrowserFlow(ctx context.Context, realmName string, flowAlias string) error {
realm, err := a.client.GetRealm(ctx, a.token.AccessToken, realmName)
if err != nil {
return errors.Wrap(err, "unable to get realm")
}

realm.BrowserFlow = &flowAlias
if err := a.client.UpdateRealm(context.Background(), a.token.AccessToken, *realm); err != nil {
if err := a.client.UpdateRealm(ctx, a.token.AccessToken, *realm); err != nil {
return errors.Wrap(err, "unable to update realm")
}

Expand Down
7 changes: 4 additions & 3 deletions pkg/client/keycloak/adapter/gocloak_adapter_auth_flow_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package adapter

import (
"context"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -243,15 +244,15 @@ func (e *ExecFlowTestSuite) TestSetRealmBrowserFlow() {
e.goCloakMockClient.On("GetRealm", mock.Anything, "token", "realm1").Return(&realm, nil)
e.goCloakMockClient.On("UpdateRealm", mock.Anything, "token", realm).Return(nil)

err := e.adapter.SetRealmBrowserFlow("realm1", "flow1")
err := e.adapter.SetRealmBrowserFlow(context.Background(), "realm1", "flow1")
assert.NoError(e.T(), err)
}

func (e *ExecFlowTestSuite) TestSetRealmBrowserFlow_FailureGetRealm() {
mockErr := errors.New("mock err")
e.goCloakMockClient.On("GetRealm", mock.Anything, "token", "realm1").Return(nil, mockErr)

err := e.adapter.SetRealmBrowserFlow("realm1", "flow1")
err := e.adapter.SetRealmBrowserFlow(context.Background(), "realm1", "flow1")
assert.Error(e.T(), err)
assert.ErrorIs(e.T(), errors.Cause(err), mockErr)
}
Expand All @@ -266,7 +267,7 @@ func (e *ExecFlowTestSuite) TestSetRealmBrowserFlow_FailureUpdateRealm() {
e.goCloakMockClient.On("GetRealm", mock.Anything, "token", "realm1").Return(&realm, nil)
e.goCloakMockClient.On("UpdateRealm", mock.Anything, "token", realm).Return(mockErr)

err := e.adapter.SetRealmBrowserFlow("realm1", "flow1")
err := e.adapter.SetRealmBrowserFlow(context.Background(), "realm1", "flow1")
assert.Error(e.T(), err)
assert.ErrorIs(e.T(), errors.Cause(err), mockErr)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/client/keycloak/keycloak_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type KIdentityProvider interface {
type KAuthFlow interface {
SyncAuthFlow(realmName string, flow *adapter.KeycloakAuthFlow) error
DeleteAuthFlow(realmName string, flow *adapter.KeycloakAuthFlow) error
SetRealmBrowserFlow(realmName string, flowAlias string) error
SetRealmBrowserFlow(ctx context.Context, realmName string, flowAlias string) error
}

type KCloakGroups interface {
Expand Down
Loading

0 comments on commit ae76bc9

Please sign in to comment.