@@ -6,194 +6,196 @@ import (
66 "net/url"
77 "strconv"
88 "strings"
9− "time"
109
1110 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1211 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1312 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
14− "golang.org/x/oauth2"
13+)
14+
15+const (
16+ gitlabProviderName = "GitLab"
17+ gitlabDefaultScope = "openid email"
1518)
1619
1720// GitLabProvider represents a GitLab based Identity Provider
1821type GitLabProvider struct {
19− *ProviderData
22+ *OIDCProvider
2023
21− Groups []string
22− Projects []*GitlabProject
24+ allowedProjects []*gitlabProject
2325}
2426
25−// GitlabProject represents a Gitlab project constraint entity
26−type GitlabProject struct {
27+var _ Provider = (*GitLabProvider)(nil)
28+
29+// NewGitLabProvider initiates a new GitLabProvider
30+func NewGitLabProvider(p *ProviderData) *GitLabProvider {
31+ p.ProviderName = gitlabProviderName
32+ if p.Scope == "" {
33+ p.Scope = gitlabDefaultScope
34+ }
35+
36+ return &GitLabProvider{
37+ OIDCProvider: &OIDCProvider{
38+ ProviderData: p,
39+ SkipNonce: false,
40+ },
41+ }
42+}
43+
44+// SetAllowedProjects adds Gitlab projects to the AllowedGroups list
45+// and tracks them to do a project API lookup during `EnrichSession`.
46+func (p *GitLabProvider) SetAllowedProjects(projects []string) error {
47+ for _, project := range projects {
48+ gp, err := newGitlabProject(project)
49+ if err != nil {
50+ return err
51+ }
52+ p.allowedProjects = append(p.allowedProjects, gp)
53+ p.AllowedGroups[formatProject(gp)] = struct{}{}
54+ }
55+ if len(p.allowedProjects) > 0 {
56+ p.setProjectScope()
57+ }
58+ return nil
59+}
60+
61+// gitlabProject represents a Gitlab project constraint entity
62+type gitlabProject struct {
2763 Name string
2864 AccessLevel int
2965}
3066
31−// newGitlabProject Creates a new GitlabProject struct from project string formatted as namespace/project=accesslevel
67+// newGitlabProject Creates a new GitlabProject struct from project string
68+// formatted as `namespace/project=accesslevel`
3269// if no accesslevel provided, use the default one
33−func newGitlabproject(project string) (*GitlabProject, error) {
34− // default access level is 20
35− defaultAccessLevel := 20
70+func newGitlabProject(project string) (*gitlabProject, error) {
71+ const defaultAccessLevel = 20
3672 // see https://docs.gitlab.com/ee/api/members.html#valid-access-levels
3773 validAccessLevel := [4]int{10, 20, 30, 40}
3874
3975 parts := strings.SplitN(project, "=", 2)
40−
4176 if len(parts) == 2 {
4277 lvl, err := strconv.Atoi(parts[1])
4378 if err != nil {
4479 return nil, err
4580 }
46−
4781 for _, valid := range validAccessLevel {
4882 if lvl == valid {
49− return &GitlabProject{
50− Name: parts[0],
51− AccessLevel: lvl},
52− err
83+ return &gitlabProject{
84+ Name: parts[0],
85+ AccessLevel: lvl,
86+ }, nil
5387 }
5488 }
55−
5689 return nil, fmt.Errorf("invalid gitlab project access level specified (%s)", parts[0])
57−
58− }
59−
60− return &GitlabProject{
61− Name: project,
62− AccessLevel: defaultAccessLevel},
63− nil
64−
65−}
66−
67−var _ Provider = (*GitLabProvider)(nil)
68−
69−const (
70− gitlabProviderName = "GitLab"
71− gitlabDefaultScope = "openid email"
72−)
73−
74−// NewGitLabProvider initiates a new GitLabProvider
75−func NewGitLabProvider(p *ProviderData) *GitLabProvider {
76− p.ProviderName = gitlabProviderName
77−
78− if p.Scope == "" {
79− p.Scope = gitlabDefaultScope
8090 }
8191
82− return &GitLabProvider{ProviderData: p}
92+ return &gitlabProject{
93+ Name: project,
94+ AccessLevel: defaultAccessLevel,
95+ }, nil
8396}
8497
85−// Redeem exchanges the OAuth2 authentication token for an ID token
86−func (p *GitLabProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) {
87− clientSecret, err := p.GetClientSecret()
88− if err != nil {
89− return
90− }
91−
92− c := oauth2.Config{
93− ClientID: p.ClientID,
94− ClientSecret: clientSecret,
95− Endpoint: oauth2.Endpoint{
96− TokenURL: p.RedeemURL.String(),
97− },
98− RedirectURL: redirectURL,
99− }
100− token, err := c.Exchange(ctx, code)
101− if err != nil {
102− return nil, fmt.Errorf("token exchange: %v", err)
103− }
104− s, err = p.createSession(ctx, token)
105− if err != nil {
106− return nil, fmt.Errorf("unable to update session: %v", err)
107− }
108− return
109−}
110−
111−// SetProjectScope ensure read_api is added to scope when filtering on projects
112−func (p *GitLabProvider) SetProjectScope() {
113− if len(p.Projects) > 0 {
114− for _, val := range strings.Split(p.Scope, " ") {
115− if val == "read_api" {
116− return
117− }
118−
98+// setProjectScope ensures read_api is added to scope when filtering on projects
99+func (p *GitLabProvider) setProjectScope() {
100+ for _, val := range strings.Split(p.Scope, " ") {
101+ if val == "read_api" {
102+ return
119103 }
120− p.Scope += " read_api"
121104 }
105+ p.Scope += " read_api"
122106}
123107
124−// RefreshSession uses the RefreshToken to fetch new Access and ID Tokens
125−func (p *GitLabProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
126− if s == nil || s.RefreshToken == "" {
127− return false, nil
128− }
129−
130− origExpiration := s.ExpiresOn
131−
132− err := p.redeemRefreshToken(ctx, s)
108+// EnrichSession enriches the session with the response from the userinfo API
109+// endpoint & projects API endpoint for allowed projects.
110+func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
111+ // Retrieve user info
112+ userinfo, err := p.getUserinfo(ctx, s)
133113 if err != nil {
134− return false, fmt.Errorf("unable to redeem refresh token: %v", err)
114+ return fmt.Errorf("failed to retrieve user info: %v", err)
135115 }
136116
137− logger.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
138− return true, nil
139−}
140−
141−func (p *GitLabProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) error {
142− clientSecret, err := p.GetClientSecret()
143− if err != nil {
144− return err
117+ // Check if email is verified
118+ if !p.AllowUnverifiedEmail && !userinfo.EmailVerified {
119+ return fmt.Errorf("user email is not verified")
145120 }
146121
147− c := oauth2.Config{
148− ClientID: p.ClientID,
149− ClientSecret: clientSecret,
150− Endpoint: oauth2.Endpoint{
151− TokenURL: p.RedeemURL.String(),
152− },
153− }
154− t := &oauth2.Token{
155− RefreshToken: s.RefreshToken,
156− Expiry: time.Now().Add(-time.Hour),
157− }
158− token, err := c.TokenSource(ctx, t).Token()
159− if err != nil {
160− return fmt.Errorf("failed to get token: %v", err)
161− }
162− newSession, err := p.createSession(ctx, token)
163− if err != nil {
164− return fmt.Errorf("unable to update session: %v", err)
165− }
166− *s = *newSession
122+ s.User = userinfo.Username
123+ s.Email = userinfo.Email
124+ s.Groups = append(s.Groups, userinfo.Groups...)
125+
126+ p.addProjectsToSession(ctx, s)
167127
168128 return nil
169129}
170130
171−type gitlabUserInfo struct {
131+type gitlabUserinfo struct {
172132 Username string `json:"nickname"`
173133 Email string `json:"email"`
174134 EmailVerified bool `json:"email_verified"`
175135 Groups []string `json:"groups"`
176136}
177137
178−func (p *GitLabProvider) getUserInfo(ctx context.Context, s *sessions.SessionState) (*gitlabUserInfo, error) {
138+func (p *GitLabProvider) getUserinfo(ctx context.Context, s *sessions.SessionState) (*gitlabUserinfo, error) {
179139 // Retrieve user info JSON
180140 // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
181141
182142 // Build user info url from login url of GitLab instance
183− userInfoURL := *p.LoginURL
184− userInfoURL.Path = "/oauth/userinfo"
143+ userinfoURL := *p.LoginURL
144+ userinfoURL.Path = "/oauth/userinfo"
185145
186− var userInfo gitlabUserInfo
187− err := requests.New(userInfoURL.String()).
146+ var userinfo gitlabUserinfo
147+ err := requests.New(userinfoURL.String()).
188148 WithContext(ctx).
189149 SetHeader("Authorization", "Bearer "+s.AccessToken).
190150 Do().
191− UnmarshalInto(&userInfo)
151+ UnmarshalInto(&userinfo)
192152 if err != nil {
193153 return nil, fmt.Errorf("error getting user info: %v", err)
194154 }
195155
196− return &userInfo, nil
156+ return &userinfo, nil
157+}
158+
159+// addProjectsToSession adds projects matching user access requirements into
160+// the session state groups list.
161+// This method prefixes projects names with `project:` to specify group kind.
162+func (p *GitLabProvider) addProjectsToSession(ctx context.Context, s *sessions.SessionState) {
163+ // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
164+ for _, project := range p.allowedProjects {
165+ projectInfo, err := p.getProjectInfo(ctx, s, project.Name)
166+ if err != nil {
167+ logger.Errorf("Warning: project info request failed: %v", err)
168+ continue
169+ }
170+
171+ if projectInfo.Archived {
172+ logger.Errorf("Warning: project %s is archived", project.Name)
173+ continue
174+ }
175+
176+ perms := projectInfo.Permissions.ProjectAccess
177+ if perms == nil {
178+ // use group project access as fallback
179+ perms = projectInfo.Permissions.GroupAccess
180+ // group project access is not set for this user then we give up
181+ if perms == nil {
182+ logger.Errorf("Warning: user %q has no project level access to %s",
183+ s.Email, project.Name)
184+ continue
185+ }
186+ }
187+
188+ if perms.AccessLevel < project.AccessLevel {
189+ logger.Errorf(
190+ "Warning: user %q does not have the minimum required access level for project %q",
191+ s.Email,
192+ project.Name,
193+ )
194+ continue
195+ }
196+
197+ s.Groups = append(s.Groups, formatProject(project))
198+ }
197199}
198200
199201type gitlabPermissionAccess struct {
@@ -226,7 +228,6 @@ func (p *GitLabProvider) getProjectInfo(ctx context.Context, s *sessions.Session
226228 SetHeader("Authorization", "Bearer "+s.AccessToken).
227229 Do().
228230 UnmarshalInto(&projectInfo)
229−
230231 if err != nil {
231232 return nil, fmt.Errorf("failed to get project info: %v", err)
232233 }
@@ -234,116 +235,6 @@ func (p *GitLabProvider) getProjectInfo(ctx context.Context, s *sessions.Session
234235 return &projectInfo, nil
235236}
236237
237−// AddProjects adds Gitlab projects from options to GitlabProvider struct
238−func (p *GitLabProvider) AddProjects(projects []string) error {
239− for _, project := range projects {
240− gp, err := newGitlabproject(project)
241− if err != nil {
242− return err
243− }
244−
245− p.Projects = append(p.Projects, gp)
246− }
247−
248− return nil
249−}
250−
251−func (p *GitLabProvider) createSession(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
252− idToken, err := p.verifyIDToken(ctx, token)
253− if err != nil {
254− switch err {
255− case ErrMissingIDToken:
256− return nil, fmt.Errorf("token response did not contain an id_token")
257− default:
258− return nil, fmt.Errorf("could not verify id_token: %v", err)
259− }
260− }
261−
262− ss := &sessions.SessionState{
263− AccessToken: token.AccessToken,
264− IDToken: getIDToken(token),
265− RefreshToken: token.RefreshToken,
266− }
267−
268− ss.CreatedAtNow()
269− ss.SetExpiresOn(idToken.Expiry)
270−
271− return ss, nil
272−}
273−
274−// ValidateSession checks that the session's IDToken is still valid
275−func (p *GitLabProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
276− _, err := p.Verifier.Verify(ctx, s.IDToken)
277− return err == nil
278−}
279−
280−// EnrichSession adds values and data from the Gitlab endpoint to current session
281−func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
282− // Retrieve user info
283− userInfo, err := p.getUserInfo(ctx, s)
284− if err != nil {
285− return fmt.Errorf("failed to retrieve user info: %v", err)
286− }
287−
288− // Check if email is verified
289− if !p.AllowUnverifiedEmail && !userInfo.EmailVerified {
290− return fmt.Errorf("user email is not verified")
291− }
292−
293− s.User = userInfo.Username
294− s.Email = userInfo.Email
295− for _, group := range userInfo.Groups {
296− s.Groups = append(s.Groups, fmt.Sprintf("group:%s", group))
297− }
298−
299− p.addProjectsToSession(ctx, s)
300−
301− return nil
302−}
303−
304−// addProjectsToSession adds projects matching user access requirements into the session state groups list
305−// This method prefix projects names with `project` to specify group kind
306−func (p *GitLabProvider) addProjectsToSession(ctx context.Context, s *sessions.SessionState) {
307− // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
308− for _, project := range p.Projects {
309− projectInfo, err := p.getProjectInfo(ctx, s, project.Name)
310−
311− if err != nil {
312− logger.Errorf("Warning: project info request failed: %v", err)
313− continue
314− }
315−
316− if !projectInfo.Archived {
317− perms := projectInfo.Permissions.ProjectAccess
318− if perms == nil {
319− // use group project access as fallback
320− perms = projectInfo.Permissions.GroupAccess
321− // group project access is not set for this user then we give up
322− if perms == nil {
323− logger.Errorf("Warning: user %q has no project level access to %s", s.Email, project.Name)
324− continue
325− }
326− }
327−
328− if perms != nil && perms.AccessLevel >= project.AccessLevel {
329− s.Groups = append(s.Groups, fmt.Sprintf("project:%s", project.Name))
330− } else {
331− logger.Errorf("Warning: user %q does not have the minimum required access level for project %q", s.Email, project.Name)
332− }
333− continue
334− }
335−
336− logger.Errorf("Warning: project %s is archived", project.Name)
337− }
338−}
339−
340−// PrefixAllowedGroups returns a list of allowed groups, prefixed by their `kind` value
341−func (p *GitLabProvider) PrefixAllowedGroups() (groups []string) {
342− for _, val := range p.Groups {
343− groups = append(groups, fmt.Sprintf("group:%s", val))
344− }
345− for _, val := range p.Projects {
346− groups = append(groups, fmt.Sprintf("project:%s", val.Name))
347− }
348− return groups
238+func formatProject(project *gitlabProject) string {
239+ return fmt.Sprintf("project:%s", project.Name)
349240}