1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-01-10 04:18:14 +02:00
oauth2-proxy/providers/gitlab.go
2021-09-25 16:48:48 -07:00

241 lines
6.5 KiB
Go

package providers
import (
"context"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
)
const (
gitlabProviderName = "GitLab"
gitlabDefaultScope = "openid email"
)
// GitLabProvider represents a GitLab based Identity Provider
type GitLabProvider struct {
*OIDCProvider
allowedProjects []*gitlabProject
}
var _ Provider = (*GitLabProvider)(nil)
// NewGitLabProvider initiates a new GitLabProvider
func NewGitLabProvider(p *ProviderData) *GitLabProvider {
p.ProviderName = gitlabProviderName
if p.Scope == "" {
p.Scope = gitlabDefaultScope
}
return &GitLabProvider{
OIDCProvider: &OIDCProvider{
ProviderData: p,
SkipNonce: false,
},
}
}
// SetAllowedProjects adds Gitlab projects to the AllowedGroups list
// and tracks them to do a project API lookup during `EnrichSession`.
func (p *GitLabProvider) SetAllowedProjects(projects []string) error {
for _, project := range projects {
gp, err := newGitlabProject(project)
if err != nil {
return err
}
p.allowedProjects = append(p.allowedProjects, gp)
p.AllowedGroups[formatProject(gp)] = struct{}{}
}
if len(p.allowedProjects) > 0 {
p.setProjectScope()
}
return nil
}
// gitlabProject represents a Gitlab project constraint entity
type gitlabProject struct {
Name string
AccessLevel int
}
// newGitlabProject Creates a new GitlabProject struct from project string
// formatted as `namespace/project=accesslevel`
// if no accesslevel provided, use the default one
func newGitlabProject(project string) (*gitlabProject, error) {
const defaultAccessLevel = 20
// see https://docs.gitlab.com/ee/api/members.html#valid-access-levels
validAccessLevel := [4]int{10, 20, 30, 40}
parts := strings.SplitN(project, "=", 2)
if len(parts) == 2 {
lvl, err := strconv.Atoi(parts[1])
if err != nil {
return nil, err
}
for _, valid := range validAccessLevel {
if lvl == valid {
return &gitlabProject{
Name: parts[0],
AccessLevel: lvl,
}, nil
}
}
return nil, fmt.Errorf("invalid gitlab project access level specified (%s)", parts[0])
}
return &gitlabProject{
Name: project,
AccessLevel: defaultAccessLevel,
}, nil
}
// setProjectScope ensures read_api is added to scope when filtering on projects
func (p *GitLabProvider) setProjectScope() {
for _, val := range strings.Split(p.Scope, " ") {
if val == "read_api" {
return
}
}
p.Scope += " read_api"
}
// EnrichSession enriches the session with the response from the userinfo API
// endpoint & projects API endpoint for allowed projects.
func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
// Retrieve user info
userinfo, err := p.getUserinfo(ctx, s)
if err != nil {
return fmt.Errorf("failed to retrieve user info: %v", err)
}
// Check if email is verified
if !p.AllowUnverifiedEmail && !userinfo.EmailVerified {
return fmt.Errorf("user email is not verified")
}
s.User = userinfo.Username
s.Email = userinfo.Email
s.Groups = append(s.Groups, userinfo.Groups...)
p.addProjectsToSession(ctx, s)
return nil
}
type gitlabUserinfo struct {
Username string `json:"nickname"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Groups []string `json:"groups"`
}
func (p *GitLabProvider) getUserinfo(ctx context.Context, s *sessions.SessionState) (*gitlabUserinfo, error) {
// Retrieve user info JSON
// https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
// Build user info url from login url of GitLab instance
userinfoURL := *p.LoginURL
userinfoURL.Path = "/oauth/userinfo"
var userinfo gitlabUserinfo
err := requests.New(userinfoURL.String()).
WithContext(ctx).
SetHeader("Authorization", "Bearer "+s.AccessToken).
Do().
UnmarshalInto(&userinfo)
if err != nil {
return nil, fmt.Errorf("error getting user info: %v", err)
}
return &userinfo, nil
}
// addProjectsToSession adds projects matching user access requirements into
// the session state groups list.
// This method prefixes projects names with `project:` to specify group kind.
func (p *GitLabProvider) addProjectsToSession(ctx context.Context, s *sessions.SessionState) {
// Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
for _, project := range p.allowedProjects {
projectInfo, err := p.getProjectInfo(ctx, s, project.Name)
if err != nil {
logger.Errorf("Warning: project info request failed: %v", err)
continue
}
if projectInfo.Archived {
logger.Errorf("Warning: project %s is archived", project.Name)
continue
}
perms := projectInfo.Permissions.ProjectAccess
if perms == nil {
// use group project access as fallback
perms = projectInfo.Permissions.GroupAccess
// group project access is not set for this user then we give up
if perms == nil {
logger.Errorf("Warning: user %q has no project level access to %s",
s.Email, project.Name)
continue
}
}
if perms.AccessLevel < project.AccessLevel {
logger.Errorf(
"Warning: user %q does not have the minimum required access level for project %q",
s.Email,
project.Name,
)
continue
}
s.Groups = append(s.Groups, formatProject(project))
}
}
type gitlabPermissionAccess struct {
AccessLevel int `json:"access_level"`
}
type gitlabProjectPermission struct {
ProjectAccess *gitlabPermissionAccess `json:"project_access"`
GroupAccess *gitlabPermissionAccess `json:"group_access"`
}
type gitlabProjectInfo struct {
Name string `json:"name"`
Archived bool `json:"archived"`
PathWithNamespace string `json:"path_with_namespace"`
Permissions gitlabProjectPermission `json:"permissions"`
}
func (p *GitLabProvider) getProjectInfo(ctx context.Context, s *sessions.SessionState, project string) (*gitlabProjectInfo, error) {
var projectInfo gitlabProjectInfo
endpointURL := &url.URL{
Scheme: p.LoginURL.Scheme,
Host: p.LoginURL.Host,
Path: "/api/v4/projects/",
}
err := requests.New(fmt.Sprintf("%s%s", endpointURL.String(), url.QueryEscape(project))).
WithContext(ctx).
SetHeader("Authorization", "Bearer "+s.AccessToken).
Do().
UnmarshalInto(&projectInfo)
if err != nil {
return nil, fmt.Errorf("failed to get project info: %v", err)
}
return &projectInfo, nil
}
func formatProject(project *gitlabProject) string {
return fmt.Sprintf("project:%s", project.Name)
}