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/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
17 gitlabProviderName = "GitLab"
18 gitlabDefaultScope = "openid email"
19 gitlabProjectPrefix = "project:"
22// GitLabProvider represents a GitLab based Identity Provider
23type GitLabProvider struct {
26 allowedProjects []*gitlabProject
27 // Expose this for unit testing
28 oidcRefreshFunc func(context.Context, *sessions.SessionState) (bool, error)
31var _ Provider = (*GitLabProvider)(nil)
33// NewGitLabProvider initiates a new GitLabProvider
34func NewGitLabProvider(p *ProviderData, opts options.Provider) (*GitLabProvider, error) {
35 p.setProviderDefaults(providerDefaults{
36 name: gitlabProviderName,
40 p.Scope = gitlabDefaultScope
43 oidcProvider := NewOIDCProvider(p, opts.OIDCConfig)
45 provider := &GitLabProvider{
46 OIDCProvider: oidcProvider,
47 oidcRefreshFunc: oidcProvider.RefreshSession,
49 provider.setAllowedGroups(opts.GitLabConfig.Group)
51 if err := provider.setAllowedProjects(opts.GitLabConfig.Projects); err != nil {
52 return nil, fmt.Errorf("could not configure allowed projects: %v", err)
58// setAllowedProjects adds Gitlab projects to the AllowedGroups list
59// and tracks them to do a project API lookup during `EnrichSession`.
60func (p *GitLabProvider) setAllowedProjects(projects []string) error {
61 for _, project := range projects {
62 gp, err := newGitlabProject(project)
66 p.allowedProjects = append(p.allowedProjects, gp)
67 p.AllowedGroups[formatProject(gp)] = struct{}{}
69 if len(p.allowedProjects) > 0 {
75// gitlabProject represents a Gitlab project constraint entity
76type gitlabProject struct {
81// newGitlabProject Creates a new GitlabProject struct from project string
82// formatted as `namespace/project=accesslevel`
83// if no accesslevel provided, use the default one
84func newGitlabProject(project string) (*gitlabProject, error) {
85 const defaultAccessLevel = 20
86 // see https://docs.gitlab.com/ee/api/members.html#valid-access-levels
87 validAccessLevel := [4]int{10, 20, 30, 40}
89 parts := strings.SplitN(project, "=", 2)
91 lvl, err := strconv.Atoi(parts[1])
95 for _, valid := range validAccessLevel {
97 return &gitlabProject{
103 return nil, fmt.Errorf("invalid gitlab project access level specified (%s)", parts[0])
106 return &gitlabProject{
108 AccessLevel: defaultAccessLevel,
112// setProjectScope ensures read_api is added to scope when filtering on projects
113func (p *GitLabProvider) setProjectScope() {
114 for _, val := range strings.Split(p.Scope, " ") {
115 if val == "read_api" {
119 p.Scope += " read_api"
122// EnrichSession enriches the session with the response from the userinfo API
123// endpoint & projects API endpoint for allowed projects.
124func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
125 // Retrieve user info
126 userinfo, err := p.getUserinfo(ctx, s)
128 return fmt.Errorf("failed to retrieve user info: %v", err)
131 // Check if email is verified
132 if !p.AllowUnverifiedEmail && !userinfo.EmailVerified {
133 return fmt.Errorf("user email is not verified")
136 if userinfo.Nickname != "" {
137 s.User = userinfo.Nickname
139 if userinfo.Email != "" {
140 s.Email = userinfo.Email
142 if len(userinfo.Groups) > 0 {
143 s.Groups = userinfo.Groups
146 // Add projects as `project:blah` to s.Groups
147 p.addProjectsToSession(ctx, s)
152type gitlabUserinfo struct {
153 Nickname string `json:"nickname"`
154 Email string `json:"email"`
155 EmailVerified bool `json:"email_verified"`
156 Groups []string `json:"groups"`
159func (p *GitLabProvider) getUserinfo(ctx context.Context, s *sessions.SessionState) (*gitlabUserinfo, error) {
160 // Retrieve user info JSON
161 // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
163 // Build user info url from login url of GitLab instance
164 userinfoURL := *p.LoginURL
165 userinfoURL.Path = "/oauth/userinfo"
167 var userinfo gitlabUserinfo
168 err := requests.New(userinfoURL.String()).
170 SetHeader("Authorization", tokenTypeBearer+" "+s.AccessToken).
172 UnmarshalInto(&userinfo)
174 return nil, fmt.Errorf("error getting user info: %v", err)
177 return &userinfo, nil
180// addProjectsToSession adds projects matching user access requirements into
181// the session state groups list.
182// This method prefixes projects names with `project:` to specify group kind.
183func (p *GitLabProvider) addProjectsToSession(ctx context.Context, s *sessions.SessionState) {
184 // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
185 for _, project := range p.allowedProjects {
186 projectInfo, err := p.getProjectInfo(ctx, s, project.Name)
188 logger.Errorf("Warning: project info request failed: %v", err)
192 if projectInfo.Archived {
193 logger.Errorf("Warning: project %s is archived", project.Name)
197 perms := projectInfo.Permissions.ProjectAccess
199 // use group project access as fallback
200 perms = projectInfo.Permissions.GroupAccess
201 // group project access is not set for this user then we give up
203 logger.Errorf("Warning: user %q has no project level access to %s",
204 s.Email, project.Name)
209 if perms.AccessLevel < project.AccessLevel {
211 "Warning: user %q does not have the minimum required access level for project %q",
218 s.Groups = append(s.Groups, formatProject(project))
222type gitlabPermissionAccess struct {
223 AccessLevel int `json:"access_level"`
226type gitlabProjectPermission struct {
227 ProjectAccess *gitlabPermissionAccess `json:"project_access"`
228 GroupAccess *gitlabPermissionAccess `json:"group_access"`
231type gitlabProjectInfo struct {
232 Name string `json:"name"`
233 Archived bool `json:"archived"`
234 PathWithNamespace string `json:"path_with_namespace"`
235 Permissions gitlabProjectPermission `json:"permissions"`
238func (p *GitLabProvider) getProjectInfo(ctx context.Context, s *sessions.SessionState, project string) (*gitlabProjectInfo, error) {
239 var projectInfo gitlabProjectInfo
241 endpointURL := &url.URL{
242 Scheme: p.LoginURL.Scheme,
243 Host: p.LoginURL.Host,
244 Path: "/api/v4/projects/",
247 err := requests.New(fmt.Sprintf("%s%s", endpointURL.String(), url.QueryEscape(project))).
249 SetHeader("Authorization", tokenTypeBearer+" "+s.AccessToken).
251 UnmarshalInto(&projectInfo)
253 return nil, fmt.Errorf("failed to get project info: %v", err)
256 return &projectInfo, nil
259func formatProject(project *gitlabProject) string {
260 return gitlabProjectPrefix + project.Name
263// RefreshSession refreshes the session with the OIDCProvider implementation
264// but preserves the custom GitLab projects added in the `EnrichSession` stage.
265func (p *GitLabProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
267 projects := getSessionProjects(s)
268 // This will overwrite s.Groups with the new IDToken's `groups` claims
269 // and s.User with the `sub` claim.
270 refreshed, err := p.oidcRefreshFunc(ctx, s)
271 if refreshed && err == nil {
273 s.Groups = append(s.Groups, projects...)
274 s.Groups = deduplicateGroups(s.Groups)
276 return refreshed, err
279func getSessionProjects(s *sessions.SessionState) []string {
280 var projects []string
281 for _, group := range s.Groups {
282 if strings.HasPrefix(group, gitlabProjectPrefix) {
283 projects = append(projects, group)
289func deduplicateGroups(groups []string) []string {
290 groupSet := make(map[string]struct{})
291 for _, group := range groups {
292 groupSet[group] = struct{}{}
295 uniqueGroups := make([]string, 0, len(groupSet))
296 for group := range groupSet {
297 uniqueGroups = append(uniqueGroups, group)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list