auth0: implement directory provider (#1479)

* add the auth0 directory provider

Signed-off-by: Jon Carl <jon.carl@auth0.com>

* fix code climate issue: context.Context should be funcs first param

Signed-off-by: Jon Carl <jon.carl@auth0.com>

* remove unused struct field

Signed-off-by: Jon Carl <jon.carl@auth0.com>

* remove vendoring

Signed-off-by: Jon Carl <jon.carl@auth0.com>

* fix auth0 imports and variable name

Signed-off-by: Jon Carl <jon.carl@auth0.com>
This commit is contained in:
Jon Carl 2020-10-02 08:56:05 -06:00 committed by GitHub
parent ec91a98157
commit f1daf336f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 750 additions and 1 deletions

View file

@ -0,0 +1,446 @@
package auth0
import (
"context"
"errors"
"fmt"
"net/url"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"gopkg.in/auth0.v4/management"
"github.com/pomerium/pomerium/internal/directory/auth0/mock_auth0"
"github.com/pomerium/pomerium/pkg/grpc/directory"
)
type mockNewRoleManagerFunc struct {
CalledWithContext context.Context
CalledWithDomain string
CalledWithServiceAccount *ServiceAccount
ReturnRoleManager RoleManager
ReturnError error
}
func (m *mockNewRoleManagerFunc) f(ctx context.Context, domain string, serviceAccount *ServiceAccount) (RoleManager, error) {
m.CalledWithContext = ctx
m.CalledWithDomain = domain
m.CalledWithServiceAccount = serviceAccount
return m.ReturnRoleManager, m.ReturnError
}
type listOptionMatcher struct {
expected management.ListOption
}
func buildValues(opt management.ListOption) map[string][]string {
v := url.Values{}
opt(v)
return v
}
func (lom listOptionMatcher) Matches(actual interface{}) bool {
return gomock.Eq(buildValues(lom.expected)).Matches(buildValues(actual.(management.ListOption)))
}
func (lom listOptionMatcher) String() string {
return fmt.Sprintf("is equal to %v", buildValues(lom.expected))
}
func stringPtr(in string) *string {
return &in
}
func TestProvider_UserGroups(t *testing.T) {
expectedDomain := "example.com"
expectedServiceAccount := &ServiceAccount{ClientID: "c_id", Secret: "secret"}
tests := []struct {
name string
setupRoleManagerExpectations func(*mock_auth0.MockRoleManager)
newRoleManagerError error
expectedGroups []*directory.Group
expectedUsers []*directory.User
expectedError error
}{
{
name: "errors if getting the role manager errors",
newRoleManagerError: errors.New("new role manager error"),
expectedError: errors.New("auth0: could not get the role manager: new role manager error"),
},
{
name: "errors if listing roles errors",
setupRoleManagerExpectations: func(mrm *mock_auth0.MockRoleManager) {
mrm.EXPECT().List(
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(nil, errors.New("list error"))
},
expectedError: errors.New("auth0: could not list roles: list error"),
},
{
name: "errors if getting user ids errors",
setupRoleManagerExpectations: func(mrm *mock_auth0.MockRoleManager) {
mrm.EXPECT().List(
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(&management.RoleList{
Roles: []*management.Role{
{
ID: stringPtr("i-am-role-id"),
Name: stringPtr("i-am-role-name"),
},
},
}, nil)
mrm.EXPECT().Users(
"i-am-role-id",
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(nil, errors.New("users error"))
},
expectedError: errors.New("auth0: could not get users for role \"i-am-role-id\": users error"),
},
{
name: "handles multiple pages of roles",
setupRoleManagerExpectations: func(mrm *mock_auth0.MockRoleManager) {
mrm.EXPECT().List(
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(&management.RoleList{
List: management.List{
Total: 3,
Start: 0,
Limit: 1,
},
Roles: []*management.Role{
{
ID: stringPtr("i-am-role-id-1"),
Name: stringPtr("i-am-role-name-1"),
},
},
}, nil)
mrm.EXPECT().List(
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(1)},
).Return(&management.RoleList{
List: management.List{
Total: 3,
Start: 1,
Limit: 1,
},
Roles: []*management.Role{
{
ID: stringPtr("i-am-role-id-2"),
Name: stringPtr("i-am-role-name-2"),
},
},
}, nil)
mrm.EXPECT().List(
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(2)},
).Return(&management.RoleList{
List: management.List{
Total: 3,
Start: 2,
Limit: 1,
},
Roles: []*management.Role{
{
ID: stringPtr("i-am-role-id-3"),
Name: stringPtr("i-am-role-name-3"),
},
},
}, nil)
mrm.EXPECT().Users(
"i-am-role-id-1",
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(&management.UserList{}, nil)
mrm.EXPECT().Users(
"i-am-role-id-2",
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(&management.UserList{}, nil)
mrm.EXPECT().Users(
"i-am-role-id-3",
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(&management.UserList{}, nil)
},
expectedGroups: []*directory.Group{
{
Id: "i-am-role-id-1",
Name: "i-am-role-name-1",
},
{
Id: "i-am-role-id-2",
Name: "i-am-role-name-2",
},
{
Id: "i-am-role-id-3",
Name: "i-am-role-name-3",
},
},
},
{
name: "handles multiple pages of users",
setupRoleManagerExpectations: func(mrm *mock_auth0.MockRoleManager) {
mrm.EXPECT().List(
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(&management.RoleList{
Roles: []*management.Role{
{
ID: stringPtr("i-am-role-id-1"),
Name: stringPtr("i-am-role-name-1"),
},
},
}, nil)
mrm.EXPECT().Users(
"i-am-role-id-1",
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(&management.UserList{
List: management.List{
Total: 3,
Start: 0,
Limit: 1,
},
Users: []*management.User{
{ID: stringPtr("i-am-user-id-1")},
},
}, nil)
mrm.EXPECT().Users(
"i-am-role-id-1",
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(1)},
).Return(&management.UserList{
List: management.List{
Total: 3,
Start: 1,
Limit: 1,
},
Users: []*management.User{
{ID: stringPtr("i-am-user-id-2")},
},
}, nil)
mrm.EXPECT().Users(
"i-am-role-id-1",
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(2)},
).Return(&management.UserList{
List: management.List{
Total: 3,
Start: 2,
Limit: 1,
},
Users: []*management.User{
{ID: stringPtr("i-am-user-id-3")},
},
}, nil)
},
expectedGroups: []*directory.Group{
{
Id: "i-am-role-id-1",
Name: "i-am-role-name-1",
},
},
expectedUsers: []*directory.User{
{
Id: "auth0/i-am-user-id-1",
GroupIds: []string{"i-am-role-id-1"},
},
{
Id: "auth0/i-am-user-id-2",
GroupIds: []string{"i-am-role-id-1"},
},
{
Id: "auth0/i-am-user-id-3",
GroupIds: []string{"i-am-role-id-1"},
},
},
},
{
name: "correctly builds out groups and users",
setupRoleManagerExpectations: func(mrm *mock_auth0.MockRoleManager) {
mrm.EXPECT().List(
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(&management.RoleList{
List: management.List{
Total: 2,
Start: 0,
Limit: 1,
},
Roles: []*management.Role{
{
ID: stringPtr("i-am-role-id-1"),
Name: stringPtr("i-am-role-name-1"),
},
},
}, nil)
mrm.EXPECT().List(
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(1)},
).Return(&management.RoleList{
List: management.List{
Total: 2,
Start: 1,
Limit: 1,
},
Roles: []*management.Role{
{
ID: stringPtr("i-am-role-id-2"),
Name: stringPtr("i-am-role-name-2"),
},
},
}, nil)
mrm.EXPECT().Users(
"i-am-role-id-1",
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(&management.UserList{
Users: []*management.User{
{ID: stringPtr("i-am-user-id-4")},
{ID: stringPtr("i-am-user-id-3")},
{ID: stringPtr("i-am-user-id-2")},
{ID: stringPtr("i-am-user-id-1")},
},
}, nil)
mrm.EXPECT().Users(
"i-am-role-id-2",
listOptionMatcher{expected: management.IncludeTotals(true)},
listOptionMatcher{expected: management.Page(0)},
).Return(&management.UserList{
Users: []*management.User{
{ID: stringPtr("i-am-user-id-1")},
{ID: stringPtr("i-am-user-id-4")},
{ID: stringPtr("i-am-user-id-5")},
},
}, nil)
},
expectedGroups: []*directory.Group{
{
Id: "i-am-role-id-1",
Name: "i-am-role-name-1",
},
{
Id: "i-am-role-id-2",
Name: "i-am-role-name-2",
},
},
expectedUsers: []*directory.User{
{
Id: "auth0/i-am-user-id-1",
GroupIds: []string{"i-am-role-id-1", "i-am-role-id-2"},
},
{
Id: "auth0/i-am-user-id-2",
GroupIds: []string{"i-am-role-id-1"},
},
{
Id: "auth0/i-am-user-id-3",
GroupIds: []string{"i-am-role-id-1"},
},
{
Id: "auth0/i-am-user-id-4",
GroupIds: []string{"i-am-role-id-1", "i-am-role-id-2"},
},
{
Id: "auth0/i-am-user-id-5",
GroupIds: []string{"i-am-role-id-2"},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mRoleManager := mock_auth0.NewMockRoleManager(ctrl)
mNewRoleManagerFunc := mockNewRoleManagerFunc{
ReturnRoleManager: mRoleManager,
ReturnError: tc.newRoleManagerError,
}
if tc.setupRoleManagerExpectations != nil {
tc.setupRoleManagerExpectations(mRoleManager)
}
p := New(
WithDomain(expectedDomain),
WithServiceAccount(expectedServiceAccount),
)
p.cfg.newRoleManager = mNewRoleManagerFunc.f
actualGroups, actualUsers, err := p.UserGroups(context.Background())
if tc.expectedError != nil {
assert.EqualError(t, err, tc.expectedError.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, tc.expectedGroups, actualGroups)
assert.Equal(t, tc.expectedUsers, actualUsers)
assert.Equal(t, expectedDomain, mNewRoleManagerFunc.CalledWithDomain)
assert.Equal(t, expectedServiceAccount, mNewRoleManagerFunc.CalledWithServiceAccount)
})
}
}
func TestParseServiceAccount(t *testing.T) {
tests := []struct {
name string
rawServiceAccount string
expectedServiceAccount *ServiceAccount
expectedError error
}{
{"valid", "eyJjbGllbnRfaWQiOiJpLWFtLWNsaWVudC1pZCIsInNlY3JldCI6ImktYW0tc2VjcmV0In0K",
&ServiceAccount{
ClientID: "i-am-client-id",
Secret: "i-am-secret",
},
nil,
},
{"base64 err", "!!!!", nil, errors.New("auth0: could not decode base64: illegal base64 data at input byte 0")},
{"json err", "PAo=", nil, errors.New("auth0: could not unmarshal json: invalid character '<' looking for beginning of value")},
{"no client_id", "eyJzZWNyZXQiOiJpLWFtLXNlY3JldCJ9Cg==", nil, errors.New("auth0: client_id is required")},
{"no secret", "eyJjbGllbnRfaWQiOiJpLWFtLWNsaWVudC1pZCJ9Cg==", nil, errors.New("auth0: secret is required")},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
actualServiceAccount, err := ParseServiceAccount(tc.rawServiceAccount)
if tc.expectedError != nil {
assert.EqualError(t, err, tc.expectedError.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, tc.expectedServiceAccount, actualServiceAccount)
})
}
}