diff --git a/api/v1alpha1/keycloakclient_types.go b/api/v1alpha1/keycloakclient_types.go index e4b149d..517e74f 100644 --- a/api/v1alpha1/keycloakclient_types.go +++ b/api/v1alpha1/keycloakclient_types.go @@ -20,22 +20,98 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // KeycloakClientSpec defines the desired state of KeycloakClient type KeycloakClientSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file + // +kubebuilder:validation:Required + // Name of the Realm the client should be created in + RealmName string `json:"realmName"` - // Foo is an example field of KeycloakClient. Edit KeycloakClient_types.go to remove/update - Foo string `json:"foo,omitempty"` + // +kubebuilder:validation:Required + // ClientID is the alphanumeric identifier of the client in a realm. + ClientID string `json:"clientId"` + + // If the client is enabled and active + Enabled *bool `json:"enabled,omitempty"` + + // Displayed Name of the Client + Name *string `json:"name,omitempty"` + + // Human Readable description + Description *string `json:"description,omitempty"` + + // Protocol, either 'openid-connect' or 'saml' + Protocol *string `json:"protocol,omitempty"` + + // How should Clients authenticate to the server? either 'client-secret' or 'client-jwt'. + ClientAuthenticatorType *string `json:"clientAuthenticatorType,omitempty"` + + // Are direct access grants enabled for this client or not (OpenID connect). + DirectAccessGrantsEnabled *bool `json:"directAccessGrantsEnabled,omitempty"` + + // Is the access type for this client public or not. + PublicClient *bool `json:"publicClient,omitempty"` + + // Enable implicit flow for this client or not (OpenID connect). + ImplicitFlowEnabled *bool `json:"implicitFlowEnabled,omitempty"` + + // Enable standard flow for this client or not (OpenID connect). + StandardFlowEnabled *bool `json:"standardFlowEnabled,omitempty"` + + // Are service accounts enabled for this client or not (OpenID connect). + ServiceAccountsEnabled *bool `json:"serviceAccountsEnabled,omitempty"` + + // Used for authentication when registering new clients + RegistrationAccessToken *string `json:"registrationAccessToken,omitempty"` + + // Whether or not surrogate auth is required. + SurrogateAuthRequired *bool `json:"surrogateAuthRequired,omitempty"` + + // The access type of this client is bearer-only. + BearerOnly *bool `json:"bearerOnly,omitempty"` + + // If enabled, users have to consent to client access. + ConsentRequired *bool `json:"consentRequired,omitempty"` + + // Which client scopes chould be granted by default, even without + // specifying them. + DefaultClientScopes *[]string `json:"defaultClientScopes,omitempty"` + + // Which additional scopes can be specified by the client + OptionalClientScopes *[]string `json:"optionalClientScopes,omitempty"` + + // Default URL to use when the auth server needs to redirect or link back to the client + BaseURL *string `json:"baseUrl,omitempty"` + + // Root URL appended to relative URLs for this client + RootURL *string `json:"rootUrl,omitempty"` + + // URL to the admin interface of the client + AdminURL *string `json:"adminUrl,omitempty"` + + // URL to the admin interface of the client + RedirectURIs *[]string `json:"redirectUris,omitempty"` + + // List of allowed CORS origins + WebOrigins *[]string `json:"webOrigins,omitempty"` + + // +kubebuilder:validation:Optional + // A client Secret is not always required + Secret *KeycloakClientSecret `json:"secret,omitempty"` +} + +// KeycloakClientSecret contains the Secret storing the Client Secret +type KeycloakClientSecret struct { + // +kubebuilder:validation:Required + // Name of the Secret containing the client Secret. + Name string `json:"name"` + // +kubebuilder:default:=password + // Key of the attribute, that holds the value in the Secret. + Key string `json:"key,omitempty"` } // KeycloakClientStatus defines the observed state of KeycloakClient type KeycloakClientStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + Available bool `json:"available"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/keycloakrealm_types.go b/api/v1alpha1/keycloakrealm_types.go index caaa312..32e0a76 100644 --- a/api/v1alpha1/keycloakrealm_types.go +++ b/api/v1alpha1/keycloakrealm_types.go @@ -22,11 +22,14 @@ import ( // KeycloakRealmSpec defines the desired state of KeycloakRealm type KeycloakRealmSpec struct { + // +kubebuilder:validation:Required // RealmName is the name and public identifier of the Realm RealmName string `json:"realmName"` - // Secret containing SMTP configuration - SMTPSecretName string `json:"smtpSecretName,omitempty"` + // If the realm is enabled and active + Enabled *bool `json:"enabled,omitempty"` + + SMTP *KeycloakRealmSMTP `json:"smtp,omitempty"` // name shown to the user DisplayName *string `json:"displayName,omitempty"` @@ -54,10 +57,69 @@ type KeycloakRealmSpec struct { RememberMe *bool `json:"rememberMe,omitempty"` } +// KeycloakRealmSMTP contains information about the SMTP server used to send +// transactional mail (for registration and password reset). +type KeycloakRealmSMTP struct { + // auth: "true" + // from: noreply@bitmask.me + // fromDisplayName: Bitmask Accounts + // host: email-smtp.eu-west-1.amazonaws.com + // password: '**********' + // port: "587" + // ssl: "false" + // starttls: "true" + // user: XXXXXXXXXXXXXXXXXXXX + + // +kubebuilder:default:=true + // If authentication should be used + Auth bool `json:"auth,omitempty"` + + // From which address the emails will be sent, takes precedence + // over the attribute defined in the secret. + From string `json:"from,omitempty"` + + // From which NAME the email should originate. + FromDisplayName string `json:"fromDisplayName,omitempty"` + + Secret *KeycloakRealmSMTPSecret `json:"secret,omitempty"` +} + +// KeycloakRealmSMTPSecret contains Credentials for connecting to a SMTP +// Server. +type KeycloakRealmSMTPSecret struct { + + // +kubebuilder:validation:Required + // Secret containing SMTP configuration + Name string `json:"name"` + + // +kubebuilder:default:=host + // Key of the host attribute + HostKey string `json:"hostKey,omitempty"` + + // +kubebuilder:default:=port + // Key of the port attribute + PortKey string `json:"portKey,omitempty"` + + // +kubebuilder:default:=ssl + // Key of the ssl attribute + SSLKey string `json:"sslKey,omitempty"` + + // +kubebuilder:default:=starttls + // Key of the starttls attribute + StartTLSKey string `json:"startTLSKey,omitempty"` + + // +kubebuilder:default:=username + // Key of the username attribute + UsernameKey string `json:"usernameKey,omitempty"` + + // +kubebuilder:default:=from + // Key of the from attribute, contains the mail address that email will be sent from. + FromKey string `json:"fromKey,omitempty"` +} + // KeycloakRealmStatus defines the observed state of KeycloakRealm type KeycloakRealmStatus struct { - Available bool `json:"available"` - ID string `json:"id"` + Available bool `json:"available"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9401c90..cfc367c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -29,7 +29,7 @@ func (in *KeycloakClient) DeepCopyInto(out *KeycloakClient) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -83,9 +83,150 @@ func (in *KeycloakClientList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeycloakClientSecret) DeepCopyInto(out *KeycloakClientSecret) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeycloakClientSecret. +func (in *KeycloakClientSecret) DeepCopy() *KeycloakClientSecret { + if in == nil { + return nil + } + out := new(KeycloakClientSecret) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KeycloakClientSpec) DeepCopyInto(out *KeycloakClientSpec) { *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } + if in.Protocol != nil { + in, out := &in.Protocol, &out.Protocol + *out = new(string) + **out = **in + } + if in.ClientAuthenticatorType != nil { + in, out := &in.ClientAuthenticatorType, &out.ClientAuthenticatorType + *out = new(string) + **out = **in + } + if in.DirectAccessGrantsEnabled != nil { + in, out := &in.DirectAccessGrantsEnabled, &out.DirectAccessGrantsEnabled + *out = new(bool) + **out = **in + } + if in.PublicClient != nil { + in, out := &in.PublicClient, &out.PublicClient + *out = new(bool) + **out = **in + } + if in.ImplicitFlowEnabled != nil { + in, out := &in.ImplicitFlowEnabled, &out.ImplicitFlowEnabled + *out = new(bool) + **out = **in + } + if in.StandardFlowEnabled != nil { + in, out := &in.StandardFlowEnabled, &out.StandardFlowEnabled + *out = new(bool) + **out = **in + } + if in.ServiceAccountsEnabled != nil { + in, out := &in.ServiceAccountsEnabled, &out.ServiceAccountsEnabled + *out = new(bool) + **out = **in + } + if in.RegistrationAccessToken != nil { + in, out := &in.RegistrationAccessToken, &out.RegistrationAccessToken + *out = new(string) + **out = **in + } + if in.SurrogateAuthRequired != nil { + in, out := &in.SurrogateAuthRequired, &out.SurrogateAuthRequired + *out = new(bool) + **out = **in + } + if in.BearerOnly != nil { + in, out := &in.BearerOnly, &out.BearerOnly + *out = new(bool) + **out = **in + } + if in.ConsentRequired != nil { + in, out := &in.ConsentRequired, &out.ConsentRequired + *out = new(bool) + **out = **in + } + if in.DefaultClientScopes != nil { + in, out := &in.DefaultClientScopes, &out.DefaultClientScopes + *out = new([]string) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + if in.OptionalClientScopes != nil { + in, out := &in.OptionalClientScopes, &out.OptionalClientScopes + *out = new([]string) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + if in.BaseURL != nil { + in, out := &in.BaseURL, &out.BaseURL + *out = new(string) + **out = **in + } + if in.RootURL != nil { + in, out := &in.RootURL, &out.RootURL + *out = new(string) + **out = **in + } + if in.AdminURL != nil { + in, out := &in.AdminURL, &out.AdminURL + *out = new(string) + **out = **in + } + if in.RedirectURIs != nil { + in, out := &in.RedirectURIs, &out.RedirectURIs + *out = new([]string) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + if in.WebOrigins != nil { + in, out := &in.WebOrigins, &out.WebOrigins + *out = new([]string) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(KeycloakClientSecret) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeycloakClientSpec. @@ -172,9 +313,54 @@ func (in *KeycloakRealmList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeycloakRealmSMTP) DeepCopyInto(out *KeycloakRealmSMTP) { + *out = *in + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(KeycloakRealmSMTPSecret) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeycloakRealmSMTP. +func (in *KeycloakRealmSMTP) DeepCopy() *KeycloakRealmSMTP { + if in == nil { + return nil + } + out := new(KeycloakRealmSMTP) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeycloakRealmSMTPSecret) DeepCopyInto(out *KeycloakRealmSMTPSecret) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeycloakRealmSMTPSecret. +func (in *KeycloakRealmSMTPSecret) DeepCopy() *KeycloakRealmSMTPSecret { + if in == nil { + return nil + } + out := new(KeycloakRealmSMTPSecret) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KeycloakRealmSpec) DeepCopyInto(out *KeycloakRealmSpec) { *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.SMTP != nil { + in, out := &in.SMTP, &out.SMTP + *out = new(KeycloakRealmSMTP) + (*in).DeepCopyInto(*out) + } if in.DisplayName != nil { in, out := &in.DisplayName, &out.DisplayName *out = new(string) diff --git a/config/crd/bases/keycloak.bitmask.me_keycloakclients.yaml b/config/crd/bases/keycloak.bitmask.me_keycloakclients.yaml index 6d3ebc9..bbdb697 100644 --- a/config/crd/bases/keycloak.bitmask.me_keycloakclients.yaml +++ b/config/crd/bases/keycloak.bitmask.me_keycloakclients.yaml @@ -36,13 +36,114 @@ spec: spec: description: KeycloakClientSpec defines the desired state of KeycloakClient properties: - foo: - description: Foo is an example field of KeycloakClient. Edit KeycloakClient_types.go - to remove/update + adminUrl: + description: URL to the admin interface of the client type: string + baseUrl: + description: Default URL to use when the auth server needs to redirect + or link back to the client + type: string + bearerOnly: + description: The access type of this client is bearer-only. + type: boolean + clientAuthenticatorType: + description: How should Clients authenticate to the server? either + 'client-secret' or 'client-jwt'. + type: string + clientId: + description: ClientID is the alphanumeric identifier of the client + in a realm. + type: string + consentRequired: + description: If enabled, users have to consent to client access. + type: boolean + defaultClientScopes: + description: Which client scopes chould be granted by default, even + without specifying them. + items: + type: string + type: array + description: + description: Human Readable description + type: string + directAccessGrantsEnabled: + description: Are direct access grants enabled for this client or not + (OpenID connect). + type: boolean + enabled: + description: If the client is enabled and active + type: boolean + implicitFlowEnabled: + description: Enable implicit flow for this client or not (OpenID connect). + type: boolean + name: + description: Displayed Name of the Client + type: string + optionalClientScopes: + description: Which additional scopes can be specified by the client + items: + type: string + type: array + protocol: + description: Protocol, either 'openid-connect' or 'saml' + type: string + publicClient: + description: Is the access type for this client public or not. + type: boolean + realmName: + description: Name of the Realm the client should be created in + type: string + redirectUris: + description: URL to the admin interface of the client + items: + type: string + type: array + registrationAccessToken: + description: Used for authentication when registering new clients + type: string + rootUrl: + description: Root URL appended to relative URLs for this client + type: string + secret: + description: A client Secret is not always required + properties: + key: + default: password + description: Key of the attribute, that holds the value in the + Secret. + type: string + name: + description: Name of the Secret containing the client Secret. + type: string + required: + - name + type: object + serviceAccountsEnabled: + description: Are service accounts enabled for this client or not (OpenID + connect). + type: boolean + standardFlowEnabled: + description: Enable standard flow for this client or not (OpenID connect). + type: boolean + surrogateAuthRequired: + description: Whether or not surrogate auth is required. + type: boolean + webOrigins: + description: List of allowed CORS origins + items: + type: string + type: array + required: + - clientId + - realmName type: object status: description: KeycloakClientStatus defines the observed state of KeycloakClient + properties: + available: + type: boolean + required: + - available type: object type: object served: true diff --git a/config/crd/bases/keycloak.bitmask.me_keycloakrealms.yaml b/config/crd/bases/keycloak.bitmask.me_keycloakrealms.yaml index b9065ae..8749f1e 100644 --- a/config/crd/bases/keycloak.bitmask.me_keycloakrealms.yaml +++ b/config/crd/bases/keycloak.bitmask.me_keycloakrealms.yaml @@ -49,6 +49,9 @@ spec: description: if the user should be able to change their username after account creation type: boolean + enabled: + description: If the realm is enabled and active + type: boolean loginTheme: description: the name of the Theme used for the login pages type: string @@ -74,9 +77,58 @@ spec: resetPasswordAllowed: description: if the user is allowed to use the reset password flow type: boolean - smtpSecretName: - description: Secret containing SMTP configuration - type: string + smtp: + description: KeycloakRealmSMTP contains information about the SMTP + server used to send transactional mail (for registration and password + reset). + properties: + auth: + default: true + description: If authentication should be used + type: boolean + from: + description: From which address the emails will be sent, takes + precedence over the attribute defined in the secret. + type: string + fromDisplayName: + description: From which NAME the email should originate. + type: string + secret: + description: KeycloakRealmSMTPSecret contains Credentials for + connecting to a SMTP Server. + properties: + fromKey: + default: from + description: Key of the from attribute, contains the mail + address that email will be sent from. + type: string + hostKey: + default: host + description: Key of the host attribute + type: string + name: + description: Secret containing SMTP configuration + type: string + portKey: + default: port + description: Key of the port attribute + type: string + sslKey: + default: ssl + description: Key of the ssl attribute + type: string + startTLSKey: + default: starttls + description: Key of the starttls attribute + type: string + usernameKey: + default: username + description: Key of the username attribute + type: string + required: + - name + type: object + type: object verifyEmail: description: if emails should be verified before the user can log into their account @@ -89,11 +141,8 @@ spec: properties: available: type: boolean - id: - type: string required: - available - - id type: object type: object served: true diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 4119a99..0022036 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -6,6 +6,32 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - keycloak.bitmask.me + resources: + - keycloakclients + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - keycloak.bitmask.me + resources: + - keycloakclients/finalizers + verbs: + - update +- apiGroups: + - keycloak.bitmask.me + resources: + - keycloakclients/status + verbs: + - get + - patch + - update - apiGroups: - keycloak.bitmask.me resources: diff --git a/controllers/keycloakclient_controller.go b/controllers/keycloakclient_controller.go index 325a52f..ba8d356 100644 --- a/controllers/keycloakclient_controller.go +++ b/controllers/keycloakclient_controller.go @@ -25,13 +25,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" keycloakv1alpha1 "git.zom.bi/images/keycloak-operator/api/v1alpha1" + "git.zom.bi/images/keycloak-operator/controllers/keycloak" ) // KeycloakClientReconciler reconciles a KeycloakClient object type KeycloakClientReconciler struct { client.Client - Log logr.Logger - Scheme *runtime.Scheme + Keycloak *keycloak.Keycloak + Log logr.Logger + Scheme *runtime.Scheme } // +kubebuilder:rbac:groups=keycloak.bitmask.me,resources=keycloakclients,verbs=get;list;watch;create;update;patch;delete diff --git a/controllers/keycloakrealm_controller.go b/controllers/keycloakrealm_controller.go index 81f9faf..3214eeb 100644 --- a/controllers/keycloakrealm_controller.go +++ b/controllers/keycloakrealm_controller.go @@ -63,10 +63,10 @@ func (r *KeycloakRealmReconciler) Reconcile(ctx context.Context, req ctrl.Reques } } - if realm.Status.ID != "" { + if realm.Status.Available { // try to get existing realm log.Info("Would try to fetch the realm by its id.", - "id", realm.Status.ID) + "id", realm.Spec.RealmName) // if found { log.Info("will act like i found it, updating.") // update() @@ -76,7 +76,6 @@ func (r *KeycloakRealmReconciler) Reconcile(ctx context.Context, req ctrl.Reques log.Info("Would now create the realm.") - realm.Status.ID = "dummy" realm.Status.Available = true r.Status().Update(ctx, &realm) diff --git a/main.go b/main.go index 21560bd..2f8cd0e 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,15 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "KeycloakRealm") os.Exit(1) } + if err = (&controllers.KeycloakClientReconciler{ + Client: mgr.GetClient(), + Keycloak: kc, + Log: ctrl.Log.WithName("controllers").WithName("KeycloakClient"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "KeycloakClient") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil {