Skip to content
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
7 changes: 6 additions & 1 deletion internal/installer/argocd/install_and_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/codesphere-cloud/oms/internal/installer/files"
"github.com/codesphere-cloud/oms/internal/installer/secrets"
"github.com/codesphere-cloud/oms/internal/installer/vault"
"github.com/codesphere-cloud/oms/internal/util"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
)
Expand Down Expand Up @@ -69,12 +70,16 @@ func (i *AppInstaller) SyncVaultSecret(ctx context.Context) error {
// InstallPCApps creates or updates the pc-applications app-of-apps ArgoCD
// Application using the chart version from the supplied installer BOM.
func (i *AppInstaller) InstallPCApps(ctx context.Context, bomPath string) error {
// Values derived from the install config form the base; an explicit pcApps block in
// config.yaml wins over them, and the --pc-apps-values files win over both.
values := util.DeepMergeMaps(installer.OpenFgaPcAppsValues(&i.cfg.Config, i.cfg.Vault), i.cfg.Config.PcApps)

pcApps, err := installer.NewPcAppsFromBom(
i.cfg.KubeClient,
bomPath,
DefaultNamespace,
i.cfg.PCAppsValues,
i.cfg.Config.PcApps,
values,
)
if err != nil {
return fmt.Errorf("failed to initialize pc-apps installer: %w", err)
Expand Down
33 changes: 33 additions & 0 deletions internal/installer/config_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
49 changes: 49 additions & 0 deletions internal/installer/config_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
34 changes: 34 additions & 0 deletions internal/installer/files/config_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
}
Expand Down
116 changes: 116 additions & 0 deletions internal/installer/openfga_pc_apps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package installer

import (
"log"

"github.com/codesphere-cloud/oms/internal/installer/files"
)

// openFgaPresharedKeysSecret is the Secret the openfga chart reads the preshared key from.
// Its content is synced out of the installation vault by the chart's own ExternalSecret, so
// oms only has to name it.
const openFgaPresharedKeysSecret = "openfga-preshared-keys"

// OpenFgaPcAppsValues derives the pc-applications values for OpenFGA. They are the *base* of
// the pc-apps values: an explicit `pcApps` block in config.yaml and any --pc-apps-values file
// still override them. Returns nil when there is nothing to say, leaving the chart defaults
// untouched.
func OpenFgaPcAppsValues(config *files.RootConfig, vault *files.InstallVault) files.ChartValues {
application := files.ChartValues{}
chartValues := files.ChartValues{}

if fga := config.Codesphere.OpenFga; fga != nil {
application["enabled"] = fga.DeploysOpenFga()

if fga.Expose != nil {
chartValues["gateway"] = gatewayValues(config, fga.Expose)
}
}

if authn := authnValues(vault); authn != nil {
chartValues["openfga"] = files.ChartValues{"authn": authn}
}

if len(chartValues) > 0 {
application["valuesObject"] = chartValues
}

if len(application) == 0 {
return nil
}

return files.ChartValues{
"applications": files.ChartValues{
"openfga": application,
},
}
}

// authnValues makes OpenFGA require the preshared key exactly when the installation has one,
// so it stays in step with the Codesphere services: they read the same vault entry and treat
// it as optional too. Returns nil for an installation without the key, which runs an
// unauthenticated OpenFGA. Services that started before the key existed only send it once
// their pods roll.
func authnValues(vault *files.InstallVault) files.ChartValues {
if !hasOpenFgaPresharedKey(vault) {
log.Printf(
"OpenFGA: %s is not in the vault, deploying OpenFGA without authentication."+
" Add the key with `oms update install-config` — a future version will require it.\n",
files.SecretOpenFgaPresharedKey,
)

return nil
}

return files.ChartValues{
"method": "preshared",
"preshared": files.ChartValues{
"keysSecret": openFgaPresharedKeysSecret,
},
}
}

// gatewayValues publishes a locally deployed OpenFGA through the Codesphere gateway, so the
// Codesphere services of the other data centers can reach it.
func gatewayValues(config *files.RootConfig, expose *files.OpenFgaExposeConfig) files.ChartValues {
gateway := files.ChartValues{"enabled": expose.Enabled}
if expose.Host != "" {
gateway["host"] = expose.Host
}

// The cert-manager ClusterIssuer the cluster step creates is named after the
// configured issuer type, so the gateway certificate follows the same issuer as
// the Codesphere frontend gateway.
gateway["tls"] = files.ChartValues{
"certificate": files.ChartValues{
"issuerRef": files.ChartValues{"name": certIssuerName(config)},
},
}

return gateway
}

// hasOpenFgaPresharedKey reports whether the vault holds a usable preshared key. A vault
// written by an older oms has no entry at all; `oms update install-config` adds one.
func hasOpenFgaPresharedKey(vault *files.InstallVault) bool {
if vault == nil {
return false
}

secret := vault.GetSecret(files.SecretOpenFgaPresharedKey)

return secret != nil && secret.Fields != nil && secret.Fields.Password != ""
}

// certIssuerName returns the name of the ClusterIssuer for this installation, matching the
// naming the cluster step uses (the issuer type is the issuer name).
func certIssuerName(config *files.RootConfig) string {
if issuer := config.Codesphere.CertIssuer; issuer != nil && issuer.Type != "" {
return string(issuer.Type)
}

return string(files.CertIssuerTypeSelfSigned)
}
Loading
Loading