@@ -6,194 +6,196 @@ import (
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
14− "golang.org/x/oauth2"
13+)
16+ gitlabProviderName = "GitLab"
17+ gitlabDefaultScope = "openid email"
17// GitLabProvider represents a GitLab based Identity Provider
20// GitLabProvider represents a GitLab based Identity Provider
18type GitLabProvider struct {
21type GitLabProvider struct {
19− *ProviderData
22+ *OIDCProvider
21− Groups []string
24+ allowedProjects []*gitlabProject
22− Projects []*GitlabProject
27+var _ Provider = (*GitLabProvider)(nil)
29+// NewGitLabProvider initiates a new GitLabProvider
30+func NewGitLabProvider(p *ProviderData) *GitLabProvider {
31+ p.ProviderName = gitlabProviderName
33+ p.Scope = gitlabDefaultScope
36+ return &GitLabProvider{
37+ OIDCProvider: &OIDCProvider{
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)
52+ p.allowedProjects = append(p.allowedProjects, gp)
53+ p.AllowedGroups[formatProject(gp)] = struct{}{}
55+ if len(p.allowedProjects) > 0 {
25−// GitlabProject represents a Gitlab project constraint entity
61+// gitlabProject represents a Gitlab project constraint entity
26−type GitlabProject struct {
62+type gitlabProject struct {
27 Name string
63 Name string
28 AccessLevel int
64 AccessLevel int
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`
32// if no accesslevel provided, use the default one
69// if no accesslevel provided, use the default one
33−func newGitlabproject(project string) (*GitlabProject, error) {
70+func newGitlabProject(project string) (*gitlabProject, error) {
34− // default access level is 20
35− defaultAccessLevel := 20
71+ const defaultAccessLevel = 20
36 // see https://docs.gitlab.com/ee/api/members.html#valid-access-levels
72 // see https://docs.gitlab.com/ee/api/members.html#valid-access-levels
37 validAccessLevel := [4]int{10, 20, 30, 40}
73 validAccessLevel := [4]int{10, 20, 30, 40}
39 parts := strings.SplitN(project, "=", 2)
75 parts := strings.SplitN(project, "=", 2)
41 if len(parts) == 2 {
76 if len(parts) == 2 {
42 lvl, err := strconv.Atoi(parts[1])
77 lvl, err := strconv.Atoi(parts[1])
43 if err != nil {
78 if err != nil {
44 return nil, err
79 return nil, err
47 for _, valid := range validAccessLevel {
81 for _, valid := range validAccessLevel {
48 if lvl == valid {
82 if lvl == valid {
49− return &GitlabProject{
83+ return &gitlabProject{
50− Name: parts[0],
84+ Name: parts[0],
51− AccessLevel: lvl},
85+ AccessLevel: lvl,
56 return nil, fmt.Errorf("invalid gitlab project access level specified (%s)", parts[0])
89 return nil, fmt.Errorf("invalid gitlab project access level specified (%s)", parts[0])
60− return &GitlabProject{
62− AccessLevel: defaultAccessLevel},
67−var _ Provider = (*GitLabProvider)(nil)
70− gitlabProviderName = "GitLab"
71− gitlabDefaultScope = "openid email"
74−// NewGitLabProvider initiates a new GitLabProvider
75−func NewGitLabProvider(p *ProviderData) *GitLabProvider {
76− p.ProviderName = gitlabProviderName
79− p.Scope = gitlabDefaultScope
82− return &GitLabProvider{ProviderData: p}
92+ return &gitlabProject{
94+ AccessLevel: defaultAccessLevel,
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()
93− ClientID: p.ClientID,
94− ClientSecret: clientSecret,
95− Endpoint: oauth2.Endpoint{
96− TokenURL: p.RedeemURL.String(),
98− RedirectURL: redirectURL,
100− token, err := c.Exchange(ctx, code)
102− return nil, fmt.Errorf("token exchange: %v", err)
104− s, err = p.createSession(ctx, token)
106− return nil, fmt.Errorf("unable to update session: %v", err)
111−// SetProjectScope ensure read_api is added to scope when filtering on projects
98+// setProjectScope ensures read_api is added to scope when filtering on projects
112−func (p *GitLabProvider) SetProjectScope() {
99+func (p *GitLabProvider) setProjectScope() {
113− if len(p.Projects) > 0 {
114− for _, val := range strings.Split(p.Scope, " ") {
100+ for _, val := range strings.Split(p.Scope, " ") {
115− if val == "read_api" {
101+ if val == "read_api" {
120− p.Scope += " read_api"
105+ p.Scope += " read_api"
124−// RefreshSession uses the RefreshToken to fetch new Access and ID Tokens
108+// EnrichSession enriches the session with the response from the userinfo API
109+// endpoint & projects API endpoint for allowed projects.
125−func (p *GitLabProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
110+func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
126− if s == nil || s.RefreshToken == "" {
111+ // Retrieve user info
127− return false, nil
112+ userinfo, err := p.getUserinfo(ctx, s)
130− origExpiration := s.ExpiresOn
132− err := p.redeemRefreshToken(ctx, s)
133 if err != nil {
113 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)
137− logger.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
117+ // Check if email is verified
138− return true, nil
118+ if !p.AllowUnverifiedEmail && !userinfo.EmailVerified {
139−}
119+ return fmt.Errorf("user email is not verified")
141−func (p *GitLabProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) error {
142− clientSecret, err := p.GetClientSecret()
147− c := oauth2.Config{
122+ s.User = userinfo.Username
148− ClientID: p.ClientID,
123+ s.Email = userinfo.Email
149− ClientSecret: clientSecret,
124+ s.Groups = append(s.Groups, userinfo.Groups...)
150− Endpoint: oauth2.Endpoint{
125+
151− TokenURL: p.RedeemURL.String(),
126+ p.addProjectsToSession(ctx, s)
155− RefreshToken: s.RefreshToken,
156− Expiry: time.Now().Add(-time.Hour),
158− token, err := c.TokenSource(ctx, t).Token()
160− return fmt.Errorf("failed to get token: %v", err)
162− newSession, err := p.createSession(ctx, token)
164− return fmt.Errorf("unable to update session: %v", err)
168 return nil
128 return nil
171−type gitlabUserInfo struct {
131+type gitlabUserinfo struct {
172 Username string `json:"nickname"`
132 Username string `json:"nickname"`
173 Email string `json:"email"`
133 Email string `json:"email"`
174 EmailVerified bool `json:"email_verified"`
134 EmailVerified bool `json:"email_verified"`
175 Groups []string `json:"groups"`
135 Groups []string `json:"groups"`
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) {
179 // Retrieve user info JSON
139 // Retrieve user info JSON
180 // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
140 // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
182 // Build user info url from login url of GitLab instance
142 // Build user info url from login url of GitLab instance
183− userInfoURL := *p.LoginURL
143+ userinfoURL := *p.LoginURL
184− userInfoURL.Path = "/oauth/userinfo"
144+ userinfoURL.Path = "/oauth/userinfo"
186− var userInfo gitlabUserInfo
146+ var userinfo gitlabUserinfo
187− err := requests.New(userInfoURL.String()).
147+ err := requests.New(userinfoURL.String()).
188 WithContext(ctx).
148 WithContext(ctx).
189 SetHeader("Authorization", "Bearer "+s.AccessToken).
149 SetHeader("Authorization", "Bearer "+s.AccessToken).
191− UnmarshalInto(&userInfo)
151+ UnmarshalInto(&userinfo)
192 if err != nil {
152 if err != nil {
193 return nil, fmt.Errorf("error getting user info: %v", err)
153 return nil, fmt.Errorf("error getting user info: %v", err)
196− return &userInfo, nil
156+ return &userinfo, nil
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)
167+ logger.Errorf("Warning: project info request failed: %v", err)
171+ if projectInfo.Archived {
172+ logger.Errorf("Warning: project %s is archived", project.Name)
176+ perms := projectInfo.Permissions.ProjectAccess
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
182+ logger.Errorf("Warning: user %q has no project level access to %s",
183+ s.Email, project.Name)
188+ if perms.AccessLevel < project.AccessLevel {
190+ "Warning: user %q does not have the minimum required access level for project %q",
197+ s.Groups = append(s.Groups, formatProject(project))
199type gitlabPermissionAccess struct {
201type gitlabPermissionAccess struct {
@@ -226,7 +228,6 @@ func (p *GitLabProvider) getProjectInfo(ctx context.Context, s *sessions.Session
226 SetHeader("Authorization", "Bearer "+s.AccessToken).
228 SetHeader("Authorization", "Bearer "+s.AccessToken).
228 UnmarshalInto(&projectInfo)
230 UnmarshalInto(&projectInfo)
230 if err != nil {
231 if err != nil {
231 return nil, fmt.Errorf("failed to get project info: %v", err)
232 return nil, fmt.Errorf("failed to get project info: %v", err)
@@ -234,116 +235,6 @@ func (p *GitLabProvider) getProjectInfo(ctx context.Context, s *sessions.Session
234 return &projectInfo, nil
235 return &projectInfo, nil
237−// AddProjects adds Gitlab projects from options to GitlabProvider struct
238+func formatProject(project *gitlabProject) string {
238−func (p *GitLabProvider) AddProjects(projects []string) error {
239− for _, project := range projects {
240− gp, err := newGitlabproject(project)
245− p.Projects = append(p.Projects, gp)
251−func (p *GitLabProvider) createSession(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
252− idToken, err := p.verifyIDToken(ctx, token)
255− case ErrMissingIDToken:
256− return nil, fmt.Errorf("token response did not contain an id_token")
258− return nil, fmt.Errorf("could not verify id_token: %v", err)
262− ss := &sessions.SessionState{
263− AccessToken: token.AccessToken,
264− IDToken: getIDToken(token),
265− RefreshToken: token.RefreshToken,
269− ss.SetExpiresOn(idToken.Expiry)
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)
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)
285− return fmt.Errorf("failed to retrieve user info: %v", err)
288− // Check if email is verified
289− if !p.AllowUnverifiedEmail && !userInfo.EmailVerified {
290− return fmt.Errorf("user email is not verified")
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))
299− p.addProjectsToSession(ctx, s)
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)
312− logger.Errorf("Warning: project info request failed: %v", err)
316− if !projectInfo.Archived {
317− perms := projectInfo.Permissions.ProjectAccess
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
323− logger.Errorf("Warning: user %q has no project level access to %s", s.Email, project.Name)
328− if perms != nil && perms.AccessLevel >= project.AccessLevel {
329− s.Groups = append(s.Groups, fmt.Sprintf("project:%s", project.Name))
239+ return fmt.Sprintf("project:%s", project.Name)
331− logger.Errorf("Warning: user %q does not have the minimum required access level for project %q", s.Email, project.Name)
336− logger.Errorf("Warning: project %s is archived", project.Name)
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))
345− for _, val := range p.Projects {
346− groups = append(groups, fmt.Sprintf("project:%s", val.Name))