Skip to content

Commit 5d2e739

Browse files
committed
Support general team whitelists in the oauth provider.
This was build to work for Discord and their https://discordapp.com/api/users/@me/guilds endpoint, but should work for anything that returns a list of `{"id": "someid"}` maps.
1 parent 012b2fd commit 5d2e739

6 files changed

Lines changed: 164 additions & 9 deletions

File tree

pkg/cfg/cfg.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,12 @@ func basicTest() error {
414414
return errors.New("configuration error: required configuration option 'oauth.client_id' is not set")
415415
}
416416

417-
// Domains is required _unless_ Cfg.AllowAllUsers is set
418-
if (!Cfg.AllowAllUsers && len(Cfg.Domains) == 0) ||
419-
(Cfg.AllowAllUsers && len(Cfg.Domains) > 0) {
420-
return fmt.Errorf("configuration error: either one of %s or %s needs to be set (but not both)", Branding.LCName+".domains", Branding.LCName+".allowAllUsers")
417+
// Domains or a whitelist is required _unless_ Cfg.AllowAllUsers is set
418+
whitelistLength := len(Cfg.Domains) + len(Cfg.WhiteList) + len(Cfg.TeamWhiteList)
419+
if (!Cfg.AllowAllUsers && whitelistLength == 0) ||
420+
(Cfg.AllowAllUsers && whitelistLength > 0) {
421+
return fmt.Errorf("configuration error: either %s.allowAllUsers or a whitelist (%s.domains, %s.whitelist, %s.teamWhitelist) needs to be set (but not both)",
422+
Branding.LCName, Branding.LCName, Branding.LCName, Branding.LCName)
421423
}
422424

423425
// issue a warning if the secret is too small

pkg/providers/common/common.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ func Configure() {
2929
log = cfg.Logging.Logger
3030
}
3131

32+
type PrepareTokensAndClientT func(r *http.Request, ptokens *structs.PTokens, setProviderToken bool, opts ...oauth2.AuthCodeOption) (*http.Client, *oauth2.Token, error)
33+
3234
// PrepareTokensAndClient setup the client, usually for a UserInfo request
3335
func PrepareTokensAndClient(r *http.Request, ptokens *structs.PTokens, setProviderToken bool, opts ...oauth2.AuthCodeOption) (*http.Client, *oauth2.Token, error) {
3436
providerToken, err := cfg.OAuthClient.Exchange(context.TODO(), r.URL.Query().Get("code"), opts...)

pkg/providers/github/github.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626

2727
// Provider provider specific functions
2828
type Provider struct {
29-
PrepareTokensAndClient func(r *http.Request, ptokens *structs.PTokens, setProviderToken bool, opts ...oauth2.AuthCodeOption) (*http.Client, *oauth2.Token, error)
29+
PrepareTokensAndClient common.PrepareTokensAndClientT
3030
}
3131

3232
var log *zap.SugaredLogger

pkg/providers/openid/openid.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ package openid
1212

1313
import (
1414
"encoding/json"
15-
"golang.org/x/oauth2"
1615
"io/ioutil"
1716
"net/http"
1817

18+
"golang.org/x/oauth2"
19+
1920
"github.com/vouch/vouch-proxy/pkg/cfg"
2021
"github.com/vouch/vouch-proxy/pkg/providers/common"
2122
"github.com/vouch/vouch-proxy/pkg/structs"
@@ -25,16 +26,22 @@ import (
2526
// Provider provider specific functions
2627
type Provider struct{}
2728

28-
var log *zap.SugaredLogger
29+
var (
30+
log *zap.SugaredLogger
31+
prepareTokensAndClient = common.PrepareTokensAndClient
32+
)
2933

3034
// Configure see main.go configure()
3135
func (Provider) Configure() {
3236
log = cfg.Logging.Logger
37+
if prepareTokensAndClient == nil {
38+
prepareTokensAndClient = common.PrepareTokensAndClient
39+
}
3340
}
3441

3542
// GetUserInfo provider specific call to get userinfomation
3643
func (Provider) GetUserInfo(r *http.Request, user *structs.User, customClaims *structs.CustomClaims, ptokens *structs.PTokens, opts ...oauth2.AuthCodeOption) (rerr error) {
37-
client, _, err := common.PrepareTokensAndClient(r, ptokens, true, opts...)
44+
client, _, err := prepareTokensAndClient(r, ptokens, true, opts...)
3845
if err != nil {
3946
return err
4047
}
@@ -57,6 +64,35 @@ func (Provider) GetUserInfo(r *http.Request, user *structs.User, customClaims *s
5764
log.Error(err)
5865
return err
5966
}
67+
68+
if cfg.GenOAuth.UserTeamURL != "" {
69+
log.Infof("OpenID teams URL: %s", cfg.GenOAuth.UserTeamURL)
70+
teams, err := client.Get(cfg.GenOAuth.UserTeamURL)
71+
if err != nil {
72+
return err
73+
}
74+
defer func() {
75+
if err := teams.Body.Close(); err != nil {
76+
rerr = err
77+
}
78+
}()
79+
teamsdata, _ := ioutil.ReadAll(teams.Body)
80+
log.Infof("OpenID teams body (%v): %v", teams.StatusCode, string(teamsdata))
81+
82+
teamMemberships := &structs.GenericTeamMembershipList{}
83+
if err := json.Unmarshal(teamsdata, teamMemberships); err != nil {
84+
return err
85+
}
86+
for _, m := range *teamMemberships {
87+
// filter to requested/whitelisted memberships only
88+
for _, wl := range cfg.Cfg.TeamWhiteList {
89+
if wl == m.ID {
90+
user.TeamMemberships = append(user.TeamMemberships, m.ID)
91+
}
92+
}
93+
}
94+
}
95+
6096
user.PrepareUserData()
6197
return nil
6298
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
3+
Copyright 2023 The Vouch Proxy Authors.
4+
Use of this source code is governed by The MIT License (MIT) that
5+
can be found in the LICENSE file. Software distributed under The
6+
MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
7+
OR CONDITIONS OF ANY KIND, either express or implied.
8+
9+
*/
10+
11+
package openid
12+
13+
import (
14+
"net/http"
15+
"testing"
16+
17+
mockhttp "github.com/karupanerura/go-mock-http-response"
18+
"github.com/stretchr/testify/assert"
19+
"github.com/vouch/vouch-proxy/pkg/cfg"
20+
"github.com/vouch/vouch-proxy/pkg/domains"
21+
"github.com/vouch/vouch-proxy/pkg/structs"
22+
"golang.org/x/oauth2"
23+
)
24+
25+
type ReqMatcher func(*http.Request) bool
26+
27+
type FunResponsePair struct {
28+
matcher ReqMatcher
29+
response *mockhttp.ResponseMock
30+
}
31+
32+
type Transport struct {
33+
MockError error
34+
}
35+
36+
func (c *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
37+
if c.MockError != nil {
38+
return nil, c.MockError
39+
}
40+
for _, p := range mockedResponses {
41+
if p.matcher(req) {
42+
requests = append(requests, req.URL.String())
43+
return p.response.MakeResponse(req), nil
44+
}
45+
}
46+
return nil, nil
47+
}
48+
49+
func mockResponse(fun ReqMatcher, statusCode int, headers map[string]string, body []byte) {
50+
mockedResponses = append(mockedResponses, FunResponsePair{matcher: fun, response: mockhttp.NewResponseMock(statusCode, headers, body)})
51+
}
52+
53+
func urlEquals(value string) ReqMatcher {
54+
return func(r *http.Request) bool {
55+
return r.URL.String() == value
56+
}
57+
}
58+
59+
var (
60+
user *structs.User
61+
token = &oauth2.Token{AccessToken: "123"}
62+
mockedResponses = []FunResponsePair{}
63+
requests []string
64+
client = &http.Client{Transport: &Transport{}}
65+
)
66+
67+
func setUp(t *testing.T) {
68+
log = cfg.Logging.Logger
69+
cfg.InitForTestPurposesWithProvider("openid")
70+
71+
cfg.Cfg.AllowAllUsers = false
72+
cfg.Cfg.WhiteList = make([]string, 0)
73+
cfg.Cfg.TeamWhiteList = make([]string, 0)
74+
cfg.Cfg.Domains = []string{"domain1"}
75+
76+
domains.Configure()
77+
78+
mockedResponses = []FunResponsePair{}
79+
requests = make([]string, 0)
80+
81+
user = &structs.User{Username: "testuser", Email: "test@example.com"}
82+
83+
origPrepareTokensAndClient := prepareTokensAndClient
84+
t.Cleanup(func() { prepareTokensAndClient = origPrepareTokensAndClient })
85+
prepareTokensAndClient = func(_ *http.Request, _ *structs.PTokens, _ bool, opts ...oauth2.AuthCodeOption) (*http.Client, *oauth2.Token, error) {
86+
return client, token, nil
87+
}
88+
}
89+
90+
func TestGetUserInfo(t *testing.T) {
91+
setUp(t)
92+
93+
cfg.GenOAuth.UserInfoURL = "https://some/api/for/info"
94+
userInfoContent := []byte(`{"id": "1234", "username": "myusername", "email": "my@email.com"}`)
95+
mockResponse(urlEquals(cfg.GenOAuth.UserInfoURL), http.StatusOK, map[string]string{}, userInfoContent)
96+
97+
cfg.GenOAuth.UserTeamURL = "https://some/api/for/teams"
98+
userTeamContent := []byte(`[{"id": "1234567890", "name": "some room name"}, {"id": "xxx-not-relevant", "name": "some other room"}]`)
99+
mockResponse(urlEquals(cfg.GenOAuth.UserTeamURL), http.StatusOK, map[string]string{}, userTeamContent)
100+
101+
cfg.Cfg.TeamWhiteList = append(cfg.Cfg.TeamWhiteList, "1234567890", "some-other-team")
102+
103+
provider := Provider{}
104+
err := provider.GetUserInfo(nil, user, &structs.CustomClaims{}, &structs.PTokens{})
105+
106+
assert.Nil(t, err)
107+
assert.Equal(t, "myusername", user.Username)
108+
assert.Equal(t, []string{"1234567890"}, user.TeamMemberships)
109+
}

pkg/structs/structs.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ func (u *ADFSUser) PrepareUserData() {
111111
u.Username = u.UPN
112112
}
113113

114+
type GenericTeamMembershipList []GenericTeamMembership
115+
116+
type GenericTeamMembership struct {
117+
ID string `json:"id"`
118+
}
119+
114120
// GitHubUser is a retrieved and authentiacted user from GitHub.
115121
type GitHubUser struct {
116122
User
@@ -148,7 +154,7 @@ type Contact struct {
148154
Verified bool `json:"is_verified"`
149155
}
150156

151-
//OpenStaxUser is a retrieved and authenticated user from OpenStax Accounts
157+
// OpenStaxUser is a retrieved and authenticated user from OpenStax Accounts
152158
type OpenStaxUser struct {
153159
User
154160
Contacts []Contact `json:"contact_infos"`

0 commit comments

Comments
 (0)