git.iko.soy/typeable/oauth2-proxy
on master help sign in
390 lines 12 kB history raw
1package providers
3import (
4 "context"
5 "errors"
6 "net/http"
7 "net/http/httptest"
8 "net/url"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12 . "github.com/onsi/ginkgo/v2"
13 . "github.com/onsi/gomega"
16func testGitLabProvider(hostname, scope string, opts options.Provider) (*GitLabProvider, error) {
17 p, err := NewGitLabProvider(
18 &ProviderData{
19 ProviderName: "",
20 LoginURL: &url.URL{},
21 RedeemURL: &url.URL{},
22 ProfileURL: &url.URL{},
23 ValidateURL: &url.URL{},
24 Scope: scope},
25 opts)
26 if err != nil {
27 return nil, err
28 }
29 if hostname != "" {
30 updateURL(p.Data().LoginURL, hostname)
31 updateURL(p.Data().RedeemURL, hostname)
32 updateURL(p.Data().ProfileURL, hostname)
33 updateURL(p.Data().ValidateURL, hostname)
34 }
36 return p, err
39func testGitLabBackend() *httptest.Server {
40 userInfo := `
41 {
42 "nickname": "FooBar",
43 "email": "foo@bar.com",
44 "email_verified": false,
45 "groups": ["foo", "bar"]
46 }
47 `
49 projectInfo := `
50 {
51 "name": "MyProject",
52 "archived": false,
53 "path_with_namespace": "my_group/my_project",
54 "permissions": {
55 "project_access": null,
56 "group_access": {
57 "access_level": 30,
58 "notification_level": 3
59 }
60 }
61 }
62 `
64 noAccessProjectInfo := `
65 {
66 "name": "NoAccessProject",
67 "archived": false,
68 "path_with_namespace": "no_access_group/no_access_project",
69 "permissions": {
70 "project_access": null,
71 "group_access": null,
72 }
73 }
74 `
76 personalProjectInfo := `
77 {
78 "name": "MyPersonalProject",
79 "archived": false,
80 "path_with_namespace": "my_profile/my_personal_project",
81 "permissions": {
82 "project_access": {
83 "access_level": 30,
84 "notification_level": 3
85 },
86 "group_access": null
87 }
88 }
89 `
91 archivedProjectInfo := `
92 {
93 "name": "MyArchivedProject",
94 "archived": true,
95 "path_with_namespace": "my_group/my_archived_project",
96 "permissions": {
97 "project_access": {
98 "access_level": 30,
99 "notification_level": 3
100 },
101 "group_access": null
102 }
103 }
104 `
106 authHeader := "Bearer gitlab_access_token"
108 return httptest.NewServer(http.HandlerFunc(
109 func(w http.ResponseWriter, r *http.Request) {
110 switch r.URL.Path {
111 case "/oauth/userinfo":
112 if r.Header["Authorization"][0] == authHeader {
113 w.WriteHeader(200)
114 w.Write([]byte(userInfo))
115 } else {
116 w.WriteHeader(401)
117 }
118 case "/api/v4/projects/my_group/my_project":
119 if r.Header["Authorization"][0] == authHeader {
120 w.WriteHeader(200)
121 w.Write([]byte(projectInfo))
122 } else {
123 w.WriteHeader(401)
124 }
125 case "/api/v4/projects/no_access_group/no_access_project":
126 if r.Header["Authorization"][0] == authHeader {
127 w.WriteHeader(200)
128 w.Write([]byte(noAccessProjectInfo))
129 } else {
130 w.WriteHeader(401)
131 }
132 case "/api/v4/projects/my_group/my_archived_project":
133 if r.Header["Authorization"][0] == authHeader {
134 w.WriteHeader(200)
135 w.Write([]byte(archivedProjectInfo))
136 } else {
137 w.WriteHeader(401)
138 }
139 case "/api/v4/projects/my_profile/my_personal_project":
140 if r.Header["Authorization"][0] == authHeader {
141 w.WriteHeader(200)
142 w.Write([]byte(personalProjectInfo))
143 } else {
144 w.WriteHeader(401)
145 }
146 case "/api/v4/projects/my_group/my_bad_project":
147 w.WriteHeader(403)
148 default:
149 w.WriteHeader(404)
150 }
151 }))
154var _ = Describe("Gitlab Provider Tests", func() {
155 var p *GitLabProvider
156 var b *httptest.Server
158 BeforeEach(func() {
159 b = testGitLabBackend()
161 bURL, err := url.Parse(b.URL)
162 Expect(err).To(BeNil())
164 p, err = testGitLabProvider(bURL.Host, "", options.Provider{})
165 Expect(err).ToNot(HaveOccurred())
166 })
168 AfterEach(func() {
169 b.Close()
170 })
172 Context("New Provider Init", func() {
173 It("creates new keycloak oidc provider with expected defaults", func() {
174 providerData := p.Data()
175 Expect(providerData.ProviderName).To(Equal(gitlabProviderName))
176 Expect(providerData.Scope).To(Equal(gitlabDefaultScope))
177 Expect(providerData.ProviderName).NotTo(Equal(oidcDefaultScope))
178 })
179 })
181 Context("with bad token", func() {
182 It("should trigger an error", func() {
183 p.AllowUnverifiedEmail = false
184 session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"}
185 err := p.EnrichSession(context.Background(), session)
186 Expect(err).To(MatchError(errors.New("failed to retrieve user info: error getting user info: unexpected status \"401\": ")))
187 })
188 })
190 Context("when filtering on email", func() {
191 type emailsTableInput struct {
192 expectedError error
193 expectedValue string
194 allowUnverifiedEmail bool
195 }
197 DescribeTable("should return expected results",
198 func(in emailsTableInput) {
199 p.AllowUnverifiedEmail = in.allowUnverifiedEmail
200 session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
202 err := p.EnrichSession(context.Background(), session)
204 if in.expectedError != nil {
205 Expect(err).To(MatchError(in.expectedError))
206 } else {
207 Expect(err).To(BeNil())
208 Expect(session.Email).To(Equal(in.expectedValue))
209 }
210 },
211 Entry("unverified email denied", emailsTableInput{
212 expectedError: errors.New("user email is not verified"),
213 allowUnverifiedEmail: false,
214 }),
215 Entry("unverified email allowed", emailsTableInput{
216 expectedError: nil,
217 expectedValue: "foo@bar.com",
218 allowUnverifiedEmail: true,
219 }),
220 )
221 })
223 Context("when filtering on gitlab entities (groups and projects)", func() {
224 type entitiesTableInput struct {
225 allowedProjects []string
226 allowedGroups []string
227 scope string
228 expectedAuthz bool
229 expectedError error
230 expectedGroups []string
231 expectedScope string
232 }
234 DescribeTable("should return expected results",
235 func(in entitiesTableInput) {
236 bURL, err := url.Parse(b.URL)
237 Expect(err).To(BeNil())
239 p, err := testGitLabProvider(bURL.Host, in.scope, options.Provider{
240 GitLabConfig: options.GitLabOptions{
241 Group: in.allowedGroups,
242 Projects: in.allowedProjects,
243 },
244 })
245 if in.expectedError == nil {
246 Expect(err).To(BeNil())
247 } else {
248 Expect(err).To(MatchError(in.expectedError))
249 return
250 }
252 p.AllowUnverifiedEmail = true
253 session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
255 Expect(p.Scope).To(Equal(in.expectedScope))
257 err = p.EnrichSession(context.Background(), session)
258 Expect(err).To(BeNil())
259 Expect(session.Groups).To(Equal(in.expectedGroups))
261 authorized, err := p.Authorize(context.Background(), session)
262 Expect(err).To(BeNil())
263 Expect(authorized).To(Equal(in.expectedAuthz))
264 },
265 Entry("project membership valid on group project", entitiesTableInput{
266 allowedProjects: []string{"my_group/my_project"},
267 expectedAuthz: true,
268 expectedGroups: []string{"foo", "bar", "project:my_group/my_project"},
269 expectedScope: "openid email read_api",
270 }),
271 Entry("project membership invalid on group project, insufficient access level level", entitiesTableInput{
272 allowedProjects: []string{"my_group/my_project=40"},
273 expectedAuthz: false,
274 expectedGroups: []string{"foo", "bar"},
275 expectedScope: "openid email read_api",
276 }),
277 Entry("project membership invalid on group project, no access at all", entitiesTableInput{
278 allowedProjects: []string{"no_access_group/no_access_project=30"},
279 expectedAuthz: false,
280 expectedGroups: []string{"foo", "bar"},
281 expectedScope: "openid email read_api",
282 }),
283 Entry("project membership valid on personnal project", entitiesTableInput{
284 allowedProjects: []string{"my_profile/my_personal_project"},
285 scope: "openid email read_api profile",
286 expectedAuthz: true,
287 expectedGroups: []string{"foo", "bar", "project:my_profile/my_personal_project"},
288 expectedScope: "openid email read_api profile",
289 }),
290 Entry("project membership invalid on personnal project, insufficient access level", entitiesTableInput{
291 allowedProjects: []string{"my_profile/my_personal_project=40"},
292 expectedAuthz: false,
293 expectedGroups: []string{"foo", "bar"},
294 expectedScope: "openid email read_api",
295 }),
296 Entry("project membership invalid", entitiesTableInput{
297 allowedProjects: []string{"my_group/my_bad_project"},
298 expectedAuthz: false,
299 expectedGroups: []string{"foo", "bar"},
300 expectedScope: "openid email read_api",
301 }),
302 Entry("group membership valid", entitiesTableInput{
303 allowedGroups: []string{"foo"},
304 expectedGroups: []string{"foo", "bar"},
305 expectedAuthz: true,
306 expectedScope: "openid email",
307 }),
308 Entry("groups and projects", entitiesTableInput{
309 allowedGroups: []string{"foo", "baz"},
310 allowedProjects: []string{"my_group/my_project", "my_profile/my_personal_project"},
311 expectedAuthz: true,
312 expectedGroups: []string{"foo", "bar", "project:my_group/my_project", "project:my_profile/my_personal_project"},
313 expectedScope: "openid email read_api",
314 }),
315 Entry("archived projects", entitiesTableInput{
316 allowedProjects: []string{"my_group/my_archived_project"},
317 expectedAuthz: false,
318 expectedGroups: []string{"foo", "bar"},
319 expectedScope: "openid email read_api",
320 }),
321 Entry("invalid project format", entitiesTableInput{
322 allowedProjects: []string{"my_group/my_invalid_project=123"},
323 expectedError: errors.New("could not configure allowed projects: invalid gitlab project access level specified (my_group/my_invalid_project)"),
324 expectedScope: "openid email read_api",
325 }),
326 )
327 })
329 Context("when refreshing", func() {
330 It("keeps the existing nickname after refreshing", func() {
331 session := &sessions.SessionState{
332 User: "nickname",
333 }
334 p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
335 s.User = "subject"
336 return true, nil
337 }
338 refreshed, err := p.RefreshSession(context.Background(), session)
339 Expect(refreshed).To(BeTrue())
340 Expect(err).ToNot(HaveOccurred())
341 Expect(session.User).To(Equal("nickname"))
342 })
343 It("keeps existing projects after refreshing groups", func() {
344 session := &sessions.SessionState{}
345 session.Groups = []string{"foo", "bar", "project:thing", "project:sample"}
347 p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
348 s.Groups = []string{"baz"}
349 return true, nil
350 }
352 refreshed, err := p.RefreshSession(context.Background(), session)
353 Expect(refreshed).To(BeTrue())
354 Expect(err).ToNot(HaveOccurred())
355 Expect(len(session.Groups)).To(Equal(3))
356 Expect(session.Groups).
357 To(ContainElements([]string{"baz", "project:thing", "project:sample"}))
358 })
359 It("leaves existing groups when not refreshed", func() {
360 session := &sessions.SessionState{}
361 session.Groups = []string{"foo", "bar", "project:thing", "project:sample"}
363 p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
364 return false, nil
365 }
367 refreshed, err := p.RefreshSession(context.Background(), session)
368 Expect(refreshed).To(BeFalse())
369 Expect(err).ToNot(HaveOccurred())
370 Expect(len(session.Groups)).To(Equal(4))
371 Expect(session.Groups).
372 To(ContainElements([]string{"foo", "bar", "project:thing", "project:sample"}))
373 })
374 It("leaves existing groups when OIDC refresh errors", func() {
375 session := &sessions.SessionState{}
376 session.Groups = []string{"foo", "bar", "project:thing", "project:sample"}
378 p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
379 return false, errors.New("failure")
380 }
382 refreshed, err := p.RefreshSession(context.Background(), session)
383 Expect(refreshed).To(BeFalse())
384 Expect(err).To(HaveOccurred())
385 Expect(len(session.Groups)).To(Equal(4))
386 Expect(session.Groups).
387 To(ContainElements([]string{"foo", "bar", "project:thing", "project:sample"}))
388 })
389 })
390})