You've already forked oauth2-proxy
mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-12-05 23:08:20 +02:00
Add authorization support for Gitlab projects (#630)
* Add support for gitlab projets * Add group membership in state * Use prefixed allowed groups everywhere * Fix: remove unused function * Fix: rename func that add data to session * Simplify projects and groups session funcs * Add project access level for gitlab projects * Fix: default access level * Add per project access level * Add user email when missing access level * Fix: harmonize errors * Update docs and flags description for gitlab project * Add test with both projects and groups * Fix: log error message Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Fix: make doc a markdown link * Add notes about read_api scope for projects * Fix: Verifier override in Gitlab Provider This commit fixes a bug caused by an override of the Verifier value from *ProviderData inside GitlabProvider struct * Fix: ensure data in session before using it * Update providers/gitlab.go Co-authored-by: Nick Meves <nick.meves@greenhouse.io> * Rename gitlab project initializer * Improve return value readbility * Use splitN * Handle space delimiters in set project scope * Reword comment for AddProjects * Fix: typo * Rework error handling in addProjectsToSession * Reduce branching complexity in addProjectsToSession * Fix: line returns * Better comment for addProjectsToSession * Fix: enrich session comment * Fix: email domains is handled before provider mechanism * Add archived project unit test * Fix: emails handling in gitlab provider Co-authored-by: Wilfried OLLIVIER <wollivier@bearstech.com> Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> Co-authored-by: Nick Meves <nick.meves@greenhouse.io>
This commit is contained in:
@@ -2,13 +2,15 @@ package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
||||
"github.com/stretchr/testify/assert"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func testGitLabProvider(hostname string) *GitLabProvider {
|
||||
@@ -39,100 +41,241 @@ func testGitLabBackend() *httptest.Server {
|
||||
"groups": ["foo", "bar"]
|
||||
}
|
||||
`
|
||||
projectInfo := `
|
||||
{
|
||||
"name": "MyProject",
|
||||
"archived": false,
|
||||
"path_with_namespace": "my_group/my_project",
|
||||
"permissions": {
|
||||
"project_access": null,
|
||||
"group_access": {
|
||||
"access_level": 30,
|
||||
"notification_level": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
personalProjectInfo := `
|
||||
{
|
||||
"name": "MyPersonalProject",
|
||||
"archived": false,
|
||||
"path_with_namespace": "my_profile/my_personal_project",
|
||||
"permissions": {
|
||||
"project_access": {
|
||||
"access_level": 30,
|
||||
"notification_level": 3
|
||||
},
|
||||
"group_access": null
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
archivedProjectInfo := `
|
||||
{
|
||||
"name": "MyArchivedProject",
|
||||
"archived": true,
|
||||
"path_with_namespace": "my_group/my_archived_project",
|
||||
"permissions": {
|
||||
"project_access": {
|
||||
"access_level": 30,
|
||||
"notification_level": 3
|
||||
},
|
||||
"group_access": null
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
authHeader := "Bearer gitlab_access_token"
|
||||
|
||||
return httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/oauth/userinfo" {
|
||||
switch r.URL.Path {
|
||||
case "/oauth/userinfo":
|
||||
if r.Header["Authorization"][0] == authHeader {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(userInfo))
|
||||
} else {
|
||||
w.WriteHeader(401)
|
||||
}
|
||||
} else {
|
||||
case "/api/v4/projects/my_group/my_project":
|
||||
if r.Header["Authorization"][0] == authHeader {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(projectInfo))
|
||||
} else {
|
||||
w.WriteHeader(401)
|
||||
}
|
||||
case "/api/v4/projects/my_group/my_archived_project":
|
||||
if r.Header["Authorization"][0] == authHeader {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(archivedProjectInfo))
|
||||
} else {
|
||||
w.WriteHeader(401)
|
||||
}
|
||||
case "/api/v4/projects/my_profile/my_personal_project":
|
||||
if r.Header["Authorization"][0] == authHeader {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(personalProjectInfo))
|
||||
} else {
|
||||
w.WriteHeader(401)
|
||||
}
|
||||
case "/api/v4/projects/my_group/my_bad_project":
|
||||
w.WriteHeader(403)
|
||||
default:
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func TestGitLabProviderBadToken(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
defer b.Close()
|
||||
var _ = Describe("Gitlab Provider Tests", func() {
|
||||
var p *GitLabProvider
|
||||
var b *httptest.Server
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
BeforeEach(func() {
|
||||
b = testGitLabBackend()
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"}
|
||||
err := p.EnrichSession(context.Background(), session)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
bURL, err := url.Parse(b.URL)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
func TestGitLabProviderUnverifiedEmailDenied(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
defer b.Close()
|
||||
p = testGitLabProvider(bURL.Host)
|
||||
})
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
AfterEach(func() {
|
||||
b.Close()
|
||||
})
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
err := p.EnrichSession(context.Background(), session)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
Context("with bad token", func() {
|
||||
It("should trigger an error", func() {
|
||||
p.AllowUnverifiedEmail = false
|
||||
session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"}
|
||||
err := p.EnrichSession(context.Background(), session)
|
||||
Expect(err).To(MatchError(errors.New("failed to retrieve user info: error getting user info: unexpected status \"401\": ")))
|
||||
})
|
||||
})
|
||||
|
||||
func TestGitLabProviderUnverifiedEmailAllowed(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
defer b.Close()
|
||||
Context("when filtering on email", func() {
|
||||
type emailsTableInput struct {
|
||||
expectedError error
|
||||
expectedValue string
|
||||
allowUnverifiedEmail bool
|
||||
}
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
p.AllowUnverifiedEmail = true
|
||||
DescribeTable("should return expected results",
|
||||
func(in emailsTableInput) {
|
||||
p.AllowUnverifiedEmail = in.allowUnverifiedEmail
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
err := p.EnrichSession(context.Background(), session)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo@bar.com", session.Email)
|
||||
}
|
||||
err := p.EnrichSession(context.Background(), session)
|
||||
|
||||
func TestGitLabProviderUsername(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
defer b.Close()
|
||||
if in.expectedError != nil {
|
||||
Expect(err).To(MatchError(err))
|
||||
} else {
|
||||
Expect(err).To(BeNil())
|
||||
Expect(session.Email).To(Equal(in.expectedValue))
|
||||
}
|
||||
},
|
||||
Entry("unverified email denied", emailsTableInput{
|
||||
expectedError: errors.New("user email is not verified"),
|
||||
allowUnverifiedEmail: false,
|
||||
}),
|
||||
Entry("unverified email allowed", emailsTableInput{
|
||||
expectedError: nil,
|
||||
expectedValue: "foo@bar.com",
|
||||
allowUnverifiedEmail: true,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
p.AllowUnverifiedEmail = true
|
||||
Context("when filtering on gitlab entities (groups and projects)", func() {
|
||||
type entitiesTableInput struct {
|
||||
expectedValue []string
|
||||
projects []string
|
||||
groups []string
|
||||
}
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
err := p.EnrichSession(context.Background(), session)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "FooBar", session.User)
|
||||
}
|
||||
DescribeTable("should return expected results",
|
||||
func(in entitiesTableInput) {
|
||||
p.AllowUnverifiedEmail = true
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
|
||||
func TestGitLabProviderGroupMembershipValid(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
defer b.Close()
|
||||
err := p.AddProjects(in.projects)
|
||||
Expect(err).To(BeNil())
|
||||
p.SetProjectScope()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
p.AllowUnverifiedEmail = true
|
||||
p.Groups = []string{"foo"}
|
||||
if len(in.groups) > 0 {
|
||||
p.Groups = in.groups
|
||||
}
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
err := p.EnrichSession(context.Background(), session)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "FooBar", session.User)
|
||||
}
|
||||
err = p.EnrichSession(context.Background(), session)
|
||||
|
||||
func TestGitLabProviderGroupMembershipMissing(t *testing.T) {
|
||||
b := testGitLabBackend()
|
||||
defer b.Close()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(session.Groups).To(Equal(in.expectedValue))
|
||||
},
|
||||
Entry("project membership valid on group project", entitiesTableInput{
|
||||
expectedValue: []string{"project:my_group/my_project"},
|
||||
projects: []string{"my_group/my_project"},
|
||||
}),
|
||||
Entry("project membership invalid on group project, insufficient access level level", entitiesTableInput{
|
||||
expectedValue: nil,
|
||||
projects: []string{"my_group/my_project=40"},
|
||||
}),
|
||||
Entry("project membership valid on personnal project", entitiesTableInput{
|
||||
expectedValue: []string{"project:my_profile/my_personal_project"},
|
||||
projects: []string{"my_profile/my_personal_project"},
|
||||
}),
|
||||
Entry("project membership invalid on personnal project, insufficient access level", entitiesTableInput{
|
||||
expectedValue: nil,
|
||||
projects: []string{"my_profile/my_personal_project=40"},
|
||||
}),
|
||||
Entry("project membership invalid", entitiesTableInput{
|
||||
expectedValue: nil,
|
||||
projects: []string{"my_group/my_bad_project"},
|
||||
}),
|
||||
Entry("group membership valid", entitiesTableInput{
|
||||
expectedValue: []string{"group:foo"},
|
||||
groups: []string{"foo"},
|
||||
}),
|
||||
Entry("groups and projects", entitiesTableInput{
|
||||
expectedValue: []string{"group:foo", "group:baz", "project:my_group/my_project", "project:my_profile/my_personal_project"},
|
||||
groups: []string{"foo", "baz"},
|
||||
projects: []string{"my_group/my_project", "my_profile/my_personal_project"},
|
||||
}),
|
||||
Entry("archived projects", entitiesTableInput{
|
||||
expectedValue: nil,
|
||||
groups: []string{},
|
||||
projects: []string{"my_group/my_archived_project"},
|
||||
}),
|
||||
)
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testGitLabProvider(bURL.Host)
|
||||
p.AllowUnverifiedEmail = true
|
||||
p.Groups = []string{"baz"}
|
||||
})
|
||||
|
||||
session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
|
||||
err := p.EnrichSession(context.Background(), session)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
Context("when generating group list from multiple kind", func() {
|
||||
type entitiesTableInput struct {
|
||||
projects []string
|
||||
groups []string
|
||||
}
|
||||
|
||||
DescribeTable("should prefix entities with group kind", func(in entitiesTableInput) {
|
||||
p.Groups = in.groups
|
||||
err := p.AddProjects(in.projects)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
all := p.PrefixAllowedGroups()
|
||||
|
||||
Expect(len(all)).To(Equal(len(in.projects) + len(in.groups)))
|
||||
},
|
||||
Entry("simple test case", entitiesTableInput{
|
||||
projects: []string{"my_group/my_project", "my_group/my_other_project"},
|
||||
groups: []string{"mygroup", "myothergroup"},
|
||||
}),
|
||||
Entry("projects only", entitiesTableInput{
|
||||
projects: []string{"my_group/my_project", "my_group/my_other_project"},
|
||||
groups: []string{},
|
||||
}),
|
||||
Entry("groups only", entitiesTableInput{
|
||||
projects: []string{},
|
||||
groups: []string{"mygroup", "myothergroup"},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user