diff --git a/Makefile b/Makefile index a9656a045..21daf5f92 100644 --- a/Makefile +++ b/Makefile @@ -139,7 +139,7 @@ ui-dependencies: ## Install UI dependencies .PHONY: lint lint: install-tools ## Lint code @echo "Running golangci-lint" - golangci-lint run --timeout 10m + golangci-lint run --timeout 15m @echo "Running zerolog linter" lint github.com/woodpecker-ci/woodpecker/cmd/agent lint github.com/woodpecker-ci/woodpecker/cmd/cli diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 3a85b9f8e..6155f23ee 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -4197,6 +4197,10 @@ const docTemplate = `{ "login": { "description": "Login is the username for this user.\n\nrequired: true", "type": "string" + }, + "org_id": { + "description": "OrgID is the of the user as model.Org.", + "type": "integer" } } }, diff --git a/server/api/org.go b/server/api/org.go index 3c9703c98..e84dbf262 100644 --- a/server/api/org.go +++ b/server/api/org.go @@ -91,12 +91,15 @@ func GetOrgPermissions(c *gin.Context) { return } - if (org.IsUser && org.Name == user.Login) || user.Admin { + if (org.IsUser && org.Name == user.Login) || (user.Admin && !org.IsUser) { c.JSON(http.StatusOK, &model.OrgPerm{ Member: true, Admin: true, }) return + } else if org.IsUser { + c.JSON(http.StatusOK, &model.OrgPerm{}) + return } perm, err := server.Config.Services.Membership.Get(c, user, org.Name) diff --git a/server/model/user.go b/server/model/user.go index 05b665819..e74b7e1ae 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -64,6 +64,9 @@ type User struct { // Hash is a unique token used to sign tokens. Hash string `json:"-" xorm:"UNIQUE varchar(500) 'user_hash'"` + + // OrgID is the of the user as model.Org. + OrgID int64 `json:"org_id" xorm:"user_org_id"` } // @name User // TableName return database table name for xorm diff --git a/server/store/datastore/feed_test.go b/server/store/datastore/feed_test.go index f92c6bc43..a68e3437c 100644 --- a/server/store/datastore/feed_test.go +++ b/server/store/datastore/feed_test.go @@ -23,7 +23,7 @@ import ( ) func TestGetPipelineQueue(t *testing.T) { - store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline)) + store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline), new(model.Org)) defer closer() user := &model.User{ @@ -64,7 +64,7 @@ func TestGetPipelineQueue(t *testing.T) { } func TestUserFeed(t *testing.T) { - store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline)) + store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline), new(model.Org)) defer closer() user := &model.User{ @@ -115,7 +115,7 @@ func TestUserFeed(t *testing.T) { } func TestRepoListLatest(t *testing.T) { - store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline)) + store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline), new(model.Org)) defer closer() user := &model.User{ diff --git a/server/store/datastore/migration/022_add_org_id.go b/server/store/datastore/migration/022_add_org_id.go new file mode 100644 index 000000000..7f9b61619 --- /dev/null +++ b/server/store/datastore/migration/022_add_org_id.go @@ -0,0 +1,61 @@ +// 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 ( + "fmt" + + "xorm.io/xorm" + + "github.com/woodpecker-ci/woodpecker/server/model" +) + +var addOrgID = task{ + name: "add-org-id", + required: true, + fn: func(sess *xorm.Session) error { + if err := sess.Sync(new(model.User)); err != nil { + return fmt.Errorf("sync new models failed: %w", err) + } + + // get all users + var users []*model.User + if err := sess.Find(&users); err != nil { + return fmt.Errorf("find all repos failed: %w", err) + } + + for _, user := range users { + org := &model.Org{} + has, err := sess.Where("name = ?", user.Login).Get(org) + if err != nil { + return fmt.Errorf("getting org failed: %w", err) + } else if !has { + org = &model.Org{ + Name: user.Login, + IsUser: true, + } + if _, err := sess.Insert(org); err != nil { + return fmt.Errorf("inserting org failed: %w", err) + } + } + user.OrgID = org.ID + if _, err := sess.Cols("user_org_id").Update(user); err != nil { + return fmt.Errorf("updating user failed: %w", err) + } + } + + return dropTableColumns(sess, "secrets", "secret_owner") + }, +} diff --git a/server/store/datastore/migration/migration.go b/server/store/datastore/migration/migration.go index 1057c2f5f..a4a663cbf 100644 --- a/server/store/datastore/migration/migration.go +++ b/server/store/datastore/migration/migration.go @@ -54,6 +54,7 @@ var migrationTasks = []*task{ &migrateLogs2LogEntries, &parentStepsToWorkflows, &addOrgs, + &addOrgID, } var allBeans = []interface{}{ diff --git a/server/store/datastore/org.go b/server/store/datastore/org.go index 232e38321..f8c7673b4 100644 --- a/server/store/datastore/org.go +++ b/server/store/datastore/org.go @@ -18,13 +18,18 @@ import ( "strings" "github.com/woodpecker-ci/woodpecker/server/model" + "xorm.io/xorm" ) func (s storage) OrgCreate(org *model.Org) error { + return s.orgCreate(org, s.engine.NewSession()) +} + +func (s storage) orgCreate(org *model.Org, sess *xorm.Session) error { // sanitize org.Name = strings.ToLower(org.Name) // insert - _, err := s.engine.Insert(org) + _, err := sess.Insert(org) return err } diff --git a/server/store/datastore/repo_test.go b/server/store/datastore/repo_test.go index 58c2677dc..1058c37f0 100644 --- a/server/store/datastore/repo_test.go +++ b/server/store/datastore/repo_test.go @@ -140,7 +140,7 @@ func TestRepos(t *testing.T) { } func TestRepoList(t *testing.T) { - store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm)) + store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Org)) defer closer() user := &model.User{ @@ -196,7 +196,7 @@ func TestRepoList(t *testing.T) { } func TestOwnedRepoList(t *testing.T) { - store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm)) + store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Org)) defer closer() user := &model.User{ diff --git a/server/store/datastore/user.go b/server/store/datastore/user.go index d21599b4c..a3efcd40c 100644 --- a/server/store/datastore/user.go +++ b/server/store/datastore/user.go @@ -53,8 +53,18 @@ func (s storage) GetUserCount() (int64, error) { } func (s storage) CreateUser(user *model.User) error { + sess := s.engine.NewSession() + org := &model.Org{ + Name: user.Login, + IsUser: true, + } + err := s.orgCreate(org, sess) + if err != nil { + return err + } + user.OrgID = org.ID // only Insert set auto created ID back to object - _, err := s.engine.Insert(user) + _, err = sess.Insert(user) return err } diff --git a/server/store/datastore/users_test.go b/server/store/datastore/users_test.go index a1cb1576e..6779b2398 100644 --- a/server/store/datastore/users_test.go +++ b/server/store/datastore/users_test.go @@ -24,7 +24,7 @@ import ( ) func TestUsers(t *testing.T) { - store, closer := newTestStore(t, new(model.User), new(model.Repo), new(model.Pipeline), new(model.Step), new(model.Perm)) + store, closer := newTestStore(t, new(model.User), new(model.Repo), new(model.Pipeline), new(model.Step), new(model.Perm), new(model.Org)) defer closer() g := goblin.Goblin(t) @@ -40,6 +40,8 @@ func TestUsers(t *testing.T) { g.Assert(err).IsNil() _, err = store.engine.Exec("DELETE FROM steps") g.Assert(err).IsNil() + _, err = store.engine.Exec("DELETE FROM orgs") + g.Assert(err).IsNil() }) g.It("Should Update a User", func() { diff --git a/web/components.d.ts b/web/components.d.ts index 29d32f4c0..38d4ab200 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -104,6 +104,7 @@ declare module '@vue/runtime-core' { TextField: typeof import('./src/components/form/TextField.vue')['default'] UserAPITab: typeof import('./src/components/user/UserAPITab.vue')['default'] UserGeneralTab: typeof import('./src/components/user/UserGeneralTab.vue')['default'] + UserSecretsTab: typeof import('./src/components/user/UserSecretsTab.vue')['default'] Warning: typeof import('./src/components/atomic/Warning.vue')['default'] } } diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 6bb5ebdfd..0ada0bbac 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -431,6 +431,28 @@ "general": "General", "language": "Language" }, + "secrets": { + "secrets": "Secrets", + "desc": "User secrets can be passed to all user's repository individual pipeline steps at runtime as environmental variables.", + "none": "There are no user secrets yet.", + "add": "Add secret", + "save": "Save secret", + "show": "Show secrets", + "name": "Name", + "value": "Value", + "deleted": "User secret deleted", + "created": "User secret created", + "saved": "User 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" + }, + "plugins_only": "Only available for plugins", + "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." + } + }, "api": { "api": "API", "desc": "Personal Access Token and API usage", diff --git a/web/src/components/user/UserSecretsTab.vue b/web/src/components/user/UserSecretsTab.vue new file mode 100644 index 000000000..e1e1d161b --- /dev/null +++ b/web/src/components/user/UserSecretsTab.vue @@ -0,0 +1,117 @@ + + + diff --git a/web/src/lib/api/types/user.ts b/web/src/lib/api/types/user.ts index 3649190e7..81a627059 100644 --- a/web/src/lib/api/types/user.ts +++ b/web/src/lib/api/types/user.ts @@ -17,4 +17,7 @@ export type User = { active: boolean; // Whether the account is currently active. + + org_id: number; + // The ID of the org assigned to the user. }; diff --git a/web/src/views/User.vue b/web/src/views/User.vue index a8fb046b9..14cac0619 100644 --- a/web/src/views/User.vue +++ b/web/src/views/User.vue @@ -5,6 +5,9 @@ + + + @@ -16,6 +19,7 @@ import Scaffold from '~/components/layout/scaffold/Scaffold.vue'; import Tab from '~/components/layout/scaffold/Tab.vue'; import UserAPITab from '~/components/user/UserAPITab.vue'; import UserGeneralTab from '~/components/user/UserGeneralTab.vue'; +import UserSecretsTab from '~/components/user/UserSecretsTab.vue'; import useConfig from '~/compositions/useConfig'; const address = `${window.location.protocol}//${window.location.host}${useConfig().rootPath}`; // port is included in location.host diff --git a/web/src/views/org/OrgRepos.vue b/web/src/views/org/OrgRepos.vue index ca96403a8..2d9adf7f8 100644 --- a/web/src/views/org/OrgRepos.vue +++ b/web/src/views/org/OrgRepos.vue @@ -6,9 +6,9 @@ diff --git a/web/src/views/org/OrgWrapper.vue b/web/src/views/org/OrgWrapper.vue index f7436feac..b78c72c2d 100644 --- a/web/src/views/org/OrgWrapper.vue +++ b/web/src/views/org/OrgWrapper.vue @@ -6,8 +6,8 @@