diff --git a/cmd/server/server.go b/cmd/server/server.go index eb5b9f0350..4baaf094f2 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -42,6 +42,7 @@ import ( woodpeckerGrpcServer "go.woodpecker-ci.org/woodpecker/v2/server/grpc" "go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/model" + // "go.woodpecker-ci.org/woodpecker/v2/server/plugins/encryption" // encryptedStore "go.woodpecker-ci.org/woodpecker/v2/server/plugins/encryption/wrapper/store" "go.woodpecker-ci.org/woodpecker/v2/server/plugins/permissions" @@ -280,39 +281,8 @@ func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) error { server.Config.Services.Queue = setupQueue(c, v) server.Config.Services.Logs = logging.New() server.Config.Services.Pubsub = pubsub.New() - var err error - server.Config.Services.Registries, err = setupRegistryService(c, v) - if err != nil { - return err - } - - // TODO(1544): fix encrypted store - // // encryption - // encryptedSecretStore := encryptedStore.NewSecretStore(v) - // err := encryption.Encryption(c, v).WithClient(encryptedSecretStore).Build() - // if err != nil { - // log.Fatal().Err(err).Msg("could not create encryption service") - // } - // server.Config.Services.Secrets = setupSecretService(c, encryptedSecretStore) - server.Config.Services.Secrets, err = setupSecretService(c, v) - if err != nil { - return err - } - server.Config.Services.Environ, err = setupEnvironService(c, v) - if err != nil { - return err - } server.Config.Services.Membership = setupMembershipService(c, f) - - server.Config.Services.SignaturePrivateKey, server.Config.Services.SignaturePublicKey, err = setupSignatureKeys(v) - if err != nil { - return err - } - - server.Config.Services.ConfigService, err = setupConfigService(c) - if err != nil { - return err - } + // TODO: setup addon manager // authentication server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos") diff --git a/cmd/server/setup.go b/cmd/server/setup.go index 9cfefe1b01..63479c901f 100644 --- a/cmd/server/setup.go +++ b/cmd/server/setup.go @@ -36,11 +36,6 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea" "go.woodpecker-ci.org/woodpecker/v2/server/forge/github" "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab" - "go.woodpecker-ci.org/woodpecker/v2/server/model" - "go.woodpecker-ci.org/woodpecker/v2/server/plugins/config" - "go.woodpecker-ci.org/woodpecker/v2/server/plugins/environments" - "go.woodpecker-ci.org/woodpecker/v2/server/plugins/registry" - "go.woodpecker-ci.org/woodpecker/v2/server/plugins/secrets" "go.woodpecker-ci.org/woodpecker/v2/server/queue" "go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store/datastore" @@ -105,48 +100,6 @@ func setupQueue(c *cli.Context, s store.Store) queue.Queue { return queue.WithTaskStore(queue.New(c.Context), s) } -func setupSecretService(c *cli.Context, s model.SecretStore) (model.SecretService, error) { - addonService, err := addon.Load[model.SecretService](c.StringSlice("addons"), addonTypes.TypeSecretService) - if err != nil { - return nil, err - } - if addonService != nil { - return addonService.Value, nil - } - - return secrets.New(c.Context, s), nil -} - -func setupRegistryService(c *cli.Context, s store.Store) (model.RegistryService, error) { - addonService, err := addon.Load[model.RegistryService](c.StringSlice("addons"), addonTypes.TypeRegistryService) - if err != nil { - return nil, err - } - if addonService != nil { - return addonService.Value, nil - } - - if c.String("docker-config") != "" { - return registry.Combined( - registry.New(s), - registry.Filesystem(c.String("docker-config")), - ), nil - } - return registry.New(s), nil -} - -func setupEnvironService(c *cli.Context, _ store.Store) (model.EnvironService, error) { - addonService, err := addon.Load[model.EnvironService](c.StringSlice("addons"), addonTypes.TypeEnvironmentService) - if err != nil { - return nil, err - } - if addonService != nil { - return addonService.Value, nil - } - - return environments.Parse(c.StringSlice("environment")), nil -} - func setupMembershipService(_ *cli.Context, r forge.Forge) cache.MembershipService { return cache.NewMembershipService(r) } @@ -286,19 +239,3 @@ func setupMetrics(g *errgroup.Group, _store store.Store) { } }) } - -func setupConfigService(c *cli.Context) (config.Extension, error) { - addonExt, err := addon.Load[config.Extension](c.StringSlice("addons"), addonTypes.TypeConfigService) - if err != nil { - return nil, err - } - if addonExt != nil { - return addonExt.Value, nil - } - - if endpoint := c.String("config-service-endpoint"); endpoint != "" { - return config.NewHTTP(endpoint, server.Config.Services.SignaturePrivateKey), nil - } - - return nil, nil -} diff --git a/server/config.go b/server/config.go index a06f42b164..31e98a1533 100644 --- a/server/config.go +++ b/server/config.go @@ -18,14 +18,12 @@ package server import ( - "crypto" "time" "go.woodpecker-ci.org/woodpecker/v2/server/cache" "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/model" - "go.woodpecker-ci.org/woodpecker/v2/server/plugins/config" "go.woodpecker-ci.org/woodpecker/v2/server/plugins/permissions" "go.woodpecker-ci.org/woodpecker/v2/server/pubsub" "go.woodpecker-ci.org/woodpecker/v2/server/queue" @@ -33,18 +31,12 @@ import ( var Config = struct { Services struct { - Pubsub *pubsub.Publisher - Queue queue.Queue - Logs logging.Log - Secrets model.SecretService - Registries model.RegistryService - Environ model.EnvironService - Forge forge.Forge - Timeout time.Duration - Membership cache.MembershipService - ConfigService config.Extension - SignaturePrivateKey crypto.PrivateKey - SignaturePublicKey crypto.PublicKey + Pubsub *pubsub.Publisher + Queue queue.Queue + Logs logging.Log + Forge forge.Forge + Timeout time.Duration + Membership cache.MembershipService } Storage struct { // Users model.UserStore diff --git a/server/model/environ.go b/server/model/environ.go index 4c70db4c61..87581ecb41 100644 --- a/server/model/environ.go +++ b/server/model/environ.go @@ -24,11 +24,6 @@ var ( errEnvironValueInvalid = errors.New("invalid Environment Variable Value") ) -// EnvironService defines a service for managing environment variables. -type EnvironService interface { - EnvironList(*Repo) ([]*Environ, error) -} - // EnvironStore persists environment information to storage. type EnvironStore interface { EnvironList(*Repo) ([]*Environ, error) diff --git a/server/plugins/config/http.go b/server/plugins/config/http.go index 5dbc2909bc..d548cdb2c5 100644 --- a/server/plugins/config/http.go +++ b/server/plugins/config/http.go @@ -46,7 +46,7 @@ type responseStructure struct { Configs []config `json:"configs"` } -func NewHTTP(endpoint string, privateKey crypto.PrivateKey) Extension { +func NewHTTP(endpoint string, privateKey crypto.PrivateKey) Service { return &http{endpoint, privateKey} } diff --git a/server/plugins/config/extension.go b/server/plugins/config/service.go similarity index 97% rename from server/plugins/config/extension.go rename to server/plugins/config/service.go index 9b9778af60..326cb50a61 100644 --- a/server/plugins/config/extension.go +++ b/server/plugins/config/service.go @@ -21,6 +21,6 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/server/model" ) -type Extension interface { +type Service interface { FetchConfig(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, currentFileMeta []*forge_types.FileMeta, netrc *model.Netrc) (configData []*forge_types.FileMeta, useOld bool, err error) } diff --git a/server/plugins/environments/parse.go b/server/plugins/environments/parse.go index b06294c2e6..a4990c3450 100644 --- a/server/plugins/environments/parse.go +++ b/server/plugins/environments/parse.go @@ -27,7 +27,7 @@ type builtin struct { } // Parse returns a model.EnvironService based on a string slice where key and value are separated by a ":" delimiter. -func Parse(params []string) model.EnvironService { +func Parse(params []string) Service { var globals []*model.Environ for _, item := range params { diff --git a/server/plugins/environments/service.go b/server/plugins/environments/service.go new file mode 100644 index 0000000000..1d9ef80b92 --- /dev/null +++ b/server/plugins/environments/service.go @@ -0,0 +1,8 @@ +package environments + +import "go.woodpecker-ci.org/woodpecker/v2/server/model" + +// Service defines a service for managing environment variables. +type Service interface { + EnvironList(*model.Repo) ([]*model.Environ, error) +} diff --git a/server/plugins/manager.go b/server/plugins/manager.go new file mode 100644 index 0000000000..410e241b1e --- /dev/null +++ b/server/plugins/manager.go @@ -0,0 +1,76 @@ +package extensions + +import ( + "crypto" + + "github.com/urfave/cli/v2" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" + "go.woodpecker-ci.org/woodpecker/v2/server/model" + "go.woodpecker-ci.org/woodpecker/v2/server/plugins/config" + "go.woodpecker-ci.org/woodpecker/v2/server/plugins/environments" + "go.woodpecker-ci.org/woodpecker/v2/server/plugins/registry" + "go.woodpecker-ci.org/woodpecker/v2/server/plugins/secret" + "go.woodpecker-ci.org/woodpecker/v2/server/store" +) + +type Manager struct { + secret secret.Service + registry registry.Service + config config.Service + environ environments.Service + signaturePrivateKey crypto.PrivateKey + signaturePublicKey crypto.PublicKey +} + +func NewManager(store store.Store, forge forge.Forge, c *cli.Context) (*Manager, error) { + signaturePrivateKey, signaturePublicKey, err := setupSignatureKeys(store) + if err != nil { + return nil, err + } + + config, err := setupConfigService(c, store, signaturePrivateKey) + if err != nil { + return nil, err + } + + return &Manager{ + signaturePrivateKey: signaturePrivateKey, + signaturePublicKey: signaturePublicKey, + secret: setupSecretExtension(store), + registry: setupRegistryExtension(store, c.String("docker-config")), + config: config, + environ: environments.Parse(c.StringSlice("environment")), + }, nil +} + +func (e *Manager) SignaturePublicKey() crypto.PublicKey { + return e.signaturePublicKey +} + +func (e *Manager) SecretAddonFromRepo(repo *model.Repo) secret.Service { + // if repo.SecretEndpoint != "" { + // return secret.NewHTTP(repo.SecretEndpoint, e.signaturePrivateKey) + // } + + return e.secret +} + +func (e *Manager) RegistryAddonFromRepo(repo *model.Repo) registry.Service { + // if repo.SecretEndpoint != "" { + // return registry.NewHTTP(repo.SecretEndpoint, e.signaturePrivateKey) + // } + + return e.registry +} + +func (e *Manager) ConfigAddonFromRepo(repo *model.Repo) config.Service { + // if repo.ConfigEndpoint != "" { + // return config.NewHTTP(repo.ConfigEndpoint, e.signaturePrivateKey) + // } + + return e.config +} + +func (e *Manager) EnvironAddonFromRepo(repo *model.Repo) environments.Service { + return e.environ +} diff --git a/server/plugins/plugins.go b/server/plugins/plugins.go deleted file mode 100644 index f9976e8de1..0000000000 --- a/server/plugins/plugins.go +++ /dev/null @@ -1,119 +0,0 @@ -package extensions - -import ( - "crypto" - "crypto/ed25519" - "crypto/rand" - "encoding/hex" - "errors" - "fmt" - - "github.com/rs/zerolog/log" - "github.com/urfave/cli/v2" - "go.woodpecker-ci.org/woodpecker/v2/server/forge" - "go.woodpecker-ci.org/woodpecker/v2/server/model" - "go.woodpecker-ci.org/woodpecker/v2/server/plugins/config" - "go.woodpecker-ci.org/woodpecker/v2/server/plugins/environments" - "go.woodpecker-ci.org/woodpecker/v2/server/plugins/registry" - "go.woodpecker-ci.org/woodpecker/v2/server/plugins/secret" - "go.woodpecker-ci.org/woodpecker/v2/server/store" - "go.woodpecker-ci.org/woodpecker/v2/server/store/types" -) - -type Manager struct { - secrets secret.SecretExtension - registries registry.RegistryService - config config.Extension - environ environments.EnvironPlugin - signaturePrivateKey crypto.PrivateKey - signaturePublicKey crypto.PublicKey -} - -func NewManager(store store.Store, forge forge.Forge, c *cli.Context) (*Manager, error) { - signaturePrivateKey, signaturePublicKey, err := setupSignatureKeys(store) - if err != nil { - return nil, err - } - - return &Manager{ - signaturePrivateKey: signaturePrivateKey, - signaturePublicKey: signaturePublicKey, - secrets: secret.NewBuiltin(store), - registries: setupRegistryExtension(store, c.String("docker-config")), - config: config.NewCombined(forge, c.String("config-service-endpoint"), signaturePrivateKey), - environ: environments.Parse(c.StringSlice("environment")), - }, nil -} - -func setupRegistryExtension(store store.Store, dockerConfig string) registry.RegistryService { - if dockerConfig != "" { - return registry.NewCombined( - registry.NewBuiltin(store), - registry.NewFilesystem(dockerConfig), - ) - } - return registry.NewBuiltin(store) -} - -// setupSignatureKeys generate or load key pair to sign webhooks requests (i.e. used for extensions) -func setupSignatureKeys(_store store.Store) (crypto.PrivateKey, crypto.PublicKey, error) { - privKeyID := "signature-private-key" - - privKey, err := _store.ServerConfigGet(privKeyID) - if errors.Is(err, types.RecordNotExist) { - _, privKey, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return nil, nil, fmt.Errorf("failed to generate private key: %w", err) - } - err = _store.ServerConfigSet(privKeyID, hex.EncodeToString(privKey)) - if err != nil { - return nil, nil, fmt.Errorf("failed to store private key: %w", err) - } - log.Debug().Msg("created private key") - return privKey, privKey.Public(), nil - } else if err != nil { - return nil, nil, fmt.Errorf("failed to load private key: %w", err) - } - privKeyStr, err := hex.DecodeString(privKey) - if err != nil { - return nil, nil, fmt.Errorf("failed to decode private key: %w", err) - } - privateKey := ed25519.PrivateKey(privKeyStr) - return privateKey, privateKey.Public(), nil -} - -func (e *Manager) SignaturePublicKey() crypto.PublicKey { - return e.signaturePublicKey -} - -func (e *Manager) SecretsFromRepo(repo *model.Repo) secret.SecretExtension { - if repo.SecretEndpoint != "" { - return secret.NewHTTP(repo.SecretEndpoint, e.signaturePrivateKey) - } - - return e.secrets -} - -func (e *Manager) RegistriesFromRepo(repo *model.Repo) registry.RegistryService { - if repo.SecretEndpoint != "" { - return registry.NewHTTP(repo.SecretEndpoint, e.signaturePrivateKey) - } - - return e.registries -} - -func (e *Manager) Config() config.Extension { - return e.config -} - -func (e *Manager) ConfigExtensionsFromRepo(repo *model.Repo) *config.HttpFetcher { - if repo.ConfigEndpoint != "" { - return config.NewHTTP(repo.ConfigEndpoint, e.signaturePrivateKey) - } - - return nil -} - -func (e *Manager) Environ() environments.EnvironExtension { - return e.environ -} diff --git a/server/plugins/registry/combine.go b/server/plugins/registry/combine.go index d4f6c7e45c..4f52bca1a5 100644 --- a/server/plugins/registry/combine.go +++ b/server/plugins/registry/combine.go @@ -19,11 +19,11 @@ import ( ) type combined struct { - registries []model.ReadOnlyRegistryService - dbRegistry model.RegistryService + registries []ReadOnlyService + dbRegistry Service } -func Combined(dbRegistry model.RegistryService, registries ...model.ReadOnlyRegistryService) model.RegistryService { +func NewCombined(dbRegistry Service, registries ...ReadOnlyService) Service { registries = append(registries, dbRegistry) return &combined{ registries: registries, @@ -31,7 +31,7 @@ func Combined(dbRegistry model.RegistryService, registries ...model.ReadOnlyRegi } } -func (c combined) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) { +func (c *combined) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) { for _, registry := range c.registries { res, err := registry.RegistryFind(repo, name) if err != nil { @@ -44,7 +44,7 @@ func (c combined) RegistryFind(repo *model.Repo, name string) (*model.Registry, return nil, nil } -func (c combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) { +func (c *combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) { var registries []*model.Registry for _, registry := range c.registries { list, err := registry.RegistryList(repo, &model.ListOptions{All: true}) @@ -56,14 +56,14 @@ func (c combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model return model.ApplyPagination(p, registries), nil } -func (c combined) RegistryCreate(repo *model.Repo, registry *model.Registry) error { +func (c *combined) RegistryCreate(repo *model.Repo, registry *model.Registry) error { return c.dbRegistry.RegistryCreate(repo, registry) } -func (c combined) RegistryUpdate(repo *model.Repo, registry *model.Registry) error { +func (c *combined) RegistryUpdate(repo *model.Repo, registry *model.Registry) error { return c.dbRegistry.RegistryUpdate(repo, registry) } -func (c combined) RegistryDelete(repo *model.Repo, name string) error { +func (c *combined) RegistryDelete(repo *model.Repo, name string) error { return c.dbRegistry.RegistryDelete(repo, name) } diff --git a/server/plugins/registry/db.go b/server/plugins/registry/db.go index d36b3aa3e5..5e7339557f 100644 --- a/server/plugins/registry/db.go +++ b/server/plugins/registry/db.go @@ -23,26 +23,26 @@ type db struct { } // New returns a new local registry service. -func New(store model.RegistryStore) model.RegistryService { +func NewDB(store model.RegistryStore) Service { return &db{store} } -func (b *db) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) { - return b.store.RegistryFind(repo, name) +func (d *db) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) { + return d.store.RegistryFind(repo, name) } -func (b *db) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) { - return b.store.RegistryList(repo, p) +func (d *db) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) { + return d.store.RegistryList(repo, p) } -func (b *db) RegistryCreate(_ *model.Repo, in *model.Registry) error { - return b.store.RegistryCreate(in) +func (d *db) RegistryCreate(_ *model.Repo, in *model.Registry) error { + return d.store.RegistryCreate(in) } -func (b *db) RegistryUpdate(_ *model.Repo, in *model.Registry) error { - return b.store.RegistryUpdate(in) +func (d *db) RegistryUpdate(_ *model.Repo, in *model.Registry) error { + return d.store.RegistryUpdate(in) } -func (b *db) RegistryDelete(repo *model.Repo, addr string) error { - return b.store.RegistryDelete(repo, addr) +func (d *db) RegistryDelete(repo *model.Repo, addr string) error { + return d.store.RegistryDelete(repo, addr) } diff --git a/server/plugins/registry/filesystem.go b/server/plugins/registry/filesystem.go index db9e24a0d3..4d98818cc0 100644 --- a/server/plugins/registry/filesystem.go +++ b/server/plugins/registry/filesystem.go @@ -31,7 +31,7 @@ type filesystem struct { path string } -func Filesystem(path string) model.ReadOnlyRegistryService { +func NewFilesystem(path string) ReadOnlyService { return &filesystem{path} } @@ -85,12 +85,12 @@ func parseDockerConfig(path string) ([]*model.Registry, error) { return auths, nil } -func (b *filesystem) RegistryFind(*model.Repo, string) (*model.Registry, error) { +func (f *filesystem) RegistryFind(*model.Repo, string) (*model.Registry, error) { return nil, nil } -func (b *filesystem) RegistryList(_ *model.Repo, p *model.ListOptions) ([]*model.Registry, error) { - regs, err := parseDockerConfig(b.path) +func (f *filesystem) RegistryList(_ *model.Repo, p *model.ListOptions) ([]*model.Registry, error) { + regs, err := parseDockerConfig(f.path) if err != nil { return nil, err } diff --git a/server/plugins/registry/plugin.go b/server/plugins/registry/service.go similarity index 70% rename from server/plugins/registry/plugin.go rename to server/plugins/registry/service.go index bd1c6077ee..03f269a82c 100644 --- a/server/plugins/registry/plugin.go +++ b/server/plugins/registry/service.go @@ -2,8 +2,8 @@ package registry import "go.woodpecker-ci.org/woodpecker/v2/server/model" -// RegistryService defines a service for managing registries. -type RegistryService interface { +// Service defines a service for managing registries. +type Service interface { RegistryFind(*model.Repo, string) (*model.Registry, error) RegistryList(*model.Repo, *model.ListOptions) ([]*model.Registry, error) RegistryCreate(*model.Repo, *model.Registry) error @@ -11,8 +11,8 @@ type RegistryService interface { RegistryDelete(*model.Repo, string) error } -// ReadOnlyRegistryService defines a service for managing registries. -type ReadOnlyRegistryService interface { +// ReadOnlyService defines a service for managing registries. +type ReadOnlyService interface { RegistryFind(*model.Repo, string) (*model.Registry, error) RegistryList(*model.Repo, *model.ListOptions) ([]*model.Registry, error) } diff --git a/server/plugins/secret/db.go b/server/plugins/secret/db.go new file mode 100644 index 0000000000..b3d3202618 --- /dev/null +++ b/server/plugins/secret/db.go @@ -0,0 +1,133 @@ +// Copyright 2022 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package secret + +import ( + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +type db struct { + store model.SecretStore +} + +// NewDB returns a new local secret service. +func NewDB(store model.SecretStore) Service { + return &db{store: store} +} + +func (d *db) SecretFind(repo *model.Repo, name string) (*model.Secret, error) { + return d.store.SecretFind(repo, name) +} + +func (d *db) SecretList(repo *model.Repo, p *model.ListOptions) ([]*model.Secret, error) { + return d.store.SecretList(repo, false, p) +} + +func (d *db) SecretListPipeline(repo *model.Repo, _ *model.Pipeline, p *model.ListOptions) ([]*model.Secret, error) { + s, err := d.store.SecretList(repo, true, p) + if err != nil { + return nil, err + } + + // Return only secrets with unique name + // Priority order in case of duplicate names are repository, user/organization, global + secrets := make([]*model.Secret, 0, len(s)) + uniq := make(map[string]struct{}) + for _, condition := range []struct { + IsRepository bool + IsOrganization bool + IsGlobal bool + }{ + {IsRepository: true}, + {IsOrganization: true}, + {IsGlobal: true}, + } { + for _, secret := range s { + if secret.IsRepository() != condition.IsRepository || secret.IsOrganization() != condition.IsOrganization || secret.IsGlobal() != condition.IsGlobal { + continue + } + if _, ok := uniq[secret.Name]; ok { + continue + } + uniq[secret.Name] = struct{}{} + secrets = append(secrets, secret) + } + } + return secrets, nil +} + +func (d *db) SecretCreate(_ *model.Repo, in *model.Secret) error { + return d.store.SecretCreate(in) +} + +func (d *db) SecretUpdate(_ *model.Repo, in *model.Secret) error { + return d.store.SecretUpdate(in) +} + +func (d *db) SecretDelete(repo *model.Repo, name string) error { + secret, err := d.store.SecretFind(repo, name) + if err != nil { + return err + } + return d.store.SecretDelete(secret) +} + +func (d *db) OrgSecretFind(owner int64, name string) (*model.Secret, error) { + return d.store.OrgSecretFind(owner, name) +} + +func (d *db) OrgSecretList(owner int64, p *model.ListOptions) ([]*model.Secret, error) { + return d.store.OrgSecretList(owner, p) +} + +func (d *db) OrgSecretCreate(_ int64, in *model.Secret) error { + return d.store.SecretCreate(in) +} + +func (d *db) OrgSecretUpdate(_ int64, in *model.Secret) error { + return d.store.SecretUpdate(in) +} + +func (d *db) OrgSecretDelete(owner int64, name string) error { + secret, err := d.store.OrgSecretFind(owner, name) + if err != nil { + return err + } + return d.store.SecretDelete(secret) +} + +func (d *db) GlobalSecretFind(owner string) (*model.Secret, error) { + return d.store.GlobalSecretFind(owner) +} + +func (d *db) GlobalSecretList(p *model.ListOptions) ([]*model.Secret, error) { + return d.store.GlobalSecretList(p) +} + +func (d *db) GlobalSecretCreate(in *model.Secret) error { + return d.store.SecretCreate(in) +} + +func (d *db) GlobalSecretUpdate(in *model.Secret) error { + return d.store.SecretUpdate(in) +} + +func (d *db) GlobalSecretDelete(name string) error { + secret, err := d.store.GlobalSecretFind(name) + if err != nil { + return err + } + return d.store.SecretDelete(secret) +} diff --git a/server/plugins/secrets/builtin_test.go b/server/plugins/secret/db_test.go similarity index 82% rename from server/plugins/secrets/builtin_test.go rename to server/plugins/secret/db_test.go index 6c35aebfd7..979d4124ed 100644 --- a/server/plugins/secrets/builtin_test.go +++ b/server/plugins/secret/db_test.go @@ -12,23 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -package secrets_test +package secret_test import ( - "context" "testing" "github.com/franela/goblin" "github.com/stretchr/testify/mock" "go.woodpecker-ci.org/woodpecker/v2/server/model" - "go.woodpecker-ci.org/woodpecker/v2/server/plugins/secrets" + "go.woodpecker-ci.org/woodpecker/v2/server/plugins/secret" mocks_store "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks" ) func TestSecretListPipeline(t *testing.T) { g := goblin.Goblin(t) - ctx := context.Background() mockStore := mocks_store.NewStore(t) // global secret @@ -66,7 +64,7 @@ func TestSecretListPipeline(t *testing.T) { repoSecret, }, nil) - s, err := secrets.New(ctx, mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{}) + s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{}) g.Assert(err).IsNil() g.Assert(len(s)).Equal(1) @@ -79,7 +77,7 @@ func TestSecretListPipeline(t *testing.T) { orgSecret, }, nil) - s, err := secrets.New(ctx, mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{}) + s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{}) g.Assert(err).IsNil() g.Assert(len(s)).Equal(1) @@ -91,7 +89,7 @@ func TestSecretListPipeline(t *testing.T) { globalSecret, }, nil) - s, err := secrets.New(ctx, mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{}) + s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{}) g.Assert(err).IsNil() g.Assert(len(s)).Equal(1) diff --git a/server/plugins/secrets/plugin.go b/server/plugins/secret/service.go similarity index 90% rename from server/plugins/secrets/plugin.go rename to server/plugins/secret/service.go index 6cd1443e49..d8a9c3bc72 100644 --- a/server/plugins/secrets/plugin.go +++ b/server/plugins/secret/service.go @@ -1,9 +1,9 @@ -package secrets +package secret import "go.woodpecker-ci.org/woodpecker/v2/server/model" -// SecretService defines a service for managing secrets. -type SecretService interface { +// Service defines a service for managing secrets. +type Service interface { SecretListPipeline(*model.Repo, *model.Pipeline, *model.ListOptions) ([]*model.Secret, error) // Repository secrets SecretFind(*model.Repo, string) (*model.Secret, error) diff --git a/server/plugins/secrets/builtin.go b/server/plugins/secrets/builtin.go deleted file mode 100644 index 206835e086..0000000000 --- a/server/plugins/secrets/builtin.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2022 Woodpecker Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package secrets - -import ( - "context" - - "go.woodpecker-ci.org/woodpecker/v2/server/model" -) - -type builtin struct { - context.Context - store model.SecretStore -} - -// New returns a new local secret service. -func New(ctx context.Context, store model.SecretStore) SecretService { - return &builtin{store: store, Context: ctx} -} - -func (b *builtin) SecretFind(repo *model.Repo, name string) (*model.Secret, error) { - return b.store.SecretFind(repo, name) -} - -func (b *builtin) SecretList(repo *model.Repo, p *model.ListOptions) ([]*model.Secret, error) { - return b.store.SecretList(repo, false, p) -} - -func (b *builtin) SecretListPipeline(repo *model.Repo, _ *model.Pipeline, p *model.ListOptions) ([]*model.Secret, error) { - s, err := b.store.SecretList(repo, true, p) - if err != nil { - return nil, err - } - - // Return only secrets with unique name - // Priority order in case of duplicate names are repository, user/organization, global - secrets := make([]*model.Secret, 0, len(s)) - uniq := make(map[string]struct{}) - for _, condition := range []struct { - IsRepository bool - IsOrganization bool - IsGlobal bool - }{ - {IsRepository: true}, - {IsOrganization: true}, - {IsGlobal: true}, - } { - for _, secret := range s { - if secret.IsRepository() != condition.IsRepository || secret.IsOrganization() != condition.IsOrganization || secret.IsGlobal() != condition.IsGlobal { - continue - } - if _, ok := uniq[secret.Name]; ok { - continue - } - uniq[secret.Name] = struct{}{} - secrets = append(secrets, secret) - } - } - return secrets, nil -} - -func (b *builtin) SecretCreate(_ *model.Repo, in *model.Secret) error { - return b.store.SecretCreate(in) -} - -func (b *builtin) SecretUpdate(_ *model.Repo, in *model.Secret) error { - return b.store.SecretUpdate(in) -} - -func (b *builtin) SecretDelete(repo *model.Repo, name string) error { - secret, err := b.store.SecretFind(repo, name) - if err != nil { - return err - } - return b.store.SecretDelete(secret) -} - -func (b *builtin) OrgSecretFind(owner int64, name string) (*model.Secret, error) { - return b.store.OrgSecretFind(owner, name) -} - -func (b *builtin) OrgSecretList(owner int64, p *model.ListOptions) ([]*model.Secret, error) { - return b.store.OrgSecretList(owner, p) -} - -func (b *builtin) OrgSecretCreate(_ int64, in *model.Secret) error { - return b.store.SecretCreate(in) -} - -func (b *builtin) OrgSecretUpdate(_ int64, in *model.Secret) error { - return b.store.SecretUpdate(in) -} - -func (b *builtin) OrgSecretDelete(owner int64, name string) error { - secret, err := b.store.OrgSecretFind(owner, name) - if err != nil { - return err - } - return b.store.SecretDelete(secret) -} - -func (b *builtin) GlobalSecretFind(owner string) (*model.Secret, error) { - return b.store.GlobalSecretFind(owner) -} - -func (b *builtin) GlobalSecretList(p *model.ListOptions) ([]*model.Secret, error) { - return b.store.GlobalSecretList(p) -} - -func (b *builtin) GlobalSecretCreate(in *model.Secret) error { - return b.store.SecretCreate(in) -} - -func (b *builtin) GlobalSecretUpdate(in *model.Secret) error { - return b.store.SecretUpdate(in) -} - -func (b *builtin) GlobalSecretDelete(name string) error { - secret, err := b.store.GlobalSecretFind(name) - if err != nil { - return err - } - return b.store.SecretDelete(secret) -} diff --git a/server/plugins/setup.go b/server/plugins/setup.go new file mode 100644 index 0000000000..045e8163ab --- /dev/null +++ b/server/plugins/setup.go @@ -0,0 +1,129 @@ +package extensions + +import ( + "crypto" + "crypto/ed25519" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + + "github.com/rs/zerolog/log" + "github.com/urfave/cli/v2" + "go.woodpecker-ci.org/woodpecker/v2/server/plugins/config" + "go.woodpecker-ci.org/woodpecker/v2/server/plugins/registry" + "go.woodpecker-ci.org/woodpecker/v2/server/plugins/secret" + "go.woodpecker-ci.org/woodpecker/v2/server/store" + "go.woodpecker-ci.org/woodpecker/v2/server/store/types" + "go.woodpecker-ci.org/woodpecker/v2/shared/addon" + addonTypes "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types" +) + +func setupRegistryExtension(store store.Store, dockerConfig string) registry.Service { + if dockerConfig != "" { + return registry.NewCombined( + registry.NewDB(store), + registry.NewFilesystem(dockerConfig), + ) + } + + return registry.NewDB(store) +} + +func setupSecretExtension(store store.Store) secret.Service { + // TODO(1544): fix encrypted store + // // encryption + // encryptedSecretStore := encryptedStore.NewSecretStore(v) + // err := encryption.Encryption(c, v).WithClient(encryptedSecretStore).Build() + // if err != nil { + // log.Fatal().Err(err).Msg("could not create encryption service") + // } + // server.Config.Services.Secrets = setupSecretService(c, encryptedSecretStore) + + return secret.NewDB(store) +} + +func setupConfigService(c *cli.Context, store store.Store, privateSignatureKey crypto.PrivateKey) (config.Service, error) { + addonExt, err := addon.Load[config.Service](c.StringSlice("addons"), addonTypes.TypeConfigService) + if err != nil { + return nil, err + } + if addonExt != nil { + return addonExt.Value, nil + } + + if endpoint := c.String("config-service-endpoint"); endpoint != "" { + return config.NewHTTP(endpoint, privateSignatureKey), nil + } + + return nil, nil +} + +// setupSignatureKeys generate or load key pair to sign webhooks requests (i.e. used for extensions) +func setupSignatureKeys(_store store.Store) (crypto.PrivateKey, crypto.PublicKey, error) { + privKeyID := "signature-private-key" + + privKey, err := _store.ServerConfigGet(privKeyID) + if errors.Is(err, types.RecordNotExist) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate private key: %w", err) + } + err = _store.ServerConfigSet(privKeyID, hex.EncodeToString(privKey)) + if err != nil { + return nil, nil, fmt.Errorf("failed to store private key: %w", err) + } + log.Debug().Msg("created private key") + return privKey, privKey.Public(), nil + } else if err != nil { + return nil, nil, fmt.Errorf("failed to load private key: %w", err) + } + privKeyStr, err := hex.DecodeString(privKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode private key: %w", err) + } + privateKey := ed25519.PrivateKey(privKeyStr) + return privateKey, privateKey.Public(), nil +} + +// func setupSecretService(c *cli.Context, s model.SecretStore) (model.SecretService, error) { +// addonService, err := addon.Load[model.SecretService](c.StringSlice("addons"), addonTypes.TypeSecretService) +// if err != nil { +// return nil, err +// } +// if addonService != nil { +// return addonService.Value, nil +// } + +// return secrets.New(c.Context, s), nil +// } + +// func setupRegistryService(c *cli.Context, s store.Store) (model.RegistryService, error) { +// addonService, err := addon.Load[model.RegistryService](c.StringSlice("addons"), addonTypes.TypeRegistryService) +// if err != nil { +// return nil, err +// } +// if addonService != nil { +// return addonService.Value, nil +// } + +// if c.String("docker-config") != "" { +// return registry.Combined( +// registry.New(s), +// registry.Filesystem(c.String("docker-config")), +// ), nil +// } +// return registry.New(s), nil +// } + +// func setupEnvironService(c *cli.Context, _ store.Store) (model.EnvironService, error) { +// addonService, err := addon.Load[model.EnvironService](c.StringSlice("addons"), addonTypes.TypeEnvironmentService) +// if err != nil { +// return nil, err +// } +// if addonService != nil { +// return addonService.Value, nil +// } + +// return environments.Parse(c.StringSlice("environment")), nil +// }