package syncgroups
import (
"errors"
"fmt"
"io/ioutil"
"reflect"
"strings"
"testing"
"gopkg.in/ldap.v2"
kapi "k8s.io/kubernetes/pkg/api"
ktestclient "k8s.io/kubernetes/pkg/client/unversioned/testclient"
"k8s.io/kubernetes/pkg/runtime"
"github.com/openshift/origin/pkg/auth/ldaputil"
"github.com/openshift/origin/pkg/client/testclient"
"github.com/openshift/origin/pkg/cmd/admin/groups/sync/interfaces"
userapi "github.com/openshift/origin/pkg/user/api"
)
func TestMakeOpenShiftGroup(t *testing.T) {
syncer := &LDAPGroupSyncer{
Out: ioutil.Discard,
Err: ioutil.Discard,
Host: "test-host:port",
GroupNameMapper: &TestGroupNameMapper{
NameMapping: map[string]string{
"alfa": "zulu",
},
},
}
tcs := map[string]struct {
ldapGroupUID string
usernames []string
startingGroups []runtime.Object
expectedGroup *userapi.Group
expectedErr string
}{
"bad ldapGroupUID": {
ldapGroupUID: "bravo",
expectedErr: "no name found for group: bravo",
},
"good": {
ldapGroupUID: "alfa",
usernames: []string{"valerie"},
expectedGroup: &userapi.Group{ObjectMeta: kapi.ObjectMeta{Name: "zulu",
Annotations: map[string]string{ldaputil.LDAPURLAnnotation: "test-host:port", ldaputil.LDAPUIDAnnotation: "alfa"},
Labels: map[string]string{ldaputil.LDAPHostLabel: "test-host"}},
Users: []string{"valerie"}},
},
"replaced good": {
ldapGroupUID: "alfa",
usernames: []string{"valerie"},
expectedGroup: &userapi.Group{ObjectMeta: kapi.ObjectMeta{Name: "zulu",
Annotations: map[string]string{ldaputil.LDAPURLAnnotation: "test-host:port", ldaputil.LDAPUIDAnnotation: "alfa"},
Labels: map[string]string{ldaputil.LDAPHostLabel: "test-host"}},
Users: []string{"valerie"}},
startingGroups: []runtime.Object{
&userapi.Group{ObjectMeta: kapi.ObjectMeta{Name: "zulu",
Annotations: map[string]string{ldaputil.LDAPURLAnnotation: "test-host:port", ldaputil.LDAPUIDAnnotation: "alfa"},
Labels: map[string]string{ldaputil.LDAPHostLabel: "test-host"}},
Users: []string{"other-user"}},
},
},
"conflicting uid": {
ldapGroupUID: "alfa",
usernames: []string{"valerie"},
startingGroups: []runtime.Object{
&userapi.Group{ObjectMeta: kapi.ObjectMeta{Name: "zulu",
Annotations: map[string]string{ldaputil.LDAPURLAnnotation: "test-host:port", ldaputil.LDAPUIDAnnotation: "bravo"},
Labels: map[string]string{ldaputil.LDAPHostLabel: "test-host"}},
Users: []string{"other-user"}},
},
expectedErr: `group "zulu": openshift.io/ldap.uid annotation did not match LDAP UID: wanted alfa, got bravo`,
},
"conflicting host": {
ldapGroupUID: "alfa",
usernames: []string{"valerie"},
startingGroups: []runtime.Object{
&userapi.Group{ObjectMeta: kapi.ObjectMeta{Name: "zulu",
Annotations: map[string]string{ldaputil.LDAPURLAnnotation: "bad-host:port", ldaputil.LDAPUIDAnnotation: "alfa"},
Labels: map[string]string{ldaputil.LDAPHostLabel: "bad-host"}},
Users: []string{"other-user"}},
},
expectedErr: `group "zulu": openshift.io/ldap.host label did not match sync host: wanted test-host, got bad-host`,
},
"conflicting port": {
ldapGroupUID: "alfa",
usernames: []string{"valerie"},
startingGroups: []runtime.Object{
&userapi.Group{ObjectMeta: kapi.ObjectMeta{Name: "zulu",
Annotations: map[string]string{ldaputil.LDAPURLAnnotation: "test-host:port2", ldaputil.LDAPUIDAnnotation: "alfa"},
Labels: map[string]string{ldaputil.LDAPHostLabel: "test-host"}},
Users: []string{"other-user"}},
},
expectedErr: `group "zulu": openshift.io/ldap.url annotation did not match sync host: wanted test-host:port, got test-host:port2`,
},
}
for name, tc := range tcs {
fakeClient := testclient.NewSimpleFake(tc.startingGroups...)
syncer.GroupClient = fakeClient.Groups()
actualGroup, err := syncer.makeOpenShiftGroup(tc.ldapGroupUID, tc.usernames)
if err != nil && len(tc.expectedErr) == 0 {
t.Errorf("%s: unexpected error %v", name, err)
} else if err == nil && len(tc.expectedErr) != 0 {
t.Errorf("%s: expected %v, got nil", name, tc.expectedErr)
} else if err != nil {
if e, a := tc.expectedErr, err.Error(); e != a {
t.Errorf("%s: expected %v, got %v", name, e, a)
}
}
if actualGroup != nil {
delete(actualGroup.Annotations, ldaputil.LDAPSyncTimeAnnotation)
}
if !reflect.DeepEqual(tc.expectedGroup, actualGroup) {
t.Errorf("%s: expected %v, got %v", name, tc.expectedGroup, actualGroup)
}
}
}
const (
Group1UID string = "group1"
Group2UID string = "group2"
Group3UID string = "group3"
UserNameAttribute string = "cn"
Member1UID string = "member1"
Member2UID string = "member2"
Member3UID string = "member3"
Member4UID string = "member4"
BaseDN string = "dc=example,dc=com"
)
var Member1 *ldap.Entry = &ldap.Entry{
DN: UserNameAttribute + "=" + Member1UID + "," + BaseDN,
Attributes: []*ldap.EntryAttribute{
{
Name: UserNameAttribute,
Values: []string{Member1UID},
ByteValues: [][]byte{[]byte(Member1UID)},
},
},
}
var Member2 *ldap.Entry = &ldap.Entry{
DN: UserNameAttribute + "=" + Member2UID + "," + BaseDN,
Attributes: []*ldap.EntryAttribute{
{
Name: UserNameAttribute,
Values: []string{Member2UID},
ByteValues: [][]byte{[]byte(Member2UID)},
},
},
}
var Member3 *ldap.Entry = &ldap.Entry{
DN: UserNameAttribute + "=" + Member3UID + "," + BaseDN,
Attributes: []*ldap.EntryAttribute{
{
Name: UserNameAttribute,
Values: []string{Member3UID},
ByteValues: [][]byte{[]byte(Member3UID)},
},
},
}
var Member4 *ldap.Entry = &ldap.Entry{
DN: UserNameAttribute + "=" + Member4UID + "," + BaseDN,
Attributes: []*ldap.EntryAttribute{
{
Name: UserNameAttribute,
Values: []string{Member4UID},
ByteValues: [][]byte{[]byte(Member4UID)},
},
},
}
var Group1Members []*ldap.Entry = []*ldap.Entry{Member1, Member2}
var Group2Members []*ldap.Entry = []*ldap.Entry{Member2, Member3}
var Group3Members []*ldap.Entry = []*ldap.Entry{Member3, Member4}
// TestGoodSync ensures that data is exchanged and rearranged correctly during the sync process.
func TestGoodSync(t *testing.T) {
testGroupSyncer, tc := newTestSyncer()
_, errs := testGroupSyncer.Sync()
for _, err := range errs {
t.Errorf("unexpected sync error: %v", err)
}
checkClientForGroups(tc, newDefaultOpenShiftGroups(testGroupSyncer.Host), t)
}
func TestListFails(t *testing.T) {
testGroupSyncer, _ := newTestSyncer()
testGroupSyncer.GroupLister.(*TestGroupLister).err = errors.New("error during listing")
groups, errs := testGroupSyncer.Sync()
if len(errs) != 1 {
t.Errorf("unexpected sync error: %v", errs)
} else if errs[0] != testGroupSyncer.GroupLister.(*TestGroupLister).err {
t.Errorf("unexpected sync error: %v", errs)
}
if groups != nil {
t.Errorf("unexpected groups %v", groups)
}
}
func TestMissingLDAPGroupUIDMapping(t *testing.T) {
testGroupSyncer, tc := newTestSyncer()
testGroupSyncer.GroupLister.(*TestGroupLister).GroupUIDs = append(testGroupSyncer.GroupLister.(*TestGroupLister).GroupUIDs, "ldapgroupwithnouid")
_, errs := testGroupSyncer.Sync()
if len(errs) != 1 {
t.Errorf("unexpected sync error: %v", errs)
} else if e, a := "no members found for group: ldapgroupwithnouid", errs[0].Error(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
checkClientForGroups(tc, newDefaultOpenShiftGroups(testGroupSyncer.Host), t)
}
func checkClientForGroups(tc *testclient.Fake, expectedGroups []*userapi.Group, t *testing.T) {
actualGroups := extractActualGroups(tc)
for _, expectedGroup := range expectedGroups {
if !groupExists(actualGroups, expectedGroup) {
t.Errorf("did not find %v, got %v", expectedGroup, actualGroups)
}
}
}
func groupExists(haystack []*userapi.Group, needle *userapi.Group) bool {
for _, actual := range haystack {
t, _ := kapi.Scheme.DeepCopy(actual)
actualGroup := t.(*userapi.Group)
delete(actualGroup.Annotations, ldaputil.LDAPSyncTimeAnnotation)
if reflect.DeepEqual(needle, actualGroup) {
return true
}
}
return false
}
func extractActualGroups(tc *testclient.Fake) []*userapi.Group {
ret := []*userapi.Group{}
for _, genericAction := range tc.Actions() {
switch action := genericAction.(type) {
case ktestclient.CreateAction:
ret = append(ret, action.GetObject().(*userapi.Group))
case ktestclient.UpdateAction:
ret = append(ret, action.GetObject().(*userapi.Group))
}
}
return ret
}
func newDefaultOpenShiftGroups(host string) []*userapi.Group {
return []*userapi.Group{
{
ObjectMeta: kapi.ObjectMeta{
Name: "os" + Group1UID,
Annotations: map[string]string{
ldaputil.LDAPURLAnnotation: host,
ldaputil.LDAPUIDAnnotation: Group1UID,
},
Labels: map[string]string{
ldaputil.LDAPHostLabel: strings.Split(host, ":")[0],
},
},
Users: []string{Member1UID, Member2UID},
},
{
ObjectMeta: kapi.ObjectMeta{
Name: "os" + Group2UID,
Annotations: map[string]string{
ldaputil.LDAPURLAnnotation: host,
ldaputil.LDAPUIDAnnotation: Group2UID,
},
Labels: map[string]string{
ldaputil.LDAPHostLabel: strings.Split(host, ":")[0],
},
},
Users: []string{Member2UID, Member3UID},
},
{
ObjectMeta: kapi.ObjectMeta{
Name: "os" + Group3UID,
Annotations: map[string]string{
ldaputil.LDAPURLAnnotation: host,
ldaputil.LDAPUIDAnnotation: Group3UID,
},
Labels: map[string]string{
ldaputil.LDAPHostLabel: strings.Split(host, ":")[0],
},
},
Users: []string{Member3UID, Member4UID},
},
}
}
func newTestSyncer() (*LDAPGroupSyncer, *testclient.Fake) {
tc := testclient.NewSimpleFake()
tc.PrependReactor("create", "groups", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
createAction := action.(ktestclient.CreateAction)
return true, createAction.GetObject(), nil
})
tc.PrependReactor("update", "groups", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
updateAction := action.(ktestclient.UpdateAction)
return true, updateAction.GetObject(), nil
})
return &LDAPGroupSyncer{
GroupLister: newTestLister(),
GroupMemberExtractor: newTestMemberExtractor(),
UserNameMapper: newTestUserNameMapper(),
GroupNameMapper: newTestGroupNameMapper(),
GroupClient: tc.Groups(),
Host: newTestHost(),
Out: ioutil.Discard,
Err: ioutil.Discard,
}, tc
}
func newTestHost() string {
return "test.host:port"
}
func newTestLister() interfaces.LDAPGroupLister {
return &TestGroupLister{
GroupUIDs: []string{Group1UID, Group2UID, Group3UID},
}
}
func newTestMemberExtractor() interfaces.LDAPMemberExtractor {
return &TestGroupMemberExtractor{
MemberMapping: map[string][]*ldap.Entry{
Group1UID: Group1Members,
Group2UID: Group2Members,
Group3UID: Group3Members,
},
}
}
func newTestUserNameMapper() interfaces.LDAPUserNameMapper {
return &TestUserNameMapper{
NameAttributes: []string{UserNameAttribute},
}
}
func newTestGroupNameMapper() interfaces.LDAPGroupNameMapper {
return &TestGroupNameMapper{
NameMapping: map[string]string{
Group1UID: "os" + Group1UID,
Group2UID: "os" + Group2UID,
Group3UID: "os" + Group3UID,
},
}
}
// The following stub implementations allow us to build a test LDAPGroupSyncer
var _ interfaces.LDAPGroupLister = &TestGroupLister{}
var _ interfaces.LDAPMemberExtractor = &TestGroupMemberExtractor{}
var _ interfaces.LDAPUserNameMapper = &TestUserNameMapper{}
var _ interfaces.LDAPGroupNameMapper = &TestGroupNameMapper{}
type TestGroupLister struct {
GroupUIDs []string
err error
}
func (l *TestGroupLister) ListGroups() ([]string, error) {
if l.err != nil {
return nil, l.err
}
return l.GroupUIDs, nil
}
type TestGroupMemberExtractor struct {
MemberMapping map[string][]*ldap.Entry
}
func (e *TestGroupMemberExtractor) ExtractMembers(ldapGroupUID string) ([]*ldap.Entry, error) {
members, exist := e.MemberMapping[ldapGroupUID]
if !exist {
return nil, fmt.Errorf("no members found for group: %s", ldapGroupUID)
}
return members, nil
}
type TestUserNameMapper struct {
NameAttributes []string
}
func (m *TestUserNameMapper) UserNameFor(user *ldap.Entry) (string, error) {
openShiftUserName := ldaputil.GetAttributeValue(user, m.NameAttributes)
if len(openShiftUserName) == 0 {
return "", fmt.Errorf("the user entry (%v) does not map to a OpenShift User name with the given mapping", user)
}
return openShiftUserName, nil
}
type TestGroupNameMapper struct {
NameMapping map[string]string
}
func (m *TestGroupNameMapper) GroupNameFor(ldapGroupUID string) (string, error) {
name, exists := m.NameMapping[ldapGroupUID]
if !exists {
return "", fmt.Errorf("no name found for group: %s", ldapGroupUID)
}
return name, nil
}