feat(config): add scm provider configuration and credential master key

The source-code providers a deployment can use were implied by
auth.oauth2.*, so disabling OAuth login also disabled code repository sync
and every build that clones source code, and there was nowhere to configure
a self-hosted GitLab or Gitea at all: the gitea client was created with an
empty base URL.

Make provider capability explicit and independent from login:

- add scm.providers[] with provider and baseUrl; github and gitlab default
  to their hosted endpoints, gitea and other self-hosted instances require
  a base URL and are skipped with a warning when it is missing
- derive the provider list from the enabled OAuth2 providers when
  scm.providers is empty, so existing configurations keep working
- expose Configuration.ResolveScmProviders and Configuration.ScmProvider
- add auth.credentialKey for the master key that encrypts SCM credentials
  at rest, plus the credential.master_key settings key used as fallback

conf/config.yaml, both configuration docs and the Helm chart (values and
configmap) document the two new sections.

Verification:
- go test -run "TestConfigurationScm|TestConfigurationResolveScm" -tags "netgo,timetzdata,exclude_graphdriver_btrfs,containers_image_openpgp" ./pkg/config/...
- helm template test ./deploy/sigma
This commit is contained in:
Tosone committed 2026-10-27 21:07:20 +08:00
1 parent 867a26a23c
commit 642b2d576c
8 files changed
+272

No files matched your search

+17
View File
@@ -220,6 +220,23 @@ auth:
lockDuration: 60s
# inmemory | redis
backend: inmemory
# master key used to encrypt source code credentials at rest.
# when empty, sigma generates one on first start and stores it in the settings table.
# generate the key with: openssl rand -base64 32
credentialKey: ""
scm:
# source code management providers available for code repository sync and image builds.
# this section is independent of auth.oauth2, which only decides whether a provider can be
# used to sign in. when empty, the providers are derived from the enabled oauth2 providers.
providers: []
# providers:
# - provider: github
# baseUrl: https://github.com
# - provider: gitlab
# baseUrl: https://gitlab.com
# - provider: gitea
# baseUrl: https://gitea.example.com
audit:
# record completed authenticated business HTTP requests
+4
View File
@@ -146,3 +146,7 @@ data:
maxFailures: {{ .Values.config.auth.loginRateLimit.maxFailures | default 5 }}
window: {{ .Values.config.auth.loginRateLimit.window | default "5m" | quote }}
delay: {{ .Values.config.auth.loginRateLimit.delay | default "1s" | quote }}
credentialKey: {{ .Values.config.auth.credentialKey | default "" | quote }}
scm:
providers:
{{- toYaml (.Values.config.scm.providers | default list) | nindent 8 }}
+13
View File
@@ -238,6 +238,19 @@ config:
maxFailures: 5
window: 5m
delay: 1s
# master key used to encrypt source code credentials at rest.
# when empty, sigma generates one on first start and stores it in the database.
credentialKey: ""
scm:
# source code management providers available for code repository sync and builds,
# independent of the oauth2 login providers. When empty, they are derived from the
# enabled oauth2 providers.
# providers:
# - provider: github
# baseUrl: https://github.com
# - provider: gitea
# baseUrl: https://gitea.example.com
providers: []
external:
postgresql:
+22
View File
@@ -205,6 +205,28 @@ auth:
window: 5m
# fixed delay applied to every authentication attempt after maxFailures
delay: 1s
# master key used to encrypt source code credentials (access tokens, ssh keys,
# passwords) at rest. when empty, sigma generates one on first start and stores it in
# the settings table. rotating it makes the stored credentials unreadable, so users have
# to enter them again.
# generate the key with: openssl rand -base64 32
credentialKey: ""
scm:
# source code management providers available for code repository sync and image builds.
# this section is independent of auth.oauth2, which only decides whether a provider can be
# used to sign in. when empty, the providers are derived from the enabled oauth2 providers,
# so an existing configuration keeps working.
providers: []
# providers:
# - provider: github
# baseUrl: https://github.com
# - provider: gitlab
# # self-hosted gitlab
# baseUrl: https://gitlab.example.com
# - provider: gitea
# # gitea has no default base url and is skipped when it is empty
# baseUrl: https://gitea.example.com
audit:
# record completed authenticated business HTTP requests
+22
View File
@@ -209,6 +209,28 @@ auth:
window: 5m
# fixed delay applied to every authentication attempt after maxFailures
delay: 1s
# master key used to encrypt source code credentials (access tokens, ssh keys,
# passwords) at rest. when empty, sigma generates one on first start and stores it in
# the settings table. rotating it makes the stored credentials unreadable, so users have
# to enter them again.
# generate the key with: openssl rand -base64 32
credentialKey: ""
scm:
# source code management providers available for code repository sync and image builds.
# this section is independent of auth.oauth2, which only decides whether a provider can be
# used to sign in. when empty, the providers are derived from the enabled oauth2 providers,
# so an existing configuration keeps working.
providers: []
# providers:
# - provider: github
# baseUrl: https://github.com
# - provider: gitlab
# # self-hosted gitlab
# baseUrl: https://gitlab.example.com
# - provider: gitea
# # gitea has no default base url and is skipped when it is empty
# baseUrl: https://gitea.example.com
audit:
# record completed authenticated business HTTP requests
+90
View File
@@ -21,6 +21,8 @@ import (
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/go-sigma/sigma/pkg/api/enums"
)
func errChecker(config Configuration) error {
@@ -199,3 +201,91 @@ func TestConfigurationApplyEnvOverridesKeepsUnsetValues(t *testing.T) {
require.Equal(t, "oss-ak-yaml", cfg.Storage.Oss.Ak)
require.Equal(t, "oss-sk-yaml", cfg.Storage.Oss.Sk)
}
func TestConfigurationScmWithDefaults(t *testing.T) {
tests := []struct {
name string
providers []ConfigurationScmProvider
want []ConfigurationScmProvider
}{
{
name: "fills hosted defaults and trims slash",
providers: []ConfigurationScmProvider{
{Provider: enums.ScmProviderGithub, BaseURL: "https://github.example.com/"},
{Provider: enums.ScmProviderGitlab},
},
want: []ConfigurationScmProvider{
{Provider: enums.ScmProviderGithub, BaseURL: "https://github.example.com"},
{Provider: enums.ScmProviderGitlab, BaseURL: defaultScmBaseURLGitlab},
},
},
{
name: "drops gitea without base url",
providers: []ConfigurationScmProvider{
{Provider: enums.ScmProviderGitea},
{Provider: enums.ScmProviderGithub},
},
want: []ConfigurationScmProvider{
{Provider: enums.ScmProviderGithub, BaseURL: defaultScmBaseURLGithub},
},
},
{
name: "drops unknown and duplicated providers",
providers: []ConfigurationScmProvider{
{Provider: enums.ScmProviderNone},
{Provider: enums.ScmProviderGithub},
{Provider: enums.ScmProviderGithub, BaseURL: "https://github.example.com"},
},
want: []ConfigurationScmProvider{
{Provider: enums.ScmProviderGithub, BaseURL: defaultScmBaseURLGithub},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scm := ConfigurationScm{Providers: tt.providers}
scm.WithDefaults()
require.Equal(t, tt.want, scm.Providers)
})
}
}
func TestConfigurationResolveScmProviders(t *testing.T) {
t.Run("configured providers win", func(t *testing.T) {
cfg := &Configuration{
Auth: ConfigurationAuth{Oauth2: ConfigurationAuthOauth2{Github: ConfigurationAuthOauth2Github{Enabled: true}}},
Scm: ConfigurationScm{Providers: []ConfigurationScmProvider{{Provider: enums.ScmProviderGitea, BaseURL: "https://gitea.example.com"}}},
}
require.Equal(t, cfg.Scm.Providers, cfg.ResolveScmProviders())
})
t.Run("derives from oauth2 login providers", func(t *testing.T) {
cfg := &Configuration{
Auth: ConfigurationAuth{Oauth2: ConfigurationAuthOauth2{
Github: ConfigurationAuthOauth2Github{Enabled: true},
Gitlab: ConfigurationAuthOauth2Gitlab{Enabled: true},
}},
}
require.Equal(t, []ConfigurationScmProvider{
{Provider: enums.ScmProviderGithub, BaseURL: defaultScmBaseURLGithub},
{Provider: enums.ScmProviderGitlab, BaseURL: defaultScmBaseURLGitlab},
}, cfg.ResolveScmProviders())
})
t.Run("no oauth2 login provider means no scm provider", func(t *testing.T) {
cfg := &Configuration{}
require.Empty(t, cfg.ResolveScmProviders())
})
t.Run("ScmProvider reports enablement", func(t *testing.T) {
cfg := &Configuration{
Scm: ConfigurationScm{Providers: []ConfigurationScmProvider{{Provider: enums.ScmProviderGithub, BaseURL: defaultScmBaseURLGithub}}},
}
provider, ok := cfg.ScmProvider(enums.ProviderGithub)
require.True(t, ok)
require.Equal(t, defaultScmBaseURLGithub, provider.BaseURL)
_, ok = cfg.ScmProvider(enums.ProviderGitlab)
require.False(t, ok)
})
}
+100
View File
@@ -15,6 +15,8 @@
package config
import (
"log/slog"
"slices"
"strings"
"sync"
"time"
@@ -64,6 +66,7 @@ type Configuration struct {
Proxy ConfigurationProxy `yaml:"proxy"`
Daemon ConfigurationDaemon `yaml:"daemon"`
Auth ConfigurationAuth `yaml:"auth"`
Scm ConfigurationScm `yaml:"scm"`
Audit ConfigurationAudit `yaml:"audit"`
Trace ConfigurationTrace `yaml:"trace"`
Analytics ConfigurationAnalytics `yaml:"analytics"`
@@ -81,6 +84,7 @@ func (c *Configuration) WithDefaults() {
c.Cache.WithDefaults()
c.Locker.WithDefaults()
c.Database.WithDefaults()
c.Scm.WithDefaults()
}
// ConfigurationAudit configures HTTP audit recording.
@@ -707,6 +711,97 @@ func (c *ConfigurationDaemon) WithDefaults() {
c.SizeReconcile.WithDefaults()
}
// Default base URLs of the hosted source-code management providers. Gitea has no
// default because self-hosted instances are the only sensible target.
const (
defaultScmBaseURLGithub = "https://github.com"
defaultScmBaseURLGitlab = "https://gitlab.com"
)
// ConfigurationScmProvider configures one source-code management provider: which
// provider is available for code repository sync and image builds, and where it lives.
//
// It is intentionally separate from auth.oauth2, which only decides whether a provider
// can be used to sign in.
type ConfigurationScmProvider struct {
Provider enums.ScmProvider `yaml:"provider"`
BaseURL string `yaml:"baseUrl"`
}
// ConfigurationScm configures the source-code management providers available to users.
type ConfigurationScm struct {
Providers []ConfigurationScmProvider `yaml:"providers"`
}
// WithDefaults normalizes the configured providers: it trims trailing slashes, fills the
// hosted default base URLs and drops entries that are not usable.
func (c *ConfigurationScm) WithDefaults() {
providers := make([]ConfigurationScmProvider, 0, len(c.Providers))
for _, provider := range c.Providers {
provider.BaseURL = strings.TrimSuffix(strings.TrimSpace(provider.BaseURL), "/")
switch provider.Provider {
case enums.ScmProviderGithub:
if provider.BaseURL == "" {
provider.BaseURL = defaultScmBaseURLGithub
}
case enums.ScmProviderGitlab:
if provider.BaseURL == "" {
provider.BaseURL = defaultScmBaseURLGitlab
}
case enums.ScmProviderGitea:
if provider.BaseURL == "" {
slog.Warn("scm provider gitea is ignored because baseUrl is empty", "provider", provider.Provider.String())
continue
}
default:
slog.Warn("scm provider is ignored because it is unknown", "provider", provider.Provider.String())
continue
}
if slices.ContainsFunc(providers, func(item ConfigurationScmProvider) bool { return item.Provider == provider.Provider }) {
slog.Warn("duplicated scm provider is ignored", "provider", provider.Provider.String())
continue
}
providers = append(providers, provider)
}
c.Providers = providers
}
// ResolveScmProviders returns the effective provider list.
//
// When scm.providers is empty it is derived from the enabled OAuth2 login providers, so
// existing configurations keep working: a deployment that could sign in with GitHub or
// GitLab could already list and clone code repositories.
func (c *Configuration) ResolveScmProviders() []ConfigurationScmProvider {
if len(c.Scm.Providers) > 0 {
return c.Scm.Providers
}
var providers []ConfigurationScmProvider
if c.Auth.Oauth2.Github.Enabled {
providers = append(providers, ConfigurationScmProvider{
Provider: enums.ScmProviderGithub,
BaseURL: defaultScmBaseURLGithub,
})
}
if c.Auth.Oauth2.Gitlab.Enabled {
providers = append(providers, ConfigurationScmProvider{
Provider: enums.ScmProviderGitlab,
BaseURL: defaultScmBaseURLGitlab,
})
}
return providers
}
// ScmProvider returns the configuration of one provider and whether it is enabled.
func (c *Configuration) ScmProvider(provider enums.Provider) (ConfigurationScmProvider, bool) {
scmProvider := enums.ScmProvider(provider.String())
for _, item := range c.ResolveScmProviders() {
if item.Provider == scmProvider {
return item, true
}
}
return ConfigurationScmProvider{}, false
}
// ConfigurationAuthInternalUser configures the built-in internal user account used by system components.
type ConfigurationAuthInternalUser struct {
Username string `yaml:"username"`
@@ -783,6 +878,11 @@ type ConfigurationAuth struct {
Oauth2 ConfigurationAuthOauth2 `yaml:"oauth2"`
Jwt ConfigurationAuthJwt `yaml:"jwt"`
LoginRateLimit ConfigurationAuthLoginRateLimit `yaml:"loginRateLimit"`
// CredentialKey is the master key used to encrypt SCM credentials at rest
// (AES-256-GCM). When empty, sigma generates a random key on first start and
// stores it in the settings table. Rotating it makes all stored SCM
// credentials unreadable, so they must be re-entered afterwards.
CredentialKey string `yaml:"credentialKey"`
}
// ConfigurationAuthLoginRateLimit configures failed-login throttling by username.
+4
View File
@@ -19,4 +19,8 @@ const (
SettingSignPrivateKey = "signing.private_key"
// SettingSignPublicKey is the public key for signing
SettingSignPublicKey = "signing.public_key"
// SettingCredentialMasterKey is the settings key holding the master key that encrypts
// SCM credentials at rest. The value is generated on first start when
// auth.credentialKey is not configured.
SettingCredentialMasterKey = "credential.master_key" // #nosec G101 -- settings key name, not a credential
)