From f949a523be740acd692ac7bd6484902035d40fed Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 11 Jan 2021 06:32:28 +0100 Subject: [PATCH] Working client and Realm controller --- controllers/keycloak/keycloak.go | 26 ++--- controllers/keycloakclient.go | 62 +++++++++++ controllers/keycloakclient_controller.go | 76 ++++++++++++- controllers/keycloakrealm.go | 131 +++++++++++++++++++++++ controllers/keycloakrealm_controller.go | 67 ++++++++++-- go.sum | 4 + 6 files changed, 339 insertions(+), 27 deletions(-) create mode 100644 controllers/keycloakclient.go create mode 100644 controllers/keycloakrealm.go diff --git a/controllers/keycloak/keycloak.go b/controllers/keycloak/keycloak.go index 9fa241f..db10516 100644 --- a/controllers/keycloak/keycloak.go +++ b/controllers/keycloak/keycloak.go @@ -20,33 +20,29 @@ type Keycloak struct { user, pass, realm string } -func (kc *Keycloak) CreateRealmIfNotExists(ctx context.Context, realm gocloak.RealmRepresentation) error { +func (kc *Keycloak) CreateRealm(ctx context.Context, realm gocloak.RealmRepresentation) error { _, err := kc.client.CreateRealm(ctx, kc.getToken(), realm) - if isConflict(err) { - log.Printf("Realm '%s' already exists, not updated", *realm.Realm) - return nil - } return err } -func (kc *Keycloak) CreateProviderIfNotExists(ctx context.Context, realm string, provider gocloak.IdentityProviderRepresentation) error { +func (kc *Keycloak) UpdateRealm(ctx context.Context, realm gocloak.RealmRepresentation) error { + return kc.client.UpdateRealm(ctx, kc.getToken(), realm) +} + +func (kc *Keycloak) CreateProvider(ctx context.Context, realm string, provider gocloak.IdentityProviderRepresentation) error { _, err := kc.client.CreateIdentityProvider(ctx, kc.getToken(), realm, provider) - if isConflict(err) { - log.Printf("Provider '%s/%s' already exists, not updated", realm, *provider.Alias) - return nil - } return err } -func (kc *Keycloak) CreateClientIfNotExists(ctx context.Context, realm string, c gocloak.Client) error { +func (kc *Keycloak) CreateClient(ctx context.Context, realm string, c gocloak.Client) error { _, err := kc.client.CreateClient(ctx, kc.getToken(), realm, c) - if isConflict(err) { - log.Printf("Client '%s/%s' already exists, not updated", realm, *c.ClientID) - return nil - } return err } +func (kc *Keycloak) UpdateClient(ctx context.Context, realm string, c gocloak.Client) error { + return kc.client.UpdateClient(ctx, kc.getToken(), realm, c) +} + func New(url, user, pass, realm string) (*Keycloak, error) { kc := &Keycloak{} kc.client = gocloak.NewClient(url) diff --git a/controllers/keycloakclient.go b/controllers/keycloakclient.go new file mode 100644 index 0000000..9690bfc --- /dev/null +++ b/controllers/keycloakclient.go @@ -0,0 +1,62 @@ +package controllers + +import ( + keycloakv1alpha1 "git.zom.bi/images/keycloak-operator/api/v1alpha1" + "github.com/Nerzal/gocloak/v7" +) + +// ConvertToClient takes a CRD representation and converts it into a datatype +// that can be understood by GoCloak. +func ConvertToClient(clientCrd keycloakv1alpha1.KeycloakClient) (gocloak.Client, error) { + var client gocloak.Client + + clientSpec := clientCrd.Spec + + // Mandatory Properties + client.ClientID = &clientSpec.ClientID + + // Optional Properties + client.Enabled = clientSpec.Enabled + client.Name = clientSpec.Name + client.Description = clientSpec.Description + client.Protocol = clientSpec.Protocol + client.ClientAuthenticatorType = clientSpec.ClientAuthenticatorType + client.DirectAccessGrantsEnabled = clientSpec.DirectAccessGrantsEnabled + client.PublicClient = clientSpec.PublicClient + client.ImplicitFlowEnabled = clientSpec.ImplicitFlowEnabled + client.StandardFlowEnabled = clientSpec.StandardFlowEnabled + client.ServiceAccountsEnabled = clientSpec.ServiceAccountsEnabled + client.RegistrationAccessToken = clientSpec.RegistrationAccessToken + client.SurrogateAuthRequired = clientSpec.SurrogateAuthRequired + client.BearerOnly = clientSpec.BearerOnly + client.ConsentRequired = clientSpec.ConsentRequired + client.DefaultClientScopes = clientSpec.DefaultClientScopes + client.OptionalClientScopes = clientSpec.OptionalClientScopes + client.BaseURL = clientSpec.BaseURL + client.RootURL = clientSpec.RootURL + client.AdminURL = clientSpec.AdminURL + client.RedirectURIs = clientSpec.RedirectURIs + client.WebOrigins = clientSpec.WebOrigins + + if clientSpec.Secret != nil { + // TODO + // client.Secret = "" + } + + // client.Access = "" + // client.Attributes = "" + // client.AuthenticationFlowBindingOverrides = "" + // client.AuthorizationServicesEnabled = "" + // client.AuthorizationSettings = "" + // client.DefaultRoles = "" + // client.FrontChannelLogout = "" + // client.FullScopeAllowed = "" + // client.ID = "" + // client.NodeReRegistrationTimeout = "" + // client.NotBefore = "" + // client.Origin = "" + // client.ProtocolMappers = "" + // client.RegisteredNodes = "" + + return client, nil +} diff --git a/controllers/keycloakclient_controller.go b/controllers/keycloakclient_controller.go index ba8d356..8354372 100644 --- a/controllers/keycloakclient_controller.go +++ b/controllers/keycloakclient_controller.go @@ -19,7 +19,9 @@ package controllers import ( "context" + "github.com/Nerzal/gocloak/v7" "github.com/go-logr/logr" + apierrs "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -50,9 +52,79 @@ type KeycloakClientReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile func (r *KeycloakClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = r.Log.WithValues("keycloakclient", req.NamespacedName) + log := r.Log.WithValues("keycloakclient", req.NamespacedName) - // your logic here + log.Info("reconciling") + + // We get the information from the CRD + var client keycloakv1alpha1.KeycloakClient + if err := r.Get(ctx, req.NamespacedName, &client); err != nil { + if apierrs.IsNotFound(err) { + // Client is already deleted via finalizer. + return ctrl.Result{}, nil + } + } + + if !client.ObjectMeta.DeletionTimestamp.IsZero() { + // is in the process of being deleted + if containsString(client.ObjectMeta.Finalizers, FinalizerName) { + // our finalizer is present, so lets handle any external dependency + + // We do not want to delete anything, so we just disable the client. + disabled := gocloak.Client{ClientID: &client.Spec.ClientID, Enabled: gocloak.BoolP(false)} + err := r.Keycloak.UpdateClient(ctx, client.Spec.RealmName, disabled) + if err != nil { + // if fail to delete the external dependency here, return with error + // so that it can be retried + return ctrl.Result{}, err + } + + // remove our finalizer from the list and update it. + client.ObjectMeta.Finalizers = removeString(client.ObjectMeta.Finalizers, FinalizerName) + if err := r.Update(ctx, &client); err != nil { + return ctrl.Result{}, err + } + log.Info("Deleted the client") + } + + // done + return ctrl.Result{}, nil + } + + // Its not being deleted, so we seize the moment to take ownership. + if !containsString(client.ObjectMeta.Finalizers, FinalizerName) { + typeMeta := client.TypeMeta + client.ObjectMeta.Finalizers = append(client.ObjectMeta.Finalizers, FinalizerName) + if err := r.Update(ctx, &client); err != nil { + return ctrl.Result{}, err + } + // restore the TypeMeta object as it is removed during Update, but need to be accessed later + client.TypeMeta = typeMeta + } + + // Convert Client + keycloakClient, err := ConvertToClient(client) + if err != nil { + log.Error(err, "Could not convert client") + return ctrl.Result{}, err + } + + err = r.Keycloak.CreateClient(ctx, client.Spec.RealmName, keycloakClient) + if err != nil { + // try updating instead + err := r.Keycloak.UpdateClient(ctx, client.Spec.RealmName, keycloakClient) + if err != nil { + log.Error(err, "Could not create/update client") + return ctrl.Result{}, err + } + + log.Info("Updated the client") + return ctrl.Result{}, nil + } + + client.Status.Available = true + r.Status().Update(ctx, &client) + log.Info("Successfully created client") return ctrl.Result{}, nil } diff --git a/controllers/keycloakrealm.go b/controllers/keycloakrealm.go new file mode 100644 index 0000000..bae8ed3 --- /dev/null +++ b/controllers/keycloakrealm.go @@ -0,0 +1,131 @@ +package controllers + +import ( + keycloakv1alpha1 "git.zom.bi/images/keycloak-operator/api/v1alpha1" + "github.com/Nerzal/gocloak/v7" +) + +// ConvertToRealm takes a CRD representation and converts it into a datatype +// that can be understood by GoCloak. +func ConvertToRealm(realmCrd keycloakv1alpha1.KeycloakRealm) (gocloak.RealmRepresentation, error) { + var realm gocloak.RealmRepresentation + + realmSpec := realmCrd.Spec + + // Mandatory Properties + realm.Realm = &realmSpec.RealmName + + // Optional Properties + realm.Enabled = realmSpec.Enabled + realm.DisplayName = realmSpec.DisplayName + realm.DisplayNameHTML = realmSpec.DisplayNameHTML + realm.LoginTheme = realmSpec.LoginTheme + realm.LoginWithEmailAllowed = realmSpec.LoginWithEmailAllowed + realm.RegistrationAllowed = realmSpec.RegistrationAllowed + realm.EditUsernameAllowed = realmSpec.EditUsernameAllowed + realm.RegistrationEmailAsUsername = realmSpec.RegistrationEmailAsUsername + realm.ResetPasswordAllowed = realmSpec.ResetPasswordAllowed + realm.DuplicateEmailsAllowed = realmSpec.DuplicateEmailsAllowed + realm.VerifyEmail = realmSpec.VerifyEmail + realm.RememberMe = realmSpec.RememberMe + + if realmSpec.SMTP != nil { + smtp := map[string]string{} + if realmSpec.SMTP.Auth { + smtp["auth"] = "true" + } + + if realmSpec.SMTP.Secret != nil { + // TODO + } + + if realmSpec.SMTP.From != "" { + smtp["from"] = realmSpec.SMTP.From + } + + realm.SMTPServer = &smtp + } + + //realm.AccessCodeLifespan = "" + //realm.AccessCodeLifespanLogin = "" + //realm.AccessCodeLifespanUserAction = "" + //realm.AccessTokenLifespan = "" + //realm.AccessTokenLifespanForImplicitFlow = "" + //realm.AccountTheme = "" + //realm.ActionTokenGeneratedByAdminLifespan = "" + //realm.ActionTokenGeneratedByUserLifespan = "" + //realm.AdminEventsDetailsEnabled = "" + //realm.AdminEventsEnabled = "" + //realm.AdminTheme = "" + //realm.Attributes = "" + //realm.AuthenticationFlows = "" + //realm.AuthenticatorConfig = "" + //realm.BrowserFlow = "" + //realm.BrowserSecurityHeaders = "" + //realm.BruteForceProtected = "" + //realm.ClientAuthenticationFlow = "" + //realm.ClientScopeMappings = "" + //realm.ClientScopes = "" + //realm.Clients = "" + //realm.Components = "" + //realm.DefaultDefaultClientScopes = "" + //realm.DefaultGroups = "" + //realm.DefaultLocale = "" + //realm.DefaultOptionalClientScopes = "" + //realm.DefaultRoles = "" + //realm.DefaultSignatureAlgorithm = "" + //realm.DirectGrantFlow = "" + //realm.DockerAuthenticationFlow = "" + //realm.EmailTheme = "" + //realm.EnabledEventTypes = "" + //realm.EventsEnabled = "" + //realm.EventsExpiration = "" + //realm.EventsListeners = "" + //realm.FailureFactor = "" + //realm.FederatedUsers = "" + //realm.Groups = "" + //realm.ID = "" + //realm.IdentityProviderMappers = "" + //realm.IdentityProviders = "" + //realm.InternationalizationEnabled = "" + //realm.KeycloakVersion = "" + //realm.MaxDeltaTimeSeconds = "" + //realm.MaxFailureWaitSeconds = "" + //realm.MinimumQuickLoginWaitSeconds = "" + //realm.NotBefore = "" + //realm.OfflineSessionIdleTimeout = "" + //realm.OfflineSessionMaxLifespan = "" + //realm.OfflineSessionMaxLifespanEnabled = "" + //realm.OtpPolicyAlgorithm = "" + //realm.OtpPolicyDigits = "" + //realm.OtpPolicyInitialCounter = "" + //realm.OtpPolicyLookAheadWindow = "" + //realm.OtpPolicyPeriod = "" + //realm.OtpPolicyType = "" + //realm.OtpSupportedApplications = "" + //realm.PasswordPolicy = "" + //realm.PermanentLockout = "" + //realm.ProtocolMappers = "" + //realm.QuickLoginCheckMilliSeconds = "" + //realm.RefreshTokenMaxReuse = "" + //realm.RegistrationFlow = "" + //realm.RequiredActions = "" + //realm.ResetCredentialsFlow = "" + //realm.RevokeRefreshToken = "" + //realm.Roles = "" + //realm.ScopeMappings = "" + //realm.SMTPServer = "" + //realm.SslRequired = "" + //realm.SsoSessionIdleTimeout = "" + //realm.SsoSessionIdleTimeoutRememberMe = "" + //realm.SsoSessionMaxLifespan = "" + //realm.SsoSessionMaxLifespanRememberMe = "" + //realm.SupportedLocales = "" + //realm.UserFederationMappers = "" + //realm.UserFederationProviders = "" + //realm.UserManagedAccessAllowed = "" + //realm.Users = "" + //realm.WaitIncrementSeconds = "" + + return realm, nil +} diff --git a/controllers/keycloakrealm_controller.go b/controllers/keycloakrealm_controller.go index 3214eeb..9448ea0 100644 --- a/controllers/keycloakrealm_controller.go +++ b/controllers/keycloakrealm_controller.go @@ -19,6 +19,7 @@ package controllers import ( "context" + "github.com/Nerzal/gocloak/v7" "github.com/go-logr/logr" apierrs "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -55,29 +56,75 @@ func (r *KeycloakRealmReconciler) Reconcile(ctx context.Context, req ctrl.Reques log.Info("reconciling") + // We get the information from the CRD var realm keycloakv1alpha1.KeycloakRealm if err := r.Get(ctx, req.NamespacedName, &realm); err != nil { if apierrs.IsNotFound(err) { - log.Info("I would now unregister the realm") + // Realm is already deleted via finalizer. return ctrl.Result{}, nil } } - if realm.Status.Available { - // try to get existing realm - log.Info("Would try to fetch the realm by its id.", - "id", realm.Spec.RealmName) - // if found { - log.Info("will act like i found it, updating.") - // update() + if !realm.ObjectMeta.DeletionTimestamp.IsZero() { + // is in the process of being deleted + if containsString(realm.ObjectMeta.Finalizers, FinalizerName) { + // our finalizer is present, so lets handle any external dependency + + // We do not want to delete anything, so we just disable the realm. + disabled := gocloak.RealmRepresentation{Realm: &realm.Spec.RealmName, Enabled: gocloak.BoolP(false)} + err := r.Keycloak.UpdateRealm(ctx, disabled) + if err != nil { + // if fail to delete the external dependency here, return with error + // so that it can be retried + return ctrl.Result{}, err + } + + // remove our finalizer from the list and update it. + realm.ObjectMeta.Finalizers = removeString(realm.ObjectMeta.Finalizers, FinalizerName) + if err := r.Update(ctx, &realm); err != nil { + return ctrl.Result{}, err + } + log.Info("Deleted the realm") + } + + // done return ctrl.Result{}, nil - // } } - log.Info("Would now create the realm.") + // Its not being deleted, so we seize the moment to take ownership. + if !containsString(realm.ObjectMeta.Finalizers, FinalizerName) { + typeMeta := realm.TypeMeta + realm.ObjectMeta.Finalizers = append(realm.ObjectMeta.Finalizers, FinalizerName) + if err := r.Update(ctx, &realm); err != nil { + return ctrl.Result{}, err + } + // restore the TypeMeta object as it is removed during Update, but need to be accessed later + realm.TypeMeta = typeMeta + } + + // Convert Realm + keycloakRealm, err := ConvertToRealm(realm) + if err != nil { + log.Error(err, "Could not convert realm") + return ctrl.Result{}, err + } + + err = r.Keycloak.CreateRealm(ctx, keycloakRealm) + if err != nil { + // try updating instead + err := r.Keycloak.UpdateRealm(ctx, keycloakRealm) + if err != nil { + log.Error(err, "Could not create/update realm") + return ctrl.Result{}, err + } + + log.Info("Updated the realm") + return ctrl.Result{}, nil + } realm.Status.Available = true r.Status().Update(ctx, &realm) + log.Info("Successfully created realm") return ctrl.Result{}, nil } diff --git a/go.sum b/go.sum index d1974d6..2515922 100644 --- a/go.sum +++ b/go.sum @@ -45,8 +45,10 @@ github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdko github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -338,6 +340,7 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -589,6 +592,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=