git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #953 from grnhse/keycloak-refactor-provider-methods
91b3f59Joel Speed
CHANGELOG.md+5
@@ -4,6 +4,8 @@
4
4
5## Important Notes
5## Important Notes
6
6
7+- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) Keycloak will now use `--profile-url` if set for the userinfo endpoint
8+ instead of `--validate-url`. `--validate-url` will still work for backwards compatibility.
7- [#936](https://github.com/oauth2-proxy/oauth2-proxy/pull/936) `--user-id-claim` option is deprecated and replaced by `--oidc-email-claim`
9- [#936](https://github.com/oauth2-proxy/oauth2-proxy/pull/936) `--user-id-claim` option is deprecated and replaced by `--oidc-email-claim`
8- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Gitlab projects needs a Gitlab application with the extra `read_api` enabled
10- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Gitlab projects needs a Gitlab application with the extra `read_api` enabled
9- [#849](https://github.com/oauth2-proxy/oauth2-proxy/pull/849) `/oauth2/auth` `allowed_groups` querystring parameter can be paired with the `allowed-groups` configuration option.
11- [#849](https://github.com/oauth2-proxy/oauth2-proxy/pull/849) `/oauth2/auth` `allowed_groups` querystring parameter can be paired with the `allowed-groups` configuration option.
@@ -33,6 +35,8 @@
33
35
34## Breaking Changes
36## Breaking Changes
35
37
38+- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) In config files & envvar configs, `keycloak_group` is now the plural `keycloak_groups`.
39+ Flag configs are still `--keycloak-group` but it can be passed multiple times.
36- [#911](https://github.com/oauth2-proxy/oauth2-proxy/pull/911) Specifying a non-existent provider will cause OAuth2-Proxy to fail on startup instead of defaulting to "google".
40- [#911](https://github.com/oauth2-proxy/oauth2-proxy/pull/911) Specifying a non-existent provider will cause OAuth2-Proxy to fail on startup instead of defaulting to "google".
37- [#797](https://github.com/oauth2-proxy/oauth2-proxy/pull/797) Security changes to Google provider group authorization flow
41- [#797](https://github.com/oauth2-proxy/oauth2-proxy/pull/797) Security changes to Google provider group authorization flow
38 - If you change the list of allowed groups, existing sessions that now don't have a valid group will be logged out immediately.
42 - If you change the list of allowed groups, existing sessions that now don't have a valid group will be logged out immediately.
@@ -54,6 +58,7 @@
54
58
55## Changes since v6.1.1
59## Changes since v6.1.1
56
60
61+- [#953](https://github.com/oauth2-proxy/oauth2-proxy/pull/953) Migrate Keycloak to EnrichSession & support multiple groups for authorization (@NickMeves)
57- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Add support for Gitlab project based authentication (@factorysh)
62- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Add support for Gitlab project based authentication (@factorysh)
58- [#907](https://github.com/oauth2-proxy/oauth2-proxy/pull/907) Introduce alpha configuration option to enable testing of structured configuration (@JoelSpeed)
63- [#907](https://github.com/oauth2-proxy/oauth2-proxy/pull/907) Introduce alpha configuration option to enable testing of structured configuration (@JoelSpeed)
59- [#938](https://github.com/oauth2-proxy/oauth2-proxy/pull/938) Cleanup missed provider renaming refactor methods (@NickMeves)
64- [#938](https://github.com/oauth2-proxy/oauth2-proxy/pull/938) Cleanup missed provider renaming refactor methods (@NickMeves)
docs/docs/configuration/auth.md+19−9
@@ -135,15 +135,25 @@ If you are using GitHub enterprise, make sure you set the following to the appro
135
135
136Make sure you set the following to the appropriate url:
136Make sure you set the following to the appropriate url:
137
137
138− -provider=keycloak
138+ --provider=keycloak
139− -client-id=<client you have created>
139+ --client-id=<client you have created>
140− -client-secret=<your client's secret>
140+ --client-secret=<your client's secret>
141− -login-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/auth"
141+ --login-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/auth"
142− -redeem-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/token"
142+ --redeem-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/token"
143+ --profile-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/userinfo"
143− -validate-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/userinfo"
144+ --validate-url="http(s)://<keycloak host>/auth/realms/<your realm>/protocol/openid-connect/userinfo"
145+ --keycloak-group=<first_allowed_user_group>
144− -keycloak-group=<user_group>
146+ --keycloak-group=<second_allowed_user_group>
145−
147+
148+For group based authorization, the optional `--keycloak-group` (legacy) or `--allowed-group` (global standard)
149+flags can be used to specify which groups to limit access to.
150+
151+If these are unset but a `groups` mapper is set up above in step (3), the provider will still
152+populate the `X-Forwarded-Groups` header to your upstream server with the `groups` data in the
153+Keycloak userinfo endpoint response.
154+
146−The group management in keycloak is using a tree. If you create a group named admin in keycloak you should define the 'keycloak-group' value to /admin.
155+The group management in keycloak is using a tree. If you create a group named admin in keycloak
156+you should define the 'keycloak-group' value to /admin.
147
157
148### GitLab Auth Provider
158### GitLab Auth Provider
149
159
pkg/apis/options/options.go+2−2
@@ -36,7 +36,7 @@ type Options struct {
36 TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"`
36 TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"`
37
37
38 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
38 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
39− KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group"`
39+ KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"`
40 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
40 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
41 BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
41 BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
42 BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
42 BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
@@ -181,7 +181,7 @@ func NewFlagSet() *pflag.FlagSet {
181
181
182 flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
182 flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
183 flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
183 flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
184− flagSet.String("keycloak-group", "", "restrict login to members of this group.")
184+ flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)")
185 flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
185 flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
186 flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
186 flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
187 flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
187 flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
pkg/validation/options.go+4−1
@@ -263,7 +263,10 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
263 p.SetRepo(o.GitHubRepo, o.GitHubToken)
263 p.SetRepo(o.GitHubRepo, o.GitHubToken)
264 p.SetUsers(o.GitHubUsers)
264 p.SetUsers(o.GitHubUsers)
265 case *providers.KeycloakProvider:
265 case *providers.KeycloakProvider:
266− p.SetGroup(o.KeycloakGroup)
266+ // Backwards compatibility with `--keycloak-group` option
267+ if len(o.KeycloakGroups) > 0 {
268+ p.SetAllowedGroups(o.KeycloakGroups)
269+ }
267 case *providers.GoogleProvider:
270 case *providers.GoogleProvider:
268 if o.GoogleServiceAccountJSON != "" {
271 if o.GoogleServiceAccountJSON != "" {
269 file, err := os.Open(o.GoogleServiceAccountJSON)
272 file, err := os.Open(o.GoogleServiceAccountJSON)
providers/keycloak.go+24−25
@@ -2,6 +2,7 @@ package providers
2
2
3import (
3import (
4 "context"
4 "context"
5+ "fmt"
5 "net/url"
6 "net/url"
6
7
7 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
@@ -11,7 +12,6 @@ import (
11
12
12type KeycloakProvider struct {
13type KeycloakProvider struct {
13 *ProviderData
14 *ProviderData
14− Group string
15}
15}
16
16
17var _ Provider = (*KeycloakProvider)(nil)
17var _ Provider = (*KeycloakProvider)(nil)
@@ -47,6 +47,7 @@ var (
47 }
47 }
48)
48)
49
49
50+// NewKeycloakProvider creates a KeyCloakProvider using the passed ProviderData
50func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
51func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
51 p.setProviderDefaults(providerDefaults{
52 p.setProviderDefaults(providerDefaults{
52 name: keycloakProviderName,
53 name: keycloakProviderName,
@@ -59,41 +60,39 @@ func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
59 return &KeycloakProvider{ProviderData: p}
60 return &KeycloakProvider{ProviderData: p}
60}
61}
61
62
62−func (p *KeycloakProvider) SetGroup(group string) {
63+// EnrichSession uses the Keycloak userinfo endpoint to populate the session's
63− p.Group = group
64+// email and groups.
65+func (p *KeycloakProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
66+ // Fallback to ValidateURL if ProfileURL not set for legacy compatibility
67+ profileURL := p.ValidateURL.String()
68+ if p.ProfileURL.String() != "" {
69+ profileURL = p.ProfileURL.String()
64−}
70+ }
65
71
66−func (p *KeycloakProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
67− json, err := requests.New(p.ValidateURL.String()).
72+ json, err := requests.New(profileURL).
68 WithContext(ctx).
73 WithContext(ctx).
69 SetHeader("Authorization", "Bearer "+s.AccessToken).
74 SetHeader("Authorization", "Bearer "+s.AccessToken).
70 Do().
75 Do().
71 UnmarshalJSON()
76 UnmarshalJSON()
72 if err != nil {
77 if err != nil {
73− logger.Errorf("failed making request %s", err)
78+ logger.Errorf("failed making request %v", err)
74− return "", err
79+ return err
75 }
80 }
76
81
77− if p.Group != "" {
78− var groups, err = json.Get("groups").Array()
82+ groups, err := json.Get("groups").StringArray()
79− if err != nil {
83+ if err == nil {
80− logger.Printf("groups not found %s", err)
81− return "", err
82− }
83−
84− var found = false
85− for i := range groups {
84+ for _, group := range groups {
86− if groups[i].(string) == p.Group {
85+ if group != "" {
87− found = true
86+ s.Groups = append(s.Groups, group)
88− break
89 }
87 }
90 }
88 }
89+ }
91
90
92− if !found {
91+ email, err := json.Get("email").String()
93− logger.Printf("group not found, access denied")
92+ if err != nil {
94− return "", nil
93+ return fmt.Errorf("unable to extract email from userinfo endpoint: %v", err)
95− }
96 }
94 }
95+ s.Email = email
97
96
98− return json.Get("email").String()
97+ return nil
99}
98}
providers/keycloak_test.go+180−133
@@ -2,17 +2,24 @@ package providers
2
2
3import (
3import (
4 "context"
4 "context"
5+ "errors"
6+ "fmt"
5 "net/http"
7 "net/http"
6 "net/http/httptest"
8 "net/http/httptest"
7 "net/url"
9 "net/url"
8− "testing"
9
10
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12+ . "github.com/onsi/ginkgo"
13+ . "github.com/onsi/ginkgo/extensions/table"
11 . "github.com/onsi/gomega"
14 . "github.com/onsi/gomega"
12− "github.com/stretchr/testify/assert"
13)
15)
14
16
17+const (
18+ keycloakAccessToken = "eyJKeycloak.eyJAccess.Token"
19+ keycloakUserinfoPath = "/api/v3/user"
20+)
21+
15−func testKeycloakProvider(hostname, group string) *KeycloakProvider {
22+func testKeycloakProvider(backend *httptest.Server) (*KeycloakProvider, error) {
16 p := NewKeycloakProvider(
23 p := NewKeycloakProvider(
17 &ProviderData{
24 &ProviderData{
18 ProviderName: "",
25 ProviderName: "",
@@ -22,142 +29,182 @@ func testKeycloakProvider(hostname, group string) *KeycloakProvider {
22 ValidateURL: &url.URL{},
29 ValidateURL: &url.URL{},
23 Scope: ""})
30 Scope: ""})
24
31
25− if group != "" {
32+ if backend != nil {
26− p.SetGroup(group)
33+ bURL, err := url.Parse(backend.URL)
34+ if err != nil {
35+ return nil, err
27− }
36+ }
37+ hostname := bURL.Host
28
38
29− if hostname != "" {
30 updateURL(p.Data().LoginURL, hostname)
39 updateURL(p.Data().LoginURL, hostname)
31 updateURL(p.Data().RedeemURL, hostname)
40 updateURL(p.Data().RedeemURL, hostname)
32 updateURL(p.Data().ProfileURL, hostname)
41 updateURL(p.Data().ProfileURL, hostname)
33 updateURL(p.Data().ValidateURL, hostname)
42 updateURL(p.Data().ValidateURL, hostname)
34 }
43 }
35− return p
36−}
37
44
38−func testKeycloakBackend(payload string) *httptest.Server {
45+ return p, nil
39− path := "/api/v3/user"
40−
41− return httptest.NewServer(http.HandlerFunc(
42− func(w http.ResponseWriter, r *http.Request) {
43− url := r.URL
44− if url.Path != path {
45− w.WriteHeader(404)
46− } else if !IsAuthorizedInHeader(r.Header) {
47− w.WriteHeader(403)
48− } else {
49− w.WriteHeader(200)
50− w.Write([]byte(payload))
51− }
52− }))
53}
46}
54
47
55−func TestKeycloakProviderDefaults(t *testing.T) {
48+var _ = Describe("Keycloak Provider Tests", func() {
56− p := testKeycloakProvider("", "")
49+ Context("New Provider Init", func() {
57− assert.NotEqual(t, nil, p)
50+ It("uses defaults", func() {
58− assert.Equal(t, "Keycloak", p.Data().ProviderName)
59− assert.Equal(t, "https://keycloak.org/oauth/authorize",
60− p.Data().LoginURL.String())
61− assert.Equal(t, "https://keycloak.org/oauth/token",
62− p.Data().RedeemURL.String())
63− assert.Equal(t, "https://keycloak.org/api/v3/user",
64− p.Data().ValidateURL.String())
65− assert.Equal(t, "api", p.Data().Scope)
66−}
67−
68−func TestNewKeycloakProvider(t *testing.T) {
69− g := NewWithT(t)
70−
71− // Test that defaults are set when calling for a new provider with nothing set
72− providerData := NewKeycloakProvider(&ProviderData{}).Data()
51+ providerData := NewKeycloakProvider(&ProviderData{}).Data()
73− g.Expect(providerData.ProviderName).To(Equal("Keycloak"))
52+ Expect(providerData.ProviderName).To(Equal("Keycloak"))
74− g.Expect(providerData.LoginURL.String()).To(Equal("https://keycloak.org/oauth/authorize"))
53+ Expect(providerData.LoginURL.String()).To(Equal("https://keycloak.org/oauth/authorize"))
75− g.Expect(providerData.RedeemURL.String()).To(Equal("https://keycloak.org/oauth/token"))
54+ Expect(providerData.RedeemURL.String()).To(Equal("https://keycloak.org/oauth/token"))
76− g.Expect(providerData.ProfileURL.String()).To(Equal(""))
55+ Expect(providerData.ProfileURL.String()).To(Equal(""))
77− g.Expect(providerData.ValidateURL.String()).To(Equal("https://keycloak.org/api/v3/user"))
56+ Expect(providerData.ValidateURL.String()).To(Equal("https://keycloak.org/api/v3/user"))
78− g.Expect(providerData.Scope).To(Equal("api"))
57+ Expect(providerData.Scope).To(Equal("api"))
79−}
58+ })
80−
59+
81−func TestKeycloakProviderOverrides(t *testing.T) {
60+ It("overrides defaults", func() {
82− p := NewKeycloakProvider(
61+ p := NewKeycloakProvider(
83− &ProviderData{
62+ &ProviderData{
84− LoginURL: &url.URL{
63+ LoginURL: &url.URL{
85− Scheme: "https",
64+ Scheme: "https",
86− Host: "example.com",
65+ Host: "example.com",
87− Path: "/oauth/auth"},
66+ Path: "/oauth/auth"},
88− RedeemURL: &url.URL{
67+ RedeemURL: &url.URL{
89− Scheme: "https",
68+ Scheme: "https",
90− Host: "example.com",
69+ Host: "example.com",
91− Path: "/oauth/token"},
70+ Path: "/oauth/token"},
92− ValidateURL: &url.URL{
71+ ProfileURL: &url.URL{
93− Scheme: "https",
72+ Scheme: "https",
94− Host: "example.com",
73+ Host: "example.com",
95− Path: "/api/v3/user"},
74+ Path: "/api/v3/user"},
96− Scope: "profile"})
75+ ValidateURL: &url.URL{
97− assert.NotEqual(t, nil, p)
76+ Scheme: "https",
98− assert.Equal(t, "Keycloak", p.Data().ProviderName)
77+ Host: "example.com",
78+ Path: "/api/v3/user"},
79+ Scope: "profile"})
80+ providerData := p.Data()
81+
82+ Expect(providerData.ProviderName).To(Equal("Keycloak"))
99− assert.Equal(t, "https://example.com/oauth/auth",
83+ Expect(providerData.LoginURL.String()).To(Equal("https://example.com/oauth/auth"))
100− p.Data().LoginURL.String())
101− assert.Equal(t, "https://example.com/oauth/token",
84+ Expect(providerData.RedeemURL.String()).To(Equal("https://example.com/oauth/token"))
102− p.Data().RedeemURL.String())
103− assert.Equal(t, "https://example.com/api/v3/user",
85+ Expect(providerData.ProfileURL.String()).To(Equal("https://example.com/api/v3/user"))
104− p.Data().ValidateURL.String())
86+ Expect(providerData.ValidateURL.String()).To(Equal("https://example.com/api/v3/user"))
105− assert.Equal(t, "profile", p.Data().Scope)
87+ Expect(providerData.Scope).To(Equal("profile"))
106−}
88+ })
107−
89+ })
108−func TestKeycloakProviderGetEmailAddress(t *testing.T) {
90+
109− b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\"}")
91+ Context("EnrichSession", func() {
110− defer b.Close()
92+ type enrichSessionTableInput struct {
111−
93+ backendHandler http.HandlerFunc
112− bURL, _ := url.Parse(b.URL)
94+ expectedError error
113− p := testKeycloakProvider(bURL.Host, "")
95+ expectedEmail string
114−
96+ expectedGroups []string
115− session := CreateAuthorizedSession()
116− email, err := p.GetEmailAddress(context.Background(), session)
117− assert.Equal(t, nil, err)
118− assert.Equal(t, "michael.bland@gsa.gov", email)
119−}
97+ }
120−
98+
121−func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) {
99+ DescribeTable("should return expected results",
122− b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\", \"groups\": [\"test-grp1\", \"test-grp2\"]}")
100+ func(in enrichSessionTableInput) {
123− defer b.Close()
101+ backend := httptest.NewServer(in.backendHandler)
124−
102+ p, err := testKeycloakProvider(backend)
125− bURL, _ := url.Parse(b.URL)
103+ Expect(err).To(BeNil())
126− p := testKeycloakProvider(bURL.Host, "test-grp1")
104+
127−
105+ p.ProfileURL, err = url.Parse(
128− session := CreateAuthorizedSession()
106+ fmt.Sprintf("%s%s", backend.URL, keycloakUserinfoPath),
129− email, err := p.GetEmailAddress(context.Background(), session)
107+ )
130− assert.Equal(t, nil, err)
108+ Expect(err).To(BeNil())
131− assert.Equal(t, "michael.bland@gsa.gov", email)
109+
132−}
133−
134−// Note that trying to trigger the "failed building request" case is not
135−// practical, since the only way it can fail is if the URL fails to parse.
136−func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) {
137− b := testKeycloakBackend("unused payload")
138− defer b.Close()
139−
140− bURL, _ := url.Parse(b.URL)
141− p := testKeycloakProvider(bURL.Host, "")
142−
143− // We'll trigger a request failure by using an unexpected access
144− // token. Alternatively, we could allow the parsing of the payload as
145− // JSON to fail.
146− session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
110+ session := &sessions.SessionState{AccessToken: keycloakAccessToken}
147− email, err := p.GetEmailAddress(context.Background(), session)
111+ err = p.EnrichSession(context.Background(), session)
148− assert.NotEqual(t, nil, err)
112+
149− assert.Equal(t, "", email)
113+ if in.expectedError != nil {
114+ Expect(err).To(Equal(in.expectedError))
115+ } else {
116+ Expect(err).To(BeNil())
150−}
117+ }
151−
118+
152−func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
119+ Expect(session.Email).To(Equal(in.expectedEmail))
153− b := testKeycloakBackend("{\"foo\": \"bar\"}")
120+
154− defer b.Close()
121+ if in.expectedGroups != nil {
155−
122+ Expect(session.Groups).To(Equal(in.expectedGroups))
156− bURL, _ := url.Parse(b.URL)
123+ } else {
157− p := testKeycloakProvider(bURL.Host, "")
124+ Expect(session.Groups).To(BeNil())
158−
125+ }
159− session := CreateAuthorizedSession()
126+ },
160− email, err := p.GetEmailAddress(context.Background(), session)
127+ Entry("email and multiple groups", enrichSessionTableInput{
161− assert.NotEqual(t, nil, err)
128+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
162− assert.Equal(t, "", email)
129+ w.WriteHeader(200)
130+ _, err := w.Write([]byte(`
131+ {
132+ "email": "michael.bland@gsa.gov",
133+ "groups": [
134+ "test-grp1",
135+ "test-grp2"
136+ ]
137+ }
138+ `))
139+ if err != nil {
140+ panic(err)
163−}
141+ }
142+ },
143+ expectedError: nil,
144+ expectedEmail: "michael.bland@gsa.gov",
145+ expectedGroups: []string{"test-grp1", "test-grp2"},
146+ }),
147+ Entry("email and single group", enrichSessionTableInput{
148+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
149+ w.WriteHeader(200)
150+ _, err := w.Write([]byte(`
151+ {
152+ "email": "michael.bland@gsa.gov",
153+ "groups": ["test-grp1"]
154+ }
155+ `))
156+ if err != nil {
157+ panic(err)
158+ }
159+ },
160+ expectedError: nil,
161+ expectedEmail: "michael.bland@gsa.gov",
162+ expectedGroups: []string{"test-grp1"},
163+ }),
164+ Entry("email and no groups", enrichSessionTableInput{
165+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
166+ w.WriteHeader(200)
167+ _, err := w.Write([]byte(`
168+ {
169+ "email": "michael.bland@gsa.gov"
170+ }
171+ `))
172+ if err != nil {
173+ panic(err)
174+ }
175+ },
176+ expectedError: nil,
177+ expectedEmail: "michael.bland@gsa.gov",
178+ expectedGroups: nil,
179+ }),
180+ Entry("missing email", enrichSessionTableInput{
181+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
182+ w.WriteHeader(200)
183+ _, err := w.Write([]byte(`
184+ {
185+ "groups": [
186+ "test-grp1",
187+ "test-grp2"
188+ ]
189+ }
190+ `))
191+ if err != nil {
192+ panic(err)
193+ }
194+ },
195+ expectedError: errors.New(
196+ "unable to extract email from userinfo endpoint: type assertion to string failed"),
197+ expectedEmail: "",
198+ expectedGroups: []string{"test-grp1", "test-grp2"},
199+ }),
200+ Entry("request failure", enrichSessionTableInput{
201+ backendHandler: func(w http.ResponseWriter, _ *http.Request) {
202+ w.WriteHeader(500)
203+ },
204+ expectedError: errors.New(`unexpected status "500": `),
205+ expectedEmail: "",
206+ expectedGroups: nil,
207+ }),
208+ )
209+ })
210+})