diff --git a/api/v1alpha1/tunnelbinding_types.go b/api/v1alpha1/tunnelbinding_types.go index cea33c5..622fbe9 100644 --- a/api/v1alpha1/tunnelbinding_types.go +++ b/api/v1alpha1/tunnelbinding_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + "github.com/cloudflare/cloudflare-go" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -118,6 +119,141 @@ type TunnelBindingStatus struct { Services []ServiceInfo `json:"services"` } +type AccessConfig struct { + // Enable handling of access configuration + //+kubebuilder:validation:Optional + //+kubebuilder:default:=false + Enabled bool `json:"enabled"` + // Application type self_hosted,saas + //+kubebuilder:validation:Optional + //+kubebuilder:validation:Enum:="";"self_hosted";"saas" + //+kubebuilder:default:="self_hosted" + Type string `json:"type"` + // List of access policies + //+kubebuilder:validation:Optional + AccessPolicies []AccessPolicy `json:"accessPolicies"` + // Application settings + //+kubebuilder:validation:Optional + Settings AccessConfigSettings `json:"settings"` +} + +type AccessConfigSettings struct { + // Authentication settins + //+kubebuilder:validation:Optional + Authentication AccessConfigAuthentication `json:"authentication"` + // Appearance settins + //+kubebuilder:validation:Optional + Appearance AccessConfigAppearance `json:"appearance"` + // Cookie settings + //+kubebuilder:validation:Optional + Cookies AccessConfigCookies `json:"cookies"` + // Additional settings + //+kubebuilder:validation:Optional + Additional AccessConfigAdditional `json:"additional"` +} + +type AccessConfigAuthentication struct { + // The list of identiy providers which application is allowed to use. If empty all idps are allowed + //+kubebuilder:validation:Optional + AllowedIdps []string `json:"allowedIdps"` + // Skip identity provider selection if only one is configured + //+kubebuilder:validation:Optional + //+kubebuilder:default:=false + InstantAuth bool `json:"instantAuth"` + // The amount of time that tokens issued for this application will be valid. Must be in the format 300ms or 2h45m. Valid time units are: ns, us (or µs), ms, s, m, h. + //+kubebuilder:validation:Optional + //+kubebuilder:default:="24h" + SessionDuration string `json:"sessionDuration"` + // The custom URL a user is redirected to when they are denied access to the application. + //+kubebuilder:validation:Optional + CustomDenyUrl string `json:"customDenyUrl"` + // The custom error message shown to a user when they are denied access to the application. + //+kubebuilder:validation:Optional + CustomDenyMessage string `json:"customDenyMessage"` +} + +type AccessConfigAppearance struct { + // Wether to show app in the launcher. Defaults to true. + //+kubebuilder:validation:Optional + //+kubebuilder:default:=true + AppLauncherVisibility bool `json:"appLauncherVisibility"` + // Custom logo url + //+kubebuilder:validation:Optional + CustomLogo string `json:"customLogo"` +} + +type AccessConfigCookies struct { + // Sets the SameSite cookie setting, which provides increased security against CSRF attacks. [none,strict,lax] + //+kubebuilder:validation:Optional + //+kubebuilder:validation:Enum:="";"none";"strict";"lax" + SameSiteAttribute string `json:"sameSiteAttribute"` + // Enables the HttpOnly cookie attribute, which increases security against XSS attacks. + //+kubebuilder:validation:Optional + //+kubebuilder:default:=true + EnableHttpOnly bool `json:"enableHttpOnly"` + // Enables the binding cookie, which increases security against compromised authorization tokens and CSRF attacks. + //+kubebuilder:validation:Optional + //+kubebuilder:default:=false + EnableBindingCookie bool `json:"enableBindingCookie"` +} + +type AccessConfigAdditional struct { + // Cloudflare will render an SSH terminal or VNC session for this application in a web browser. [ssh,vnc] + //+kubebuilder:validation:Optional + //+kubebuilder:validation:Enum:="";"vnc";"ssh" + BrowserRendering string `json:"browserRendering"` +} + +type AccessPolicy struct { + // The name of the Access policy. + //+kubebuilder:validation:Required + Name string `json:"name"` + // Decision if a policy is met + //+kubebuilder:validation:Required + //+kubebuilder:validation:Enum:="allow";"deny";"non_identity";"bypass" + Action string `json:"action"` + // Array of Access group names. Access groups are not managed by this operator + //+kubebuilder:validation:Optional + Include []string `json:"include"` + // Array of Access group names. Access groups are not managed by this operator + //+kubebuilder:validation:Optional + Exclude []string `json:"exclude"` + // Array of Access group names. Access groups are not managed by this operator + //+kubebuilder:validation:Optional + Require []string `json:"require"` + // The amount of time that tokens issued for the application will be valid. Must be in the format 300ms or 2h45m. Valid time units are: ns, us (or µs), ms, s, m, h. + //+kubebuilder:validation:Optional + //+kubebuilder:default:="24h" + // SessionDuration string `json:"sessionDuration"` + // Require users to enter a justification when they log in to the application. + //+kubebuilder:validation:Optional + //+kubebuilder:default:=false + PurposeJustificationRequired bool `json:"purposeJustificationRequired"` + // A custom message that will appear on the purpose justification screen. + //+kubebuilder:validation:Optional + //+kubebuilder:default:="Please enter a justification for entering this protected domain." + PurposeJustificationPrompt string `json:"purposeJustificationPrompt"` +} + +func (c *AccessConfig) NewAccessApplication(hostname string) cloudflare.AccessApplication { + + return cloudflare.AccessApplication{ + AllowedIdps: c.Settings.Authentication.AllowedIdps, + CustomDenyMessage: c.Settings.Authentication.CustomDenyMessage, + LogoURL: c.Settings.Appearance.CustomLogo, + Domain: hostname, + Type: cloudflare.AccessApplicationType(c.Type), + SessionDuration: c.Settings.Authentication.SessionDuration, + SameSiteCookieAttribute: c.Settings.Cookies.SameSiteAttribute, + CustomDenyURL: c.Settings.Authentication.CustomDenyUrl, + Name: hostname, + AutoRedirectToIdentity: &c.Settings.Authentication.InstantAuth, + AppLauncherVisible: &c.Settings.Appearance.AppLauncherVisibility, + EnableBindingCookie: &c.Settings.Cookies.EnableBindingCookie, + HttpOnlyCookieAttribute: &c.Settings.Cookies.EnableHttpOnly, + } +} + //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:printcolumn:name="FQDNs",type=string,JSONPath=`.status.hostnames` @@ -129,7 +265,9 @@ type TunnelBinding struct { Subjects []TunnelBindingSubject `json:"subjects"` TunnelRef TunnelRef `json:"tunnelRef"` - Status TunnelBindingStatus `json:"status,omitempty"` + //+kubebuilder:validation:Optional + AccessConfig AccessConfig `json:"accessConfig"` + Status TunnelBindingStatus `json:"status,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8b7031f..cee7a13 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,143 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessConfig) DeepCopyInto(out *AccessConfig) { + *out = *in + if in.AccessPolicies != nil { + in, out := &in.AccessPolicies, &out.AccessPolicies + *out = make([]AccessPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Settings.DeepCopyInto(&out.Settings) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConfig. +func (in *AccessConfig) DeepCopy() *AccessConfig { + if in == nil { + return nil + } + out := new(AccessConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessConfigAdditional) DeepCopyInto(out *AccessConfigAdditional) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConfigAdditional. +func (in *AccessConfigAdditional) DeepCopy() *AccessConfigAdditional { + if in == nil { + return nil + } + out := new(AccessConfigAdditional) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessConfigAppearance) DeepCopyInto(out *AccessConfigAppearance) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConfigAppearance. +func (in *AccessConfigAppearance) DeepCopy() *AccessConfigAppearance { + if in == nil { + return nil + } + out := new(AccessConfigAppearance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessConfigAuthentication) DeepCopyInto(out *AccessConfigAuthentication) { + *out = *in + if in.AllowedIdps != nil { + in, out := &in.AllowedIdps, &out.AllowedIdps + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConfigAuthentication. +func (in *AccessConfigAuthentication) DeepCopy() *AccessConfigAuthentication { + if in == nil { + return nil + } + out := new(AccessConfigAuthentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessConfigCookies) DeepCopyInto(out *AccessConfigCookies) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConfigCookies. +func (in *AccessConfigCookies) DeepCopy() *AccessConfigCookies { + if in == nil { + return nil + } + out := new(AccessConfigCookies) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessConfigSettings) DeepCopyInto(out *AccessConfigSettings) { + *out = *in + in.Authentication.DeepCopyInto(&out.Authentication) + out.Appearance = in.Appearance + out.Cookies = in.Cookies + out.Additional = in.Additional +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConfigSettings. +func (in *AccessConfigSettings) DeepCopy() *AccessConfigSettings { + if in == nil { + return nil + } + out := new(AccessConfigSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessPolicy) DeepCopyInto(out *AccessPolicy) { + *out = *in + if in.Include != nil { + in, out := &in.Include, &out.Include + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Exclude != nil { + in, out := &in.Exclude, &out.Exclude + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Require != nil { + in, out := &in.Require, &out.Require + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessPolicy. +func (in *AccessPolicy) DeepCopy() *AccessPolicy { + if in == nil { + return nil + } + out := new(AccessPolicy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CloudflareDetails) DeepCopyInto(out *CloudflareDetails) { *out = *in @@ -183,6 +320,7 @@ func (in *TunnelBinding) DeepCopyInto(out *TunnelBinding) { copy(*out, *in) } out.TunnelRef = in.TunnelRef + in.AccessConfig.DeepCopyInto(&out.AccessConfig) in.Status.DeepCopyInto(&out.Status) } diff --git a/config/crd/bases/networking.cfargotunnel.com_clustertunnels.yaml b/config/crd/bases/networking.cfargotunnel.com_clustertunnels.yaml index 57e47f3..e7464c0 100644 --- a/config/crd/bases/networking.cfargotunnel.com_clustertunnels.yaml +++ b/config/crd/bases/networking.cfargotunnel.com_clustertunnels.yaml @@ -1,10 +1,9 @@ - --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.7.0 + controller-gen.kubebuilder.io/version: v0.8.0 creationTimestamp: null name: clustertunnels.networking.cfargotunnel.com spec: diff --git a/config/crd/bases/networking.cfargotunnel.com_tunnelbindings.yaml b/config/crd/bases/networking.cfargotunnel.com_tunnelbindings.yaml index 33aee05..9f9755d 100644 --- a/config/crd/bases/networking.cfargotunnel.com_tunnelbindings.yaml +++ b/config/crd/bases/networking.cfargotunnel.com_tunnelbindings.yaml @@ -1,10 +1,9 @@ - --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.7.0 + controller-gen.kubebuilder.io/version: v0.8.0 creationTimestamp: null name: tunnelbindings.networking.cfargotunnel.com spec: @@ -25,6 +24,156 @@ spec: openAPIV3Schema: description: TunnelBinding is the Schema for the tunnelbindings API properties: + accessConfig: + properties: + accessPolicies: + description: List of access policies + items: + properties: + action: + description: Decision if a policy is met + enum: + - allow + - deny + - non_identity + - bypass + type: string + exclude: + description: Array of Access group names. Access groups are + not managed by this operator + items: + type: string + type: array + include: + description: Array of Access group names. Access groups are + not managed by this operator + items: + type: string + type: array + name: + description: The name of the Access policy. + type: string + purposeJustificationPrompt: + default: Please enter a justification for entering this protected + domain. + description: A custom message that will appear on the purpose + justification screen. + type: string + purposeJustificationRequired: + default: false + description: Require users to enter a justification when they + log in to the application. + type: boolean + require: + description: Array of Access group names. Access groups are + not managed by this operator + items: + type: string + type: array + sessionDuration: + default: 24h + description: 'The amount of time that tokens issued for the + application will be valid. Must be in the format 300ms or + 2h45m. Valid time units are: ns, us (or µs), ms, s, m, h.' + type: string + required: + - action + - name + type: object + type: array + enabled: + default: false + description: Enable handling of access configuration + type: boolean + settings: + description: Application settings + properties: + additional: + description: Additional settings + properties: + browserRendering: + description: Cloudflare will render an SSH terminal or VNC + session for this application in a web browser. [ssh,vnc] + enum: + - "" + - vnc + - ssh + type: string + type: object + appearance: + description: Appearance settins + properties: + appLauncherVisibility: + default: true + description: Wether to show app in the launcher. Defaults + to true. + type: boolean + customLogo: + description: Custom logo url + type: string + type: object + authentication: + description: Authentication settins + properties: + allowedIdps: + description: The list of identiy providers which application + is allowed to use. If empty all idps are allowed + items: + type: string + type: array + customDenyMessage: + description: The custom error message shown to a user when + they are denied access to the application. + type: string + customDenyUrl: + description: The custom URL a user is redirected to when they + are denied access to the application. + type: string + instantAuth: + default: false + description: Skip identity provider selection if only one + is configured + type: boolean + sessionDuration: + default: 24h + description: 'The amount of time that tokens issued for this + application will be valid. Must be in the format 300ms or + 2h45m. Valid time units are: ns, us (or µs), ms, s, m, h.' + type: string + type: object + cookies: + description: Cookie settings + properties: + enableBindingCookie: + default: false + description: Enables the binding cookie, which increases security + against compromised authorization tokens and CSRF attacks. + type: boolean + enableHttpOnly: + default: true + description: Enables the HttpOnly cookie attribute, which + increases security against XSS attacks. + type: boolean + sameSiteAttribute: + description: Sets the SameSite cookie setting, which provides + increased security against CSRF attacks. [none,strict,lax] + enum: + - "" + - none + - strict + - lax + type: string + type: object + type: object + type: + default: self_hosted + description: Application type self_hosted,saas + enum: + - "" + - self_hosted + - saas + type: string + type: object apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest diff --git a/config/crd/bases/networking.cfargotunnel.com_tunnels.yaml b/config/crd/bases/networking.cfargotunnel.com_tunnels.yaml index d0d65c2..55c3135 100644 --- a/config/crd/bases/networking.cfargotunnel.com_tunnels.yaml +++ b/config/crd/bases/networking.cfargotunnel.com_tunnels.yaml @@ -1,10 +1,9 @@ - --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.7.0 + controller-gen.kubebuilder.io/version: v0.8.0 creationTimestamp: null name: tunnels.networking.cfargotunnel.com spec: diff --git a/controllers/cloudflare_api.go b/controllers/cloudflare_api.go index 6e7135d..75fe2b4 100644 --- a/controllers/cloudflare_api.go +++ b/controllers/cloudflare_api.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" + networkingv1alpha1 "github.com/adyanth/cloudflare-operator/api/v1alpha1" "github.com/cloudflare/cloudflare-go" "github.com/go-logr/logr" ) @@ -546,3 +547,267 @@ func (c *CloudflareAPI) InsertOrUpdateTXT(fqdn, txtId, dnsId string) error { return nil } } + +func (c *CloudflareAPI) getAccessApplicationIdByName(name string) (exists bool, id string, error error) { + ctx := context.Background() + accountId := c.ValidAccountId + + apps, _, err := c.CloudflareClient.AccessApplications(ctx, accountId, cloudflare.PaginationOptions{}) + if err != nil { + return false, "", err + } + + exists = false + for _, app := range apps { + if app.Name == name { + exists = true + id = app.ID + break + } + } + return exists, id, nil +} + +func (c *CloudflareAPI) getAccessPolicyIdByName(applicationId string, name string) (exists bool, id string, error error) { + ctx := context.Background() + accountId := c.ValidAccountId + + policies, _, err := c.CloudflareClient.AccessPolicies(ctx, accountId, applicationId, cloudflare.PaginationOptions{}) + if err != nil { + return false, "", err + } + + exists = false + for _, policy := range policies { + if policy.Name == name { + exists = true + id = policy.ID + break + } + } + return exists, id, nil +} + +func (c *CloudflareAPI) getAccessGroupIdsByNames(names []string) (ids []string, error error) { + ctx := context.Background() + accountId := c.ValidAccountId + + groups, _, err := c.CloudflareClient.AccessGroups(ctx, accountId, cloudflare.PaginationOptions{}) + if err != nil { + return ids, err + } + +outerLoop: + // Match Access group names to ids + for _, group := range groups { + for _, name := range names { + if group.Name == name { + ids = append(ids, group.ID) + continue outerLoop // Break inner loop and continue the outer loop + } + } + } + + return ids, nil +} + +func (c *CloudflareAPI) CreateAccessConfig(name string, config networkingv1alpha1.AccessConfig) error { + ctx := context.Background() + newApp := config.NewAccessApplication(name) + accountId := c.ValidAccountId + + exists, id, err := c.getAccessApplicationIdByName(name) + if err != nil { + c.Log.Error(err, "failed retrieving application for account", "accountId", accountId, "app", name) + } + + if exists { + newApp.ID = id + c.Log.Info("updating access application", "name", name, "id", id) + _, err := c.CloudflareClient.UpdateAccessApplication(ctx, accountId, newApp) + if err != nil { + c.Log.Error(err, "error updating access application", "name", name) + return err + } + } else { + c.Log.Info("creating access application", "name", name) + newApp.ID = "" + createdApp, err := c.CloudflareClient.CreateAccessApplication(ctx, accountId, newApp) + if err != nil { + c.Log.Error(err, "error creating access application", "name", name) + return err + } + + // Set id, so that it can be used in policy creation + id = createdApp.ID + } + + // Handle application policies + err = c.createAccessApplicationPolicies(accountId, id, config) + if err != nil { + c.Log.Error(err, "error creating access application policies", "name", name) + return err + } + + c.Log.Info("access application reconciled successfully", "name", name, "existing", exists) + return nil +} + +func (c *CloudflareAPI) DeleteAccessConfig(name string, config networkingv1alpha1.AccessConfig) error { + ctx := context.Background() + accountId := c.ValidAccountId + + exists, id, err := c.getAccessApplicationIdByName(name) + if err != nil { + c.Log.Error(err, "failed retrieving application for account", "accountId", accountId, "app", name) + } + + if exists { + c.Log.Info("deleting access application", "name", name, "id", id) + err := c.CloudflareClient.DeleteAccessApplication(ctx, accountId, id) + if err != nil { + c.Log.Error(err, "error deleting access application", "name", name) + return err + } + } else { + err := fmt.Errorf("application does not exist", "name", name, "id", id) + return err + } + + c.Log.Info("access application deleted successfully", "name", name, "existing", exists) + return nil +} + +func (c *CloudflareAPI) createAccessApplicationPolicies(accountId string, applicationId string, config networkingv1alpha1.AccessConfig) error { + ctx := context.Background() + + // Check if there are any policies we need to delete + existingPolicies, _, err := c.CloudflareClient.AccessPolicies(ctx, accountId, applicationId, cloudflare.PaginationOptions{}) + if err != nil { + c.Log.Error(err, "failed retrieving existing access policies for application", "applicationId", applicationId) + } + + if len(existingPolicies) > 0 { + for _, policy := range existingPolicies { + + // We need to delete all existing policies + if len(config.AccessPolicies) == 0 { + c.Log.Info("deleting access policy for application", "applicationId", applicationId, "policyId", policy.ID) + + err := c.CloudflareClient.DeleteAccessPolicy(ctx, accountId, applicationId, policy.ID) + if err != nil { + c.Log.Error(err, "error deleting access policy for application", "applicationId", applicationId, "policyId", policy.ID) + return err + } + } + + // Check if the policy is still required + required := false + for _, configPolicy := range config.AccessPolicies { + if configPolicy.Name == policy.Name { + required = true + break + } + } + + // Delete since policy is not required + if !required { + err := c.CloudflareClient.DeleteAccessPolicy(ctx, accountId, applicationId, policy.ID) + if err != nil { + c.Log.Error(err, "error deleting access policy for application", "applicationId", applicationId, "policyId", policy.ID) + return err + } + } + + } + } + + // If we have policies to apply + if len(config.AccessPolicies) > 0 { + + // Process policies + for precedence, policy := range config.AccessPolicies { + + // Process include rules + if len(policy.Include) > 0 { + ids, err := c.getAccessGroupIdsByNames(policy.Include) + if err != nil { + c.Log.Error(err, "failed retrieving access group ids from names", "type", "include", "groups", policy.Include) + } + policy.Include = ids + } + + // Process exclude rules + if len(policy.Exclude) > 0 { + ids, err := c.getAccessGroupIdsByNames(policy.Exclude) + if err != nil { + c.Log.Error(err, "failed retrieving access group ids from names", "type", "exclude", "groups", policy.Include) + } + policy.Exclude = ids + } + + // Process require rules + if len(policy.Require) > 0 { + ids, err := c.getAccessGroupIdsByNames(policy.Require) + if err != nil { + c.Log.Error(err, "failed retrieving access group ids from names", "type", "require", "groups", policy.Include) + } + policy.Require = ids + } + + // Check access policy exists + name := policy.Name + exists, id, err := c.getAccessPolicyIdByName(applicationId, name) + if err != nil { + c.Log.Error(err, "failed retrieving access policy by name", "policy", name) + } + + // Handle policy creation + cfPolicy := cloudflare.AccessPolicy{ + ID: applicationId, + Precedence: precedence + 1, + Decision: policy.Action, + Name: policy.Name, + PurposeJustificationRequired: &policy.PurposeJustificationRequired, + PurposeJustificationPrompt: &policy.PurposeJustificationPrompt, + Include: wrapIdsInGroup(policy.Include), + Exclude: wrapIdsInGroup(policy.Exclude), + Require: wrapIdsInGroup(policy.Require), + } + + if exists { + cfPolicy.ID = id + c.Log.Info("updating access policy for application", "applicationId", applicationId, "policyName", name, "precedence", precedence) + _, err := c.CloudflareClient.UpdateAccessPolicy(ctx, accountId, applicationId, cfPolicy) + if err != nil { + c.Log.Error(err, "error updating access policy for application", "applicationId", applicationId, "policyName", name, "precedence", precedence) + return err + } + } else { + c.Log.Info("creating access policy for application", "applicationId", applicationId, "policyName", name, "precedence", precedence) + cfPolicy.ID = "" + _, err := c.CloudflareClient.CreateAccessPolicy(ctx, accountId, applicationId, cfPolicy) + if err != nil { + c.Log.Error(err, "error creating access policy for application", "applicationId", applicationId, "policyName", name, "precedence", precedence) + return err + } + } + } + } + + c.Log.Info("access policies reconciled successfully", "applicationId", applicationId) + + return nil +} + +func wrapIdsInGroup(strings []string) []interface{} { + interfaces := make([]interface{}, len(strings)) + for i, s := range strings { + interfaces[i] = map[string]interface{}{ + "group": map[string]interface{}{ + "id": s, + }, + } + } + return interfaces +} diff --git a/controllers/tunnelbinding_controller.go b/controllers/tunnelbinding_controller.go index f462ff4..c0a5f84 100644 --- a/controllers/tunnelbinding_controller.go +++ b/controllers/tunnelbinding_controller.go @@ -56,6 +56,7 @@ type TunnelBindingReconciler struct { configmap *corev1.ConfigMap fallbackTarget string cfAPI *CloudflareAPI + domain string } // labelsForBinding returns the labels for selecting the Bindings served by a Tunnel. @@ -222,10 +223,23 @@ func (r *TunnelBindingReconciler) deletionLogic() error { errors := false var err error + + // Iterate over healthy services for _, info := range r.binding.Status.Services { + + // Delete DNS records if err = r.deleteDNSLogic(info.Hostname); err != nil { errors = true } + + // Remove AccessApp + if r.binding.AccessConfig.Enabled { + err := r.deleteAccessConfigLogic(info.Hostname, r.binding.AccessConfig) + if err != nil { + return err + } + } + } if errors { r.Recorder.Event(r.binding, corev1.EventTypeWarning, "FinalizerNotUnset", "Not removing Finalizer due to errors") @@ -284,8 +298,19 @@ func (r *TunnelBindingReconciler) creationLogic() error { errors := false var err error - // Create DNS entries + + // Iterate over healthy services for _, info := range r.binding.Status.Services { + + // Create AccessApp + if r.binding.AccessConfig.Enabled { + err := r.createAccessConfigLogic(info.Hostname, r.binding.AccessConfig) + if err != nil { + return err + } + } + + // Create DNS entries err = r.createDNSLogic(info.Hostname) if err != nil { errors = true @@ -295,6 +320,7 @@ func (r *TunnelBindingReconciler) creationLogic() error { r.Recorder.Event(r.binding, corev1.EventTypeWarning, "FailedDNSCreatePartial", "Some DNS entries failed to create") return err } + return nil } @@ -388,6 +414,26 @@ func (r *TunnelBindingReconciler) deleteDNSLogic(hostname string) error { return nil } +func (r *TunnelBindingReconciler) createAccessConfigLogic(name string, config networkingv1alpha1.AccessConfig) error { + err := r.cfAPI.CreateAccessConfig(name, config) + if err != nil { + r.Recorder.Event(r.binding, corev1.EventTypeWarning, "FailedAccessConfig", "Failed to apply Access Configuration") + return err + } + r.Recorder.Event(r.binding, corev1.EventTypeNormal, "CreateAccessConfig", "Access Configuration applied successfully") + return nil +} + +func (r *TunnelBindingReconciler) deleteAccessConfigLogic(name string, config networkingv1alpha1.AccessConfig) error { + err := r.cfAPI.DeleteAccessConfig(name, config) + if err != nil { + r.Recorder.Event(r.binding, corev1.EventTypeWarning, "FailedAccessConfig", "Failed to delete Access Configuration") + return err + } + r.Recorder.Event(r.binding, corev1.EventTypeNormal, "DeleteAccessConfig", "Access Configuration deleted successfully") + return nil +} + func (r *TunnelBindingReconciler) getRelevantTunnelBindings() ([]networkingv1alpha1.TunnelBinding, error) { // Fetch TunnelBindings from API listOpts := []client.ListOption{client.MatchingLabels(map[string]string{ diff --git a/go.mod b/go.mod index f7244ad..ce2f3ea 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/go-logr/logr v1.2.3 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.19.0 + go.uber.org/zap v1.21.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.25.0 k8s.io/apimachinery v0.25.0 @@ -15,7 +16,6 @@ require ( ) require ( - 9fans.net/go v0.0.0-20181112161441-237454027057 // indirect cloud.google.com/go v0.97.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.27 // indirect @@ -43,7 +43,7 @@ require ( github.com/google/go-cmp v0.5.8 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.1.0 // indirect - github.com/google/uuid v1.1.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.2 // indirect github.com/imdario/mergo v0.3.12 // indirect @@ -60,20 +60,16 @@ require ( github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect - github.com/rogpeppe/godef v1.1.2 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.21.0 // indirect golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect - golang.org/x/mod v0.8.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.6.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.0 // indirect diff --git a/go.sum b/go.sum index 5df1cdd..03f4611 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -9fans.net/go v0.0.0-20181112161441-237454027057 h1:OcHlKWkAMJEF1ndWLGxp5dnJQkYM/YImUOvsBoz6h5E= -9fans.net/go v0.0.0-20181112161441-237454027057/go.mod h1:diCsxrliIURU9xsYtjCp5AbpQKqdhKmf0ujWDUSkfoY= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -229,8 +227,9 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -337,8 +336,6 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rogpeppe/godef v1.1.2 h1:c5mCx0EcCORJOdVMREX7Lgh1raTxAHFmOfXdEB9u8Jw= -github.com/rogpeppe/godef v1.1.2/go.mod h1:WtY9A/ovuQ+UakAJ1/CEqwwulX/WJjb2kgkokCHi/GY= 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/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -424,8 +421,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -609,7 +604,6 @@ golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200226224502-204d844ad48d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= @@ -635,8 +629,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=