diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d5c51bbe..813dd2ec 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: # go: ['1.14', '1.15'] - go: ['1.18'] + go: ['1.20'] steps: - uses: actions/setup-go@v2 diff --git a/.travis.yml b/.travis.yml index 06cb1c46..761b74f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ services: - docker go: - - "1.18" + - "1.20" env: - ISTRAVIS=true diff --git a/Dockerfile b/Dockerfile index 291b9109..16426e7f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # quay.io/vouch/vouch-proxy # https://github.com/vouch/vouch-proxy -FROM golang:1.18 AS builder +FROM golang:1.20 AS builder ARG UID=999 ARG GID=999 diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 435a98c8..20a05491 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,6 +1,6 @@ # quay.io/vouch/vouch-proxy # https://github.com/vouch/vouch-proxy -FROM golang:1.18 AS builder +FROM golang:1.20 AS builder ARG UID=999 ARG GID=999 diff --git a/do.sh b/do.sh index fb0b42a4..c30e00d2 100755 --- a/do.sh +++ b/do.sh @@ -13,7 +13,7 @@ fi IMAGE=quay.io/vouch/vouch-proxy:latest ALPINE=quay.io/vouch/vouch-proxy:alpine-latest -GOIMAGE=golang:1.18 +GOIMAGE=golang:1.20 NAME=vouch-proxy HTTPPORT=9090 GODOC_PORT=5050 diff --git a/go.mod b/go.mod index 0f4c9128..dfb7e388 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/vouch/vouch-proxy -go 1.18 +go 1.20 require ( github.com/golang-jwt/jwt v3.2.2+incompatible diff --git a/go.sum b/go.sum index a1de1b51..c494c002 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b h1:AP/Y7sqYicnjGDfD5VcY4CIfh1hRXBUavxrvELjTiOE= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -223,6 +224,7 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 9aedba64..5266963b 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -414,10 +414,12 @@ func basicTest() error { return errors.New("configuration error: required configuration option 'oauth.client_id' is not set") } - // Domains is required _unless_ Cfg.AllowAllUsers is set - if (!Cfg.AllowAllUsers && len(Cfg.Domains) == 0) || - (Cfg.AllowAllUsers && len(Cfg.Domains) > 0) { - return fmt.Errorf("configuration error: either one of %s or %s needs to be set (but not both)", Branding.LCName+".domains", Branding.LCName+".allowAllUsers") + // Domains or a whitelist is required _unless_ Cfg.AllowAllUsers is set + whitelistLength := len(Cfg.Domains) + len(Cfg.WhiteList) + len(Cfg.TeamWhiteList) + if (!Cfg.AllowAllUsers && whitelistLength == 0) || + (Cfg.AllowAllUsers && whitelistLength > 0) { + return fmt.Errorf("configuration error: either %s.allowAllUsers or a whitelist (%s.domains, %s.whitelist, %s.teamWhitelist) needs to be set (but not both)", + Branding.LCName, Branding.LCName, Branding.LCName, Branding.LCName) } // issue a warning if the secret is too small diff --git a/pkg/providers/common/common.go b/pkg/providers/common/common.go index 461b1d84..2abc2b30 100644 --- a/pkg/providers/common/common.go +++ b/pkg/providers/common/common.go @@ -29,6 +29,8 @@ func Configure() { log = cfg.Logging.Logger } +type PrepareTokensAndClientT func(r *http.Request, ptokens *structs.PTokens, setProviderToken bool, opts ...oauth2.AuthCodeOption) (*http.Client, *oauth2.Token, error) + // PrepareTokensAndClient setup the client, usually for a UserInfo request func PrepareTokensAndClient(r *http.Request, ptokens *structs.PTokens, setProviderToken bool, opts ...oauth2.AuthCodeOption) (*http.Client, *oauth2.Token, error) { providerToken, err := cfg.OAuthClient.Exchange(context.TODO(), r.URL.Query().Get("code"), opts...) diff --git a/pkg/providers/github/github.go b/pkg/providers/github/github.go index 8fa7e932..83ed4d7c 100644 --- a/pkg/providers/github/github.go +++ b/pkg/providers/github/github.go @@ -26,7 +26,7 @@ import ( // Provider provider specific functions type Provider struct { - PrepareTokensAndClient func(r *http.Request, ptokens *structs.PTokens, setProviderToken bool, opts ...oauth2.AuthCodeOption) (*http.Client, *oauth2.Token, error) + PrepareTokensAndClient common.PrepareTokensAndClientT } var log *zap.SugaredLogger diff --git a/pkg/providers/openid/openid.go b/pkg/providers/openid/openid.go index bdc6bb7f..8ecce3d5 100644 --- a/pkg/providers/openid/openid.go +++ b/pkg/providers/openid/openid.go @@ -12,10 +12,11 @@ package openid import ( "encoding/json" - "golang.org/x/oauth2" "io/ioutil" "net/http" + "golang.org/x/oauth2" + "github.com/vouch/vouch-proxy/pkg/cfg" "github.com/vouch/vouch-proxy/pkg/providers/common" "github.com/vouch/vouch-proxy/pkg/structs" @@ -25,16 +26,22 @@ import ( // Provider provider specific functions type Provider struct{} -var log *zap.SugaredLogger +var ( + log *zap.SugaredLogger + prepareTokensAndClient = common.PrepareTokensAndClient +) // Configure see main.go configure() func (Provider) Configure() { log = cfg.Logging.Logger + if prepareTokensAndClient == nil { + prepareTokensAndClient = common.PrepareTokensAndClient + } } // GetUserInfo provider specific call to get userinfomation func (Provider) GetUserInfo(r *http.Request, user *structs.User, customClaims *structs.CustomClaims, ptokens *structs.PTokens, opts ...oauth2.AuthCodeOption) (rerr error) { - client, _, err := common.PrepareTokensAndClient(r, ptokens, true, opts...) + client, _, err := prepareTokensAndClient(r, ptokens, true, opts...) if err != nil { return err } @@ -57,6 +64,35 @@ func (Provider) GetUserInfo(r *http.Request, user *structs.User, customClaims *s log.Error(err) return err } + + if cfg.GenOAuth.UserTeamURL != "" { + log.Infof("OpenID teams URL: %s", cfg.GenOAuth.UserTeamURL) + teams, err := client.Get(cfg.GenOAuth.UserTeamURL) + if err != nil { + return err + } + defer func() { + if err := teams.Body.Close(); err != nil { + rerr = err + } + }() + teamsdata, _ := ioutil.ReadAll(teams.Body) + log.Infof("OpenID teams body (%v): %v", teams.StatusCode, string(teamsdata)) + + teamMemberships := &structs.GenericTeamMembershipList{} + if err := json.Unmarshal(teamsdata, teamMemberships); err != nil { + return err + } + for _, m := range *teamMemberships { + // filter to requested/whitelisted memberships only + for _, wl := range cfg.Cfg.TeamWhiteList { + if wl == m.ID { + user.TeamMemberships = append(user.TeamMemberships, m.ID) + } + } + } + } + user.PrepareUserData() return nil } diff --git a/pkg/providers/openid/openid_test.go b/pkg/providers/openid/openid_test.go new file mode 100644 index 00000000..09e14390 --- /dev/null +++ b/pkg/providers/openid/openid_test.go @@ -0,0 +1,109 @@ +/* + +Copyright 2023 The Vouch Proxy Authors. +Use of this source code is governed by The MIT License (MIT) that +can be found in the LICENSE file. Software distributed under The +MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES +OR CONDITIONS OF ANY KIND, either express or implied. + +*/ + +package openid + +import ( + "net/http" + "testing" + + mockhttp "github.com/karupanerura/go-mock-http-response" + "github.com/stretchr/testify/assert" + "github.com/vouch/vouch-proxy/pkg/cfg" + "github.com/vouch/vouch-proxy/pkg/domains" + "github.com/vouch/vouch-proxy/pkg/structs" + "golang.org/x/oauth2" +) + +type ReqMatcher func(*http.Request) bool + +type FunResponsePair struct { + matcher ReqMatcher + response *mockhttp.ResponseMock +} + +type Transport struct { + MockError error +} + +func (c *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + if c.MockError != nil { + return nil, c.MockError + } + for _, p := range mockedResponses { + if p.matcher(req) { + requests = append(requests, req.URL.String()) + return p.response.MakeResponse(req), nil + } + } + return nil, nil +} + +func mockResponse(fun ReqMatcher, statusCode int, headers map[string]string, body []byte) { + mockedResponses = append(mockedResponses, FunResponsePair{matcher: fun, response: mockhttp.NewResponseMock(statusCode, headers, body)}) +} + +func urlEquals(value string) ReqMatcher { + return func(r *http.Request) bool { + return r.URL.String() == value + } +} + +var ( + user *structs.User + token = &oauth2.Token{AccessToken: "123"} + mockedResponses = []FunResponsePair{} + requests []string + client = &http.Client{Transport: &Transport{}} +) + +func setUp(t *testing.T) { + log = cfg.Logging.Logger + cfg.InitForTestPurposesWithProvider("openid") + + cfg.Cfg.AllowAllUsers = false + cfg.Cfg.WhiteList = make([]string, 0) + cfg.Cfg.TeamWhiteList = make([]string, 0) + cfg.Cfg.Domains = []string{"domain1"} + + domains.Configure() + + mockedResponses = []FunResponsePair{} + requests = make([]string, 0) + + user = &structs.User{Username: "testuser", Email: "test@example.com"} + + origPrepareTokensAndClient := prepareTokensAndClient + t.Cleanup(func() { prepareTokensAndClient = origPrepareTokensAndClient }) + prepareTokensAndClient = func(_ *http.Request, _ *structs.PTokens, _ bool, opts ...oauth2.AuthCodeOption) (*http.Client, *oauth2.Token, error) { + return client, token, nil + } +} + +func TestGetUserInfo(t *testing.T) { + setUp(t) + + cfg.GenOAuth.UserInfoURL = "https://some/api/for/info" + userInfoContent := []byte(`{"id": "1234", "username": "myusername", "email": "my@email.com"}`) + mockResponse(urlEquals(cfg.GenOAuth.UserInfoURL), http.StatusOK, map[string]string{}, userInfoContent) + + cfg.GenOAuth.UserTeamURL = "https://some/api/for/teams" + userTeamContent := []byte(`[{"id": "1234567890", "name": "some room name"}, {"id": "xxx-not-relevant", "name": "some other room"}]`) + mockResponse(urlEquals(cfg.GenOAuth.UserTeamURL), http.StatusOK, map[string]string{}, userTeamContent) + + cfg.Cfg.TeamWhiteList = append(cfg.Cfg.TeamWhiteList, "1234567890", "some-other-team") + + provider := Provider{} + err := provider.GetUserInfo(nil, user, &structs.CustomClaims{}, &structs.PTokens{}) + + assert.Nil(t, err) + assert.Equal(t, "myusername", user.Username) + assert.Equal(t, []string{"1234567890"}, user.TeamMemberships) +} diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index bccc0180..7065e415 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -111,6 +111,12 @@ func (u *ADFSUser) PrepareUserData() { u.Username = u.UPN } +type GenericTeamMembershipList []GenericTeamMembership + +type GenericTeamMembership struct { + ID string `json:"id"` +} + // GitHubUser is a retrieved and authentiacted user from GitHub. type GitHubUser struct { User @@ -148,7 +154,7 @@ type Contact struct { Verified bool `json:"is_verified"` } -//OpenStaxUser is a retrieved and authenticated user from OpenStax Accounts +// OpenStaxUser is a retrieved and authenticated user from OpenStax Accounts type OpenStaxUser struct { User Contacts []Contact `json:"contact_infos"`