diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index d2a338e407..fba35f3805 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -6,7 +6,7 @@
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"voorjaar.windicss-intellisense",
- "johnsoncodehk.volar",
+ "Vue.volar",
"redhat.vscode-yaml",
"davidanson.vscode-markdownlint"
],
diff --git a/go.mod b/go.mod
index b5dd2b358d..474b9e9094 100644
--- a/go.mod
+++ b/go.mod
@@ -42,8 +42,8 @@ require (
google.golang.org/grpc v1.47.0
google.golang.org/protobuf v1.28.0
gopkg.in/yaml.v3 v3.0.1
- xorm.io/builder v0.3.10
- xorm.io/xorm v1.3.0
+ xorm.io/builder v0.3.12
+ xorm.io/xorm v1.3.1
)
require (
diff --git a/go.sum b/go.sum
index a2c555568c..05b9ef9985 100644
--- a/go.sum
+++ b/go.sum
@@ -1199,8 +1199,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
-xorm.io/builder v0.3.9/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
-xorm.io/builder v0.3.10 h1:Rvkncad3Lo9YIVqCbgIf6QnpR/HcW3IEr0AANNpuyMQ=
-xorm.io/builder v0.3.10/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
-xorm.io/xorm v1.3.0 h1:UsVke0wyAk3tJcb0j15gLWv2DEshVUnySVyvcYDny8w=
-xorm.io/xorm v1.3.0/go.mod h1:cEaWjDPqoIusTkmDAG+krCcPcTglqo8CDU8geX/yhko=
+xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
+xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
+xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
+xorm.io/xorm v1.3.1 h1:z5egKrDoOLqZFhMjcGF4FBHiTmE5/feQoHclfhNidfM=
+xorm.io/xorm v1.3.1/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=
diff --git a/server/api/global_secret.go b/server/api/global_secret.go
new file mode 100644
index 0000000000..d08967f285
--- /dev/null
+++ b/server/api/global_secret.go
@@ -0,0 +1,123 @@
+// 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 api
+
+import (
+ "net/http"
+
+ "github.com/woodpecker-ci/woodpecker/server"
+ "github.com/woodpecker-ci/woodpecker/server/model"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetGlobalSecretList get the global secret list from
+// the database and writes to the response in json format.
+func GetGlobalSecretList(c *gin.Context) {
+ list, err := server.Config.Services.Secrets.GlobalSecretList()
+ if err != nil {
+ c.String(http.StatusInternalServerError, "Error getting global secret list. %s", err)
+ return
+ }
+ // copy the secret detail to remove the sensitive
+ // password and token fields.
+ for i, secret := range list {
+ list[i] = secret.Copy()
+ }
+ c.JSON(http.StatusOK, list)
+}
+
+// GetGlobalSecret gets the named global secret from the database
+// and writes to the response in json format.
+func GetGlobalSecret(c *gin.Context) {
+ name := c.Param("secret")
+ secret, err := server.Config.Services.Secrets.GlobalSecretFind(name)
+ if err != nil {
+ c.String(404, "Error getting global secret %q. %s", name, err)
+ return
+ }
+ c.JSON(200, secret.Copy())
+}
+
+// PostGlobalSecret persists the global secret to the database.
+func PostGlobalSecret(c *gin.Context) {
+ in := new(model.Secret)
+ if err := c.Bind(in); err != nil {
+ c.String(http.StatusBadRequest, "Error parsing global secret. %s", err)
+ return
+ }
+ secret := &model.Secret{
+ Name: in.Name,
+ Value: in.Value,
+ Events: in.Events,
+ Images: in.Images,
+ }
+ if err := secret.Validate(); err != nil {
+ c.String(400, "Error inserting global secret. %s", err)
+ return
+ }
+ if err := server.Config.Services.Secrets.GlobalSecretCreate(secret); err != nil {
+ c.String(500, "Error inserting global secret %q. %s", in.Name, err)
+ return
+ }
+ c.JSON(200, secret.Copy())
+}
+
+// PatchGlobalSecret updates the global secret in the database.
+func PatchGlobalSecret(c *gin.Context) {
+ name := c.Param("secret")
+
+ in := new(model.Secret)
+ err := c.Bind(in)
+ if err != nil {
+ c.String(http.StatusBadRequest, "Error parsing secret. %s", err)
+ return
+ }
+
+ secret, err := server.Config.Services.Secrets.GlobalSecretFind(name)
+ if err != nil {
+ c.String(404, "Error getting global secret %q. %s", name, err)
+ return
+ }
+ if in.Value != "" {
+ secret.Value = in.Value
+ }
+ if in.Events != nil {
+ secret.Events = in.Events
+ }
+ if in.Images != nil {
+ secret.Images = in.Images
+ }
+
+ if err := secret.Validate(); err != nil {
+ c.String(400, "Error updating global secret. %s", err)
+ return
+ }
+ if err := server.Config.Services.Secrets.GlobalSecretUpdate(secret); err != nil {
+ c.String(500, "Error updating global secret %q. %s", in.Name, err)
+ return
+ }
+ c.JSON(200, secret.Copy())
+}
+
+// DeleteGlobalSecret deletes the named global secret from the database.
+func DeleteGlobalSecret(c *gin.Context) {
+ name := c.Param("secret")
+ if err := server.Config.Services.Secrets.GlobalSecretDelete(name); err != nil {
+ c.String(500, "Error deleting global secret %q. %s", name, err)
+ return
+ }
+ c.String(204, "")
+}
diff --git a/server/api/org.go b/server/api/org.go
new file mode 100644
index 0000000000..5754958f1c
--- /dev/null
+++ b/server/api/org.go
@@ -0,0 +1,41 @@
+// 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 api
+
+import (
+ "net/http"
+
+ "github.com/woodpecker-ci/woodpecker/server"
+ "github.com/woodpecker-ci/woodpecker/server/router/middleware/session"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetOrgPermissions returns the permissions of the current user in the given organization.
+func GetOrgPermissions(c *gin.Context) {
+ var (
+ err error
+ user = session.User(c)
+ owner = c.Param("owner")
+ )
+
+ perm, err := server.Config.Services.Membership.Get(c, user, owner)
+ if err != nil {
+ c.String(http.StatusInternalServerError, "Error getting membership for %q. %s", owner, err)
+ return
+ }
+
+ c.JSON(http.StatusOK, perm)
+}
diff --git a/server/api/org_secret.go b/server/api/org_secret.go
new file mode 100644
index 0000000000..47d55d9e1e
--- /dev/null
+++ b/server/api/org_secret.go
@@ -0,0 +1,136 @@
+// 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 api
+
+import (
+ "net/http"
+
+ "github.com/woodpecker-ci/woodpecker/server"
+ "github.com/woodpecker-ci/woodpecker/server/model"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetOrgSecret gets the named organization secret from the database
+// and writes to the response in json format.
+func GetOrgSecret(c *gin.Context) {
+ var (
+ owner = c.Param("owner")
+ name = c.Param("secret")
+ )
+ secret, err := server.Config.Services.Secrets.OrgSecretFind(owner, name)
+ if err != nil {
+ c.String(404, "Error getting org %q secret %q. %s", owner, name, err)
+ return
+ }
+ c.JSON(200, secret.Copy())
+}
+
+// GetOrgSecretList get the organization secret list from
+// the database and writes to the response in json format.
+func GetOrgSecretList(c *gin.Context) {
+ owner := c.Param("owner")
+ list, err := server.Config.Services.Secrets.OrgSecretList(owner)
+ if err != nil {
+ c.String(http.StatusInternalServerError, "Error getting secret list for %q. %s", owner, err)
+ return
+ }
+ // copy the secret detail to remove the sensitive
+ // password and token fields.
+ for i, secret := range list {
+ list[i] = secret.Copy()
+ }
+ c.JSON(http.StatusOK, list)
+}
+
+// PostOrgSecret persists the organization secret to the database.
+func PostOrgSecret(c *gin.Context) {
+ owner := c.Param("owner")
+
+ in := new(model.Secret)
+ if err := c.Bind(in); err != nil {
+ c.String(http.StatusBadRequest, "Error parsing org %q secret. %s", owner, err)
+ return
+ }
+ secret := &model.Secret{
+ Owner: owner,
+ Name: in.Name,
+ Value: in.Value,
+ Events: in.Events,
+ Images: in.Images,
+ }
+ if err := secret.Validate(); err != nil {
+ c.String(400, "Error inserting org %q secret. %s", owner, err)
+ return
+ }
+ if err := server.Config.Services.Secrets.OrgSecretCreate(owner, secret); err != nil {
+ c.String(500, "Error inserting org %q secret %q. %s", owner, in.Name, err)
+ return
+ }
+ c.JSON(200, secret.Copy())
+}
+
+// PatchOrgSecret updates the organization secret in the database.
+func PatchOrgSecret(c *gin.Context) {
+ var (
+ owner = c.Param("owner")
+ name = c.Param("secret")
+ )
+
+ in := new(model.Secret)
+ err := c.Bind(in)
+ if err != nil {
+ c.String(http.StatusBadRequest, "Error parsing secret. %s", err)
+ return
+ }
+
+ secret, err := server.Config.Services.Secrets.OrgSecretFind(owner, name)
+ if err != nil {
+ c.String(404, "Error getting org %q secret %q. %s", owner, name, err)
+ return
+ }
+ if in.Value != "" {
+ secret.Value = in.Value
+ }
+ if in.Events != nil {
+ secret.Events = in.Events
+ }
+ if in.Images != nil {
+ secret.Images = in.Images
+ }
+
+ if err := secret.Validate(); err != nil {
+ c.String(400, "Error updating org %q secret. %s", owner, err)
+ return
+ }
+ if err := server.Config.Services.Secrets.OrgSecretUpdate(owner, secret); err != nil {
+ c.String(500, "Error updating org %q secret %q. %s", owner, in.Name, err)
+ return
+ }
+ c.JSON(200, secret.Copy())
+}
+
+// DeleteOrgSecret deletes the named organization secret from the database.
+func DeleteOrgSecret(c *gin.Context) {
+ var (
+ owner = c.Param("owner")
+ name = c.Param("secret")
+ )
+ if err := server.Config.Services.Secrets.OrgSecretDelete(owner, name); err != nil {
+ c.String(500, "Error deleting org %q secret %q. %s", owner, name, err)
+ return
+ }
+ c.String(204, "")
+}
diff --git a/server/api/secret.go b/server/api/repo_secret.go
similarity index 100%
rename from server/api/secret.go
rename to server/api/repo_secret.go
diff --git a/server/model/secret.go b/server/model/secret.go
index c483444880..9a0a7dac86 100644
--- a/server/model/secret.go
+++ b/server/model/secret.go
@@ -30,29 +30,47 @@ var (
// SecretService defines a service for managing secrets.
type SecretService interface {
+ SecretListBuild(*Repo, *Build) ([]*Secret, error)
+ // Repository secrets
SecretFind(*Repo, string) (*Secret, error)
SecretList(*Repo) ([]*Secret, error)
- SecretListBuild(*Repo, *Build) ([]*Secret, error)
SecretCreate(*Repo, *Secret) error
SecretUpdate(*Repo, *Secret) error
SecretDelete(*Repo, string) error
+ // Organization secrets
+ OrgSecretFind(string, string) (*Secret, error)
+ OrgSecretList(string) ([]*Secret, error)
+ OrgSecretCreate(string, *Secret) error
+ OrgSecretUpdate(string, *Secret) error
+ OrgSecretDelete(string, string) error
+ // Global secrets
+ GlobalSecretFind(string) (*Secret, error)
+ GlobalSecretList() ([]*Secret, error)
+ GlobalSecretCreate(*Secret) error
+ GlobalSecretUpdate(*Secret) error
+ GlobalSecretDelete(string) error
}
// SecretStore persists secret information to storage.
type SecretStore interface {
SecretFind(*Repo, string) (*Secret, error)
- SecretList(*Repo) ([]*Secret, error)
+ SecretList(*Repo, bool) ([]*Secret, error)
SecretCreate(*Secret) error
SecretUpdate(*Secret) error
SecretDelete(*Secret) error
+ OrgSecretFind(string, string) (*Secret, error)
+ OrgSecretList(string) ([]*Secret, error)
+ GlobalSecretFind(string) (*Secret, error)
+ GlobalSecretList() ([]*Secret, error)
}
// Secret represents a secret variable, such as a password or token.
// swagger:model registry
type Secret struct {
ID int64 `json:"id" xorm:"pk autoincr 'secret_id'"`
- RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'secret_repo_id'"`
- Name string `json:"name" xorm:"UNIQUE(s) INDEX 'secret_name'"`
+ Owner string `json:"-" xorm:"NOT NULL DEFAULT '' UNIQUE(s) INDEX 'secret_owner'"`
+ RepoID int64 `json:"-" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_repo_id'"`
+ Name string `json:"name" xorm:"NOT NULL UNIQUE(s) INDEX 'secret_name'"`
Value string `json:"value,omitempty" xorm:"TEXT 'secret_value'"`
Images []string `json:"image" xorm:"json 'secret_images'"`
Events []WebhookEvent `json:"event" xorm:"json 'secret_events'"`
@@ -65,6 +83,16 @@ func (Secret) TableName() string {
return "secrets"
}
+// Global secret.
+func (s Secret) Global() bool {
+ return s.RepoID == 0 && s.Owner == ""
+}
+
+// Organization secret.
+func (s Secret) Organization() bool {
+ return s.RepoID == 0 && s.Owner != ""
+}
+
// Match returns true if an image and event match the restricted list.
func (s *Secret) Match(event WebhookEvent) bool {
if len(s.Events) == 0 {
@@ -119,6 +147,7 @@ func (s *Secret) Validate() error {
func (s *Secret) Copy() *Secret {
return &Secret{
ID: s.ID,
+ Owner: s.Owner,
RepoID: s.RepoID,
Name: s.Name,
Images: s.Images,
diff --git a/server/plugins/secrets/builtin.go b/server/plugins/secrets/builtin.go
index fab49585d0..67ae8476e1 100644
--- a/server/plugins/secrets/builtin.go
+++ b/server/plugins/secrets/builtin.go
@@ -21,11 +21,39 @@ func (b *builtin) SecretFind(repo *model.Repo, name string) (*model.Secret, erro
}
func (b *builtin) SecretList(repo *model.Repo) ([]*model.Secret, error) {
- return b.store.SecretList(repo)
+ return b.store.SecretList(repo, false)
}
func (b *builtin) SecretListBuild(repo *model.Repo, build *model.Build) ([]*model.Secret, error) {
- return b.store.SecretList(repo)
+ s, err := b.store.SecretList(repo, true)
+ 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 _, cond := range []struct {
+ Global bool
+ Organization bool
+ }{
+ {},
+ {Organization: true},
+ {Global: true},
+ } {
+ for _, secret := range s {
+ if secret.Global() == cond.Global && secret.Organization() == cond.Organization {
+ continue
+ }
+ if _, ok := uniq[secret.Name]; ok {
+ continue
+ }
+ uniq[secret.Name] = struct{}{}
+ secrets = append(secrets, secret)
+ }
+ }
+ return secrets, nil
}
func (b *builtin) SecretCreate(repo *model.Repo, in *model.Secret) error {
@@ -43,3 +71,51 @@ func (b *builtin) SecretDelete(repo *model.Repo, name string) error {
}
return b.store.SecretDelete(secret)
}
+
+func (b *builtin) OrgSecretFind(owner, name string) (*model.Secret, error) {
+ return b.store.OrgSecretFind(owner, name)
+}
+
+func (b *builtin) OrgSecretList(owner string) ([]*model.Secret, error) {
+ return b.store.OrgSecretList(owner)
+}
+
+func (b *builtin) OrgSecretCreate(owner string, in *model.Secret) error {
+ return b.store.SecretCreate(in)
+}
+
+func (b *builtin) OrgSecretUpdate(owner string, in *model.Secret) error {
+ return b.store.SecretUpdate(in)
+}
+
+func (b *builtin) OrgSecretDelete(owner, 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() ([]*model.Secret, error) {
+ return b.store.GlobalSecretList()
+}
+
+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/router/api.go b/server/router/api.go
index 467059de29..6dddc4ac34 100644
--- a/server/router/api.go
+++ b/server/router/api.go
@@ -43,6 +43,21 @@ func apiRoutes(e *gin.Engine) {
users.DELETE("/:login", api.DeleteUser)
}
+ orgBase := e.Group("/api/orgs/:owner")
+ {
+ orgBase.GET("/permissions", api.GetOrgPermissions)
+
+ org := orgBase.Group("")
+ {
+ org.Use(session.MustOrgMember(true))
+ org.GET("/secrets", api.GetOrgSecretList)
+ org.POST("/secrets", api.PostOrgSecret)
+ org.GET("/secrets/:secret", api.GetOrgSecret)
+ org.PATCH("/secrets/:secret", api.PatchOrgSecret)
+ org.DELETE("/secrets/:secret", api.DeleteOrgSecret)
+ }
+ }
+
repoBase := e.Group("/api/repos/:owner/:name")
{
repoBase.Use(session.SetRepo())
@@ -123,6 +138,16 @@ func apiRoutes(e *gin.Engine) {
queue.GET("/norunningbuilds", api.BlockTilQueueHasRunningItem)
}
+ secrets := e.Group("/api/secrets")
+ {
+ secrets.Use(session.MustAdmin())
+ secrets.GET("", api.GetGlobalSecretList)
+ secrets.POST("", api.PostGlobalSecret)
+ secrets.GET("/:secret", api.GetGlobalSecret)
+ secrets.PATCH("/:secret", api.PatchGlobalSecret)
+ secrets.DELETE("/:secret", api.DeleteGlobalSecret)
+ }
+
debugger := e.Group("/api/debug")
{
debugger.Use(session.MustAdmin())
diff --git a/server/store/datastore/migration/006_secrets_add_user.go b/server/store/datastore/migration/006_secrets_add_user.go
new file mode 100644
index 0000000000..0eca42f544
--- /dev/null
+++ b/server/store/datastore/migration/006_secrets_add_user.go
@@ -0,0 +1,46 @@
+// 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 migration
+
+import (
+ "xorm.io/xorm"
+)
+
+type SecretV006 struct {
+ Owner string `json:"-" xorm:"NOT NULL DEFAULT '' UNIQUE(s) INDEX 'secret_owner'"`
+ RepoID int64 `json:"-" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_repo_id'"`
+ Name string `json:"name" xorm:"NOT NULL UNIQUE(s) INDEX 'secret_name'"`
+}
+
+// TableName return database table name for xorm
+func (SecretV006) TableName() string {
+ return "secrets"
+}
+
+var alterTableSecretsAddUserCol = task{
+ name: "alter-table-add-secrets-user-id",
+ fn: func(sess *xorm.Session) error {
+ if err := sess.Sync2(new(SecretV006)); err != nil {
+ return err
+ }
+ if err := alterColumnDefault(sess, "secrets", "secret_repo_id", "0"); err != nil {
+ return err
+ }
+ if err := alterColumnNull(sess, "secrets", "secret_repo_id", false); err != nil {
+ return err
+ }
+ return alterColumnNull(sess, "secrets", "secret_name", false)
+ },
+}
diff --git a/server/store/datastore/migration/common.go b/server/store/datastore/migration/common.go
index 000df760a2..c3b6dc903b 100644
--- a/server/store/datastore/migration/common.go
+++ b/server/store/datastore/migration/common.go
@@ -212,6 +212,42 @@ func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
return nil
}
+func alterColumnDefault(sess *xorm.Session, table, column, defValue string) error {
+ dialect := sess.Engine().Dialect().URI().DBType
+ switch dialect {
+ case schemas.MYSQL:
+ _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` COLUMN `%s` SET DEFAULT %s;", table, column, defValue))
+ return err
+ case schemas.POSTGRES:
+ _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` SET DEFAULT %s;", table, column, defValue))
+ return err
+ case schemas.SQLITE:
+ return nil
+ default:
+ return fmt.Errorf("dialect '%s' not supported", dialect)
+ }
+}
+
+func alterColumnNull(sess *xorm.Session, table, column string, null bool) error {
+ val := "NULL"
+ if !null {
+ val = "NOT NULL"
+ }
+ dialect := sess.Engine().Dialect().URI().DBType
+ switch dialect {
+ case schemas.MYSQL:
+ _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` COLUMN `%s` SET %s;", table, column, val))
+ return err
+ case schemas.POSTGRES:
+ _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` SET %s;", table, column, val))
+ return err
+ case schemas.SQLITE:
+ return nil
+ default:
+ return fmt.Errorf("dialect '%s' not supported", dialect)
+ }
+}
+
var (
whitespaces = regexp.MustCompile(`\s+`)
columnSeparator = regexp.MustCompile(`\s?,\s?`)
diff --git a/server/store/datastore/migration/migration.go b/server/store/datastore/migration/migration.go
index bc5f6e814d..a9f13085a9 100644
--- a/server/store/datastore/migration/migration.go
+++ b/server/store/datastore/migration/migration.go
@@ -34,6 +34,7 @@ var migrationTasks = []*task{
&alterTableReposDropCounter,
&dropSenders,
&alterTableLogUpdateColumnLogDataType,
+ &alterTableSecretsAddUserCol,
}
var allBeans = []interface{}{
diff --git a/server/store/datastore/secret.go b/server/store/datastore/secret.go
index d64ad0e27a..00a7263b8a 100644
--- a/server/store/datastore/secret.go
+++ b/server/store/datastore/secret.go
@@ -16,6 +16,8 @@ package datastore
import (
"github.com/woodpecker-ci/woodpecker/server/model"
+
+ "xorm.io/builder"
)
func (s storage) SecretFind(repo *model.Repo, name string) (*model.Secret, error) {
@@ -26,9 +28,14 @@ func (s storage) SecretFind(repo *model.Repo, name string) (*model.Secret, error
return secret, wrapGet(s.engine.Get(secret))
}
-func (s storage) SecretList(repo *model.Repo) ([]*model.Secret, error) {
+func (s storage) SecretList(repo *model.Repo, all bool) ([]*model.Secret, error) {
secrets := make([]*model.Secret, 0, perPage)
- return secrets, s.engine.Where("secret_repo_id = ?", repo.ID).Find(&secrets)
+ var cond builder.Cond = builder.Eq{"secret_repo_id": repo.ID}
+ if all {
+ cond = cond.Or(builder.Eq{"secret_owner": repo.Owner}).
+ Or(builder.And(builder.Eq{"secret_owner": ""}, builder.Eq{"secret_repo_id": 0}))
+ }
+ return secrets, s.engine.Where(cond).Find(&secrets)
}
func (s storage) SecretCreate(secret *model.Secret) error {
@@ -46,3 +53,28 @@ func (s storage) SecretDelete(secret *model.Secret) error {
_, err := s.engine.ID(secret.ID).Delete(new(model.Secret))
return err
}
+
+func (s storage) OrgSecretFind(owner, name string) (*model.Secret, error) {
+ secret := &model.Secret{
+ Owner: owner,
+ Name: name,
+ }
+ return secret, wrapGet(s.engine.Get(secret))
+}
+
+func (s storage) OrgSecretList(owner string) ([]*model.Secret, error) {
+ secrets := make([]*model.Secret, 0, perPage)
+ return secrets, s.engine.Where("secret_owner = ?", owner).Find(&secrets)
+}
+
+func (s storage) GlobalSecretFind(name string) (*model.Secret, error) {
+ secret := &model.Secret{
+ Name: name,
+ }
+ return secret, wrapGet(s.engine.Where(builder.And(builder.Eq{"secret_owner": ""}, builder.Eq{"secret_repo_id": 0})).Get(secret))
+}
+
+func (s storage) GlobalSecretList() ([]*model.Secret, error) {
+ secrets := make([]*model.Secret, 0, perPage)
+ return secrets, s.engine.Where(builder.And(builder.Eq{"secret_owner": ""}, builder.Eq{"secret_repo_id": 0})).Find(&secrets)
+}
diff --git a/server/store/datastore/secret_test.go b/server/store/datastore/secret_test.go
index 0285a13cd7..66278569de 100644
--- a/server/store/datastore/secret_test.go
+++ b/server/store/datastore/secret_test.go
@@ -70,22 +70,24 @@ func TestSecretList(t *testing.T) {
store, closer := newTestStore(t, new(model.Secret))
defer closer()
- assert.NoError(t, store.SecretCreate(&model.Secret{
- RepoID: 1,
- Name: "foo",
- Value: "bar",
- }))
- assert.NoError(t, store.SecretCreate(&model.Secret{
- RepoID: 1,
- Name: "baz",
- Value: "qux",
- }))
+ createTestSecrets(t, store)
- list, err := store.SecretList(&model.Repo{ID: 1})
+ list, err := store.SecretList(&model.Repo{ID: 1, Owner: "org"}, false)
assert.NoError(t, err)
assert.Len(t, list, 2)
}
+func TestSecretBuildList(t *testing.T) {
+ store, closer := newTestStore(t, new(model.Secret))
+ defer closer()
+
+ createTestSecrets(t, store)
+
+ list, err := store.SecretList(&model.Repo{ID: 1, Owner: "org"}, true)
+ assert.NoError(t, err)
+ assert.Len(t, list, 4)
+}
+
func TestSecretUpdate(t *testing.T) {
store, closer := newTestStore(t, new(model.Secret))
defer closer()
@@ -162,3 +164,135 @@ func TestSecretIndexes(t *testing.T) {
t.Errorf("Unexpected error: duplicate name")
}
}
+
+func createTestSecrets(t *testing.T, store *storage) {
+ assert.NoError(t, store.SecretCreate(&model.Secret{
+ Owner: "org",
+ Name: "usr",
+ Value: "sec",
+ }))
+ assert.NoError(t, store.SecretCreate(&model.Secret{
+ RepoID: 1,
+ Name: "foo",
+ Value: "bar",
+ }))
+ assert.NoError(t, store.SecretCreate(&model.Secret{
+ RepoID: 1,
+ Name: "baz",
+ Value: "qux",
+ }))
+ assert.NoError(t, store.SecretCreate(&model.Secret{
+ Name: "global",
+ Value: "val",
+ }))
+}
+
+func TestOrgSecretFind(t *testing.T) {
+ store, closer := newTestStore(t, new(model.Secret))
+ defer closer()
+
+ err := store.SecretCreate(&model.Secret{
+ Owner: "org",
+ Name: "password",
+ Value: "correct-horse-battery-staple",
+ Images: []string{"golang", "node"},
+ Events: []model.WebhookEvent{"push", "tag"},
+ })
+ if err != nil {
+ t.Errorf("Unexpected error: insert secret: %s", err)
+ return
+ }
+
+ secret, err := store.OrgSecretFind("org", "password")
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if got, want := secret.Owner, "org"; got != want {
+ t.Errorf("Want owner %s, got %s", want, got)
+ }
+ if got, want := secret.Name, "password"; got != want {
+ t.Errorf("Want secret name %s, got %s", want, got)
+ }
+ if got, want := secret.Value, "correct-horse-battery-staple"; got != want {
+ t.Errorf("Want secret value %s, got %s", want, got)
+ }
+ if got, want := secret.Events[0], model.EventPush; got != want {
+ t.Errorf("Want secret event %s, got %s", want, got)
+ }
+ if got, want := secret.Events[1], model.EventTag; got != want {
+ t.Errorf("Want secret event %s, got %s", want, got)
+ }
+ if got, want := secret.Images[0], "golang"; got != want {
+ t.Errorf("Want secret image %s, got %s", want, got)
+ }
+ if got, want := secret.Images[1], "node"; got != want {
+ t.Errorf("Want secret image %s, got %s", want, got)
+ }
+}
+
+func TestOrgSecretList(t *testing.T) {
+ store, closer := newTestStore(t, new(model.Secret))
+ defer closer()
+
+ createTestSecrets(t, store)
+
+ list, err := store.OrgSecretList("org")
+ assert.NoError(t, err)
+ assert.Len(t, list, 1)
+
+ assert.True(t, list[0].Organization())
+}
+
+func TestGlobalSecretFind(t *testing.T) {
+ store, closer := newTestStore(t, new(model.Secret))
+ defer closer()
+
+ err := store.SecretCreate(&model.Secret{
+ Name: "password",
+ Value: "correct-horse-battery-staple",
+ Images: []string{"golang", "node"},
+ Events: []model.WebhookEvent{"push", "tag"},
+ })
+ if err != nil {
+ t.Errorf("Unexpected error: insert secret: %s", err)
+ return
+ }
+
+ secret, err := store.GlobalSecretFind("password")
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if got, want := secret.Name, "password"; got != want {
+ t.Errorf("Want secret name %s, got %s", want, got)
+ }
+ if got, want := secret.Value, "correct-horse-battery-staple"; got != want {
+ t.Errorf("Want secret value %s, got %s", want, got)
+ }
+ if got, want := secret.Events[0], model.EventPush; got != want {
+ t.Errorf("Want secret event %s, got %s", want, got)
+ }
+ if got, want := secret.Events[1], model.EventTag; got != want {
+ t.Errorf("Want secret event %s, got %s", want, got)
+ }
+ if got, want := secret.Images[0], "golang"; got != want {
+ t.Errorf("Want secret image %s, got %s", want, got)
+ }
+ if got, want := secret.Images[1], "node"; got != want {
+ t.Errorf("Want secret image %s, got %s", want, got)
+ }
+}
+
+func TestGlobalSecretList(t *testing.T) {
+ store, closer := newTestStore(t, new(model.Secret))
+ defer closer()
+
+ createTestSecrets(t, store)
+
+ list, err := store.GlobalSecretList()
+ assert.NoError(t, err)
+ assert.Len(t, list, 1)
+
+ assert.True(t, list[0].Global())
+}
diff --git a/server/store/store.go b/server/store/store.go
index 2279983bf6..5cd32b14e6 100644
--- a/server/store/store.go
+++ b/server/store/store.go
@@ -106,10 +106,14 @@ type Store interface {
// Secrets
SecretFind(*model.Repo, string) (*model.Secret, error)
- SecretList(*model.Repo) ([]*model.Secret, error)
+ SecretList(*model.Repo, bool) ([]*model.Secret, error)
SecretCreate(*model.Secret) error
SecretUpdate(*model.Secret) error
SecretDelete(*model.Secret) error
+ OrgSecretFind(string, string) (*model.Secret, error)
+ OrgSecretList(string) ([]*model.Secret, error)
+ GlobalSecretFind(string) (*model.Secret, error)
+ GlobalSecretList() ([]*model.Secret, error)
// Registrys
RegistryFind(*model.Repo, string) (*model.Registry, error)
diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json
index 1f3fc46b36..9bfd5bc6cc 100644
--- a/web/src/assets/locales/en.json
+++ b/web/src/assets/locales/en.json
@@ -205,6 +205,66 @@
}
},
+ "org": {
+ "settings": {
+ "settings": "Settings",
+ "not_allowed": "Not allowed to access this organization's settings",
+
+ "secrets": {
+ "secrets": "Secrets",
+ "desc": "Organization secrets can be passed to all organization's repository individual pipeline steps at runtime as environmental variables.",
+ "none": "There are no organization secrets yet.",
+ "add": "Add secret",
+ "save": "Save secret",
+ "show": "Show secrets",
+ "name": "Name",
+ "value": "Value",
+ "deleted": "Organization secret deleted",
+ "created": "Organization secret created",
+ "saved": "Organization secret saved",
+
+ "images": {
+ "images": "Available for following images",
+ "desc": "Comma separated list of images where this secret is available, leave empty to allow all images"
+ },
+ "events": {
+ "events": "Available at following events",
+ "pr_warning": "Please be careful with this option as a bad actor can submit a malicious pull request that exposes your secrets."
+ }
+ }
+ }
+ },
+
+ "admin": {
+ "settings": {
+ "settings": "Settings",
+ "not_allowed": "Not allowed to access server settings",
+
+ "secrets": {
+ "secrets": "Secrets",
+ "desc": "Global secrets can be passed to all repositories individual pipeline steps at runtime as environmental variables.",
+ "none": "There are no global secrets yet.",
+ "add": "Add secret",
+ "save": "Save secret",
+ "show": "Show secrets",
+ "name": "Name",
+ "value": "Value",
+ "deleted": "Global secret deleted",
+ "created": "Global secret created",
+ "saved": "Global secret saved",
+
+ "images": {
+ "images": "Available for following images",
+ "desc": "Comma separated list of images where this secret is available, leave empty to allow all images"
+ },
+ "events": {
+ "events": "Available at following events",
+ "pr_warning": "Please be careful with this option as a bad actor can submit a malicious pull request that exposes your secrets."
+ }
+ }
+ }
+ },
+
"user": {
"oauth_error": "Error while authenticating against OAuth provider",
"internal_error": "Some internal error occurred",
diff --git a/web/src/assets/locales/lv.json b/web/src/assets/locales/lv.json
index 798028f302..e9c3e8f45b 100644
--- a/web/src/assets/locales/lv.json
+++ b/web/src/assets/locales/lv.json
@@ -205,6 +205,66 @@
}
},
+ "org": {
+ "settings": {
+ "settings": "Iestatījumi",
+ "not_allowed": "Nav piekļuves šīs organizācijas iestatījumiem",
+
+ "secrets": {
+ "secrets": "Noslēpumi",
+ "desc": "Noslēpumus var padot visu organizācijas repozitoriju individuāliem konvejerdarba soļiem izpildes laikā kā vides mainīgos.",
+ "none": "Pagaidām nav neviena organizācijas noslēpuma.",
+ "add": "Pievienot noslēpumu",
+ "save": "Saglabāt noslēpumu",
+ "show": "Noslēpumu saraksts",
+ "name": "Nosaukums",
+ "value": "Vērtība",
+ "deleted": "Organizācijas noslēpums dzēsts",
+ "created": "Organizācijas noslēpums izveidots",
+ "saved": "Organizācijas noslēpums saglabāts",
+
+ "images": {
+ "images": "Pieejami šādiem attēliem",
+ "desc": "Ar komatiem atdalīts saraksts ar attēliem, kam šis noslēpums būs pieejams, atstājot tukšu, tas būs pieejams visiem attēliem."
+ },
+ "events": {
+ "events": "Pieejams šādiem notikumiem",
+ "pr_warning": "Uzmanieties, jo šādā veidā tas būs pieejams visiem cilvēkiem, kas var iesūtīt izmaiņu pieprasījumu!"
+ }
+ }
+ }
+ },
+
+ "admin": {
+ "settings": {
+ "settings": "Iestatījumi",
+ "not_allowed": "Nav piekļuves servera iestatījumiem",
+
+ "secrets": {
+ "secrets": "Noslēpumi",
+ "desc": "Noslēpumus var padot visu repozitoriju individuāliem konvejerdarba soļiem izpildes laikā kā vides mainīgos.",
+ "none": "Pagaidām nav neviena globālā noslēpuma.",
+ "add": "Pievienot noslēpumu",
+ "save": "Saglabāt noslēpumu",
+ "show": "Noslēpumu saraksts",
+ "name": "Nosaukums",
+ "value": "Vērtība",
+ "deleted": "Globālais noslēpums dzēsts",
+ "created": "Globālais noslēpums izveidots",
+ "saved": "Globālais noslēpums saglabāts",
+
+ "images": {
+ "images": "Pieejami šādiem attēliem",
+ "desc": "Ar komatiem atdalīts saraksts ar attēliem, kam šis noslēpums būs pieejams, atstājot tukšu, tas būs pieejams visiem attēliem."
+ },
+ "events": {
+ "events": "Pieejams šādiem notikumiem",
+ "pr_warning": "Uzmanieties, jo šādā veidā tas būs pieejams visiem cilvēkiem, kas var iesūtīt izmaiņu pieprasījumu!"
+ }
+ }
+ }
+ },
+
"user": {
"oauth_error": "Neizdevās autorizēties, izmantojot, OAuth piegādātāju",
"internal_error": "Notikusi sistēmas iekšējā kļūda",
diff --git a/web/src/components/admin/settings/AdminSecretsTab.vue b/web/src/components/admin/settings/AdminSecretsTab.vue
new file mode 100644
index 0000000000..4cd2735151
--- /dev/null
+++ b/web/src/components/admin/settings/AdminSecretsTab.vue
@@ -0,0 +1,140 @@
+
+
+ {{ $t('admin.settings.secrets.desc') }}
+
+ {{ $t('org.settings.secrets.desc') }}
+ {{ $t('admin.settings.secrets.secrets') }}
+ {{ $t('org.settings.secrets.secrets') }}
+