Merge pull request #884 from vmware-tanzu/upstream-ad-refresh
Upstream active directory refresh checks for password changes, deactivated and locked users
This commit is contained in:
commit
ca2ee26c86
@ -37,4 +37,5 @@ type UserAuthenticator interface {
|
|||||||
type Response struct {
|
type Response struct {
|
||||||
User user.Info
|
User user.Info
|
||||||
DN string
|
DN string
|
||||||
|
ExtraRefreshAttributes map[string]string
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,12 @@ package activedirectoryupstreamwatcher
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/equality"
|
"k8s.io/apimachinery/pkg/api/equality"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -52,6 +56,21 @@ const (
|
|||||||
// - has a member that matches the DN of the user we successfully logged in as.
|
// - has a member that matches the DN of the user we successfully logged in as.
|
||||||
// - perform nested group search by default.
|
// - perform nested group search by default.
|
||||||
defaultActiveDirectoryGroupSearchFilter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))"
|
defaultActiveDirectoryGroupSearchFilter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))"
|
||||||
|
|
||||||
|
sAMAccountNameAttribute = "sAMAccountName"
|
||||||
|
// PwdLastSetAttribute is the date and time that the password for this account was last changed.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/adschema/a-pwdlastset
|
||||||
|
PwdLastSetAttribute = "pwdLastSet"
|
||||||
|
// UserAccountControlAttribute represents a bitmap of user properties.
|
||||||
|
// https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
|
||||||
|
UserAccountControlAttribute = "userAccountControl"
|
||||||
|
// UserAccountControlComputedAttribute represents a bitmap of user properties.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/adschema/a-msds-user-account-control-computed
|
||||||
|
UserAccountControlComputedAttribute = "msDS-User-Account-Control-Computed"
|
||||||
|
// 0x0002 ACCOUNTDISABLE in userAccountControl bitmap.
|
||||||
|
accountDisabledBitmapValue = 2
|
||||||
|
// 0x0010 UF_LOCKOUT in msDS-User-Account-Control-Computed bitmap.
|
||||||
|
accountLockedBitmapValue = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
type activeDirectoryUpstreamGenericLDAPImpl struct {
|
type activeDirectoryUpstreamGenericLDAPImpl struct {
|
||||||
@ -316,11 +335,16 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context,
|
|||||||
GroupNameAttribute: adUpstreamImpl.Spec().GroupSearch().GroupNameAttribute(),
|
GroupNameAttribute: adUpstreamImpl.Spec().GroupSearch().GroupNameAttribute(),
|
||||||
},
|
},
|
||||||
Dialer: c.ldapDialer,
|
Dialer: c.ldapDialer,
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
PwdLastSetAttribute: upstreamldap.AttributeUnchangedSinceLogin(PwdLastSetAttribute),
|
||||||
|
UserAccountControlAttribute: ValidUserAccountControl,
|
||||||
|
UserAccountControlComputedAttribute: ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if spec.GroupSearch.Attributes.GroupName == "" {
|
if spec.GroupSearch.Attributes.GroupName == "" {
|
||||||
config.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){defaultActiveDirectoryGroupNameAttributeName: upstreamldap.GroupSAMAccountNameWithDomainSuffix}
|
config.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){defaultActiveDirectoryGroupNameAttributeName: GroupSAMAccountNameWithDomainSuffix}
|
||||||
}
|
}
|
||||||
|
|
||||||
conditions := upstreamwatchers.ValidateGenericLDAP(ctx, adUpstreamImpl, c.secretInformer, c.validatedSecretVersionsCache, config)
|
conditions := upstreamwatchers.ValidateGenericLDAP(ctx, adUpstreamImpl, c.secretInformer, c.validatedSecretVersionsCache, config)
|
||||||
@ -353,3 +377,84 @@ func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, ups
|
|||||||
log.Error(err, "failed to update status")
|
log.Error(err, "failed to update status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MicrosoftUUIDFromBinary(attributeName string) func(entry *ldap.Entry) (string, error) {
|
||||||
|
// validation has already been done so we can just get the attribute...
|
||||||
|
return func(entry *ldap.Entry) (string, error) {
|
||||||
|
binaryUUID := entry.GetRawAttributeValue(attributeName)
|
||||||
|
return microsoftUUIDFromBinary(binaryUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func microsoftUUIDFromBinary(binaryUUID []byte) (string, error) {
|
||||||
|
uuidVal, err := uuid.FromBytes(binaryUUID) // start out with the RFC4122 version
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// then swap it because AD stores the first 3 fields little-endian rather than the expected
|
||||||
|
// big-endian.
|
||||||
|
uuidVal[0], uuidVal[1], uuidVal[2], uuidVal[3] = uuidVal[3], uuidVal[2], uuidVal[1], uuidVal[0]
|
||||||
|
uuidVal[4], uuidVal[5] = uuidVal[5], uuidVal[4]
|
||||||
|
uuidVal[6], uuidVal[7] = uuidVal[7], uuidVal[6]
|
||||||
|
return uuidVal.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GroupSAMAccountNameWithDomainSuffix(entry *ldap.Entry) (string, error) {
|
||||||
|
sAMAccountNameAttributeValues := entry.GetAttributeValues(sAMAccountNameAttribute)
|
||||||
|
|
||||||
|
if len(sAMAccountNameAttributeValues) != 1 {
|
||||||
|
return "", fmt.Errorf(`found %d values for attribute "%s", but expected 1 result`,
|
||||||
|
len(sAMAccountNameAttributeValues), sAMAccountNameAttribute,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sAMAccountName := sAMAccountNameAttributeValues[0]
|
||||||
|
if len(sAMAccountName) == 0 {
|
||||||
|
return "", fmt.Errorf(`found empty value for attribute "%s", but expected value to be non-empty`,
|
||||||
|
sAMAccountNameAttribute,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
distinguishedName := entry.DN
|
||||||
|
domain, err := getDomainFromDistinguishedName(distinguishedName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return sAMAccountName + "@" + domain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var domainComponentsRegexp = regexp.MustCompile(",DC=|,dc=")
|
||||||
|
|
||||||
|
func getDomainFromDistinguishedName(distinguishedName string) (string, error) {
|
||||||
|
domainComponents := domainComponentsRegexp.Split(distinguishedName, -1)
|
||||||
|
if len(domainComponents) == 1 {
|
||||||
|
return "", fmt.Errorf("did not find domain components in group dn: %s", distinguishedName)
|
||||||
|
}
|
||||||
|
return strings.Join(domainComponents[1:], "."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidUserAccountControl(entry *ldap.Entry, _ provider.StoredRefreshAttributes) error {
|
||||||
|
userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(UserAccountControlAttribute))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivated := userAccountControl & accountDisabledBitmapValue // bitwise and.
|
||||||
|
if deactivated != 0 {
|
||||||
|
return fmt.Errorf("user has been deactivated")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidComputedUserAccountControl(entry *ldap.Entry, _ provider.StoredRefreshAttributes) error {
|
||||||
|
userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(UserAccountControlComputedAttribute))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
locked := userAccountControl & accountLockedBitmapValue // bitwise and
|
||||||
|
if locked != 0 {
|
||||||
|
return fmt.Errorf("user has been locked")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -220,7 +220,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a copy with targeted changes.
|
// Make a copy with targeted changes.
|
||||||
@ -536,7 +541,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -592,7 +602,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: "sAMAccountName",
|
GroupNameAttribute: "sAMAccountName",
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -651,7 +666,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -710,7 +730,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
@ -768,7 +793,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -897,7 +927,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1021,8 +1056,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
},
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
|
||||||
@ -1072,7 +1111,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1271,8 +1315,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))",
|
Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))",
|
||||||
GroupNameAttribute: "sAMAccountName",
|
GroupNameAttribute: "sAMAccountName",
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": upstreamldap.GroupSAMAccountNameWithDomainSuffix},
|
GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": GroupSAMAccountNameWithDomainSuffix},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1324,7 +1373,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1380,7 +1434,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1430,7 +1489,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1626,7 +1690,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": MicrosoftUUIDFromBinary("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": ValidUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": ValidComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1753,6 +1822,16 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualGroupAttributeParsingOverrides[k]).Pointer())
|
require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualGroupAttributeParsingOverrides[k]).Pointer())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedRefreshAttributeChecks := copyOfExpectedValueForResultingCache.RefreshAttributeChecks
|
||||||
|
actualRefreshAttributeChecks := actualConfig.RefreshAttributeChecks
|
||||||
|
copyOfExpectedValueForResultingCache.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{}
|
||||||
|
actualConfig.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{}
|
||||||
|
require.Equal(t, len(expectedRefreshAttributeChecks), len(actualRefreshAttributeChecks))
|
||||||
|
for k, v := range expectedRefreshAttributeChecks {
|
||||||
|
require.NotNil(t, actualRefreshAttributeChecks[k])
|
||||||
|
require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualRefreshAttributeChecks[k]).Pointer())
|
||||||
|
}
|
||||||
|
|
||||||
require.Equal(t, copyOfExpectedValueForResultingCache, actualConfig)
|
require.Equal(t, copyOfExpectedValueForResultingCache, actualConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1800,3 +1879,270 @@ func normalizeActiveDirectoryUpstreams(upstreams []v1alpha1.ActiveDirectoryIdent
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGroupSAMAccountNameWithDomainSuffix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entry *ldap.Entry
|
||||||
|
wantResult string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path with DN and valid sAMAccountName",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "CN=animals,OU=Users,OU=pinniped-ad,DC=mycompany,DC=example,DC=com",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantResult: "Mammals@mycompany.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no domain components in DN",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "no-domain-components",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "did not find domain components in group dn: no-domain-components",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple values for sAMAccountName attribute",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "CN=animals,OU=Users,OU=pinniped-ad,DC=mycompany,DC=example,DC=com",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals", "Eukaryotes"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "found 2 values for attribute \"sAMAccountName\", but expected 1 result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no values for sAMAccountName attribute",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "CN=animals,OU=Users,OU=pinniped-ad,DC=mycompany,DC=example,DC=com",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
ldap.NewEntryAttribute("sAMAccountName", []string{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "found 0 values for attribute \"sAMAccountName\", but expected 1 result",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
suffixedSAMAccountName, err := GroupSAMAccountNameWithDomainSuffix(tt.entry)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tt.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, tt.wantResult, suffixedSAMAccountName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMicrosoftFormattedUUID(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
binaryUUID []byte
|
||||||
|
wantString string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
binaryUUID: []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16"),
|
||||||
|
wantString: "04030201-0605-0807-0910-111213141516",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not the right length",
|
||||||
|
binaryUUID: []byte("2\xf8\xb0\xaa\xb6V\xb1D\x8b(\xee"),
|
||||||
|
wantErr: "invalid UUID (got 11 bytes)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actualUUIDString, err := microsoftUUIDFromBinary(tt.binaryUUID)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tt.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, tt.wantString, actualUUIDString)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDomainFromDistinguishedName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
distinguishedName string
|
||||||
|
wantDomain string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
distinguishedName: "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com",
|
||||||
|
wantDomain: "activedirectory.mycompany.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lowercased happy path",
|
||||||
|
distinguishedName: "cn=Mammals,ou=Users,ou=pinniped-ad,dc=activedirectory,dc=mycompany,dc=example,dc=com",
|
||||||
|
wantDomain: "activedirectory.mycompany.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no domain components",
|
||||||
|
distinguishedName: "not-a-dn",
|
||||||
|
wantErr: "did not find domain components in group dn: not-a-dn",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actualDomain, err := getDomainFromDistinguishedName(tt.distinguishedName)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tt.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, tt.wantDomain, actualDomain)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidUserAccountControl(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entry *ldap.Entry
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy normal user",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "userAccountControl",
|
||||||
|
Values: []string{"512"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy user whose password doesn't expire",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "userAccountControl",
|
||||||
|
Values: []string{"65536"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deactivated user",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "userAccountControl",
|
||||||
|
Values: []string{"514"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "user has been deactivated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-integer result",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "userAccountControl",
|
||||||
|
Values: []string{"not-an-int"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "strconv.Atoi: parsing \"not-an-int\": invalid syntax",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := ValidUserAccountControl(tt.entry, provider.StoredRefreshAttributes{})
|
||||||
|
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, tt.wantErr, err.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidComputedUserAccountControl(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entry *ldap.Entry
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy normal user",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "msDS-User-Account-Control-Computed",
|
||||||
|
Values: []string{"0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "locked user",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "msDS-User-Account-Control-Computed",
|
||||||
|
Values: []string{"16"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "user has been locked",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-integer result",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "msDS-User-Account-Control-Computed",
|
||||||
|
Values: []string{"not-an-int"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "strconv.Atoi: parsing \"not-an-int\": invalid syntax",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := ValidComputedUserAccountControl(tt.entry, provider.StoredRefreshAttributes{})
|
||||||
|
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, tt.wantErr, err.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -348,20 +348,25 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
|
|||||||
"upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬"
|
"upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬"
|
||||||
},
|
},
|
||||||
"ldap": {
|
"ldap": {
|
||||||
"userDN": "6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾"
|
"userDN": "6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾",
|
||||||
|
"extraRefreshAttributes": {
|
||||||
|
"ď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ¡圔": "墀jMʥ",
|
||||||
|
"齁š%OpKȱ藚ɏ¬Ê蒭堜]ȗ韚ʫ": "鷞aŚB碠k9帴ʘ赱"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"activedirectory": {
|
"activedirectory": {
|
||||||
"userDN": "|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮rʨ鷞"
|
"userDN": "0D餹sêĝɓ",
|
||||||
|
"extraRefreshAttributes": {
|
||||||
|
"摱ì": "bEǎ儯惝Io"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"requestedAudience": [
|
"requestedAudience": [
|
||||||
"ŚB碠k9"
|
"Ł"
|
||||||
],
|
],
|
||||||
"grantedAudience": [
|
"grantedAudience": [
|
||||||
"ʘ赱",
|
"r"
|
||||||
"ď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ¡圔",
|
|
||||||
"墀jMʥ"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": "2"
|
"version": "2"
|
||||||
|
@ -123,11 +123,13 @@ func handleAuthRequestForLDAPUpstream(
|
|||||||
if idpType == psession.ProviderTypeLDAP {
|
if idpType == psession.ProviderTypeLDAP {
|
||||||
customSessionData.LDAP = &psession.LDAPSessionData{
|
customSessionData.LDAP = &psession.LDAPSessionData{
|
||||||
UserDN: dn,
|
UserDN: dn,
|
||||||
|
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if idpType == psession.ProviderTypeActiveDirectory {
|
if idpType == psession.ProviderTypeActiveDirectory {
|
||||||
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
|
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
|
||||||
UserDN: dn,
|
UserDN: dn,
|
||||||
|
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,6 +269,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
happyLDAPUID := "some-ldap-uid"
|
happyLDAPUID := "some-ldap-uid"
|
||||||
happyLDAPUserDN := "cn=foo,dn=bar"
|
happyLDAPUserDN := "cn=foo,dn=bar"
|
||||||
happyLDAPGroups := []string{"group1", "group2", "group3"}
|
happyLDAPGroups := []string{"group1", "group2", "group3"}
|
||||||
|
happyLDAPExtraRefreshAttribute := "some-refresh-attribute"
|
||||||
|
happyLDAPExtraRefreshValue := "some-refresh-attribute-value"
|
||||||
|
|
||||||
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
|
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -285,6 +287,9 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
Groups: happyLDAPGroups,
|
Groups: happyLDAPGroups,
|
||||||
},
|
},
|
||||||
DN: happyLDAPUserDN,
|
DN: happyLDAPUserDN,
|
||||||
|
ExtraRefreshAttributes: map[string]string{
|
||||||
|
happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue,
|
||||||
|
},
|
||||||
}, true, nil
|
}, true, nil
|
||||||
}
|
}
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
@ -443,6 +448,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
LDAP: nil,
|
LDAP: nil,
|
||||||
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
||||||
UserDN: happyLDAPUserDN,
|
UserDN: happyLDAPUserDN,
|
||||||
|
ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,6 +459,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
OIDC: nil,
|
OIDC: nil,
|
||||||
LDAP: &psession.LDAPSessionData{
|
LDAP: &psession.LDAPSessionData{
|
||||||
UserDN: happyLDAPUserDN,
|
UserDN: happyLDAPUserDN,
|
||||||
|
ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue},
|
||||||
},
|
},
|
||||||
ActiveDirectory: nil,
|
ActiveDirectory: nil,
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
@ -93,7 +94,15 @@ type UpstreamLDAPIdentityProviderI interface {
|
|||||||
authenticators.UserAuthenticator
|
authenticators.UserAuthenticator
|
||||||
|
|
||||||
// PerformRefresh performs a refresh against the upstream LDAP identity provider
|
// PerformRefresh performs a refresh against the upstream LDAP identity provider
|
||||||
PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error
|
PerformRefresh(ctx context.Context, storedRefreshAttributes StoredRefreshAttributes) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoredRefreshAttributes struct {
|
||||||
|
Username string
|
||||||
|
Subject string
|
||||||
|
DN string
|
||||||
|
AuthTime time.Time
|
||||||
|
AdditionalAttributes map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type DynamicUpstreamIDPProvider interface {
|
type DynamicUpstreamIDPProvider interface {
|
||||||
|
@ -75,11 +75,6 @@ func NewHandler(
|
|||||||
|
|
||||||
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
||||||
session := accessRequest.GetSession().(*psession.PinnipedSession)
|
session := accessRequest.GetSession().(*psession.PinnipedSession)
|
||||||
downstreamUsername, err := getDownstreamUsernameFromPinnipedSession(session)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
downstreamSubject := session.Fosite.Claims.Subject
|
|
||||||
|
|
||||||
customSessionData := session.Custom
|
customSessionData := session.Custom
|
||||||
if customSessionData == nil {
|
if customSessionData == nil {
|
||||||
@ -95,9 +90,9 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester,
|
|||||||
case psession.ProviderTypeOIDC:
|
case psession.ProviderTypeOIDC:
|
||||||
return upstreamOIDCRefresh(ctx, customSessionData, providerCache)
|
return upstreamOIDCRefresh(ctx, customSessionData, providerCache)
|
||||||
case psession.ProviderTypeLDAP:
|
case psession.ProviderTypeLDAP:
|
||||||
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
|
return upstreamLDAPRefresh(ctx, providerCache, session)
|
||||||
case psession.ProviderTypeActiveDirectory:
|
case psession.ProviderTypeActiveDirectory:
|
||||||
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
|
return upstreamLDAPRefresh(ctx, providerCache, session)
|
||||||
default:
|
default:
|
||||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
}
|
}
|
||||||
@ -169,7 +164,15 @@ func findOIDCProviderByNameAndValidateUID(
|
|||||||
WithHint("Provider from upstream session data was not found.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
WithHint("Provider from upstream session data was not found.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
}
|
}
|
||||||
|
|
||||||
func upstreamLDAPRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister, username string, subject string) error {
|
func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentityProvidersLister, session *psession.PinnipedSession) error {
|
||||||
|
username, err := getDownstreamUsernameFromPinnipedSession(session)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
subject := session.Fosite.Claims.Subject
|
||||||
|
|
||||||
|
s := session.Custom
|
||||||
|
|
||||||
// if you have neither a valid ldap session config nor a valid active directory session config
|
// if you have neither a valid ldap session config nor a valid active directory session config
|
||||||
validLDAP := s.ProviderType == psession.ProviderTypeLDAP && s.LDAP != nil && s.LDAP.UserDN != ""
|
validLDAP := s.ProviderType == psession.ProviderTypeLDAP && s.LDAP != nil && s.LDAP.UserDN != ""
|
||||||
validAD := s.ProviderType == psession.ProviderTypeActiveDirectory && s.ActiveDirectory != nil && s.ActiveDirectory.UserDN != ""
|
validAD := s.ProviderType == psession.ProviderTypeActiveDirectory && s.ActiveDirectory != nil && s.ActiveDirectory.UserDN != ""
|
||||||
@ -177,13 +180,28 @@ func upstreamLDAPRefresh(ctx context.Context, s *psession.CustomSessionData, pro
|
|||||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var additionalAttributes map[string]string
|
||||||
|
if s.ProviderType == psession.ProviderTypeLDAP {
|
||||||
|
additionalAttributes = s.LDAP.ExtraRefreshAttributes
|
||||||
|
} else {
|
||||||
|
additionalAttributes = s.ActiveDirectory.ExtraRefreshAttributes
|
||||||
|
}
|
||||||
|
|
||||||
// get ldap/ad provider out of cache
|
// get ldap/ad provider out of cache
|
||||||
p, dn, err := findLDAPProviderByNameAndValidateUID(s, providerCache)
|
p, dn, err := findLDAPProviderByNameAndValidateUID(s, providerCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if session.IDTokenClaims().AuthTime.IsZero() {
|
||||||
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
|
}
|
||||||
// run PerformRefresh
|
// run PerformRefresh
|
||||||
err = p.PerformRefresh(ctx, dn, username, subject)
|
err = p.PerformRefresh(ctx, provider.StoredRefreshAttributes{
|
||||||
|
Username: username,
|
||||||
|
Subject: subject,
|
||||||
|
DN: dn,
|
||||||
|
AdditionalAttributes: additionalAttributes,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
||||||
"Upstream refresh failed.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
"Upstream refresh failed.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
@ -2058,6 +2058,219 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "upstream ldap idp not found",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder(),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data was not found."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream active directory idp not found",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder(),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyActiveDirectoryCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyActiveDirectoryCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data was not found."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fosite session is empty",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
|
||||||
|
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
|
||||||
|
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
session := firstRequester.GetSession().(*psession.PinnipedSession)
|
||||||
|
session.Fosite = &openid.DefaultSession{}
|
||||||
|
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username not found in extra field",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
|
||||||
|
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
|
||||||
|
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
session := firstRequester.GetSession().(*psession.PinnipedSession)
|
||||||
|
session.Fosite = &openid.DefaultSession{
|
||||||
|
Claims: &jwt.IDTokenClaims{
|
||||||
|
Extra: map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "auth time is the zero value", // time.Times can never be nil, but it is possible that it would be the zero value which would mean something's wrong
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
|
||||||
|
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
|
||||||
|
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
session := firstRequester.GetSession().(*psession.PinnipedSession)
|
||||||
|
fositeSessionClaims := session.Fosite.IDTokenClaims()
|
||||||
|
fositeSessionClaims.AuthTime = time.Time{}
|
||||||
|
session.Fosite.Claims = fositeSessionClaims
|
||||||
|
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when the ldap provider in the session storage is found but has the wrong resource UID during the refresh request",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: "the-wrong-uid",
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when the active directory provider in the session storage is found but has the wrong resource UID during the refresh request",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: activeDirectoryUpstreamName,
|
||||||
|
ResourceUID: "the-wrong-uid",
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyActiveDirectoryCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyActiveDirectoryCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
test := test
|
test := test
|
||||||
|
@ -67,11 +67,13 @@ type OIDCSessionData struct {
|
|||||||
// LDAPSessionData is the additional data needed by Pinniped when the upstream IDP is an LDAP provider.
|
// LDAPSessionData is the additional data needed by Pinniped when the upstream IDP is an LDAP provider.
|
||||||
type LDAPSessionData struct {
|
type LDAPSessionData struct {
|
||||||
UserDN string `json:"userDN"`
|
UserDN string `json:"userDN"`
|
||||||
|
ExtraRefreshAttributes map[string]string `json:"extraRefreshAttributes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActiveDirectorySessionData is the additional data needed by Pinniped when the upstream IDP is an Active Directory provider.
|
// ActiveDirectorySessionData is the additional data needed by Pinniped when the upstream IDP is an Active Directory provider.
|
||||||
type ActiveDirectorySessionData struct {
|
type ActiveDirectorySessionData struct {
|
||||||
UserDN string `json:"userDN"`
|
UserDN string `json:"userDN"`
|
||||||
|
ExtraRefreshAttributes map[string]string `json:"extraRefreshAttributes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPinnipedSession returns a new empty session.
|
// NewPinnipedSession returns a new empty session.
|
||||||
|
@ -111,16 +111,16 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
|
|||||||
return u.URL
|
return u.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
|
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) error {
|
||||||
if u.performRefreshArgs == nil {
|
if u.performRefreshArgs == nil {
|
||||||
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
|
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
|
||||||
}
|
}
|
||||||
u.performRefreshCallCount++
|
u.performRefreshCallCount++
|
||||||
u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{
|
u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
DN: userDN,
|
DN: storedRefreshAttributes.DN,
|
||||||
ExpectedUsername: expectedUsername,
|
ExpectedUsername: storedRefreshAttributes.Username,
|
||||||
ExpectedSubject: expectedSubject,
|
ExpectedSubject: storedRefreshAttributes.Subject,
|
||||||
})
|
})
|
||||||
if u.PerformRefreshErr != nil {
|
if u.PerformRefreshErr != nil {
|
||||||
return u.PerformRefreshErr
|
return u.PerformRefreshErr
|
||||||
|
@ -13,13 +13,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
"github.com/google/uuid"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/utils/trace"
|
"k8s.io/utils/trace"
|
||||||
@ -39,7 +37,6 @@ const (
|
|||||||
groupSearchPageSize = uint32(250)
|
groupSearchPageSize = uint32(250)
|
||||||
defaultLDAPPort = uint16(389)
|
defaultLDAPPort = uint16(389)
|
||||||
defaultLDAPSPort = uint16(636)
|
defaultLDAPSPort = uint16(636)
|
||||||
sAMAccountNameAttribute = "sAMAccountName"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Conn abstracts the upstream LDAP communication protocol (mostly for testing).
|
// Conn abstracts the upstream LDAP communication protocol (mostly for testing).
|
||||||
@ -119,6 +116,9 @@ type ProviderConfig struct {
|
|||||||
// GroupNameMappingOverrides are the mappings between an attribute name and a way to parse it as a group
|
// GroupNameMappingOverrides are the mappings between an attribute name and a way to parse it as a group
|
||||||
// name when it comes out of LDAP.
|
// name when it comes out of LDAP.
|
||||||
GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error)
|
GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error)
|
||||||
|
|
||||||
|
// RefreshAttributeChecks are extra checks that attributes in a refresh response are as expected.
|
||||||
|
RefreshAttributeChecks map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserSearchConfig contains information about how to search for users in the upstream LDAP IDP.
|
// UserSearchConfig contains information about how to search for users in the upstream LDAP IDP.
|
||||||
@ -170,9 +170,11 @@ func (p *Provider) GetConfig() ProviderConfig {
|
|||||||
return p.c
|
return p.c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
|
func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) error {
|
||||||
t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()})
|
t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()})
|
||||||
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
|
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
|
||||||
|
userDN := storedRefreshAttributes.DN
|
||||||
|
|
||||||
searchResult, err := p.performRefresh(ctx, userDN)
|
searchResult, err := p.performRefresh(ctx, userDN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.traceRefreshFailure(t, err)
|
p.traceRefreshFailure(t, err)
|
||||||
@ -196,9 +198,9 @@ func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if newUsername != expectedUsername {
|
if newUsername != storedRefreshAttributes.Username {
|
||||||
return fmt.Errorf(`searching for user "%s" returned a different username than the previous value. expected: "%s", actual: "%s"`,
|
return fmt.Errorf(`searching for user "%s" returned a different username than the previous value. expected: "%s", actual: "%s"`,
|
||||||
userDN, expectedUsername, newUsername,
|
userDN, storedRefreshAttributes.Username, newUsername,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,10 +209,15 @@ func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL())
|
newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL())
|
||||||
if newSubject != expectedSubject {
|
if newSubject != storedRefreshAttributes.Subject {
|
||||||
return fmt.Errorf(`searching for user "%s" produced a different subject than the previous value. expected: "%s", actual: "%s"`, userDN, expectedSubject, newSubject)
|
return fmt.Errorf(`searching for user "%s" produced a different subject than the previous value. expected: "%s", actual: "%s"`, userDN, storedRefreshAttributes.Subject, newSubject)
|
||||||
|
}
|
||||||
|
for attribute, validateFunc := range p.c.RefreshAttributeChecks {
|
||||||
|
err = validateFunc(userEntry, storedRefreshAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(`validation for attribute "%s" failed during upstream refresh: %w`, attribute, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we checked that the user still exists and their information is the same, so just return.
|
// we checked that the user still exists and their information is the same, so just return.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -569,6 +576,15 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
|||||||
}
|
}
|
||||||
sort.Strings(mappedGroupNames)
|
sort.Strings(mappedGroupNames)
|
||||||
|
|
||||||
|
mappedRefreshAttributes := make(map[string]string)
|
||||||
|
for k := range p.c.RefreshAttributeChecks {
|
||||||
|
mappedVal, err := p.getSearchResultAttributeRawValueEncoded(k, userEntry, username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mappedRefreshAttributes[k] = mappedVal
|
||||||
|
}
|
||||||
|
|
||||||
// Caution: Note that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername!
|
// Caution: Note that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername!
|
||||||
err = bindFunc(conn, userEntry.DN)
|
err = bindFunc(conn, userEntry.DN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -593,6 +609,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
|||||||
Groups: mappedGroupNames,
|
Groups: mappedGroupNames,
|
||||||
},
|
},
|
||||||
DN: userEntry.DN,
|
DN: userEntry.DN,
|
||||||
|
ExtraRefreshAttributes: mappedRefreshAttributes,
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
@ -665,6 +682,9 @@ func (p *Provider) userSearchRequestedAttributes() []string {
|
|||||||
if p.c.UserSearch.UIDAttribute != distinguishedNameAttributeName {
|
if p.c.UserSearch.UIDAttribute != distinguishedNameAttributeName {
|
||||||
attributes = append(attributes, p.c.UserSearch.UIDAttribute)
|
attributes = append(attributes, p.c.UserSearch.UIDAttribute)
|
||||||
}
|
}
|
||||||
|
for k := range p.c.RefreshAttributeChecks {
|
||||||
|
attributes = append(attributes, k)
|
||||||
|
}
|
||||||
return attributes
|
return attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -782,57 +802,18 @@ func (p *Provider) traceRefreshFailure(t *trace.Trace, err error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MicrosoftUUIDFromBinary(attributeName string) func(entry *ldap.Entry) (string, error) {
|
func AttributeUnchangedSinceLogin(attribute string) func(*ldap.Entry, provider.StoredRefreshAttributes) error {
|
||||||
// validation has already been done so we can just get the attribute...
|
return func(entry *ldap.Entry, storedAttributes provider.StoredRefreshAttributes) error {
|
||||||
return func(entry *ldap.Entry) (string, error) {
|
prevAttributeValue := storedAttributes.AdditionalAttributes[attribute]
|
||||||
binaryUUID := entry.GetRawAttributeValue(attributeName)
|
newValues := entry.GetAttributeValues(attribute)
|
||||||
return microsoftUUIDFromBinary(binaryUUID)
|
|
||||||
|
if len(newValues) != 1 {
|
||||||
|
return fmt.Errorf(`expected to find 1 value for "%s" attribute, but found %d`, attribute, len(newValues))
|
||||||
|
}
|
||||||
|
encodedNewValue := base64.RawURLEncoding.EncodeToString(entry.GetRawAttributeValue(attribute))
|
||||||
|
if prevAttributeValue != encodedNewValue {
|
||||||
|
return fmt.Errorf(`value for attribute "%s" has changed since initial value at login`, attribute)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func microsoftUUIDFromBinary(binaryUUID []byte) (string, error) {
|
|
||||||
uuidVal, err := uuid.FromBytes(binaryUUID) // start out with the RFC4122 version
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// then swap it because AD stores the first 3 fields little-endian rather than the expected
|
|
||||||
// big-endian.
|
|
||||||
uuidVal[0], uuidVal[1], uuidVal[2], uuidVal[3] = uuidVal[3], uuidVal[2], uuidVal[1], uuidVal[0]
|
|
||||||
uuidVal[4], uuidVal[5] = uuidVal[5], uuidVal[4]
|
|
||||||
uuidVal[6], uuidVal[7] = uuidVal[7], uuidVal[6]
|
|
||||||
return uuidVal.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GroupSAMAccountNameWithDomainSuffix(entry *ldap.Entry) (string, error) {
|
|
||||||
sAMAccountNameAttributeValues := entry.GetAttributeValues(sAMAccountNameAttribute)
|
|
||||||
|
|
||||||
if len(sAMAccountNameAttributeValues) != 1 {
|
|
||||||
return "", fmt.Errorf(`found %d values for attribute "%s", but expected 1 result`,
|
|
||||||
len(sAMAccountNameAttributeValues), sAMAccountNameAttribute,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sAMAccountName := sAMAccountNameAttributeValues[0]
|
|
||||||
if len(sAMAccountName) == 0 {
|
|
||||||
return "", fmt.Errorf(`found empty value for attribute "%s", but expected value to be non-empty`,
|
|
||||||
sAMAccountNameAttribute,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
distinguishedName := entry.DN
|
|
||||||
domain, err := getDomainFromDistinguishedName(distinguishedName)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return sAMAccountName + "@" + domain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var domainComponentsRegexp = regexp.MustCompile(",DC=|,dc=")
|
|
||||||
|
|
||||||
func getDomainFromDistinguishedName(distinguishedName string) (string, error) {
|
|
||||||
domainComponents := domainComponentsRegexp.Split(distinguishedName, -1)
|
|
||||||
if len(domainComponents) == 1 {
|
|
||||||
return "", fmt.Errorf("did not find domain components in group dn: %s", distinguishedName)
|
|
||||||
}
|
|
||||||
return strings.Join(domainComponents[1:], "."), nil
|
|
||||||
}
|
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"go.pinniped.dev/internal/crypto/ptls"
|
"go.pinniped.dev/internal/crypto/ptls"
|
||||||
"go.pinniped.dev/internal/endpointaddr"
|
"go.pinniped.dev/internal/endpointaddr"
|
||||||
"go.pinniped.dev/internal/mocks/mockldapconn"
|
"go.pinniped.dev/internal/mocks/mockldapconn"
|
||||||
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
"go.pinniped.dev/internal/testutil/tlsserver"
|
"go.pinniped.dev/internal/testutil/tlsserver"
|
||||||
)
|
)
|
||||||
@ -154,16 +155,17 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult.
|
// The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult.
|
||||||
expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticators.Response {
|
expectedAuthResponse := func(editFunc func(r *authenticators.Response)) *authenticators.Response {
|
||||||
u := &user.DefaultInfo{
|
u := &user.DefaultInfo{
|
||||||
Name: testUserSearchResultUsernameAttributeValue,
|
Name: testUserSearchResultUsernameAttributeValue,
|
||||||
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
||||||
Groups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
|
Groups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
|
||||||
}
|
}
|
||||||
|
response := &authenticators.Response{User: u, DN: testUserSearchResultDNValue, ExtraRefreshAttributes: map[string]string{}}
|
||||||
if editFunc != nil {
|
if editFunc != nil {
|
||||||
editFunc(u)
|
editFunc(response)
|
||||||
}
|
}
|
||||||
return &authenticators.Response{User: u, DN: testUserSearchResultDNValue}
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -250,8 +252,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.Groups = []string{}
|
info := r.User.(*user.DefaultInfo)
|
||||||
|
info.Groups = []string{}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -282,8 +285,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.Name = testUserSearchResultDNValue
|
info := r.User.(*user.DefaultInfo)
|
||||||
|
info.Name = testUserSearchResultDNValue
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -314,8 +318,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.UID = base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultDNValue))
|
info := r.User.(*user.DefaultInfo)
|
||||||
|
info.UID = base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultDNValue))
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -337,8 +342,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2}
|
info := r.User.(*user.DefaultInfo)
|
||||||
|
info.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -360,8 +366,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2}
|
info := r.User.(*user.DefaultInfo)
|
||||||
|
info.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -508,32 +515,36 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
Groups: []string{"a", "b", "c"},
|
Groups: []string{"a", "b", "c"},
|
||||||
},
|
},
|
||||||
DN: testUserSearchResultDNValue,
|
DN: testUserSearchResultDNValue,
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "override UID parsing to work with microsoft style objectGUIDs",
|
name: "requesting additional refresh related attributes",
|
||||||
username: testUpstreamUsername,
|
username: testUpstreamUsername,
|
||||||
password: testUpstreamPassword,
|
password: testUpstreamPassword,
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
providerConfig: providerConfig(func(p *ProviderConfig) {
|
||||||
p.UIDAttributeParsingOverrides = map[string]func(entry *ldap.Entry) (string, error){
|
p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error{
|
||||||
"objectGUID": MicrosoftUUIDFromBinary("objectGUID"),
|
"some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
p.UserSearch.UIDAttribute = "objectGUID"
|
|
||||||
}),
|
}),
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
searchMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
|
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
|
||||||
r.Attributes = []string{testUserSearchUsernameAttribute, "objectGUID"}
|
r.Attributes = append(r.Attributes, "some-attribute-to-check-during-refresh")
|
||||||
})).Return(&ldap.SearchResult{
|
})).Return(&ldap.SearchResult{
|
||||||
Entries: []*ldap.Entry{
|
Entries: []*ldap.Entry{
|
||||||
{
|
{
|
||||||
DN: testUserSearchResultDNValue,
|
DN: testUserSearchResultDNValue,
|
||||||
Attributes: []*ldap.EntryAttribute{
|
Attributes: []*ldap.EntryAttribute{
|
||||||
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
|
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
|
||||||
ldap.NewEntryAttribute("objectGUID", []string{"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16"}),
|
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
|
||||||
|
ldap.NewEntryAttribute("some-attribute-to-check-during-refresh", []string{"some-attribute-value"}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}, nil).Times(1)
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
|
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
|
||||||
Return(exampleGroupSearchResult, nil).Times(1)
|
Return(exampleGroupSearchResult, nil).Times(1)
|
||||||
conn.EXPECT().Close().Times(1)
|
conn.EXPECT().Close().Times(1)
|
||||||
@ -541,172 +552,31 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.UID = "04030201-0605-0807-0910-111213141516"
|
r.ExtraRefreshAttributes = map[string]string{"some-attribute-to-check-during-refresh": "c29tZS1hdHRyaWJ1dGUtdmFsdWU"}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "override UID parsing when the attribute name doesn't match what's returned does default parsing",
|
name: "requesting additional refresh related attributes, but they aren't returned",
|
||||||
username: testUpstreamUsername,
|
username: testUpstreamUsername,
|
||||||
password: testUpstreamPassword,
|
password: testUpstreamPassword,
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
providerConfig: providerConfig(func(p *ProviderConfig) {
|
||||||
p.UIDAttributeParsingOverrides = map[string]func(entry *ldap.Entry) (string, error){
|
p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error{
|
||||||
"objectGUID": MicrosoftUUIDFromBinary("objectGUID"),
|
"some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
searchMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
|
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
|
||||||
|
r.Attributes = append(r.Attributes, "some-attribute-to-check-during-refresh")
|
||||||
|
})).Return(exampleUserSearchResult, nil).Times(1)
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
|
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
|
||||||
Return(exampleGroupSearchResult, nil).Times(1)
|
Return(exampleGroupSearchResult, nil).Times(1)
|
||||||
conn.EXPECT().Close().Times(1)
|
conn.EXPECT().Close().Times(1)
|
||||||
},
|
},
|
||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
wantError: "found 0 values for attribute \"some-attribute-to-check-during-refresh\" while searching for user \"some-upstream-username\", but expected 1 result",
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
|
||||||
},
|
|
||||||
wantAuthResponse: expectedAuthResponse(nil),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "override group parsing to create new group names",
|
|
||||||
username: testUpstreamUsername,
|
|
||||||
password: testUpstreamPassword,
|
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
|
||||||
p.GroupSearch.GroupNameAttribute = "sAMAccountName"
|
|
||||||
p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){
|
|
||||||
"sAMAccountName": GroupSAMAccountNameWithDomainSuffix,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
|
||||||
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
|
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
|
|
||||||
r.Attributes = []string{"sAMAccountName"}
|
|
||||||
}), expectedGroupSearchPageSize).
|
|
||||||
Return(&ldap.SearchResult{
|
|
||||||
Entries: []*ldap.Entry{
|
|
||||||
{
|
|
||||||
DN: "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DN: "CN=Animals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
ldap.NewEntryAttribute("sAMAccountName", []string{"Animals"}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Referrals: []string{}, // note that we are not following referrals at this time
|
|
||||||
Controls: []ldap.Control{},
|
|
||||||
}, nil).Times(1)
|
|
||||||
conn.EXPECT().Close().Times(1)
|
|
||||||
},
|
|
||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
|
||||||
},
|
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
|
||||||
r.Groups = []string{"Animals@activedirectory.mycompany.example.com", "Mammals@activedirectory.mycompany.example.com"}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "override group parsing when domain can't be determined from dn",
|
|
||||||
username: testUpstreamUsername,
|
|
||||||
password: testUpstreamPassword,
|
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
|
||||||
p.GroupSearch.GroupNameAttribute = "sAMAccountName"
|
|
||||||
p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){
|
|
||||||
"sAMAccountName": GroupSAMAccountNameWithDomainSuffix,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
|
||||||
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
|
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
|
|
||||||
r.Attributes = []string{"sAMAccountName"}
|
|
||||||
}), expectedGroupSearchPageSize).
|
|
||||||
Return(&ldap.SearchResult{
|
|
||||||
Entries: []*ldap.Entry{
|
|
||||||
{
|
|
||||||
DN: "no-domain-components",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DN: "CN=Animals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
ldap.NewEntryAttribute("sAMAccountName", []string{"Animals"}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Referrals: []string{}, // note that we are not following referrals at this time
|
|
||||||
Controls: []ldap.Control{},
|
|
||||||
}, nil).Times(1)
|
|
||||||
conn.EXPECT().Close().Times(1)
|
|
||||||
},
|
|
||||||
wantError: "error finding groups for user some-upstream-user-dn: did not find domain components in group dn: no-domain-components",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "override group parsing when entry has multiple values for attribute",
|
|
||||||
username: testUpstreamUsername,
|
|
||||||
password: testUpstreamPassword,
|
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
|
||||||
p.GroupSearch.GroupNameAttribute = "sAMAccountName"
|
|
||||||
p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){
|
|
||||||
"sAMAccountName": GroupSAMAccountNameWithDomainSuffix,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
|
||||||
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
|
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
|
|
||||||
r.Attributes = []string{"sAMAccountName"}
|
|
||||||
}), expectedGroupSearchPageSize).
|
|
||||||
Return(&ldap.SearchResult{
|
|
||||||
Entries: []*ldap.Entry{
|
|
||||||
{
|
|
||||||
DN: "no-domain-components",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals", "Eukaryotes"}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Referrals: []string{}, // note that we are not following referrals at this time
|
|
||||||
Controls: []ldap.Control{},
|
|
||||||
}, nil).Times(1)
|
|
||||||
conn.EXPECT().Close().Times(1)
|
|
||||||
},
|
|
||||||
wantError: "error finding groups for user some-upstream-user-dn: found 2 values for attribute \"sAMAccountName\", but expected 1 result",
|
|
||||||
}, {
|
|
||||||
name: "override group parsing when entry has no values for attribute",
|
|
||||||
username: testUpstreamUsername,
|
|
||||||
password: testUpstreamPassword,
|
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
|
||||||
p.GroupSearch.GroupNameAttribute = "sAMAccountName"
|
|
||||||
p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){
|
|
||||||
"sAMAccountName": GroupSAMAccountNameWithDomainSuffix,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
|
||||||
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
|
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
|
|
||||||
r.Attributes = []string{"sAMAccountName"}
|
|
||||||
}), expectedGroupSearchPageSize).
|
|
||||||
Return(&ldap.SearchResult{
|
|
||||||
Entries: []*ldap.Entry{
|
|
||||||
{
|
|
||||||
DN: "no-domain-components",
|
|
||||||
Attributes: []*ldap.EntryAttribute{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Referrals: []string{}, // note that we are not following referrals at this time
|
|
||||||
Controls: []ldap.Control{},
|
|
||||||
}, nil).Times(1)
|
|
||||||
conn.EXPECT().Close().Times(1)
|
|
||||||
},
|
|
||||||
wantError: "error finding groups for user some-upstream-user-dn: found 0 values for attribute \"sAMAccountName\", but expected 1 result",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "when dial fails",
|
name: "when dial fails",
|
||||||
@ -1162,9 +1032,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
provider := New(*tt.providerConfig)
|
ldapProvider := New(*tt.providerConfig)
|
||||||
|
|
||||||
authResponse, authenticated, err := provider.AuthenticateUser(context.Background(), tt.username, tt.password)
|
authResponse, authenticated, err := ldapProvider.AuthenticateUser(context.Background(), tt.username, tt.password)
|
||||||
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
|
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
|
||||||
switch {
|
switch {
|
||||||
case tt.wantError != "":
|
case tt.wantError != "":
|
||||||
@ -1196,7 +1066,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Skip tt.bindEndUserMocks since DryRunAuthenticateUser() never binds as the end user.
|
// Skip tt.bindEndUserMocks since DryRunAuthenticateUser() never binds as the end user.
|
||||||
|
|
||||||
authResponse, authenticated, err = provider.DryRunAuthenticateUser(context.Background(), tt.username)
|
authResponse, authenticated, err = ldapProvider.DryRunAuthenticateUser(context.Background(), tt.username)
|
||||||
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
|
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
|
||||||
switch {
|
switch {
|
||||||
case tt.wantError != "":
|
case tt.wantError != "":
|
||||||
@ -1217,6 +1087,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUpstreamRefresh(t *testing.T) {
|
func TestUpstreamRefresh(t *testing.T) {
|
||||||
|
pwdLastSetAttribute := "pwdLastSet"
|
||||||
expectedUserSearch := &ldap.SearchRequest{
|
expectedUserSearch := &ldap.SearchRequest{
|
||||||
BaseDN: testUserSearchResultDNValue,
|
BaseDN: testUserSearchResultDNValue,
|
||||||
Scope: ldap.ScopeBaseObject,
|
Scope: ldap.ScopeBaseObject,
|
||||||
@ -1225,7 +1096,7 @@ func TestUpstreamRefresh(t *testing.T) {
|
|||||||
TimeLimit: 90,
|
TimeLimit: 90,
|
||||||
TypesOnly: false,
|
TypesOnly: false,
|
||||||
Filter: "(objectClass=*)",
|
Filter: "(objectClass=*)",
|
||||||
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute},
|
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, pwdLastSetAttribute},
|
||||||
Controls: nil, // don't need paging because we set the SizeLimit so small
|
Controls: nil, // don't need paging because we set the SizeLimit so small
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1242,6 +1113,11 @@ func TestUpstreamRefresh(t *testing.T) {
|
|||||||
Name: testUserSearchUIDAttribute,
|
Name: testUserSearchUIDAttribute,
|
||||||
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: pwdLastSetAttribute,
|
||||||
|
Values: []string{"132801740800000000"},
|
||||||
|
ByteValues: [][]byte{[]byte("132801740800000000")},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1259,6 +1135,9 @@ func TestUpstreamRefresh(t *testing.T) {
|
|||||||
UIDAttribute: testUserSearchUIDAttribute,
|
UIDAttribute: testUserSearchUIDAttribute,
|
||||||
UsernameAttribute: testUserSearchUsernameAttribute,
|
UsernameAttribute: testUserSearchUsernameAttribute,
|
||||||
},
|
},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -1512,6 +1391,36 @@ func TestUpstreamRefresh(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: "found 2 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
wantErr: "found 2 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "search result has a changed pwdLastSet value",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: testUserSearchUsernameAttribute,
|
||||||
|
Values: []string{testUserSearchResultUsernameAttributeValue},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: testUserSearchUIDAttribute,
|
||||||
|
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: pwdLastSetAttribute,
|
||||||
|
Values: []string{"132801740800000001"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "validation for attribute \"pwdLastSet\" failed during upstream refresh: value for attribute \"pwdLastSet\" has changed since initial value at login",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -1536,9 +1445,15 @@ func TestUpstreamRefresh(t *testing.T) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
provider := New(*providerConfig)
|
initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000"))
|
||||||
|
ldapProvider := New(*providerConfig)
|
||||||
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
|
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
|
||||||
err := provider.PerformRefresh(context.Background(), testUserSearchResultDNValue, testUserSearchResultUsernameAttributeValue, subject)
|
err := ldapProvider.PerformRefresh(context.Background(), provider.StoredRefreshAttributes{
|
||||||
|
Username: testUserSearchResultUsernameAttributeValue,
|
||||||
|
Subject: subject,
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
AdditionalAttributes: map[string]string{pwdLastSetAttribute: initialPwdLastSetEncoded},
|
||||||
|
})
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Equal(t, tt.wantErr, err.Error())
|
require.Equal(t, tt.wantErr, err.Error())
|
||||||
@ -1846,73 +1761,76 @@ func TestRealTLSDialing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetMicrosoftFormattedUUID(t *testing.T) {
|
func TestAttributeUnchangedSinceLogin(t *testing.T) {
|
||||||
|
initialVal := "some-attribute-value"
|
||||||
|
changedVal := "some-different-attribute-value"
|
||||||
|
attributeName := "some-attribute-name"
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
binaryUUID []byte
|
entry *ldap.Entry
|
||||||
wantString string
|
wantResult bool
|
||||||
wantErr string
|
wantErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "happy path",
|
name: "happy path where value has not changed since login",
|
||||||
binaryUUID: []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16"),
|
entry: &ldap.Entry{
|
||||||
wantString: "04030201-0605-0807-0910-111213141516",
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: attributeName,
|
||||||
|
Values: []string{initialVal},
|
||||||
|
ByteValues: [][]byte{[]byte(initialVal)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "not the right length",
|
name: "password has been reset since login",
|
||||||
binaryUUID: []byte("2\xf8\xb0\xaa\xb6V\xb1D\x8b(\xee"),
|
entry: &ldap.Entry{
|
||||||
wantErr: "invalid UUID (got 11 bytes)",
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: attributeName,
|
||||||
|
Values: []string{changedVal},
|
||||||
|
ByteValues: [][]byte{[]byte(changedVal)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "value for attribute \"some-attribute-name\" has changed since initial value at login",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no value for attribute attribute",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{},
|
||||||
|
},
|
||||||
|
wantErr: "expected to find 1 value for \"some-attribute-name\" attribute, but found 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many values for attribute",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: attributeName,
|
||||||
|
Values: []string{"val1", "val2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "expected to find 1 value for \"some-attribute-name\" attribute, but found 2",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
tt := test
|
tt := test
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
actualUUIDString, err := microsoftUUIDFromBinary(tt.binaryUUID)
|
initialValRawEncoded := base64.RawURLEncoding.EncodeToString([]byte(initialVal))
|
||||||
|
err := AttributeUnchangedSinceLogin(attributeName)(tt.entry, provider.StoredRefreshAttributes{AdditionalAttributes: map[string]string{attributeName: initialValRawEncoded}})
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
require.EqualError(t, err, tt.wantErr)
|
require.Error(t, err)
|
||||||
|
require.Equal(t, tt.wantErr, err.Error())
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
require.Equal(t, tt.wantString, actualUUIDString)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetDomainFromDistinguishedName(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
distinguishedName string
|
|
||||||
wantDomain string
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "happy path",
|
|
||||||
distinguishedName: "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com",
|
|
||||||
wantDomain: "activedirectory.mycompany.example.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "lowercased happy path",
|
|
||||||
distinguishedName: "cn=Mammals,ou=Users,ou=pinniped-ad,dc=activedirectory,dc=mycompany,dc=example,dc=com",
|
|
||||||
wantDomain: "activedirectory.mycompany.example.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no domain components",
|
|
||||||
distinguishedName: "not-a-dn",
|
|
||||||
wantErr: "did not find domain components in group dn: not-a-dn",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
tt := test
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
actualDomain, err := getDomainFromDistinguishedName(tt.distinguishedName)
|
|
||||||
if tt.wantErr != "" {
|
|
||||||
require.EqualError(t, err, tt.wantErr)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
require.Equal(t, tt.wantDomain, actualDomain)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(nil)),
|
provider: upstreamldap.New(*providerConfig(nil)),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -95,7 +97,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.ConnectionProtocol = upstreamldap.StartTLS
|
p.ConnectionProtocol = upstreamldap.StartTLS
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -104,7 +108,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -113,7 +119,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -125,7 +133,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.Filter = "cn={}"
|
p.UserSearch.Filter = "cn={}"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -136,7 +146,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -147,7 +159,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -156,7 +170,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("cn=pinny,ou=users,dc=pinniped,dc=dev"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("cn=pinny,ou=users,dc=pinniped,dc=dev"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -165,7 +181,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("Seal"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("Seal"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -174,7 +192,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "Seal", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev", // note that the final answer has case preserved from the entry
|
User: &user.DefaultInfo{Name: "Seal", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev", // note that the final answer has case preserved from the entry
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -187,7 +207,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.UIDAttribute = "givenName"
|
p.UserSearch.UIDAttribute = "givenName"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: b64("Pinny the 🦭"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: b64("Pinny the 🦭"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -199,7 +221,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.UsernameAttribute = "cn"
|
p.UserSearch.UsernameAttribute = "cn"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -220,7 +244,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.GroupSearch.Base = ""
|
p.GroupSearch.Base = ""
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -231,7 +257,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
|
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -245,7 +273,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
||||||
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
||||||
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
||||||
}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -259,7 +289,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
||||||
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
||||||
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
||||||
}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -270,7 +302,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
|
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames", "groupOfNames"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames", "groupOfNames"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -281,7 +315,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))"
|
p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -292,7 +328,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
|
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -673,6 +711,7 @@ func TestSimultaneousLDAPRequestsOnSingleProvider(t *testing.T) {
|
|||||||
assert.Equal(t, &authenticators.Response{
|
assert.Equal(t, &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
}, result.response)
|
}, result.response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,16 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -19,15 +24,18 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/text/encoding/unicode"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
|
"go.pinniped.dev/internal/crypto/ptls"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
"go.pinniped.dev/internal/psession"
|
"go.pinniped.dev/internal/psession"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
@ -38,24 +46,26 @@ import (
|
|||||||
"go.pinniped.dev/test/testlib/browsertest"
|
"go.pinniped.dev/test/testlib/browsertest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// nolint:gocyclo
|
||||||
func TestSupervisorLogin(t *testing.T) {
|
func TestSupervisorLogin(t *testing.T) {
|
||||||
env := testlib.IntegrationEnv(t)
|
env := testlib.IntegrationEnv(t)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
maybeSkip func(t *testing.T)
|
maybeSkip func(t *testing.T)
|
||||||
|
createTestUser func(t *testing.T) (string, string)
|
||||||
|
deleteTestUser func(t *testing.T, username string)
|
||||||
|
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client)
|
||||||
createIDP func(t *testing.T) string
|
createIDP func(t *testing.T) string
|
||||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client)
|
|
||||||
wantDownstreamIDTokenSubjectToMatch string
|
wantDownstreamIDTokenSubjectToMatch string
|
||||||
wantDownstreamIDTokenUsernameToMatch string
|
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
||||||
wantDownstreamIDTokenGroups []string
|
wantDownstreamIDTokenGroups []string
|
||||||
wantErrorDescription string
|
wantErrorDescription string
|
||||||
wantErrorType string
|
wantErrorType string
|
||||||
|
|
||||||
// We don't necessarily have any way to revoke the user's session on the upstream provider,
|
// Either revoke the user's session on the upstream provider, or manipulate the user's session
|
||||||
// so to cause the upstream refresh to fail we can cheat by manipulating the user's session
|
|
||||||
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
||||||
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName string)
|
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "oidc with default username and groups claim settings",
|
name: "oidc with default username and groups claim settings",
|
||||||
@ -76,7 +86,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
return oidcIDP.Name
|
return oidcIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||||
@ -85,7 +95,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
// the ID token Username should include the upstream user ID after the upstream issuer name
|
// the ID token Username should include the upstream user ID after the upstream issuer name
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "oidc with custom username and groups claim settings",
|
name: "oidc with custom username and groups claim settings",
|
||||||
@ -113,14 +123,14 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
return oidcIDP.Name
|
return oidcIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||||
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
||||||
},
|
},
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -144,7 +154,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
}, idpv1alpha1.PhaseReady)
|
}, idpv1alpha1.PhaseReady)
|
||||||
return oidcIDP.Name
|
return oidcIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamOIDC.Username, // username to present to server during login
|
env.SupervisorUpstreamOIDC.Username, // username to present to server during login
|
||||||
@ -153,7 +163,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||||
@ -162,7 +172,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
// the ID token Username should include the upstream user ID after the upstream issuer name
|
// the ID token Username should include the upstream user ID after the upstream issuer name
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS",
|
name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS",
|
||||||
@ -212,7 +222,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -221,7 +231,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||||
@ -235,7 +245,9 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -286,7 +298,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserCN, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserCN, // username to present to server during login
|
||||||
@ -295,7 +307,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||||
@ -309,7 +321,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN) + "$" },
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -360,7 +372,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -438,7 +450,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -447,7 +459,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||||
@ -460,7 +472,9 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -543,7 +557,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -552,7 +566,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||||
@ -565,7 +579,9 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -604,7 +620,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
return adIDP.Name
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
||||||
@ -613,7 +629,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
@ -627,7 +643,9 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$"
|
||||||
|
},
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||||
}, {
|
}, {
|
||||||
name: "activedirectory with custom options",
|
name: "activedirectory with custom options",
|
||||||
@ -679,7 +697,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
return adIDP.Name
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -688,7 +706,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
@ -702,7 +720,9 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -759,7 +779,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
return adIDP.Name
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
||||||
@ -768,7 +788,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
@ -781,7 +801,9 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$"
|
||||||
|
},
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -853,7 +875,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
return adIDP.Name
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
||||||
@ -862,7 +884,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
@ -875,9 +897,197 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$"
|
||||||
|
},
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "active directory login fails after the user password is changed",
|
||||||
|
maybeSkip: func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
|
||||||
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
|
}
|
||||||
|
if env.SupervisorUpstreamActiveDirectory.Host == "" {
|
||||||
|
t.Skip("Active Directory hostname not specified")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
|
map[string]string{
|
||||||
|
v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{
|
||||||
|
Host: env.SupervisorUpstreamActiveDirectory.Host,
|
||||||
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)),
|
||||||
|
},
|
||||||
|
Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{
|
||||||
|
SecretName: secret.Name,
|
||||||
|
},
|
||||||
|
}, idpv1alpha1.ActiveDirectoryPhaseReady)
|
||||||
|
expectedMsg := fmt.Sprintf(
|
||||||
|
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||||
|
env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
secret.Name, secret.ResourceVersion,
|
||||||
|
)
|
||||||
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
|
return adIDP.Name
|
||||||
|
},
|
||||||
|
createTestUser: func(t *testing.T) (string, string) {
|
||||||
|
return createFreshADTestUser(t, env)
|
||||||
|
},
|
||||||
|
deleteTestUser: func(t *testing.T, username string) {
|
||||||
|
deleteTestADUser(t, env, username)
|
||||||
|
},
|
||||||
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) {
|
||||||
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
|
downstreamAuthorizeURL,
|
||||||
|
testUserName, // username to present to server during login
|
||||||
|
testUserPassword, // password to present to server during login
|
||||||
|
httpClient,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) {
|
||||||
|
changeADTestUserPassword(t, env, username)
|
||||||
|
},
|
||||||
|
// we can't know the subject ahead of time because we created a new user and don't know their uid,
|
||||||
|
// so skip wantDownstreamIDTokenSubjectToMatch
|
||||||
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(username string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: []string{}, // none for now.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "active directory login fails after the user is deactivated",
|
||||||
|
maybeSkip: func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
|
||||||
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
|
}
|
||||||
|
if env.SupervisorUpstreamActiveDirectory.Host == "" {
|
||||||
|
t.Skip("Active Directory hostname not specified")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
|
map[string]string{
|
||||||
|
v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{
|
||||||
|
Host: env.SupervisorUpstreamActiveDirectory.Host,
|
||||||
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)),
|
||||||
|
},
|
||||||
|
Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{
|
||||||
|
SecretName: secret.Name,
|
||||||
|
},
|
||||||
|
}, idpv1alpha1.ActiveDirectoryPhaseReady)
|
||||||
|
expectedMsg := fmt.Sprintf(
|
||||||
|
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||||
|
env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
secret.Name, secret.ResourceVersion,
|
||||||
|
)
|
||||||
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
|
return adIDP.Name
|
||||||
|
},
|
||||||
|
createTestUser: func(t *testing.T) (string, string) {
|
||||||
|
return createFreshADTestUser(t, env)
|
||||||
|
},
|
||||||
|
deleteTestUser: func(t *testing.T, username string) {
|
||||||
|
deleteTestADUser(t, env, username)
|
||||||
|
},
|
||||||
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) {
|
||||||
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
|
downstreamAuthorizeURL,
|
||||||
|
testUserName, // username to present to server during login
|
||||||
|
testUserPassword, // password to present to server during login
|
||||||
|
httpClient,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) {
|
||||||
|
deactivateADTestUser(t, env, username)
|
||||||
|
},
|
||||||
|
// we can't know the subject ahead of time because we created a new user and don't know their uid,
|
||||||
|
// so skip wantDownstreamIDTokenSubjectToMatch
|
||||||
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(username string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: []string{}, // none for now.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "active directory login fails after the user is locked",
|
||||||
|
maybeSkip: func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
|
||||||
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
|
}
|
||||||
|
if env.SupervisorUpstreamActiveDirectory.Host == "" {
|
||||||
|
t.Skip("Active Directory hostname not specified")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
|
map[string]string{
|
||||||
|
v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{
|
||||||
|
Host: env.SupervisorUpstreamActiveDirectory.Host,
|
||||||
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)),
|
||||||
|
},
|
||||||
|
Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{
|
||||||
|
SecretName: secret.Name,
|
||||||
|
},
|
||||||
|
}, idpv1alpha1.ActiveDirectoryPhaseReady)
|
||||||
|
expectedMsg := fmt.Sprintf(
|
||||||
|
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||||
|
env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
secret.Name, secret.ResourceVersion,
|
||||||
|
)
|
||||||
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
|
return adIDP.Name
|
||||||
|
},
|
||||||
|
createTestUser: func(t *testing.T) (string, string) {
|
||||||
|
return createFreshADTestUser(t, env)
|
||||||
|
},
|
||||||
|
deleteTestUser: func(t *testing.T, username string) {
|
||||||
|
deleteTestADUser(t, env, username)
|
||||||
|
},
|
||||||
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) {
|
||||||
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
|
downstreamAuthorizeURL,
|
||||||
|
testUserName, // username to present to server during login
|
||||||
|
testUserPassword, // password to present to server during login
|
||||||
|
httpClient,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) {
|
||||||
|
lockADTestUser(t, env, username)
|
||||||
|
},
|
||||||
|
// we can't know the subject ahead of time because we created a new user and don't know their uid,
|
||||||
|
// so skip wantDownstreamIDTokenSubjectToMatch
|
||||||
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(username string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: []string{},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "logging in to activedirectory with a deactivated user fails",
|
name: "logging in to activedirectory with a deactivated user fails",
|
||||||
maybeSkip: func(t *testing.T) {
|
maybeSkip: func(t *testing.T) {
|
||||||
@ -914,7 +1124,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
return adIDP.Name
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamActiveDirectory.TestDeactivatedUserSAMAccountNameValue, // username to present to server during login
|
env.SupervisorUpstreamActiveDirectory.TestDeactivatedUserSAMAccountNameValue, // username to present to server during login
|
||||||
@ -975,7 +1185,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -984,7 +1194,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, _ string) {
|
||||||
// get the idp, update the config.
|
// get the idp, update the config.
|
||||||
client := testlib.NewSupervisorClientset(t)
|
client := testlib.NewSupervisorClientset(t)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
@ -1007,7 +1217,9 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1020,6 +1232,8 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
tt.createIDP,
|
tt.createIDP,
|
||||||
tt.requestAuthorization,
|
tt.requestAuthorization,
|
||||||
tt.breakRefreshSessionData,
|
tt.breakRefreshSessionData,
|
||||||
|
tt.createTestUser,
|
||||||
|
tt.deleteTestUser,
|
||||||
tt.wantDownstreamIDTokenSubjectToMatch,
|
tt.wantDownstreamIDTokenSubjectToMatch,
|
||||||
tt.wantDownstreamIDTokenUsernameToMatch,
|
tt.wantDownstreamIDTokenUsernameToMatch,
|
||||||
tt.wantDownstreamIDTokenGroups,
|
tt.wantDownstreamIDTokenGroups,
|
||||||
@ -1150,10 +1364,15 @@ func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t *tes
|
|||||||
func testSupervisorLogin(
|
func testSupervisorLogin(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
createIDP func(t *testing.T) string,
|
createIDP func(t *testing.T) string,
|
||||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client),
|
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client),
|
||||||
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string),
|
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string),
|
||||||
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string,
|
createTestUser func(t *testing.T) (string, string),
|
||||||
wantErrorDescription string, wantErrorType string,
|
deleteTestUser func(t *testing.T, username string),
|
||||||
|
wantDownstreamIDTokenSubjectToMatch string,
|
||||||
|
wantDownstreamIDTokenUsernameToMatch func(username string) string,
|
||||||
|
wantDownstreamIDTokenGroups []string,
|
||||||
|
wantErrorDescription string,
|
||||||
|
wantErrorType string,
|
||||||
) {
|
) {
|
||||||
env := testlib.IntegrationEnv(t)
|
env := testlib.IntegrationEnv(t)
|
||||||
|
|
||||||
@ -1241,6 +1460,12 @@ func testSupervisorLogin(
|
|||||||
// Create upstream IDP and wait for it to become ready.
|
// Create upstream IDP and wait for it to become ready.
|
||||||
idpName := createIDP(t)
|
idpName := createIDP(t)
|
||||||
|
|
||||||
|
username, password := "", ""
|
||||||
|
if createTestUser != nil {
|
||||||
|
username, password = createTestUser(t)
|
||||||
|
defer deleteTestUser(t, username)
|
||||||
|
}
|
||||||
|
|
||||||
// Perform OIDC discovery for our downstream.
|
// Perform OIDC discovery for our downstream.
|
||||||
var discovery *coreosoidc.Provider
|
var discovery *coreosoidc.Provider
|
||||||
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
||||||
@ -1276,7 +1501,7 @@ func testSupervisorLogin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Perform parameterized auth code acquisition.
|
// Perform parameterized auth code acquisition.
|
||||||
requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL, httpClient)
|
requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient)
|
||||||
|
|
||||||
// Expect that our callback handler was invoked.
|
// Expect that our callback handler was invoked.
|
||||||
callback := localCallbackServer.waitForCallback(10 * time.Second)
|
callback := localCallbackServer.waitForCallback(10 * time.Second)
|
||||||
@ -1294,7 +1519,7 @@ func testSupervisorLogin(
|
|||||||
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"}
|
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"}
|
||||||
verifyTokenResponse(t,
|
verifyTokenResponse(t,
|
||||||
tokenResponse, discovery, downstreamOAuth2Config, nonceParam,
|
tokenResponse, discovery, downstreamOAuth2Config, nonceParam,
|
||||||
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups)
|
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroups)
|
||||||
|
|
||||||
// token exchange on the original token
|
// token exchange on the original token
|
||||||
doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery)
|
doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery)
|
||||||
@ -1308,7 +1533,7 @@ func testSupervisorLogin(
|
|||||||
expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "groups", "at_hash"}
|
expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "groups", "at_hash"}
|
||||||
verifyTokenResponse(t,
|
verifyTokenResponse(t,
|
||||||
refreshedTokenResponse, discovery, downstreamOAuth2Config, "",
|
refreshedTokenResponse, discovery, downstreamOAuth2Config, "",
|
||||||
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups)
|
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroups)
|
||||||
|
|
||||||
require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken)
|
require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken)
|
||||||
require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken)
|
require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken)
|
||||||
@ -1333,7 +1558,7 @@ func testSupervisorLogin(
|
|||||||
// Next mutate the part of the session that is used during upstream refresh.
|
// Next mutate the part of the session that is used during upstream refresh.
|
||||||
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
||||||
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
||||||
breakRefreshSessionData(t, pinnipedSession, idpName)
|
breakRefreshSessionData(t, pinnipedSession, idpName, username)
|
||||||
|
|
||||||
// Then save the mutated Secret back to Kubernetes.
|
// Then save the mutated Secret back to Kubernetes.
|
||||||
// There is no update function, so delete and create again at the same name.
|
// There is no update function, so delete and create again at the same name.
|
||||||
@ -1423,7 +1648,7 @@ func verifyTokenResponse(
|
|||||||
require.NotEmpty(t, tokenResponse.RefreshToken)
|
require.NotEmpty(t, tokenResponse.RefreshToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestAuthorizationUsingBrowserAuthcodeFlow(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client) {
|
func requestAuthorizationUsingBrowserAuthcodeFlow(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
env := testlib.IntegrationEnv(t)
|
env := testlib.IntegrationEnv(t)
|
||||||
|
|
||||||
@ -1595,3 +1820,153 @@ func expectSecurityHeaders(t *testing.T, response *http.Response, expectFositeTo
|
|||||||
assert.Equal(t, "no-cache", h.Get("Pragma"))
|
assert.Equal(t, "no-cache", h.Get("Pragma"))
|
||||||
assert.Equal(t, "0", h.Get("Expires"))
|
assert.Equal(t, "0", h.Get("Expires"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a fresh test user in AD to use for this test.
|
||||||
|
func createFreshADTestUser(t *testing.T, env *testlib.TestEnv) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
// dial tls
|
||||||
|
conn := dialTLS(t, env)
|
||||||
|
// bind
|
||||||
|
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testUserName := "user-" + createRandomHexString(t, 7) // sAMAccountNames are limited to 20 characters, so this is as long as we can make it.
|
||||||
|
// create
|
||||||
|
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||||
|
a := ldap.NewAddRequest(userDN, []ldap.Control{})
|
||||||
|
a.Attribute("objectClass", []string{"top", "person", "organizationalPerson", "user"})
|
||||||
|
a.Attribute("userPrincipalName", []string{fmt.Sprintf("%s@%s", testUserName, env.SupervisorUpstreamActiveDirectory.Domain)})
|
||||||
|
a.Attribute("sAMAccountName", []string{testUserName})
|
||||||
|
|
||||||
|
err = conn.Add(a)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// modify password and enable account
|
||||||
|
testUserPassword := createRandomASCIIString(t, 20)
|
||||||
|
enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
||||||
|
encodedTestUserPassword, err := enc.String("\"" + testUserPassword + "\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m := ldap.NewModifyRequest(userDN, []ldap.Control{})
|
||||||
|
m.Replace("unicodePwd", []string{encodedTestUserPassword})
|
||||||
|
m.Replace("userAccountControl", []string{"512"})
|
||||||
|
err = conn.Modify(m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Second) // intrasite domain controller replication can take up to 15 seconds, so wait to ensure the change has propogated.
|
||||||
|
return testUserName, testUserPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// deactivate the test user.
|
||||||
|
func deactivateADTestUser(t *testing.T, env *testlib.TestEnv, testUserName string) {
|
||||||
|
conn := dialTLS(t, env)
|
||||||
|
// bind
|
||||||
|
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||||
|
m := ldap.NewModifyRequest(userDN, []ldap.Control{})
|
||||||
|
m.Replace("userAccountControl", []string{"514"}) // normal user, account disabled
|
||||||
|
err = conn.Modify(m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Second) // intrasite domain controller replication can take up to 15 seconds, so wait to ensure the change has propogated.
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock the test user's account by entering the wrong password a bunch of times.
|
||||||
|
func lockADTestUser(t *testing.T, env *testlib.TestEnv, testUserName string) {
|
||||||
|
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||||
|
conn := dialTLS(t, env)
|
||||||
|
|
||||||
|
// our password policy allows 20 wrong attempts before locking the account, so do 21.
|
||||||
|
// these wrong password attempts could go to different domain controllers, but account
|
||||||
|
// lockout changes are urgently replicated, meaning that the domain controllers will be
|
||||||
|
// synced asap rather than in the usual 15 second interval.
|
||||||
|
// See https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/cc961787(v=technet.10)#urgent-replication-of-account-lockout-changes
|
||||||
|
for i := 0; i <= 21; i++ {
|
||||||
|
err := conn.Bind(userDN, "not-the-right-password-"+fmt.Sprint(i))
|
||||||
|
require.Error(t, err) // this should be an error
|
||||||
|
}
|
||||||
|
|
||||||
|
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Second) // intrasite domain controller replication can take up to 15 seconds, so wait to ensure the change has propogated.
|
||||||
|
}
|
||||||
|
|
||||||
|
// change the user's password to a new one.
|
||||||
|
func changeADTestUserPassword(t *testing.T, env *testlib.TestEnv, testUserName string) {
|
||||||
|
conn := dialTLS(t, env)
|
||||||
|
// bind
|
||||||
|
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
newTestUserPassword := createRandomASCIIString(t, 20)
|
||||||
|
enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
||||||
|
encodedTestUserPassword, err := enc.String(`"` + newTestUserPassword + `"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||||
|
m := ldap.NewModifyRequest(userDN, []ldap.Control{})
|
||||||
|
m.Replace("unicodePwd", []string{encodedTestUserPassword})
|
||||||
|
err = conn.Modify(m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Second) // intrasite domain controller replication can take up to 15 seconds, so wait to ensure the change has propogated.
|
||||||
|
// don't bother to return the new password... we won't be using it, just checking that it's changed.
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the test user created for this test.
|
||||||
|
func deleteTestADUser(t *testing.T, env *testlib.TestEnv, testUserName string) {
|
||||||
|
t.Helper()
|
||||||
|
conn := dialTLS(t, env)
|
||||||
|
// bind
|
||||||
|
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||||
|
d := ldap.NewDelRequest(userDN, []ldap.Control{})
|
||||||
|
err = conn.Del(d)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialTLS(t *testing.T, env *testlib.TestEnv) *ldap.Conn {
|
||||||
|
t.Helper()
|
||||||
|
// dial tls
|
||||||
|
rootCAs := x509.NewCertPool()
|
||||||
|
success := rootCAs.AppendCertsFromPEM([]byte(env.SupervisorUpstreamActiveDirectory.CABundle))
|
||||||
|
require.True(t, success)
|
||||||
|
tlsConfig := ptls.DefaultLDAP(rootCAs)
|
||||||
|
dialer := &tls.Dialer{NetDialer: &net.Dialer{Timeout: time.Minute}, Config: tlsConfig}
|
||||||
|
c, err := dialer.DialContext(context.Background(), "tcp", env.SupervisorUpstreamActiveDirectory.Host)
|
||||||
|
require.NoError(t, err)
|
||||||
|
conn := ldap.NewConn(c, true)
|
||||||
|
conn.Start()
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRandomHexString(t *testing.T, length int) string {
|
||||||
|
t.Helper()
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
_, err := rand.Read(bytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
randomString := hex.EncodeToString(bytes)
|
||||||
|
return randomString
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRandomASCIIString(t *testing.T, length int) string {
|
||||||
|
result := ""
|
||||||
|
for {
|
||||||
|
if len(result) >= length {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(127)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
n := num.Int64()
|
||||||
|
// Make sure that the number/byte/letter is inside
|
||||||
|
// the range of printable ASCII characters (excluding space and DEL)
|
||||||
|
if n > 32 && n < 127 {
|
||||||
|
result += string(rune(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -84,6 +84,7 @@ type TestOIDCUpstream struct {
|
|||||||
|
|
||||||
type TestLDAPUpstream struct {
|
type TestLDAPUpstream struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
StartTLSOnlyHost string `json:"startTLSOnlyHost"`
|
StartTLSOnlyHost string `json:"startTLSOnlyHost"`
|
||||||
CABundle string `json:"caBundle"`
|
CABundle string `json:"caBundle"`
|
||||||
BindUsername string `json:"bindUsername"`
|
BindUsername string `json:"bindUsername"`
|
||||||
@ -279,6 +280,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
|
|||||||
|
|
||||||
result.SupervisorUpstreamActiveDirectory = TestLDAPUpstream{
|
result.SupervisorUpstreamActiveDirectory = TestLDAPUpstream{
|
||||||
Host: wantEnv("PINNIPED_TEST_AD_HOST", ""),
|
Host: wantEnv("PINNIPED_TEST_AD_HOST", ""),
|
||||||
|
Domain: wantEnv("PINNIPED_TEST_AD_DOMAIN", ""),
|
||||||
CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")),
|
CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")),
|
||||||
BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""),
|
BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""),
|
||||||
BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""),
|
BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""),
|
||||||
|
Loading…
Reference in New Issue
Block a user