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
9 changes: 9 additions & 0 deletions api/v1/keycloakclient_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ type KeycloakClientSpec struct {
// +nullable
// +optional
Authorization *Authorization `json:"authorization,omitempty"`

// AuthenticationFlowBindingOverrides client auth flow overrides
// +optional
AuthenticationFlowBindingOverrides *AuthenticationFlowBindingOverrides `json:"authenticationFlowBindingOverrides,omitempty"`
}

type ServiceAccount struct {
Expand Down Expand Up @@ -252,6 +256,11 @@ type Authorization struct {
Resources []Resource `json:"resources,omitempty"`
}

type AuthenticationFlowBindingOverrides struct {
Browser string `json:"browser,omitempty"`
DirectGrant string `json:"directGrant,omitempty"`
}

// KeycloakClientStatus defines the observed state of KeycloakClient.
type KeycloakClientStatus struct {
// +optional
Expand Down
16 changes: 16 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.

8 changes: 8 additions & 0 deletions config/crd/bases/v1.edp.epam.com_keycloakclients.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ spec:
description: Attributes is a map of client attributes.
nullable: true
type: object
authenticationFlowBindingOverrides:
description: AuthenticationFlowBindingOverrides client auth flow overrides
properties:
browser:
type: string
directGrant:
type: string
type: object
authorization:
description: Authorization is a client authorization configuration.
nullable: true
Expand Down
3 changes: 3 additions & 0 deletions config/samples/v1_v1_keycloakclient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ spec:
webUrl: https://argocd.example.com
defaultClientScopes:
- argocd_groups
authenticationFlowBindingOverrides:
browser: "browser"
directGrant: "direct grant"

---

Expand Down
57 changes: 53 additions & 4 deletions controllers/keycloakclient/chain/put_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const (
passwordLength = 36
passwordDigits = 9
passwordSymbols = 0

browserAuthFlow = "browser"
directGrantAuthFlow = "direct_grant"
)

// secretRef is an interface for getting secret from ref.
Expand Down Expand Up @@ -57,7 +60,19 @@ func (el *PutClient) putKeycloakClient(ctx context.Context, keycloakClient *keyc
log := ctrl.LoggerFrom(ctx)
log.Info("Start creation of Keycloak client")

clientDto, err := el.convertCrToDto(ctx, keycloakClient, realmName)
var (
authFlowOverrides map[string]string
err error
)

if keycloakClient.Spec.AuthenticationFlowBindingOverrides != nil {
authFlowOverrides, err = el.getAuthFlows(keycloakClient, realmName)
if err != nil {
return "", fmt.Errorf("unable to get auth flows: %w", err)
}
}

clientDto, err := el.convertCrToDto(ctx, keycloakClient, realmName, authFlowOverrides)
if err != nil {
return "", fmt.Errorf("error during convertCrToDto: %w", err)
}
Expand Down Expand Up @@ -93,9 +108,9 @@ func (el *PutClient) putKeycloakClient(ctx context.Context, keycloakClient *keyc
return id, nil
}

func (el *PutClient) convertCrToDto(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string) (*dto.Client, error) {
func (el *PutClient) convertCrToDto(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, realmName string, authflowOverrides map[string]string) (*dto.Client, error) {
if keycloakClient.Spec.Public {
res := dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName)
res := dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName, authflowOverrides)
return res, nil
}

Expand All @@ -104,7 +119,7 @@ func (el *PutClient) convertCrToDto(ctx context.Context, keycloakClient *keycloa
return nil, fmt.Errorf("unable to get secret, err: %w", err)
}

return dto.ConvertSpecToClient(&keycloakClient.Spec, secret, realmName), nil
return dto.ConvertSpecToClient(&keycloakClient.Spec, secret, realmName, authflowOverrides), nil
}

func (el *PutClient) getSecret(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient) (string, error) {
Expand Down Expand Up @@ -181,3 +196,37 @@ func (el *PutClient) setSecretRef(ctx context.Context, keycloakClient *keycloakA

return nil
}

func (el *PutClient) getAuthFlows(keycloakClient *keycloakApi.KeycloakClient, realmName string) (map[string]string, error) {
clientAuthFlows := keycloakClient.Spec.AuthenticationFlowBindingOverrides

flows, err := el.keycloakApiClient.GetRealmAuthFlows(realmName)
if err != nil {
return nil, fmt.Errorf("unable to get realm: %w", err)
}

realmAuthFlows := make(map[string]string)
for i := range flows {
realmAuthFlows[flows[i].Alias] = flows[i].ID
}

authFlowOverrides := make(map[string]string)

if clientAuthFlows.Browser != "" {
if _, ok := realmAuthFlows[clientAuthFlows.Browser]; !ok {
return nil, fmt.Errorf("auth flow %s not found in realm %s", clientAuthFlows.Browser, realmName)
}

authFlowOverrides[browserAuthFlow] = realmAuthFlows[clientAuthFlows.Browser]
}

if clientAuthFlows.DirectGrant != "" {
if _, ok := realmAuthFlows[clientAuthFlows.DirectGrant]; !ok {
return nil, fmt.Errorf("auth flow %s not found in realm %s", clientAuthFlows.DirectGrant, realmName)
}

authFlowOverrides[directGrantAuthFlow] = realmAuthFlows[clientAuthFlows.DirectGrant]
}

return authFlowOverrides, nil
}
2 changes: 1 addition & 1 deletion controllers/keycloakclient/chain/put_client_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (el *PutClientRole) putKeycloakClientRole(ctx context.Context, keycloakClie
reqLog := ctrl.LoggerFrom(ctx)
reqLog.Info("Start put keycloak client role")

clientDto := dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName)
clientDto := dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName, nil)

for _, role := range clientDto.Roles {
exist, err := el.keycloakApiClient.ExistClientRole(clientDto, role)
Expand Down
65 changes: 65 additions & 0 deletions controllers/keycloakclient/chain/put_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,71 @@ func TestPutClient_Serve(t *testing.T) {
},
wantErr: require.NoError,
},
{
name: "create client with auth flows",
fields: fields{
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",
AuthenticationFlowBindingOverrides: &keycloakApi.AuthenticationFlowBindingOverrides{
Browser: "browser",
DirectGrant: "direct grant",
},
},
},
).
Build()
},
secretRef: func(t *testing.T) secretRef {
return mocks.NewRefClient(t)
},
},
args: args{
keycloakClient: client.ObjectKey{
Name: "test-client",
Namespace: "default",
},
adapterClient: func(t *testing.T) keycloak.Client {
m := keycloakmocks.NewMockClient(t)

m.On("GetClientID", "test-client-id", "realm").
Return("", adapter.NotFoundError("not found")).
Once()

m.On("CreateClient", testifymock.Anything, testifymock.Anything).
Return(nil)

m.On("GetClientID", "test-client-id", "realm").
Return("123", nil)
m.On("GetRealmAuthFlows", "realm").
Return([]adapter.KeycloakAuthFlow{
{
ID: "A39C9C6E-430A-4891-8592-FC195AF2581B",
Alias: "browser",
},
{
ID: "8BF514C6-922C-44A0-8F90-488D1B9DE437",
Alias: "direct grant",
},
}, nil)

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

for _, tt := range tests {
Expand Down
2 changes: 1 addition & 1 deletion controllers/keycloakclient/chain/put_protocol_mappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (el *PutProtocolMappers) putProtocolMappers(keycloakClient *keycloakApi.Key
}

if err := el.keycloakApiClient.SyncClientProtocolMapper(
dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName),
dto.ConvertSpecToClient(&keycloakClient.Spec, "", realmName, nil),
protocolMappers, keycloakClient.GetReconciliationStrategy() == keycloakApi.ReconciliationStrategyAddOnly); err != nil {
return errors.Wrap(err, "unable to sync protocol mapper")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
Name: "test name",
StandardFlowEnabled: true,
ClientAuthenticatorType: "client-secret",
AuthenticationFlowBindingOverrides: &keycloakApi.AuthenticationFlowBindingOverrides{
Browser: "browser",
DirectGrant: "direct grant",
},
},
}
Expect(k8sClient.Create(ctx, keycloakClient)).Should(Succeed())
Expand Down Expand Up @@ -101,6 +105,10 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
Name: KeycloakRealmCR,
Kind: keycloakApi.KeycloakRealmKind,
},
AuthenticationFlowBindingOverrides: &keycloakApi.AuthenticationFlowBindingOverrides{
Browser: "browser",
DirectGrant: "direct grant",
},
},
}
Expect(k8sClient.Create(ctx, keycloakClient)).Should(Succeed())
Expand Down Expand Up @@ -140,6 +148,10 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
Kind: keycloakApi.KeycloakRealmKind,
},
Secret: clientSecret.Name,
AuthenticationFlowBindingOverrides: &keycloakApi.AuthenticationFlowBindingOverrides{
Browser: "browser",
DirectGrant: "direct grant",
},
},
}
Expect(k8sClient.Create(ctx, keycloakClient)).Should(Succeed())
Expand Down Expand Up @@ -208,6 +220,10 @@ var _ = Describe("KeycloakClient controller", Ordered, func() {
ServiceAccount: &keycloakApi.ServiceAccount{
Enabled: true,
},
AuthenticationFlowBindingOverrides: &keycloakApi.AuthenticationFlowBindingOverrides{
Browser: "browser",
DirectGrant: "direct grant",
},
Authorization: &keycloakApi.Authorization{
Policies: []keycloakApi.Policy{
{
Expand Down
8 changes: 8 additions & 0 deletions deploy-templates/crds/v1.edp.epam.com_keycloakclients.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ spec:
description: Attributes is a map of client attributes.
nullable: true
type: object
authenticationFlowBindingOverrides:
description: AuthenticationFlowBindingOverrides client auth flow overrides
properties:
browser:
type: string
directGrant:
type: string
type: object
authorization:
description: Authorization is a client authorization configuration.
nullable: true
Expand Down
41 changes: 41 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1954,6 +1954,13 @@ If empty - WebUrl will be used.<br/>
<i>Default</i>: map[post.logout.redirect.uris:+]<br/>
</td>
<td>false</td>
</tr><tr>
<td><b><a href="#keycloakclientspecauthenticationflowbindingoverrides">authenticationFlowBindingOverrides</a></b></td>
<td>object</td>
<td>
AuthenticationFlowBindingOverrides client auth flow overrides<br/>
</td>
<td>false</td>
</tr><tr>
<td><b><a href="#keycloakclientspecauthorization">authorization</a></b></td>
<td>object</td>
Expand Down Expand Up @@ -2190,6 +2197,40 @@ If not specified, the value from `WebUrl` is used<br/>
</table>


### KeycloakClient.spec.authenticationFlowBindingOverrides
<sup><sup>[↩ Parent](#keycloakclientspec)</sup></sup>



AuthenticationFlowBindingOverrides client auth flow overrides

<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Required</th>
</tr>
</thead>
<tbody><tr>
<td><b>browser</b></td>
<td>string</td>
<td>
<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>directGrant</b></td>
<td>string</td>
<td>
<br/>
</td>
<td>false</td>
</tr></tbody>
</table>


### KeycloakClient.spec.authorization
<sup><sup>[↩ Parent](#keycloakclientspec)</sup></sup>

Expand Down
19 changes: 10 additions & 9 deletions pkg/client/keycloak/adapter/gocloak_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,15 +524,16 @@ func getGclCln(client *dto.Client) gocloak.Client {
RedirectURIs: &[]string{
client.WebUrl + "/*",
},
RegistrationAccessToken: &client.RegistrationAccessToken,
RootURL: &client.WebUrl,
AdminURL: &client.AdminUrl,
BaseURL: &client.HomeUrl,
Secret: &client.ClientSecret,
ServiceAccountsEnabled: &client.ServiceAccountEnabled,
StandardFlowEnabled: &client.StandardFlowEnabled,
SurrogateAuthRequired: &client.SurrogateAuthRequired,
WebOrigins: &client.WebOrigins,
RegistrationAccessToken: &client.RegistrationAccessToken,
RootURL: &client.WebUrl,
AdminURL: &client.AdminUrl,
BaseURL: &client.HomeUrl,
Secret: &client.ClientSecret,
ServiceAccountsEnabled: &client.ServiceAccountEnabled,
StandardFlowEnabled: &client.StandardFlowEnabled,
SurrogateAuthRequired: &client.SurrogateAuthRequired,
WebOrigins: &client.WebOrigins,
AuthenticationFlowBindingOverrides: &client.AuthenticationFlowBindingOverrides,
}

// Set the admin URL to the web URL for backwards compatibility.
Expand Down
Loading
Loading