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