Migrate Keycloak to EnrichSession & support multiple groups
This commit is contained in:
parent
89e0a77a8f
commit
3369799853
@ -36,7 +36,7 @@ type Options struct {
|
||||
TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"`
|
||||
|
||||
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
|
||||
KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group"`
|
||||
KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"`
|
||||
AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
|
||||
BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
|
||||
BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
|
||||
@ -181,7 +181,7 @@ func NewFlagSet() *pflag.FlagSet {
|
||||
|
||||
flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
|
||||
flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
|
||||
flagSet.String("keycloak-group", "", "restrict login to members of this group.")
|
||||
flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)")
|
||||
flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
|
||||
flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
|
||||
flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
|
||||
|
@ -263,7 +263,10 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
|
||||
p.SetRepo(o.GitHubRepo, o.GitHubToken)
|
||||
p.SetUsers(o.GitHubUsers)
|
||||
case *providers.KeycloakProvider:
|
||||
p.SetGroup(o.KeycloakGroup)
|
||||
// Backwards compatibility with `--keycloak-group` option
|
||||
if len(o.KeycloakGroups) > 0 {
|
||||
p.SetAllowedGroups(o.KeycloakGroups)
|
||||
}
|
||||
case *providers.GoogleProvider:
|
||||
if o.GoogleServiceAccountJSON != "" {
|
||||
file, err := os.Open(o.GoogleServiceAccountJSON)
|
||||
|
@ -2,6 +2,7 @@ package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
||||
@ -11,7 +12,6 @@ import (
|
||||
|
||||
type KeycloakProvider struct {
|
||||
*ProviderData
|
||||
Group string
|
||||
}
|
||||
|
||||
var _ Provider = (*KeycloakProvider)(nil)
|
||||
@ -59,41 +59,33 @@ func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
|
||||
return &KeycloakProvider{ProviderData: p}
|
||||
}
|
||||
|
||||
func (p *KeycloakProvider) SetGroup(group string) {
|
||||
p.Group = group
|
||||
}
|
||||
|
||||
func (p *KeycloakProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
|
||||
func (p *KeycloakProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
|
||||
json, err := requests.New(p.ValidateURL.String()).
|
||||
WithContext(ctx).
|
||||
SetHeader("Authorization", "Bearer "+s.AccessToken).
|
||||
Do().
|
||||
UnmarshalJSON()
|
||||
if err != nil {
|
||||
logger.Errorf("failed making request %s", err)
|
||||
return "", err
|
||||
logger.Errorf("failed making request %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Group != "" {
|
||||
var groups, err = json.Get("groups").Array()
|
||||
if err != nil {
|
||||
logger.Printf("groups not found %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
var found = false
|
||||
for i := range groups {
|
||||
if groups[i].(string) == p.Group {
|
||||
found = true
|
||||
break
|
||||
groups, err := json.Get("groups").StringArray()
|
||||
if err != nil {
|
||||
logger.Errorf("Warning: unable to extract groups from userinfo endpoint: %v", err)
|
||||
} else {
|
||||
for _, group := range groups {
|
||||
if group != "" {
|
||||
s.Groups = append(s.Groups, group)
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
logger.Printf("group not found, access denied")
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.Get("email").String()
|
||||
email, err := json.Get("email").String()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to extract email from userinfo endpoint: %v", err)
|
||||
}
|
||||
s.Email = email
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -2,17 +2,33 @@ package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testKeycloakProvider(hostname, group string) *KeycloakProvider {
|
||||
const (
|
||||
keycloakAccessToken = "eyJKeycloak.eyJAccess.Token"
|
||||
keycloakUserinfoPath = "/api/v3/user"
|
||||
|
||||
// Userinfo Test Cases
|
||||
tcUIStandard = "userinfo-standard"
|
||||
tcUIFail = "userinfo-fail"
|
||||
tcUISingleGroup = "userinfo-single-group"
|
||||
tcUIMissingEmail = "userinfo-missing-email"
|
||||
tcUIMissingGroups = "userinfo-missing-groups"
|
||||
)
|
||||
|
||||
func testKeycloakProvider(backend *httptest.Server) (*KeycloakProvider, error) {
|
||||
p := NewKeycloakProvider(
|
||||
&ProviderData{
|
||||
ProviderName: "",
|
||||
@ -22,38 +38,165 @@ func testKeycloakProvider(hostname, group string) *KeycloakProvider {
|
||||
ValidateURL: &url.URL{},
|
||||
Scope: ""})
|
||||
|
||||
if group != "" {
|
||||
p.SetGroup(group)
|
||||
}
|
||||
if backend != nil {
|
||||
bURL, err := url.Parse(backend.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostname := bURL.Host
|
||||
|
||||
if hostname != "" {
|
||||
updateURL(p.Data().LoginURL, hostname)
|
||||
updateURL(p.Data().RedeemURL, hostname)
|
||||
updateURL(p.Data().ProfileURL, hostname)
|
||||
updateURL(p.Data().ValidateURL, hostname)
|
||||
}
|
||||
return p
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func testKeycloakBackend(payload string) *httptest.Server {
|
||||
path := "/api/v3/user"
|
||||
|
||||
func testKeycloakBackend() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL
|
||||
if url.Path != path {
|
||||
if r.URL.Path != keycloakUserinfoPath {
|
||||
w.WriteHeader(404)
|
||||
} else if !IsAuthorizedInHeader(r.Header) {
|
||||
w.WriteHeader(403)
|
||||
} else {
|
||||
}
|
||||
|
||||
var err error
|
||||
switch r.URL.Query().Get("testcase") {
|
||||
case tcUIStandard:
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(payload))
|
||||
_, err = w.Write([]byte(`
|
||||
{
|
||||
"email": "michael.bland@gsa.gov",
|
||||
"groups": [
|
||||
"test-grp1",
|
||||
"test-grp2"
|
||||
]
|
||||
}
|
||||
`))
|
||||
case tcUIFail:
|
||||
w.WriteHeader(500)
|
||||
case tcUISingleGroup:
|
||||
w.WriteHeader(200)
|
||||
_, err = w.Write([]byte(`
|
||||
{
|
||||
"email": "michael.bland@gsa.gov",
|
||||
"groups": ["test-grp1"]
|
||||
}
|
||||
`))
|
||||
case tcUIMissingEmail:
|
||||
w.WriteHeader(200)
|
||||
_, err = w.Write([]byte(`
|
||||
{
|
||||
"groups": [
|
||||
"test-grp1",
|
||||
"test-grp2"
|
||||
]
|
||||
}
|
||||
`))
|
||||
case tcUIMissingGroups:
|
||||
w.WriteHeader(200)
|
||||
_, err = w.Write([]byte(`
|
||||
{
|
||||
"email": "michael.bland@gsa.gov"
|
||||
}
|
||||
`))
|
||||
default:
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
var _ = Describe("Keycloak Provider Tests", func() {
|
||||
var p *KeycloakProvider
|
||||
var b *httptest.Server
|
||||
|
||||
BeforeEach(func() {
|
||||
b = testKeycloakBackend()
|
||||
|
||||
var err error
|
||||
p, err = testKeycloakProvider(b)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
b.Close()
|
||||
})
|
||||
|
||||
Context("EnrichSession", func() {
|
||||
type enrichSessionTableInput struct {
|
||||
testcase string
|
||||
expectedError error
|
||||
expectedEmail string
|
||||
expectedGroups []string
|
||||
}
|
||||
|
||||
DescribeTable("should return expected results",
|
||||
func(in enrichSessionTableInput) {
|
||||
var err error
|
||||
p.ValidateURL, err = url.Parse(
|
||||
fmt.Sprintf("%s%s?testcase=%s", b.URL, keycloakUserinfoPath, in.testcase),
|
||||
)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
session := &sessions.SessionState{AccessToken: keycloakAccessToken}
|
||||
err = p.EnrichSession(context.Background(), session)
|
||||
|
||||
if in.expectedError != nil {
|
||||
Expect(err).To(Equal(in.expectedError))
|
||||
} else {
|
||||
Expect(err).To(BeNil())
|
||||
}
|
||||
|
||||
Expect(session.Email).To(Equal(in.expectedEmail))
|
||||
|
||||
if in.expectedGroups != nil {
|
||||
Expect(session.Groups).To(Equal(in.expectedGroups))
|
||||
} else {
|
||||
Expect(session.Groups).To(BeNil())
|
||||
}
|
||||
},
|
||||
Entry("email and multiple groups", enrichSessionTableInput{
|
||||
testcase: tcUIStandard,
|
||||
expectedError: nil,
|
||||
expectedEmail: "michael.bland@gsa.gov",
|
||||
expectedGroups: []string{"test-grp1", "test-grp2"},
|
||||
}),
|
||||
Entry("email and single group", enrichSessionTableInput{
|
||||
testcase: tcUISingleGroup,
|
||||
expectedError: nil,
|
||||
expectedEmail: "michael.bland@gsa.gov",
|
||||
expectedGroups: []string{"test-grp1"},
|
||||
}),
|
||||
Entry("email and no groups", enrichSessionTableInput{
|
||||
testcase: tcUIMissingGroups,
|
||||
expectedError: nil,
|
||||
expectedEmail: "michael.bland@gsa.gov",
|
||||
expectedGroups: nil,
|
||||
}),
|
||||
Entry("missing email", enrichSessionTableInput{
|
||||
testcase: tcUIMissingEmail,
|
||||
expectedError: errors.New(
|
||||
"unable to extract email from userinfo endpoint: type assertion to string failed"),
|
||||
expectedEmail: "",
|
||||
expectedGroups: []string{"test-grp1", "test-grp2"},
|
||||
}),
|
||||
Entry("request failure", enrichSessionTableInput{
|
||||
testcase: tcUIFail,
|
||||
expectedError: errors.New(`unexpected status "500": `),
|
||||
expectedEmail: "",
|
||||
expectedGroups: nil,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
func TestKeycloakProviderDefaults(t *testing.T) {
|
||||
p := testKeycloakProvider("", "")
|
||||
p, err := testKeycloakProvider(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, nil, p)
|
||||
assert.Equal(t, "Keycloak", p.Data().ProviderName)
|
||||
assert.Equal(t, "https://keycloak.org/oauth/authorize",
|
||||
@ -104,60 +247,3 @@ func TestKeycloakProviderOverrides(t *testing.T) {
|
||||
p.Data().ValidateURL.String())
|
||||
assert.Equal(t, "profile", p.Data().Scope)
|
||||
}
|
||||
|
||||
func TestKeycloakProviderGetEmailAddress(t *testing.T) {
|
||||
b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\"}")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testKeycloakProvider(bURL.Host, "")
|
||||
|
||||
session := CreateAuthorizedSession()
|
||||
email, err := p.GetEmailAddress(context.Background(), session)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "michael.bland@gsa.gov", email)
|
||||
}
|
||||
|
||||
func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) {
|
||||
b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\", \"groups\": [\"test-grp1\", \"test-grp2\"]}")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testKeycloakProvider(bURL.Host, "test-grp1")
|
||||
|
||||
session := CreateAuthorizedSession()
|
||||
email, err := p.GetEmailAddress(context.Background(), session)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "michael.bland@gsa.gov", email)
|
||||
}
|
||||
|
||||
// Note that trying to trigger the "failed building request" case is not
|
||||
// practical, since the only way it can fail is if the URL fails to parse.
|
||||
func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) {
|
||||
b := testKeycloakBackend("unused payload")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testKeycloakProvider(bURL.Host, "")
|
||||
|
||||
// We'll trigger a request failure by using an unexpected access
|
||||
// token. Alternatively, we could allow the parsing of the payload as
|
||||
// JSON to fail.
|
||||
session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
|
||||
email, err := p.GetEmailAddress(context.Background(), session)
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, "", email)
|
||||
}
|
||||
|
||||
func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
|
||||
b := testKeycloakBackend("{\"foo\": \"bar\"}")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
p := testKeycloakProvider(bURL.Host, "")
|
||||
|
||||
session := CreateAuthorizedSession()
|
||||
email, err := p.GetEmailAddress(context.Background(), session)
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, "", email)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user