Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for flows, subflows and executions #215

Merged
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
52 changes: 52 additions & 0 deletions example/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -643,3 +643,55 @@ resource "keycloak_openid_client_service_account_role" "read_token" {
service_account_user_id = "${keycloak_openid_client.test_client_auth.service_account_user_id}"
role = "read-token"
}

resource "keycloak_authentication_flow" "browser-copy-flow" {
alias = "browserCopyFlow"
realm_id = "${keycloak_realm.test.id}"
description = "browser based authentication"
}

resource "keycloak_authentication_execution" "browser-copy-cookie" {
realm_id = "${keycloak_realm.test.id}"
parent_flow_alias = "${keycloak_authentication_flow.browser-copy-flow.alias}"
authenticator = "auth-cookie"
requirement = "ALTERNATIVE"
depends_on = ["keycloak_authentication_execution.browser-copy-kerberos"]
}

resource "keycloak_authentication_execution" "browser-copy-kerberos" {
realm_id = "${keycloak_realm.test.id}"
parent_flow_alias = "${keycloak_authentication_flow.browser-copy-flow.alias}"
authenticator = "auth-spnego"
requirement = "DISABLED"
}

resource "keycloak_authentication_execution" "browser-copy-idp-redirect" {
realm_id = "${keycloak_realm.test.id}"
parent_flow_alias = "${keycloak_authentication_flow.browser-copy-flow.alias}"
authenticator = "identity-provider-redirector"
requirement = "ALTERNATIVE"
depends_on = ["keycloak_authentication_execution.browser-copy-cookie"]
}

resource "keycloak_authentication_subflow" "browser-copy-flow-forms" {
realm_id = "${keycloak_realm.test.id}"
parent_flow_alias = "${keycloak_authentication_flow.browser-copy-flow.alias}"
alias = "browser-copy-flow-forms"
requirement = "ALTERNATIVE"
depends_on = ["keycloak_authentication_execution.browser-copy-idp-redirect"]
}

resource "keycloak_authentication_execution" "browser-copy-auth-username-password-form" {
realm_id = "${keycloak_realm.test.id}"
parent_flow_alias = "${keycloak_authentication_subflow.browser-copy-flow-forms.alias}"
authenticator = "auth-username-password-form"
requirement = "REQUIRED"
}

resource "keycloak_authentication_execution" "browser-copy-otp" {
realm_id = "${keycloak_realm.test.id}"
parent_flow_alias = "${keycloak_authentication_subflow.browser-copy-flow-forms.alias}"
authenticator = "auth-otp-form"
requirement = "REQUIRED"
depends_on = ["keycloak_authentication_execution.browser-copy-auth-username-password-form"]
}
144 changes: 144 additions & 0 deletions keycloak/authentication_execution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package keycloak

import (
"fmt"
)

// this is only used when creating an execution on a flow.
// other fields can be provided to the API but they are ignored
// POST /realms/${realmId}/authentication/flows/${flowAlias}/executions/execution
type authenticationExecutionCreate struct {
Provider string `json:"provider"` //authenticator of the execution
}

type authenticationExecutionRequirementUpdate struct {
RealmId string `json:"-"`
ParentFlowAlias string `json:"-"`
Id string `json:"id"`
Requirement string `json:"requirement"`
}

// this type is returned by GET /realms/${realmId}/authentication/flows/${flowAlias}/executions
type AuthenticationExecution struct {
Id string `json:"id"`
RealmId string `json:"-"`
ParentFlowAlias string `json:"-"`
Authenticator string `json:"authenticator"` //can be any authenticator from GET realms/{realm}/authentication/authenticator-providers OR GET realms/{realm}/authentication/client-authenticator-providers OR GET realms/{realm}/authentication/form-action-providers
AuthenticationConfig string `json:"authenticationConfig"`
AuthenticationFlow bool `json:"authenticationFlow"`
FlowId string `json:"flowId"`
ParentFlowId string `json:"parentFlow"`
Priority int `json:"priority"`
Requirement string `json:"requirement"`
}

// another model is used for GET /realms/${realmId}/authentication/executions/${executionId}, but I am going to try to avoid using this API
type AuthenticationExecutionInfo struct {
Id string `json:"id"`
RealmId string `json:"-"`
ParentFlowAlias string `json:"-"`
Alias string `json:"alias"`
AuthenticationConfig string `json:"authenticationConfig"`
AuthenticationFlow bool `json:"authenticationFlow"`
Configurable bool `json:"configurable"`
FlowId string `json:"flowId"`
Index int `json:"index"`
Level int `json:"level"`
ProviderId string `json:"providerId"`
Requirement string `json:"requirement"`
}

type AuthenticationExecutionList []*AuthenticationExecutionInfo

func (list AuthenticationExecutionList) Len() int {
return len(list)
}

func (list AuthenticationExecutionList) Less(i, j int) bool {
return list[i].Index < list[j].Index
}

func (list AuthenticationExecutionList) Swap(i, j int) {
list[i], list[j] = list[j], list[i]
}

func (keycloakClient *KeycloakClient) ListAuthenticationExecutions(realmId, parentFlowAlias string) (AuthenticationExecutionList, error) {
var authenticationExecutions []*AuthenticationExecutionInfo

err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions", realmId, parentFlowAlias), &authenticationExecutions, nil)
if err != nil {
return nil, err
}

return authenticationExecutions, err
}

func (keycloakClient *KeycloakClient) NewAuthenticationExecution(execution *AuthenticationExecution) error {
_, location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions/execution", execution.RealmId, execution.ParentFlowAlias), &authenticationExecutionCreate{Provider: execution.Authenticator})
if err != nil {
return err
}

execution.Id = getIdFromLocationHeader(location)

err = keycloakClient.UpdateAuthenticationExecution(execution)
if err != nil {
return err
}

return nil
}

func (keycloakClient *KeycloakClient) GetAuthenticationExecution(realmId, parentFlowAlias, id string) (*AuthenticationExecution, error) {
var authenticationExecution AuthenticationExecution

err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/executions/%s", realmId, id), &authenticationExecution, nil)
if err != nil {
return nil, err
}

authenticationExecution.RealmId = realmId
authenticationExecution.ParentFlowAlias = parentFlowAlias

return &authenticationExecution, nil
}

func (keycloakClient *KeycloakClient) UpdateAuthenticationExecution(execution *AuthenticationExecution) error {
authenticationExecutionUpdateRequirement := &authenticationExecutionRequirementUpdate{
RealmId: execution.RealmId,
ParentFlowAlias: execution.ParentFlowAlias,
Id: execution.Id,
Requirement: execution.Requirement,
}
return keycloakClient.UpdateAuthenticationExecutionRequirement(authenticationExecutionUpdateRequirement)
}

func (keycloakClient *KeycloakClient) UpdateAuthenticationExecutionRequirement(executionRequirementUpdate *authenticationExecutionRequirementUpdate) error {
return keycloakClient.put(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions", executionRequirementUpdate.RealmId, executionRequirementUpdate.ParentFlowAlias), executionRequirementUpdate)
}

func (keycloakClient *KeycloakClient) DeleteAuthenticationExecution(realmId, id string) error {
err := keycloakClient.delete(fmt.Sprintf("/realms/%s/authentication/executions/%s", realmId, id), nil)
if err != nil {
// For whatever reason, this fails sometimes with a 500 during acceptance tests. try again
return keycloakClient.delete(fmt.Sprintf("/realms/%s/authentication/executions/%s", realmId, id), nil)
}

return nil
}

func (keycloakClient *KeycloakClient) RaiseAuthenticationExecutionPriority(realmId, id string) error {
_, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/executions/%s/raise-priority", realmId, id), nil)
if err != nil {
return err
}
return nil
}

func (keycloakClient *KeycloakClient) LowerAuthenticationExecutionPriority(realmId, id string) error {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these two functions used? if not, how should users raise and lower the priority for executions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are not used, but they do work.
I left in here in the case some one worked out a better solution, and could use these methods....

Should I remove them?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I was just curious how the priority was determined, but then I realized in your example that you used depends_on in order to ensure subflows with higher priorities were created before subflows with lower priorities.

This approach makes sense, although it would be impossible to update this aftwards. But if that's good enough for you then I'm okay with this. I'm hoping that this is good enough for most users until Keycloak X is released, in which case I'll probably have to rewrite a lot of this provider anyways.

_, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/executions/%s/lower-priority", realmId, id), nil)
if err != nil {
return err
}
return nil
}
55 changes: 55 additions & 0 deletions keycloak/authentication_flow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package keycloak

import (
"fmt"
)

type AuthenticationFlow struct {
Id string `json:"id,omitempty"`
RealmId string `json:"-"`
Alias string `json:"alias"`
Description string `json:"description"`
ProviderId string `json:"providerId"` // "basic-flow" or "client-flow"
TopLevel bool `json:"topLevel"`
BuiltIn bool `json:"builtIn"`
}

func (keycloakClient *KeycloakClient) NewAuthenticationFlow(authenticationFlow *AuthenticationFlow) error {
authenticationFlow.TopLevel = true
authenticationFlow.BuiltIn = false

_, location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/flows", authenticationFlow.RealmId), authenticationFlow)
if err != nil {
return err
}
authenticationFlow.Id = getIdFromLocationHeader(location)

return nil
}

func (keycloakClient *KeycloakClient) GetAuthenticationFlow(realmId, id string) (*AuthenticationFlow, error) {
var authenticationFlow AuthenticationFlow
err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/flows/%s", realmId, id), &authenticationFlow, nil)
if err != nil {
return nil, err
}

authenticationFlow.RealmId = realmId
return &authenticationFlow, nil
}

func (keycloakClient *KeycloakClient) UpdateAuthenticationFlow(authenticationFlow *AuthenticationFlow) error {
authenticationFlow.TopLevel = true
authenticationFlow.BuiltIn = false

return keycloakClient.put(fmt.Sprintf("/realms/%s/authentication/flows/%s", authenticationFlow.RealmId, authenticationFlow.Id), authenticationFlow)
}

func (keycloakClient *KeycloakClient) DeleteAuthenticationFlow(realmId, id string) error {
err := keycloakClient.delete(fmt.Sprintf("/realms/%s/authentication/flows/%s", realmId, id), nil)
if err != nil {
// For whatever reason, this fails sometimes with a 500 during acceptance tests. try again
return keycloakClient.delete(fmt.Sprintf("/realms/%s/authentication/flows/%s", realmId, id), nil)
}
return nil
}
Loading