Skip to content

Commit

Permalink
fixup filters and roles
Browse files Browse the repository at this point in the history
  • Loading branch information
pquerna committed Oct 16, 2024
1 parent 4753d5a commit a918af0
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 120 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ password: `admin`
After you login you can create new resources to be synced by baton.

After creating new resources on the LDAP server, use the `baton-ldap` cli to sync the data from the LDAP server with the example command below.
`baton-ldap --base-dn dc=example,dc=org --user-dn cn=admin,dc=example,dc=org --password admin --domain localhost`
`baton-ldap --base-dn dc=example,dc=org --bind-dn cn=admin,dc=example,dc=org --password admin --domain localhost`

After successfully syncing data, use the baton CLI to list the resources and see the synced data.
`baton resources`
Expand Down
11 changes: 11 additions & 0 deletions pkg/connector/connector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func createConnector(ctx context.Context, t *testing.T, fixtureName string) (*LD
BaseDN: mustParseDN(t, "dc=example,dc=org"),
GroupSearchDN: mustParseDN(t, "ou=groups,dc=example,dc=org"),
UserSearchDN: mustParseDN(t, "ou=users,dc=example,dc=org"),
RoleSearchDN: mustParseDN(t, "ou=roles,dc=example,dc=org"),
BindPassword: "hunter2",
}
return New(ctx, cf)
Expand All @@ -97,3 +98,13 @@ func mustParseDN(t *testing.T, input string) *ldap.DN {
require.NoError(t, err)
return dn
}

func pluck[T any](slice []T, fn func(v T) bool) T {
var emptyT T
for _, v := range slice {
if fn(v) {
return v
}
}
return emptyT
}
148 changes: 82 additions & 66 deletions pkg/connector/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"regexp"
"sync"

"github.com/conductorone/baton-ldap/pkg/ldap"
Expand All @@ -21,17 +20,12 @@ import (
)

const (
groupObjectClasses = "(objectClass=groupOfUniqueNames)(objectClass=posixGroup)(objectClass=group)"
groupFilter = "(|" + groupObjectClasses + ")"
groupIdFilter = "(&(gidNumber=%s)(|" + groupObjectClasses + "))"
groupMemberUidFilter = `(&
(|` + userObjectClasses + `)
(uid=%s)
)`
groupMemberCommonNameFilter = `(&
(|` + userObjectClasses + `)
(cn=%s)
)`
groupObjectClasses = "(objectClass=groupOfUniqueNames)(objectClass=posixGroup)(objectClass=group)"
groupFilter = "(|" + groupObjectClasses + ")"
groupIdFilter = "(&(gidNumber=%s)(|" + groupObjectClasses + "))"

groupMemberUIDFilter = `(&` + userFilter + `(uid=%s))`
groupMemberCommonNameFilter = `(&` + userFilter + `(cn=%s))`

attrGroupCommonName = "cn"
attrGroupIdPosix = "gidNumber"
Expand Down Expand Up @@ -166,8 +160,6 @@ func newGrantFromDN(resource *v2.Resource, userDN string) *v2.Grant {
return g
}

var numericUidRe = regexp.MustCompile(`^\d+$`)

func (g *groupResourceType) Grants(ctx context.Context, resource *v2.Resource, token *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) {
l := ctxzap.Extract(ctx)
groupDN, err := ldap.CanonicalizeDN(resource.Id.Resource)
Expand All @@ -193,74 +185,98 @@ func (g *groupResourceType) Grants(ctx context.Context, resource *v2.Resource, t
// create membership grants
var rv []*v2.Grant
for memberId := range memberIDs.Iter() {
var memberEntry []*ldap.Entry

if parsedDN, err := ldap.CanonicalizeDN(memberId); err == nil {
parsedDN, err := ldap.CanonicalizeDN(memberId)
if err == nil {
g := newGrantFromDN(resource, parsedDN.String())
rv = append(rv, g)
continue
}

g.uid2dnMtx.Lock()
if dn, ok := g.uid2dnCache[memberId]; ok {
g.uid2dnMtx.Unlock()
g := newGrantFromDN(resource, dn)
rv = append(rv, g)
continue
}
g.uid2dnMtx.Unlock()

var filter string
if numericUidRe.MatchString(memberId) {
filter = fmt.Sprintf(groupMemberUidFilter, ldap3.EscapeFilter(memberId))
} else {
filter = fmt.Sprintf(groupMemberCommonNameFilter, ldap3.EscapeFilter(memberId))
}

// Group member doesn't look like it is a DN, search for it as a UID
memberEntry, _, err = g.client.LdapSearch(
ctx,
ldap3.ScopeWholeSubtree,
g.userSearchDN,
filter,
nil,
"",
1,
)

memberDN, err := g.findMember(ctx, memberId)
if err != nil {
// TODO: collect errors
l.Error("ldap-connector: failed to get user", zap.String("member_id", memberId), zap.Error(err))
continue
}
if len(memberEntry) == 0 {
l.Error("ldap-connector: expanding group: failed to find user by DN or UID", zap.String("member_id", memberId), zap.String("search_filter", filter))
continue
}

if len(memberEntry) > 1 {
l.Error("ldap-connector: expanding group: multiple users found by DN or UID", zap.String("member_id", memberId), zap.String("search_filter", filter))
continue
return nil, "", nil, err
}
mem := memberEntry[0]
memDN, err := ldap.CanonicalizeDN(mem.DN)
if err != nil {
l.Error("ldap-connector: expanding group: invalid DN", zap.String("member_id", memberId), zap.String("search_filter", filter), zap.Error(err))
if memberDN == "" {
continue
}
memberDN := memDN.String()
g.uid2dnMtx.Lock()
if len(memberEntry) == 1 {
g.uid2dnCache[memberId] = memberDN
}
g.uid2dnMtx.Unlock()
g := newGrantFromDN(resource, memberDN)
rv = append(rv, g)
}

return rv, "", nil, nil
}

func (g *groupResourceType) findMember(ctx context.Context, memberId string) (string, error) {
g.uid2dnMtx.Lock()
if dn, ok := g.uid2dnCache[memberId]; ok {
g.uid2dnMtx.Unlock()
return dn, nil
}
g.uid2dnMtx.Unlock()

filter := fmt.Sprintf(groupMemberUIDFilter, ldap3.EscapeFilter(memberId))
dn, err := g.findMemberByFilter(ctx, memberId, filter)
if err != nil {
return "", err
}
if dn != "" {
return dn, nil
}

filter = fmt.Sprintf(groupMemberCommonNameFilter, ldap3.EscapeFilter(memberId))
dn, err = g.findMemberByFilter(ctx, memberId, filter)
if err != nil {
return "", err
}
if dn != "" {
return dn, nil
}
return "", nil
}

func (g *groupResourceType) findMemberByFilter(ctx context.Context, memberId string, filter string) (string, error) {
l := ctxzap.Extract(ctx)

memberEntry, _, err := g.client.LdapSearch(
ctx,
ldap3.ScopeWholeSubtree,
g.userSearchDN,
filter,
nil,
"",
1,
)

if err != nil {
l.Error("ldap-connector: expanding group: failed to get user", zap.String("member_id", memberId), zap.Error(err))
return "", err
}

if len(memberEntry) == 0 {
l.Error("ldap-connector: expanding group: failed to find user by uid", zap.String("member_id", memberId), zap.String("search_filter", filter))
return "", nil
}

if len(memberEntry) > 1 {
err := fmt.Errorf("multiple users found by search")
l.Error("ldap-connector: expanding group: multiple users found by search", zap.String("member_id", memberId), zap.String("search_filter", filter))
return "", err
}

mem := memberEntry[0]
memDN, err := ldap.CanonicalizeDN(mem.DN)
if err != nil {
l.Error("ldap-connector: expanding group: invalid DN", zap.String("member_id", memberId), zap.String("search_filter", filter), zap.Error(err), zap.String("member_dn", mem.DN))
return "", err
}

memberDN := memDN.String()
g.uid2dnMtx.Lock()
g.uid2dnCache[memberId] = memberDN
g.uid2dnMtx.Unlock()
return memberDN, nil
}

func (g *groupResourceType) getGroup(ctx context.Context, groupDN string) (*ldap3.Entry, error) {
gdn, err := ldap.CanonicalizeDN(groupDN)
if err != nil {
Expand Down
14 changes: 4 additions & 10 deletions pkg/connector/group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import (

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/pagination"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)

func TestGroupGrantRevoke(t *testing.T) {
ctx, done := context.WithCancel(context.Background())
defer done()

ctx = ctxzap.ToContext(ctx, zap.Must(zap.NewDevelopment()))

connector, err := createConnector(ctx, t, "simple..ldif")
require.NoError(t, err)

Expand Down Expand Up @@ -75,13 +79,3 @@ func TestGroupGrantRevoke(t *testing.T) {
require.EqualExportedValues(t, rogerGrant.Entitlement, grants[0].Entitlement)
require.Equal(t, rogerGrant.Id, grants[0].Id)
}

func pluck[T any](slice []T, fn func(v T) bool) T {
var emptyT T
for _, v := range slice {
if fn(v) {
return v
}
}
return emptyT
}
36 changes: 1 addition & 35 deletions pkg/connector/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/conductorone/baton-sdk/pkg/pagination"
mapset "github.com/deckarep/golang-set/v2"
"github.com/go-ldap/ldap/v3"
"google.golang.org/protobuf/types/known/structpb"
)

var ResourcesPageSize uint32 = 50
Expand Down Expand Up @@ -59,7 +58,7 @@ func parseValues(entry *ldap.Entry, targetAttrs []string) mapset.Set[string] {

func parseValue(entry *ldap.Entry, targetAttrs []string) string {
for _, targetAttr := range targetAttrs {
payload := entry.GetAttributeValue(targetAttr)
payload := entry.GetEqualFoldAttributeValue(targetAttr)

if payload != "" {
return payload
Expand All @@ -68,36 +67,3 @@ func parseValue(entry *ldap.Entry, targetAttrs []string) string {

return ""
}

func getProfileStringArray(profile *structpb.Struct, k string) ([]string, bool) {
var values []string
if profile == nil {
return nil, false
}

v, ok := profile.Fields[k]
if !ok {
return nil, false
}

s, ok := v.Kind.(*structpb.Value_ListValue)
if !ok {
return nil, false
}

for _, v := range s.ListValue.Values {
if strVal := v.GetStringValue(); strVal != "" {
values = append(values, strVal)
}
}

return values, true
}

func stringSliceToInterfaceSlice(s []string) []interface{} {
var i []interface{}
for _, v := range s {
i = append(i, v)
}
return i
}
73 changes: 73 additions & 0 deletions pkg/connector/role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package connector

import (
"context"
"testing"

"github.com/conductorone/baton-sdk/pkg/pagination"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)

func TestRoleGrantRevoke(t *testing.T) {
ctx, done := context.WithCancel(context.Background())
defer done()

ctx = ctxzap.ToContext(ctx, zap.Must(zap.NewDevelopment()))

connector, err := createConnector(ctx, t, "roles.ldif")
require.NoError(t, err)

rb := roleBuilder(connector.client, connector.config.RoleSearchDN)

roles, pt, _, err := rb.List(ctx, nil, &pagination.Token{})
require.NoError(t, err)
require.Len(t, roles, 1)
require.Empty(t, pt)
require.Equal(t, roles[0].GetDisplayName(), "managers")

managerRole := roles[0]

ents, pt, _, err := rb.Entitlements(ctx, managerRole, &pagination.Token{})
require.NoError(t, err)
require.Empty(t, pt)
require.Len(t, ents, 1)

membershipEnt := ents[0]

grants, pt, _, err := rb.Grants(ctx, managerRole, &pagination.Token{})
require.NoError(t, err)
require.Empty(t, pt)
require.Len(t, grants, 1)

rogerGrant := grants[0]
_, err = rb.Revoke(ctx, rogerGrant)
require.NoError(t, err)
// test double revoke doesn't cause a hard error
_, err = rb.Revoke(ctx, rogerGrant)
require.NoError(t, err)

// verify 0 grants
grants, pt, _, err = rb.Grants(ctx, managerRole, &pagination.Token{})
require.NoError(t, err)
require.Empty(t, pt)
require.Len(t, grants, 0)

_, err = rb.Grant(ctx, rogerGrant.Principal, membershipEnt)
require.NoError(t, err)
// test double revoke doesn't cause a hard error
_, err = rb.Grant(ctx, rogerGrant.Principal, membershipEnt)
require.NoError(t, err)

// verify 1 grant
grants, pt, _, err = rb.Grants(ctx, managerRole, &pagination.Token{})
require.NoError(t, err)
require.Empty(t, pt)
require.Len(t, grants, 1)

// verify its roger!
require.EqualExportedValues(t, rogerGrant.Principal, grants[0].Principal)
require.EqualExportedValues(t, rogerGrant.Entitlement, grants[0].Entitlement)
require.Equal(t, rogerGrant.Id, grants[0].Id)
}
Loading

0 comments on commit a918af0

Please sign in to comment.