diff --git a/cmd/server/server.go b/cmd/server/server.go index 26887c3495..fae4f9b217 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -43,6 +43,7 @@ import ( "go.woodpecker-ci.org/woodpecker/server/logging" "go.woodpecker-ci.org/woodpecker/server/model" "go.woodpecker-ci.org/woodpecker/server/plugins/config" + "go.woodpecker-ci.org/woodpecker/server/plugins/permissions" "go.woodpecker-ci.org/woodpecker/server/pubsub" "go.woodpecker-ci.org/woodpecker/server/router" "go.woodpecker-ci.org/woodpecker/server/router/middleware" @@ -177,7 +178,6 @@ func run(c *cli.Context) error { webUIServe, middleware.Logger(time.RFC3339, true), middleware.Version, - middleware.Config(c), middleware.Store(c, _store), ) @@ -367,4 +367,10 @@ func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) { // prometheus server.Config.Prometheus.AuthToken = c.String("prometheus-auth-token") + + // permissions + server.Config.Permissions.Open = c.Bool("open") + server.Config.Permissions.Admins = permissions.NewAdmins(c.StringSlice("admin")) + server.Config.Permissions.Orgs = permissions.NewOrgs(c.StringSlice("orgs")) + server.Config.Permissions.OwnersAllowlist = permissions.NewOwnersAllowlist(c.StringSlice("repo-owners")) } diff --git a/server/api/login.go b/server/api/login.go index 77b3acbb72..ae95e67c4c 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -26,7 +26,6 @@ import ( "go.woodpecker-ci.org/woodpecker/server" "go.woodpecker-ci.org/woodpecker/server/model" - "go.woodpecker-ci.org/woodpecker/server/router/middleware" "go.woodpecker-ci.org/woodpecker/server/store" "go.woodpecker-ci.org/woodpecker/server/store/types" "go.woodpecker-ci.org/woodpecker/shared/httputil" @@ -60,7 +59,6 @@ func HandleAuth(c *gin.Context) { if tmpuser == nil { return } - config := middleware.GetConfig(c) // get the user from the database u, err := _store.GetUserRemoteID(tmpuser.ForgeRemoteID, tmpuser.Login) @@ -71,17 +69,17 @@ func HandleAuth(c *gin.Context) { } // if self-registration is disabled we should return a not authorized error - if !config.Open && !config.IsAdmin(tmpuser) { + if !server.Config.Permissions.Open && !server.Config.Permissions.Admins.IsAdmin(tmpuser) { log.Error().Msgf("cannot register %s. registration closed", tmpuser.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied") return } - // if self-registration is enabled for whitelisted organizations we need to + // if self-registration is enabled for allowed organizations we need to // check the user's organization membership. - if len(config.Orgs) != 0 { + if server.Config.Permissions.Orgs.IsConfigured { teams, terr := _forge.Teams(c, tmpuser) - if terr != nil || !config.IsMember(teams) { + if terr != nil || !server.Config.Permissions.Orgs.IsMember(teams) { log.Error().Err(terr).Msgf("cannot verify team membership for %s.", u.Login) c.Redirect(303, server.Config.Server.RootPath+"/login?error=access_denied") return @@ -125,13 +123,13 @@ func HandleAuth(c *gin.Context) { u.Avatar = tmpuser.Avatar u.ForgeRemoteID = tmpuser.ForgeRemoteID u.Login = tmpuser.Login - u.Admin = u.Admin || config.IsAdmin(tmpuser) + u.Admin = u.Admin || server.Config.Permissions.Admins.IsAdmin(tmpuser) - // if self-registration is enabled for whitelisted organizations we need to + // if self-registration is enabled for allowed organizations we need to // check the user's organization membership. - if len(config.Orgs) != 0 { + if server.Config.Permissions.Orgs.IsConfigured { teams, terr := _forge.Teams(c, u) - if terr != nil || !config.IsMember(teams) { + if terr != nil || !server.Config.Permissions.Orgs.IsMember(teams) { log.Error().Err(terr).Msgf("cannot verify team membership for %s.", u.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied") return diff --git a/server/api/repo.go b/server/api/repo.go index 4530b54e0f..9d7d4f3eb9 100644 --- a/server/api/repo.go +++ b/server/api/repo.go @@ -72,6 +72,11 @@ func PostRepo(c *gin.Context) { } if !from.Perm.Admin { c.String(http.StatusForbidden, "User has to be a admin of this repository") + return + } + if !server.Config.Permissions.OwnersAllowlist.IsAllowed(from) { + c.String(http.StatusForbidden, "Repo owner is not allowed") + return } if enabledOnce { diff --git a/server/api/user.go b/server/api/user.go index c39014d7c7..8c85df89d1 100644 --- a/server/api/user.go +++ b/server/api/user.go @@ -111,14 +111,17 @@ func GetRepos(c *gin.Context) { var repos []*model.Repo for _, r := range _repos { - if r.Perm.Push { + if r.Perm.Push && server.Config.Permissions.OwnersAllowlist.IsAllowed(r) { if active[r.ForgeRemoteID] != nil { existingRepo := active[r.ForgeRemoteID] existingRepo.Update(r) existingRepo.IsActive = active[r.ForgeRemoteID].IsActive repos = append(repos, existingRepo) } else { - repos = append(repos, r) + if r.Perm.Admin { + // you must be admin to enable the repo + repos = append(repos, r) + } } } } diff --git a/server/config.go b/server/config.go index 9dc7a69639..af381b7a99 100644 --- a/server/config.go +++ b/server/config.go @@ -26,6 +26,7 @@ import ( "go.woodpecker-ci.org/woodpecker/server/logging" "go.woodpecker-ci.org/woodpecker/server/model" "go.woodpecker-ci.org/woodpecker/server/plugins/config" + "go.woodpecker-ci.org/woodpecker/server/plugins/permissions" "go.woodpecker-ci.org/woodpecker/server/pubsub" "go.woodpecker-ci.org/woodpecker/server/queue" ) @@ -96,4 +97,10 @@ var Config = struct { HTTPS string } } + Permissions struct { + Open bool + Admins *permissions.Admins + Orgs *permissions.Orgs + OwnersAllowlist *permissions.OwnersAllowlist + } }{} diff --git a/server/model/settings.go b/server/model/settings.go deleted file mode 100644 index d82cbf7ae0..0000000000 --- a/server/model/settings.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2018 Drone.IO Inc. -// -// 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 model - -// Settings defines system configuration parameters. -type Settings struct { - Open bool // Enables open registration - Admins map[string]bool // Administrative users - Orgs map[string]bool // Organization whitelist - OwnersWhitelist map[string]bool // Owners whitelist -} - -// IsAdmin returns true if the user is a member of the administrator list. -func (c *Settings) IsAdmin(user *User) bool { - return c.Admins[user.Login] -} - -// IsMember returns true if the user is a member of the whitelisted teams. -func (c *Settings) IsMember(teams []*Team) bool { - for _, team := range teams { - if c.Orgs[team.Login] { - return true - } - } - return false -} diff --git a/server/plugins/permissions/admin.go b/server/plugins/permissions/admin.go new file mode 100644 index 0000000000..1156ddef02 --- /dev/null +++ b/server/plugins/permissions/admin.go @@ -0,0 +1,18 @@ +package permissions + +import ( + "go.woodpecker-ci.org/woodpecker/server/model" + "go.woodpecker-ci.org/woodpecker/shared/utils" +) + +func NewAdmins(admins []string) *Admins { + return &Admins{admins: utils.SliceToBoolMap(admins)} +} + +type Admins struct { + admins map[string]bool +} + +func (a *Admins) IsAdmin(user *model.User) bool { + return a.admins[user.Login] +} diff --git a/server/plugins/permissions/orgs.go b/server/plugins/permissions/orgs.go new file mode 100644 index 0000000000..b950db9d7c --- /dev/null +++ b/server/plugins/permissions/orgs.go @@ -0,0 +1,27 @@ +package permissions + +import ( + "go.woodpecker-ci.org/woodpecker/server/model" + "go.woodpecker-ci.org/woodpecker/shared/utils" +) + +func NewOrgs(orgs []string) *Orgs { + return &Orgs{ + IsConfigured: len(orgs) > 0, + orgs: utils.SliceToBoolMap(orgs), + } +} + +type Orgs struct { + IsConfigured bool + orgs map[string]bool +} + +func (o *Orgs) IsMember(teams []*model.Team) bool { + for _, team := range teams { + if o.orgs[team.Login] { + return true + } + } + return false +} diff --git a/server/plugins/permissions/repo_owners.go b/server/plugins/permissions/repo_owners.go new file mode 100644 index 0000000000..a26f543420 --- /dev/null +++ b/server/plugins/permissions/repo_owners.go @@ -0,0 +1,18 @@ +package permissions + +import ( + "go.woodpecker-ci.org/woodpecker/server/model" + "go.woodpecker-ci.org/woodpecker/shared/utils" +) + +func NewOwnersAllowlist(owners []string) *OwnersAllowlist { + return &OwnersAllowlist{owners: utils.SliceToBoolMap(owners)} +} + +type OwnersAllowlist struct { + owners map[string]bool +} + +func (o *OwnersAllowlist) IsAllowed(repo *model.Repo) bool { + return len(o.owners) > 0 && o.owners[repo.Owner] +} diff --git a/server/router/middleware/config.go b/server/router/middleware/config.go deleted file mode 100644 index 07b7b2419f..0000000000 --- a/server/router/middleware/config.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2018 Drone.IO Inc. -// -// 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 middleware - -import ( - "github.com/gin-gonic/gin" - "github.com/urfave/cli/v2" - - "go.woodpecker-ci.org/woodpecker/server/model" -) - -const configKey = "config" - -// Config is a middleware function that initializes the Configuration and -// attaches to the context of every http.Request. -func Config(cli *cli.Context) gin.HandlerFunc { - v := setupConfig(cli) - return func(c *gin.Context) { - c.Set(configKey, v) - } -} - -// helper function to create the configuration from the CLI context. -func setupConfig(c *cli.Context) *model.Settings { - return &model.Settings{ - Open: c.Bool("open"), - Admins: sliceToMap2(c.StringSlice("admin")), - Orgs: sliceToMap2(c.StringSlice("orgs")), - OwnersWhitelist: sliceToMap2(c.StringSlice("repo-owners")), - } -} - -// helper function to convert a string slice to a map. -func sliceToMap2(s []string) map[string]bool { - v := map[string]bool{} - for _, ss := range s { - if ss == "" { - continue - } - v[ss] = true - } - return v -} - -// GetConfig returns the config from the Context -func GetConfig(c *gin.Context) *model.Settings { - v := c.MustGet(configKey) - return v.(*model.Settings) -} diff --git a/shared/utils/slices.go b/shared/utils/slices.go index 5aa6a832da..b2f64eb8b8 100644 --- a/shared/utils/slices.go +++ b/shared/utils/slices.go @@ -57,3 +57,15 @@ func sliceToCountMap[E comparable](list []E) map[E]int { } return m } + +// sliceToMap is a helper function to convert a string slice to a map. +func SliceToBoolMap(s []string) map[string]bool { + v := map[string]bool{} + for _, ss := range s { + if ss == "" { + continue + } + v[ss] = true + } + return v +}