From a015f4dff05e8c53e19af012891b3832bb349729 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Tue, 19 Jul 2022 14:34:08 +0300 Subject: [PATCH 1/8] Add remote method to check organization membership --- server/remote/bitbucket/bitbucket.go | 10 ++++ server/remote/bitbucket/internal/client.go | 23 ++++++++ server/remote/bitbucket/internal/types.go | 13 +++++ .../remote/bitbucketserver/bitbucketserver.go | 7 +++ server/remote/coding/coding.go | 7 +++ server/remote/gitea/gitea.go | 36 ++++++++++++ server/remote/github/github.go | 12 ++++ server/remote/gitlab/gitlab.go | 56 +++++++++++++++++++ server/remote/gogs/gogs.go | 19 +++++++ server/remote/mocks/remote.go | 28 ++++++++++ server/remote/remote.go | 4 ++ 11 files changed, 215 insertions(+) diff --git a/server/remote/bitbucket/bitbucket.go b/server/remote/bitbucket/bitbucket.go index 6769c41801..ab835a4b7f 100644 --- a/server/remote/bitbucket/bitbucket.go +++ b/server/remote/bitbucket/bitbucket.go @@ -301,6 +301,16 @@ func (c *config) Hook(ctx context.Context, req *http.Request) (*model.Repo, *mod return parseHook(req) } +// OrgMembership returns if user is member of organization and if user +// is admin/owner in this organization. +func (c *config) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { + perm, err := c.newClient(ctx, u).GetUserWorkspaceMembership(owner, u.Login) + if err != nil { + return false, false, err + } + return perm != "", perm == "owner", nil +} + // helper function to return the bitbucket oauth2 client func (c *config) newClient(ctx context.Context, u *model.User) *internal.Client { if u == nil { diff --git a/server/remote/bitbucket/internal/client.go b/server/remote/bitbucket/internal/client.go index 0f81e379d9..c935f2cbe8 100644 --- a/server/remote/bitbucket/internal/client.go +++ b/server/remote/bitbucket/internal/client.go @@ -46,6 +46,7 @@ const ( pathSource = "%s/2.0/repositories/%s/%s/src/%s/%s" pathStatus = "%s/2.0/repositories/%s/%s/commit/%s/statuses/build" pathBranches = "%s/2.0/repositories/%s/%s/refs/branches" + pathOrgPerms = "%s/2.0/workspaces/%s/permissions?%s" ) type Client struct { @@ -182,6 +183,28 @@ func (c *Client) ListBranches(owner, name string) ([]*Branch, error) { return out.Values, err } +func (c *Client) GetUserWorkspaceMembership(workspace, user string) (string, error) { + out := new(WorkspaceMembershipResp) + opts := &ListOpts{Page: 1, PageLen: 100} + for { + uri := fmt.Sprintf(pathOrgPerms, c.base, workspace, opts.Encode()) + _, err := c.do(uri, get, nil, out) + if err != nil { + return "", err + } + for _, m := range out.Values { + if m.User.Nickname == user { + return m.Permission, nil + } + } + if len(out.Next) == 0 { + break + } + opts.Page++ + } + return "", nil +} + func (c *Client) do(rawurl, method string, in, out interface{}) (*string, error) { uri, err := url.Parse(rawurl) if err != nil { diff --git a/server/remote/bitbucket/internal/types.go b/server/remote/bitbucket/internal/types.go index 8039625859..9327b5c215 100644 --- a/server/remote/bitbucket/internal/types.go +++ b/server/remote/bitbucket/internal/types.go @@ -171,6 +171,19 @@ type PullRequestHook struct { } `json:"pullrequest"` } +type WorkspaceMembershipResp struct { + Page int `json:"page"` + Pages int `json:"pagelen"` + Size int `json:"size"` + Next string `json:"next"` + Values []struct { + Permission string `json:"permission"` + User struct { + Nickname string `json:"nickname"` + } + } `json:"values"` +} + type ListOpts struct { Page int PageLen int diff --git a/server/remote/bitbucketserver/bitbucketserver.go b/server/remote/bitbucketserver/bitbucketserver.go index 5f395bb64b..9a85c7de85 100644 --- a/server/remote/bitbucketserver/bitbucketserver.go +++ b/server/remote/bitbucketserver/bitbucketserver.go @@ -245,6 +245,13 @@ func (c *Config) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model return parseHook(r, c.URL) } +// OrgMembership returns if user is member of organization and if user +// is admin/owner in this organization. +func (c *Config) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { + // TODO: Not implemented currently + return false, false, nil +} + func CreateConsumer(URL, ConsumerKey string, PrivateKey *rsa.PrivateKey) *oauth.Consumer { consumer := oauth.NewRSAConsumer( ConsumerKey, diff --git a/server/remote/coding/coding.go b/server/remote/coding/coding.go index d5882d075d..232d016a3f 100644 --- a/server/remote/coding/coding.go +++ b/server/remote/coding/coding.go @@ -301,6 +301,13 @@ func (c *Coding) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model return repo, build, err } +// OrgMembership returns if user is member of organization and if user +// is admin/owner in this organization. +func (c *Coding) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { + // TODO: Not supported in Coding OAuth API + return false, false, nil +} + // helper function to return the Coding oauth2 context using an HTTPClient that // disables TLS verification if disabled in the remote settings. func (c *Coding) newContext(ctx context.Context) context.Context { diff --git a/server/remote/gitea/gitea.go b/server/remote/gitea/gitea.go index 33af4e266f..93a5687f4a 100644 --- a/server/remote/gitea/gitea.go +++ b/server/remote/gitea/gitea.go @@ -20,7 +20,9 @@ package gitea import ( "context" "crypto/tls" + "encoding/json" "fmt" + "io" "net" "net/http" "net/url" @@ -458,6 +460,40 @@ func (c *Gitea) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model. return parseHook(r) } +// OrgMembership returns if user is member of organization and if user +// is admin/owner in this organization. +func (c *Gitea) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { + client, err := c.newClientToken(ctx, u.Token) + if err != nil { + return false, false, err + } + + member, resp, err := client.CheckOrgMembership(owner, u.Login) + if err != nil { + return false, false, err + } + + if !member { + return false, false, nil + } + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return member, false, err + } + + p := struct { + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + }{} + + if err := json.Unmarshal(buf, &p); err != nil { + return member, false, err + } + + return member, p.IsAdmin || p.IsOwner, nil +} + // helper function to return the Gitea client with Token func (c *Gitea) newClientToken(ctx context.Context, token string) (*gitea.Client, error) { httpClient := &http.Client{} diff --git a/server/remote/github/github.go b/server/remote/github/github.go index 96c0163107..0a1807449e 100644 --- a/server/remote/github/github.go +++ b/server/remote/github/github.go @@ -305,6 +305,18 @@ func (c *client) Deactivate(ctx context.Context, u *model.User, r *model.Repo, l return err } +// OrgMembership returns if user is member of organization and if user +// is admin/owner in this organization. +func (c *client) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { + client := c.newClientToken(ctx, u.Token) + org, _, err := client.Organizations.GetOrgMembership(ctx, u.Login, owner) + if err != nil { + return false, false, err + } + + return org.GetState() == "active", org.GetRole() == "admin", nil +} + // helper function to return the GitHub oauth2 context using an HTTPClient that // disables TLS verification if disabled in the remote settings. func (c *client) newContext(ctx context.Context) context.Context { diff --git a/server/remote/gitlab/gitlab.go b/server/remote/gitlab/gitlab.go index ebe7427e5a..ddabbdd1d9 100644 --- a/server/remote/gitlab/gitlab.go +++ b/server/remote/gitlab/gitlab.go @@ -554,6 +554,62 @@ func (g *Gitlab) Hook(ctx context.Context, req *http.Request) (*model.Repo, *mod } } +// OrgMembership returns if user is member of organization and if user +// is admin/owner in this organization. +func (g *Gitlab) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { + client, err := newClient(g.URL, u.Token, g.SkipVerify) + if err != nil { + return false, false, err + } + + groups, _, err := client.Groups.ListGroups(&gitlab.ListGroupsOptions{ + ListOptions: gitlab.ListOptions{ + Page: 1, + PerPage: 100, + }, + Search: gitlab.String(owner), + }, gitlab.WithContext(ctx)) + if err != nil { + return false, false, err + } + var gid int + for _, group := range groups { + if group.Name == owner { + gid = group.ID + break + } + } + if gid == 0 { + return false, false, nil + } + + opts := &gitlab.ListGroupMembersOptions{ + ListOptions: gitlab.ListOptions{ + Page: 1, + PerPage: 100, + }, + } + + for i := 1; true; i++ { + opts.Page = i + members, _, err := client.Groups.ListAllGroupMembers(gid, opts, gitlab.WithContext(ctx)) + if err != nil { + return false, false, err + } + for _, member := range members { + if member.Username == u.Login { + return true, member.AccessLevel >= gitlab.OwnerPermissions, nil + } + } + + if len(members) < opts.PerPage { + break + } + } + + return false, false, nil +} + func (g *Gitlab) loadChangedFilesFromMergeRequest(ctx context.Context, tmpRepo *model.Repo, build *model.Build, mergeIID int) (*model.Build, error) { _store, ok := store.TryFromContext(ctx) if !ok { diff --git a/server/remote/gogs/gogs.go b/server/remote/gogs/gogs.go index 6d24ab3a53..5a3b0d164c 100644 --- a/server/remote/gogs/gogs.go +++ b/server/remote/gogs/gogs.go @@ -290,6 +290,25 @@ func (c *client) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model return parseHook(r) } +// OrgMembership returns if user is member of organization and if user +// is admin/owner in this organization. +func (c *client) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { + client := c.newClientToken(u.Token) + + orgs, err := client.ListMyOrgs() + if err != nil { + return false, false, err + } + + for _, org := range orgs { + if org.UserName == owner { + // TODO: API does not support checking if user is admin/owner of org + return true, false, nil + } + } + return false, false, nil +} + // helper function to return the Gogs client func (c *client) newClient() *gogs.Client { return c.newClientToken("") diff --git a/server/remote/mocks/remote.go b/server/remote/mocks/remote.go index f0e4b94b41..9b92734b4b 100644 --- a/server/remote/mocks/remote.go +++ b/server/remote/mocks/remote.go @@ -228,6 +228,34 @@ func (_m *Remote) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { return r0, r1 } +// OrgMembership provides a mock function with given fields: ctx, u, owner +func (_m *Remote) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { + ret := _m.Called(ctx, u, owner) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *model.User, string) bool); ok { + r0 = rf(ctx, u, owner) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 bool + if rf, ok := ret.Get(1).(func(context.Context, *model.User, string) bool); ok { + r1 = rf(ctx, u, owner) + } else { + r1 = ret.Get(1).(bool) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *model.User, string) error); ok { + r2 = rf(ctx, u, owner) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // Perm provides a mock function with given fields: ctx, u, r func (_m *Remote) Perm(ctx context.Context, u *model.User, r *model.Repo) (*model.Perm, error) { ret := _m.Called(ctx, u, r) diff --git a/server/remote/remote.go b/server/remote/remote.go index a5b18316ce..30ece859ef 100644 --- a/server/remote/remote.go +++ b/server/remote/remote.go @@ -79,6 +79,10 @@ type Remote interface { // Hook parses the post-commit hook from the Request body and returns the // required data in a standard format. Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error) + + // OrgMembership returns if user is member of organization and if user + // is admin/owner in that organization. + OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) } // FileMeta represents a file in version control From afb8358147090eba80448369dd6131dd042a0b2e Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Tue, 19 Jul 2022 16:36:46 +0300 Subject: [PATCH 2/8] Use named return parameters in interface --- server/remote/remote.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/remote/remote.go b/server/remote/remote.go index 30ece859ef..acec2764fb 100644 --- a/server/remote/remote.go +++ b/server/remote/remote.go @@ -82,7 +82,7 @@ type Remote interface { // OrgMembership returns if user is member of organization and if user // is admin/owner in that organization. - OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) + OrgMembership(ctx context.Context, u *model.User, owner string) (member, admin bool, err error) } // FileMeta represents a file in version control From d3719579186459cd6ab2db698a839eb856913754 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Wed, 20 Jul 2022 00:01:51 +0300 Subject: [PATCH 3/8] Add membership check service --- cmd/server/server.go | 1 + cmd/server/setup.go | 5 ++ go.mod | 11 ++-- go.sum | 27 ++++++--- server/cache/membership.go | 74 ++++++++++++++++++++++++ server/config.go | 2 + server/remote/gitea/gitea.go | 17 +----- server/router/middleware/session/user.go | 61 +++++++++++++++++++ 8 files changed, 172 insertions(+), 26 deletions(-) create mode 100644 server/cache/membership.go diff --git a/cmd/server/server.go b/cmd/server/server.go index 77ca3e7a9c..12fb87dadb 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -266,6 +266,7 @@ func setupEvilGlobals(c *cli.Context, v store.Store, r remote.Remote) { server.Config.Services.Registries = setupRegistryService(c, v) server.Config.Services.Secrets = setupSecretService(c, v) server.Config.Services.Environ = setupEnvironService(c, v) + server.Config.Services.Membership = setupMembershipService(c, r) server.Config.Services.SignaturePrivateKey, server.Config.Services.SignaturePublicKey = setupSignatureKeys(v) diff --git a/cmd/server/setup.go b/cmd/server/setup.go index 88add92a86..a4b3da3c0d 100644 --- a/cmd/server/setup.go +++ b/cmd/server/setup.go @@ -33,6 +33,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/woodpecker-ci/woodpecker/server" + "github.com/woodpecker-ci/woodpecker/server/cache" "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/plugins/environments" "github.com/woodpecker-ci/woodpecker/server/plugins/registry" @@ -180,6 +181,10 @@ func setupEnvironService(c *cli.Context, s store.Store) model.EnvironService { return environments.Parse(c.StringSlice("environment")) } +func setupMembershipService(_ *cli.Context, r remote.Remote) cache.MembershipService { + return cache.NewMembershipService(r) +} + // setupRemote helper function to setup the remote from the CLI arguments. func setupRemote(c *cli.Context) (remote.Remote, error) { switch { diff --git a/go.mod b/go.mod index 5c11fb7e96..19bc25ada1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/woodpecker-ci/woodpecker go 1.18 require ( - code.gitea.io/sdk/gitea v0.15.1-0.20220501190934-319a978c6c71 + code.gitea.io/sdk/gitea v0.15.1-0.20220719204045-db8a2d99e210 codeberg.org/6543/go-yaml2json v0.1.0 github.com/bmatcuk/doublestar/v4 v4.0.2 github.com/docker/cli v20.10.14+incompatible @@ -20,6 +20,7 @@ require ( github.com/google/go-github/v39 v39.2.0 github.com/gorilla/securecookie v1.1.1 github.com/joho/godotenv v1.4.0 + github.com/lafriks/ttlcache/v3 v3.1.0 github.com/lib/pq v1.10.5 github.com/mattn/go-sqlite3 v1.14.12 github.com/melbahja/goph v1.3.0 @@ -34,7 +35,7 @@ require ( github.com/urfave/cli/v2 v2.5.1 github.com/xanzy/go-gitlab v0.64.0 github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/net v0.0.0-20220615171555-694bf12d69de golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f @@ -53,11 +54,13 @@ require ( github.com/containerd/containerd v1.6.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.10.1 // indirect @@ -71,7 +74,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.0.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.1 // indirect - github.com/hashicorp/go-version v1.4.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kr/fs v0.1.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect @@ -98,7 +101,7 @@ require ( github.com/ugorji/go/codec v1.2.7 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect golang.org/x/tools v0.1.10 // indirect diff --git a/go.sum b/go.sum index 3f0de448b3..181995ea15 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -code.gitea.io/sdk/gitea v0.15.1-0.20220501190934-319a978c6c71 h1:+ZqwhfAftVOAd7AcLpfh4LBdTeJIyt60vGU39zhQPyA= -code.gitea.io/sdk/gitea v0.15.1-0.20220501190934-319a978c6c71/go.mod h1:MuMGvUxT8BmFHa0gHhHsrnz91QfmziXuFffm9AuhMCo= +code.gitea.io/sdk/gitea v0.15.1-0.20220719204045-db8a2d99e210 h1:kZKzz58nhfeUcEF8uW+WuwTt/TefPN+RcEAHCJGk/WI= +code.gitea.io/sdk/gitea v0.15.1-0.20220719204045-db8a2d99e210/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE= codeberg.org/6543/go-yaml2json v0.1.0 h1:njuf3a8QgsmBXJFiH+7wNR01biBS4MU+XeWE7W3bnus= codeberg.org/6543/go-yaml2json v0.1.0/go.mod h1:mz61q14LWF4ZABrgMEDMmk3t9dPi6zgR1uBh2VKV2RQ= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -111,6 +111,8 @@ github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7h github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docker/cli v20.10.14+incompatible h1:dSBKJOVesDgHo7rbxlYjYsXe7gPzrTT+/cKQgpDAazg= @@ -158,6 +160,8 @@ github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= github.com/go-ap/httpsig v0.0.0-20210714162115-62a09257db51 h1:cytjZGyqtAu9JspfDt9ThCJ2KKCT/kPGDPCKWIZv8dw= github.com/go-ap/httpsig v0.0.0-20210714162115-62a09257db51/go.mod h1:+4SUDMvPlRMUPW5PlMTbxj3U5a4fWasBIbakUw7Kp6c= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -305,8 +309,9 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= -github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -405,6 +410,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lafriks/ttlcache/v3 v3.1.0 h1:/ths7O17AjV3hDkNo06fEfqwsDMd/Vbx9Em7yhD4V2k= +github.com/lafriks/ttlcache/v3 v3.1.0/go.mod h1:oxu8nlxF496noT0VKBf2gDQWHA6VDjoGFnn4qw9wAac= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= @@ -658,6 +665,7 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -681,13 +689,15 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -710,6 +720,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -855,8 +866,8 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= diff --git a/server/cache/membership.go b/server/cache/membership.go new file mode 100644 index 0000000000..31fefcb100 --- /dev/null +++ b/server/cache/membership.go @@ -0,0 +1,74 @@ +package cache + +import ( + "context" + "time" + + "github.com/woodpecker-ci/woodpecker/server/model" + "github.com/woodpecker-ci/woodpecker/server/remote" + + "github.com/lafriks/ttlcache/v3" +) + +// MembershipService is a service to check for user membership. +type MembershipService interface { + // IsMember returns true if the user is a member of the organization. + IsMember(ctx context.Context, u *model.User, owner string) (bool, error) + + // IsAdmin returns true if the user is an admin of the organization. + IsAdmin(ctx context.Context, u *model.User, owner string) (bool, error) +} + +type membership struct { + Member bool + Admin bool +} + +type membershipCache struct { + Remote remote.Remote + Cache *ttlcache.Cache[string, membership] + TTL time.Duration +} + +// NewMembershipService creates a new membership service. +func NewMembershipService(r remote.Remote) MembershipService { + return &membershipCache{ + TTL: 10 * time.Minute, + Remote: r, + Cache: ttlcache.New(ttlcache.WithDisableTouchOnHit[string, membership]()), + } +} + +func (c *membershipCache) get(ctx context.Context, u *model.User, owner string) (bool, bool, error) { + key := u.Login + "/" + owner + // Error can be safely ignored, as cache can only return error from loaders. + item, _ := c.Cache.Get(key) + if item != nil && !item.IsExpired() { + return item.Value().Member, item.Value().Admin, nil + } + + member, admin, err := c.Remote.OrgMembership(ctx, u, owner) + if err != nil { + return false, false, err + } + c.Cache.Set(key, membership{Member: member, Admin: admin}, c.TTL) + return member, admin, nil +} + +// IsMember returns true if the user is a member of the organization. +func (c *membershipCache) IsMember(ctx context.Context, u *model.User, owner string) (bool, error) { + member, _, err := c.get(ctx, u, owner) + if err != nil { + return false, err + } + return member, nil +} + +// IsAdmin returns true if the user is an admin of the organization. +func (c *membershipCache) IsAdmin(ctx context.Context, u *model.User, owner string) (bool, error) { + _, admin, err := c.get(ctx, u, owner) + if err != nil { + return false, err + } + return admin, nil +} diff --git a/server/config.go b/server/config.go index e675c06d93..5ab6fe3f29 100644 --- a/server/config.go +++ b/server/config.go @@ -21,6 +21,7 @@ import ( "crypto" "time" + "github.com/woodpecker-ci/woodpecker/server/cache" "github.com/woodpecker-ci/woodpecker/server/logging" "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/plugins/config" @@ -38,6 +39,7 @@ var Config = struct { Registries model.RegistryService Environ model.EnvironService Remote remote.Remote + Membership cache.MembershipService ConfigService config.Extension SignaturePrivateKey crypto.PrivateKey SignaturePublicKey crypto.PublicKey diff --git a/server/remote/gitea/gitea.go b/server/remote/gitea/gitea.go index 93a5687f4a..fe2ea3cbb3 100644 --- a/server/remote/gitea/gitea.go +++ b/server/remote/gitea/gitea.go @@ -20,9 +20,7 @@ package gitea import ( "context" "crypto/tls" - "encoding/json" "fmt" - "io" "net" "net/http" "net/url" @@ -468,7 +466,7 @@ func (c *Gitea) OrgMembership(ctx context.Context, u *model.User, owner string) return false, false, err } - member, resp, err := client.CheckOrgMembership(owner, u.Login) + member, _, err := client.CheckOrgMembership(owner, u.Login) if err != nil { return false, false, err } @@ -477,21 +475,12 @@ func (c *Gitea) OrgMembership(ctx context.Context, u *model.User, owner string) return false, false, nil } - buf, err := io.ReadAll(resp.Body) + perm, _, err := client.GetOrgPermissions(owner, u.Login) if err != nil { return member, false, err } - p := struct { - IsAdmin bool `json:"is_admin"` - IsOwner bool `json:"is_owner"` - }{} - - if err := json.Unmarshal(buf, &p); err != nil { - return member, false, err - } - - return member, p.IsAdmin || p.IsOwner, nil + return member, perm.IsAdmin || perm.IsOwner, nil } // helper function to return the Gitea client with Token diff --git a/server/router/middleware/session/user.go b/server/router/middleware/session/user.go index 42a13edcd0..ba5cf3fcf9 100644 --- a/server/router/middleware/session/user.go +++ b/server/router/middleware/session/user.go @@ -19,6 +19,7 @@ import ( "github.com/gin-gonic/gin" + "github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/store" "github.com/woodpecker-ci/woodpecker/shared/token" @@ -116,3 +117,63 @@ func MustUser() gin.HandlerFunc { } } } + +func MustOrgMember() gin.HandlerFunc { + return func(c *gin.Context) { + user := User(c) + owner := c.Param("owner") + switch { + case user == nil: + c.String(401, "User not authorized") + c.Abort() + case owner == "": + c.String(http.StatusForbidden, "User not authorized") + c.Abort() + default: + if user.Login != owner && !user.Admin { + member, err := server.Config.Services.Membership.IsMember(c, user, owner) + if err != nil { + c.String(http.StatusInternalServerError, "Failed to check membership") + c.Abort() + return + } + if !member { + c.String(http.StatusForbidden, "User not authorized") + c.Abort() + return + } + } + c.Next() + } + } +} + +func MustOrgAdmin() gin.HandlerFunc { + return func(c *gin.Context) { + user := User(c) + owner := c.Param("owner") + switch { + case user == nil: + c.String(401, "User not authorized") + c.Abort() + case owner == "": + c.String(http.StatusForbidden, "User not authorized") + c.Abort() + default: + if user.Login != owner && !user.Admin { + admin, err := server.Config.Services.Membership.IsAdmin(c, user, owner) + if err != nil { + c.String(http.StatusInternalServerError, "Failed to check membership") + c.Abort() + return + } + if !admin { + c.String(http.StatusForbidden, "User not authorized") + c.Abort() + return + } + } + c.Next() + } + } +} From 474431757e029c57a7f0d964a036699040b870e9 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Wed, 20 Jul 2022 11:28:45 +0300 Subject: [PATCH 4/8] Update Gitea SDK --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 19bc25ada1..b5dd2b358d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/woodpecker-ci/woodpecker go 1.18 require ( - code.gitea.io/sdk/gitea v0.15.1-0.20220719204045-db8a2d99e210 + code.gitea.io/sdk/gitea v0.15.1-0.20220720025709-de34275bb64e codeberg.org/6543/go-yaml2json v0.1.0 github.com/bmatcuk/doublestar/v4 v4.0.2 github.com/docker/cli v20.10.14+incompatible diff --git a/go.sum b/go.sum index 181995ea15..a2c555568c 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -code.gitea.io/sdk/gitea v0.15.1-0.20220719204045-db8a2d99e210 h1:kZKzz58nhfeUcEF8uW+WuwTt/TefPN+RcEAHCJGk/WI= -code.gitea.io/sdk/gitea v0.15.1-0.20220719204045-db8a2d99e210/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE= +code.gitea.io/sdk/gitea v0.15.1-0.20220720025709-de34275bb64e h1:xayGBU2DwsrA5ZyqKNpXB91w3BfnkNcLDWZ7Ynn/w+g= +code.gitea.io/sdk/gitea v0.15.1-0.20220720025709-de34275bb64e/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE= codeberg.org/6543/go-yaml2json v0.1.0 h1:njuf3a8QgsmBXJFiH+7wNR01biBS4MU+XeWE7W3bnus= codeberg.org/6543/go-yaml2json v0.1.0/go.mod h1:mz61q14LWF4ZABrgMEDMmk3t9dPi6zgR1uBh2VKV2RQ= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= From e85c16400c416851330be5fa65aed4031a630529 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Wed, 20 Jul 2022 11:40:59 +0300 Subject: [PATCH 5/8] Refactor to have single org membership check method to not duplicate code --- server/router/middleware/session/user.go | 76 ++++++++++-------------- 1 file changed, 30 insertions(+), 46 deletions(-) diff --git a/server/router/middleware/session/user.go b/server/router/middleware/session/user.go index ba5cf3fcf9..82da1e458f 100644 --- a/server/router/middleware/session/user.go +++ b/server/router/middleware/session/user.go @@ -17,12 +17,13 @@ package session import ( "net/http" - "github.com/gin-gonic/gin" - "github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/store" "github.com/woodpecker-ci/woodpecker/shared/token" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" ) func User(c *gin.Context) *model.User { @@ -118,62 +119,45 @@ func MustUser() gin.HandlerFunc { } } -func MustOrgMember() gin.HandlerFunc { +func MustOrgMember(admin bool) gin.HandlerFunc { return func(c *gin.Context) { user := User(c) owner := c.Param("owner") - switch { - case user == nil: - c.String(401, "User not authorized") + if user == nil { + c.String(http.StatusUnauthorized, "User not authorized") c.Abort() - case owner == "": + return + } + if owner == "" { c.String(http.StatusForbidden, "User not authorized") c.Abort() - default: - if user.Login != owner && !user.Admin { - member, err := server.Config.Services.Membership.IsMember(c, user, owner) - if err != nil { - c.String(http.StatusInternalServerError, "Failed to check membership") - c.Abort() - return - } - if !member { - c.String(http.StatusForbidden, "User not authorized") - c.Abort() - return - } - } + return + } + // User can access his own, admin can access all + if user.Login == owner || user.Admin { c.Next() + return } - } -} -func MustOrgAdmin() gin.HandlerFunc { - return func(c *gin.Context) { - user := User(c) - owner := c.Param("owner") - switch { - case user == nil: - c.String(401, "User not authorized") + var perm bool + var err error + + if admin { + perm, err = server.Config.Services.Membership.IsAdmin(c, user, owner) + } else { + perm, err = server.Config.Services.Membership.IsMember(c, user, owner) + } + if err != nil { + log.Error().Msgf("Failed to check membership: %v", err) + c.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) c.Abort() - case owner == "": + return + } + if !perm { c.String(http.StatusForbidden, "User not authorized") c.Abort() - default: - if user.Login != owner && !user.Admin { - admin, err := server.Config.Services.Membership.IsAdmin(c, user, owner) - if err != nil { - c.String(http.StatusInternalServerError, "Failed to check membership") - c.Abort() - return - } - if !admin { - c.String(http.StatusForbidden, "User not authorized") - c.Abort() - return - } - } - c.Next() + return } + c.Next() } } From f0220f4b12a6df1d9444143844797588ed42bcc6 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Fri, 22 Jul 2022 20:16:13 +0300 Subject: [PATCH 6/8] Add missing copyright header --- server/cache/membership.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/cache/membership.go b/server/cache/membership.go index 31fefcb100..5698dbef3e 100644 --- a/server/cache/membership.go +++ b/server/cache/membership.go @@ -1,3 +1,17 @@ +// 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 cache import ( From 5931df52c44a2449d708bc818c62d75705f7b082 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Mon, 25 Jul 2022 01:13:12 +0300 Subject: [PATCH 7/8] Refactor membership service --- server/cache/membership.go | 50 ++++++------------------ server/model/perm.go | 6 +++ server/router/middleware/session/user.go | 11 +----- 3 files changed, 21 insertions(+), 46 deletions(-) diff --git a/server/cache/membership.go b/server/cache/membership.go index 5698dbef3e..2bf8ae7f6e 100644 --- a/server/cache/membership.go +++ b/server/cache/membership.go @@ -26,21 +26,13 @@ import ( // MembershipService is a service to check for user membership. type MembershipService interface { - // IsMember returns true if the user is a member of the organization. - IsMember(ctx context.Context, u *model.User, owner string) (bool, error) - - // IsAdmin returns true if the user is an admin of the organization. - IsAdmin(ctx context.Context, u *model.User, owner string) (bool, error) -} - -type membership struct { - Member bool - Admin bool + // Get returns if the user is a member of the organization. + Get(ctx context.Context, u *model.User, name string) (*model.OrgPerm, error) } type membershipCache struct { Remote remote.Remote - Cache *ttlcache.Cache[string, membership] + Cache *ttlcache.Cache[string, *model.OrgPerm] TTL time.Duration } @@ -49,40 +41,24 @@ func NewMembershipService(r remote.Remote) MembershipService { return &membershipCache{ TTL: 10 * time.Minute, Remote: r, - Cache: ttlcache.New(ttlcache.WithDisableTouchOnHit[string, membership]()), + Cache: ttlcache.New(ttlcache.WithDisableTouchOnHit[string, *model.OrgPerm]()), } } -func (c *membershipCache) get(ctx context.Context, u *model.User, owner string) (bool, bool, error) { - key := u.Login + "/" + owner +// Get returns if the user is a member of the organization. +func (c *membershipCache) Get(ctx context.Context, u *model.User, name string) (*model.OrgPerm, error) { + key := u.Login + "/" + name // Error can be safely ignored, as cache can only return error from loaders. item, _ := c.Cache.Get(key) if item != nil && !item.IsExpired() { - return item.Value().Member, item.Value().Admin, nil - } - - member, admin, err := c.Remote.OrgMembership(ctx, u, owner) - if err != nil { - return false, false, err + return item.Value(), nil } - c.Cache.Set(key, membership{Member: member, Admin: admin}, c.TTL) - return member, admin, nil -} - -// IsMember returns true if the user is a member of the organization. -func (c *membershipCache) IsMember(ctx context.Context, u *model.User, owner string) (bool, error) { - member, _, err := c.get(ctx, u, owner) - if err != nil { - return false, err - } - return member, nil -} -// IsAdmin returns true if the user is an admin of the organization. -func (c *membershipCache) IsAdmin(ctx context.Context, u *model.User, owner string) (bool, error) { - _, admin, err := c.get(ctx, u, owner) + member, admin, err := c.Remote.OrgMembership(ctx, u, name) if err != nil { - return false, err + return nil, err } - return admin, nil + perm := &model.OrgPerm{Member: member, Admin: admin} + c.Cache.Set(key, perm, c.TTL) + return perm, nil } diff --git a/server/model/perm.go b/server/model/perm.go index 2d2abd7b17..309848ad1b 100644 --- a/server/model/perm.go +++ b/server/model/perm.go @@ -40,3 +40,9 @@ type Perm struct { func (Perm) TableName() string { return "perms" } + +// OrgPerm defines a organization permission for an individual user. +type OrgPerm struct { + Member bool `json:"member"` + Admin bool `json:"admin"` +} diff --git a/server/router/middleware/session/user.go b/server/router/middleware/session/user.go index 82da1e458f..cf9678b90a 100644 --- a/server/router/middleware/session/user.go +++ b/server/router/middleware/session/user.go @@ -139,21 +139,14 @@ func MustOrgMember(admin bool) gin.HandlerFunc { return } - var perm bool - var err error - - if admin { - perm, err = server.Config.Services.Membership.IsAdmin(c, user, owner) - } else { - perm, err = server.Config.Services.Membership.IsMember(c, user, owner) - } + perm, err := server.Config.Services.Membership.Get(c, user, owner) if err != nil { log.Error().Msgf("Failed to check membership: %v", err) c.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) c.Abort() return } - if !perm { + if perm == nil || (!admin && !perm.Member) || (admin && !perm.Admin) { c.String(http.StatusForbidden, "User not authorized") c.Abort() return From fc62a6db6c709af020447782a2cb06764a071610 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Mon, 25 Jul 2022 01:24:13 +0300 Subject: [PATCH 8/8] Refactor remote OrgMembership method --- server/cache/membership.go | 3 +-- server/remote/bitbucket/bitbucket.go | 6 ++--- .../remote/bitbucketserver/bitbucketserver.go | 4 +-- server/remote/coding/coding.go | 4 +-- server/remote/gitea/gitea.go | 12 ++++----- server/remote/github/github.go | 6 ++--- server/remote/gitlab/gitlab.go | 14 +++++------ server/remote/gogs/gogs.go | 8 +++--- server/remote/mocks/remote.go | 25 ++++++++----------- server/remote/remote.go | 2 +- 10 files changed, 39 insertions(+), 45 deletions(-) diff --git a/server/cache/membership.go b/server/cache/membership.go index 2bf8ae7f6e..bafd7a04ce 100644 --- a/server/cache/membership.go +++ b/server/cache/membership.go @@ -54,11 +54,10 @@ func (c *membershipCache) Get(ctx context.Context, u *model.User, name string) ( return item.Value(), nil } - member, admin, err := c.Remote.OrgMembership(ctx, u, name) + perm, err := c.Remote.OrgMembership(ctx, u, name) if err != nil { return nil, err } - perm := &model.OrgPerm{Member: member, Admin: admin} c.Cache.Set(key, perm, c.TTL) return perm, nil } diff --git a/server/remote/bitbucket/bitbucket.go b/server/remote/bitbucket/bitbucket.go index ab835a4b7f..f17dc227be 100644 --- a/server/remote/bitbucket/bitbucket.go +++ b/server/remote/bitbucket/bitbucket.go @@ -303,12 +303,12 @@ func (c *config) Hook(ctx context.Context, req *http.Request) (*model.Repo, *mod // OrgMembership returns if user is member of organization and if user // is admin/owner in this organization. -func (c *config) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { +func (c *config) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) { perm, err := c.newClient(ctx, u).GetUserWorkspaceMembership(owner, u.Login) if err != nil { - return false, false, err + return nil, err } - return perm != "", perm == "owner", nil + return &model.OrgPerm{Member: perm != "", Admin: perm == "owner"}, nil } // helper function to return the bitbucket oauth2 client diff --git a/server/remote/bitbucketserver/bitbucketserver.go b/server/remote/bitbucketserver/bitbucketserver.go index 9a85c7de85..1f91404d71 100644 --- a/server/remote/bitbucketserver/bitbucketserver.go +++ b/server/remote/bitbucketserver/bitbucketserver.go @@ -247,9 +247,9 @@ func (c *Config) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model // OrgMembership returns if user is member of organization and if user // is admin/owner in this organization. -func (c *Config) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { +func (c *Config) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) { // TODO: Not implemented currently - return false, false, nil + return nil, nil } func CreateConsumer(URL, ConsumerKey string, PrivateKey *rsa.PrivateKey) *oauth.Consumer { diff --git a/server/remote/coding/coding.go b/server/remote/coding/coding.go index 232d016a3f..194d64010a 100644 --- a/server/remote/coding/coding.go +++ b/server/remote/coding/coding.go @@ -303,9 +303,9 @@ func (c *Coding) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model // OrgMembership returns if user is member of organization and if user // is admin/owner in this organization. -func (c *Coding) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { +func (c *Coding) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) { // TODO: Not supported in Coding OAuth API - return false, false, nil + return nil, nil } // helper function to return the Coding oauth2 context using an HTTPClient that diff --git a/server/remote/gitea/gitea.go b/server/remote/gitea/gitea.go index fe2ea3cbb3..3667a74c8e 100644 --- a/server/remote/gitea/gitea.go +++ b/server/remote/gitea/gitea.go @@ -460,27 +460,27 @@ func (c *Gitea) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model. // OrgMembership returns if user is member of organization and if user // is admin/owner in this organization. -func (c *Gitea) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { +func (c *Gitea) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) { client, err := c.newClientToken(ctx, u.Token) if err != nil { - return false, false, err + return nil, err } member, _, err := client.CheckOrgMembership(owner, u.Login) if err != nil { - return false, false, err + return nil, err } if !member { - return false, false, nil + return &model.OrgPerm{}, nil } perm, _, err := client.GetOrgPermissions(owner, u.Login) if err != nil { - return member, false, err + return &model.OrgPerm{Member: member}, err } - return member, perm.IsAdmin || perm.IsOwner, nil + return &model.OrgPerm{Member: member, Admin: perm.IsAdmin || perm.IsOwner}, nil } // helper function to return the Gitea client with Token diff --git a/server/remote/github/github.go b/server/remote/github/github.go index 0a1807449e..0b90aaa80e 100644 --- a/server/remote/github/github.go +++ b/server/remote/github/github.go @@ -307,14 +307,14 @@ func (c *client) Deactivate(ctx context.Context, u *model.User, r *model.Repo, l // OrgMembership returns if user is member of organization and if user // is admin/owner in this organization. -func (c *client) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { +func (c *client) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) { client := c.newClientToken(ctx, u.Token) org, _, err := client.Organizations.GetOrgMembership(ctx, u.Login, owner) if err != nil { - return false, false, err + return nil, err } - return org.GetState() == "active", org.GetRole() == "admin", nil + return &model.OrgPerm{Member: org.GetState() == "active", Admin: org.GetRole() == "admin"}, nil } // helper function to return the GitHub oauth2 context using an HTTPClient that diff --git a/server/remote/gitlab/gitlab.go b/server/remote/gitlab/gitlab.go index ddabbdd1d9..775791f915 100644 --- a/server/remote/gitlab/gitlab.go +++ b/server/remote/gitlab/gitlab.go @@ -556,10 +556,10 @@ func (g *Gitlab) Hook(ctx context.Context, req *http.Request) (*model.Repo, *mod // OrgMembership returns if user is member of organization and if user // is admin/owner in this organization. -func (g *Gitlab) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { +func (g *Gitlab) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) { client, err := newClient(g.URL, u.Token, g.SkipVerify) if err != nil { - return false, false, err + return nil, err } groups, _, err := client.Groups.ListGroups(&gitlab.ListGroupsOptions{ @@ -570,7 +570,7 @@ func (g *Gitlab) OrgMembership(ctx context.Context, u *model.User, owner string) Search: gitlab.String(owner), }, gitlab.WithContext(ctx)) if err != nil { - return false, false, err + return nil, err } var gid int for _, group := range groups { @@ -580,7 +580,7 @@ func (g *Gitlab) OrgMembership(ctx context.Context, u *model.User, owner string) } } if gid == 0 { - return false, false, nil + return &model.OrgPerm{}, nil } opts := &gitlab.ListGroupMembersOptions{ @@ -594,11 +594,11 @@ func (g *Gitlab) OrgMembership(ctx context.Context, u *model.User, owner string) opts.Page = i members, _, err := client.Groups.ListAllGroupMembers(gid, opts, gitlab.WithContext(ctx)) if err != nil { - return false, false, err + return nil, err } for _, member := range members { if member.Username == u.Login { - return true, member.AccessLevel >= gitlab.OwnerPermissions, nil + return &model.OrgPerm{Member: true, Admin: member.AccessLevel >= gitlab.OwnerPermissions}, nil } } @@ -607,7 +607,7 @@ func (g *Gitlab) OrgMembership(ctx context.Context, u *model.User, owner string) } } - return false, false, nil + return &model.OrgPerm{}, nil } func (g *Gitlab) loadChangedFilesFromMergeRequest(ctx context.Context, tmpRepo *model.Repo, build *model.Build, mergeIID int) (*model.Build, error) { diff --git a/server/remote/gogs/gogs.go b/server/remote/gogs/gogs.go index 5a3b0d164c..41d6341bdb 100644 --- a/server/remote/gogs/gogs.go +++ b/server/remote/gogs/gogs.go @@ -292,21 +292,21 @@ func (c *client) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model // OrgMembership returns if user is member of organization and if user // is admin/owner in this organization. -func (c *client) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { +func (c *client) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) { client := c.newClientToken(u.Token) orgs, err := client.ListMyOrgs() if err != nil { - return false, false, err + return nil, err } for _, org := range orgs { if org.UserName == owner { // TODO: API does not support checking if user is admin/owner of org - return true, false, nil + return &model.OrgPerm{Member: true}, nil } } - return false, false, nil + return &model.OrgPerm{}, nil } // helper function to return the Gogs client diff --git a/server/remote/mocks/remote.go b/server/remote/mocks/remote.go index 9b92734b4b..a3fb7f472e 100644 --- a/server/remote/mocks/remote.go +++ b/server/remote/mocks/remote.go @@ -229,31 +229,26 @@ func (_m *Remote) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { } // OrgMembership provides a mock function with given fields: ctx, u, owner -func (_m *Remote) OrgMembership(ctx context.Context, u *model.User, owner string) (bool, bool, error) { +func (_m *Remote) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) { ret := _m.Called(ctx, u, owner) - var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, *model.User, string) bool); ok { + var r0 *model.OrgPerm + if rf, ok := ret.Get(0).(func(context.Context, *model.User, string) *model.OrgPerm); ok { r0 = rf(ctx, u, owner) } else { - r0 = ret.Get(0).(bool) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.OrgPerm) + } } - var r1 bool - if rf, ok := ret.Get(1).(func(context.Context, *model.User, string) bool); ok { + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *model.User, string) error); ok { r1 = rf(ctx, u, owner) } else { - r1 = ret.Get(1).(bool) - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *model.User, string) error); ok { - r2 = rf(ctx, u, owner) - } else { - r2 = ret.Error(2) + r1 = ret.Error(1) } - return r0, r1, r2 + return r0, r1 } // Perm provides a mock function with given fields: ctx, u, r diff --git a/server/remote/remote.go b/server/remote/remote.go index acec2764fb..bef473ec95 100644 --- a/server/remote/remote.go +++ b/server/remote/remote.go @@ -82,7 +82,7 @@ type Remote interface { // OrgMembership returns if user is member of organization and if user // is admin/owner in that organization. - OrgMembership(ctx context.Context, u *model.User, owner string) (member, admin bool, err error) + OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) } // FileMeta represents a file in version control