diff --git a/internal/installer/config_manager.go b/internal/installer/config_manager.go index 8d3eacaa..21081fb1 100644 --- a/internal/installer/config_manager.go +++ b/internal/installer/config_manager.go @@ -230,6 +230,39 @@ func (g *InstallConfig) ValidateInstallConfig() []string { } } + errors = append(errors, validateOpenFga(g.Config.Codesphere.OpenFga)...) + + return errors +} + +// validateOpenFga checks the codesphere.openFga block. A data center that does not deploy +// OpenFGA has nowhere to fall back to, so it must name the instance it uses; a data center that +// exposes one must say under which host. +func validateOpenFga(config *files.OpenFgaConfig) []string { + if config == nil { + return nil + } + + errors := []string{} + if !config.DeploysOpenFga() && config.APIURL == "" { + errors = append(errors, "OpenFGA apiUrl is required when codesphere.openFga.deploy is false") + } + + if config.APIURL != "" { + if _, err := url.ParseRequestURI(config.APIURL); err != nil { + errors = append(errors, "OpenFGA apiUrl must be a valid URL") + } + } + + if config.ExposesOpenFga() { + if config.Expose.Host == "" { + errors = append(errors, "OpenFGA expose host is required when codesphere.openFga.expose.enabled is true") + } + + if !config.DeploysOpenFga() { + errors = append(errors, "OpenFGA cannot be exposed by a data center that does not deploy it") + } + } return errors } diff --git a/internal/installer/config_manager_test.go b/internal/installer/config_manager_test.go index 3b6830b8..8aed5c64 100644 --- a/internal/installer/config_manager_test.go +++ b/internal/installer/config_manager_test.go @@ -342,6 +342,55 @@ var _ = Describe("ConfigManager", func() { }) }) + Context("openFga validation", func() { + It("should accept an absent openFga block", func() { + configManager.Config.Codesphere.OpenFga = nil + errors := configManager.ValidateInstallConfig() + Expect(errors).NotTo(ContainElement(ContainSubstring("OpenFGA"))) + }) + + It("should require an apiUrl when the data center does not deploy OpenFGA", func() { + deploy := false + configManager.Config.Codesphere.OpenFga = &files.OpenFgaConfig{Deploy: &deploy} + errors := configManager.ValidateInstallConfig() + Expect(errors).To(ContainElement(ContainSubstring("OpenFGA apiUrl is required"))) + }) + + It("should validate the apiUrl format", func() { + configManager.Config.Codesphere.OpenFga = &files.OpenFgaConfig{APIURL: "not-a-valid-url"} + errors := configManager.ValidateInstallConfig() + Expect(errors).To(ContainElement(ContainSubstring("OpenFGA apiUrl must be a valid URL"))) + }) + + It("should require a host when exposing OpenFGA", func() { + configManager.Config.Codesphere.OpenFga = &files.OpenFgaConfig{ + Expose: &files.OpenFgaExposeConfig{Enabled: true}, + } + errors := configManager.ValidateInstallConfig() + Expect(errors).To(ContainElement(ContainSubstring("OpenFGA expose host is required"))) + }) + + It("should reject exposing an OpenFGA the data center does not deploy", func() { + deploy := false + configManager.Config.Codesphere.OpenFga = &files.OpenFgaConfig{ + Deploy: &deploy, + APIURL: "https://openfga.1.cs.example.com", + Expose: &files.OpenFgaExposeConfig{Enabled: true, Host: "openfga.2.cs.example.com"}, + } + errors := configManager.ValidateInstallConfig() + Expect(errors).To(ContainElement(ContainSubstring("cannot be exposed by a data center that does not deploy it"))) + }) + + It("should accept a data center that deploys and exposes OpenFGA", func() { + configManager.Config.Codesphere.OpenFga = &files.OpenFgaConfig{ + APIURL: "https://openfga.1.cs.example.com", + Expose: &files.OpenFgaExposeConfig{Enabled: true, Host: "openfga.1.cs.example.com"}, + } + errors := configManager.ValidateInstallConfig() + Expect(errors).NotTo(ContainElement(ContainSubstring("OpenFGA"))) + }) + }) + Context("ceph validation", func() { It("should require at least one Ceph host", func() { configManager.Config.Ceph.Hosts = []files.CephHost{} diff --git a/internal/installer/files/config_yaml.go b/internal/installer/files/config_yaml.go index cce8757e..b91f8a56 100644 --- a/internal/installer/files/config_yaml.go +++ b/internal/installer/files/config_yaml.go @@ -361,6 +361,7 @@ type CodesphereConfig struct { ManagedServices []ManagedServiceConfig `yaml:"managedServices,omitempty"` OpenBao *OpenBaoConfig `yaml:"openBao,omitempty"` OpenfgaBackups *OpenfgaBackupsConfig `yaml:"openfgaBackups,omitempty"` + OpenFga *OpenFgaConfig `yaml:"openFga,omitempty"` Migration *MigrationConfig `yaml:"migration,omitempty"` TelemetryExport *TelemetryExport `yaml:"telemetryExport,omitempty"` Override ChartOverride `yaml:"override,omitempty"` @@ -398,6 +399,39 @@ type OpenfgaBackupsConfig struct { RetentionPolicy string `yaml:"retentionPolicy,omitempty"` } +// OpenFgaConfig configures the authorization store. One OpenFGA instance serves a whole +// installation, so in a multi-data-center setup exactly one data center deploys and exposes +// it (Deploy + Expose) and every other one only points at it (APIURL). +type OpenFgaConfig struct { + // Deploy controls whether pc-applications deploys OpenFGA in this data center. + // Defaults to true when unset, matching the pc-applications chart. + Deploy *bool `yaml:"deploy,omitempty"` + // APIURL is the URL the Codesphere services reach OpenFGA at. Defaults to the + // in-cluster service of a locally deployed OpenFGA; required when Deploy is false. + APIURL string `yaml:"apiUrl,omitempty"` + // Expose publishes the deployed OpenFGA through the Codesphere gateway so the other + // data centers can reach it. + Expose *OpenFgaExposeConfig `yaml:"expose,omitempty"` +} + +// OpenFgaExposeConfig publishes a locally deployed OpenFGA through the Codesphere gateway. +type OpenFgaExposeConfig struct { + Enabled bool `yaml:"enabled"` + // Host OpenFGA is served under. Must resolve to this data center's public IP and + // is what the other data centers put in their APIURL. + Host string `yaml:"host,omitempty"` +} + +// DeploysOpenFga reports whether pc-applications should deploy OpenFGA in this data center. +func (c *OpenFgaConfig) DeploysOpenFga() bool { + return c == nil || c.Deploy == nil || *c.Deploy +} + +// ExposesOpenFga reports whether the deployed OpenFGA is published through the gateway. +func (c *OpenFgaConfig) ExposesOpenFga() bool { + return c != nil && c.Expose != nil && c.Expose.Enabled +} + type OAuthProvidersConfig struct { Oidc *OidcOAuthProvider `yaml:"oidc,omitempty"` }