diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 45f1c043a..7a22b8167 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -317,7 +317,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, }, Dialer: c.ldapDialer, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{upstreamldap.PwdLastSetAttribute: upstreamldap.PwdUnchangedSinceLogin, upstreamldap.UserAccountControlAttribute: upstreamldap.ValidUserAccountControl, upstreamldap.UserAccountControlComputedAttribute: upstreamldap.ValidComputedUserAccountControl}, } if spec.GroupSearch.Attributes.GroupName == "" { diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 470f9bcbb..a4cec5985 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -221,7 +221,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, } // Make a copy with targeted changes. @@ -538,7 +542,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -595,7 +603,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: "sAMAccountName", }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -655,7 +667,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -715,7 +731,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), @@ -774,7 +794,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -904,7 +928,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1029,8 +1057,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, - }, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }}, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234}, @@ -1081,7 +1112,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1282,7 +1317,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": upstreamldap.GroupSAMAccountNameWithDomainSuffix}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1335,7 +1374,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1392,7 +1435,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1443,7 +1490,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1640,7 +1691,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{"pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, "userAccountControl": upstreamldap.ValidUserAccountControl}, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + "pwdLastSet": upstreamldap.PwdUnchangedSinceLogin, + "userAccountControl": upstreamldap.ValidUserAccountControl, + "msDS-User-Account-Control-Computed": upstreamldap.ValidComputedUserAccountControl, + }, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 444223272..f20fb8a1a 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -41,8 +41,11 @@ const ( defaultLDAPPort = uint16(389) defaultLDAPSPort = uint16(636) sAMAccountNameAttribute = "sAMAccountName" - pwdLastSetAttribute = "pwdLastSet" - userAccountControlAttribute = "userAccountControl" + PwdLastSetAttribute = "pwdLastSet" + UserAccountControlAttribute = "userAccountControl" + UserAccountControlComputedAttribute = "msDS-User-Account-Control-Computed" + accountDisabledBitmapValue = 2 + accountLockedBitmapValue = 16 ) // Conn abstracts the upstream LDAP communication protocol (mostly for testing). @@ -860,9 +863,9 @@ func getDomainFromDistinguishedName(distinguishedName string) (string, error) { func PwdUnchangedSinceLogin(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error { authTime := attributes.AuthTime - pwdLastSetWin32Format := entry.GetAttributeValues(pwdLastSetAttribute) + pwdLastSetWin32Format := entry.GetAttributeValues(PwdLastSetAttribute) if len(pwdLastSetWin32Format) != 1 { - return fmt.Errorf("expected to find 1 value for %s attribute, but found %d", pwdLastSetAttribute, len(pwdLastSetWin32Format)) + return fmt.Errorf("expected to find 1 value for %s attribute, but found %d", PwdLastSetAttribute, len(pwdLastSetWin32Format)) } // convert to a time.Time pwdLastSetParsed, err := win32timestampToTime(pwdLastSetWin32Format[0]) @@ -898,14 +901,27 @@ func win32timestampToTime(win32timestamp string) (*time.Time, error) { } func ValidUserAccountControl(entry *ldap.Entry, _ provider.StoredRefreshAttributes) error { - userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlAttribute)) + userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(UserAccountControlAttribute)) if err != nil { return err } - deactivated := userAccountControl & 2 // bitwise and. + 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 +} diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 59c54d14b..fc056cc81 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -16,8 +16,6 @@ import ( "testing" "time" - provider2 "go.pinniped.dev/internal/oidc/provider" - "github.com/go-ldap/ldap/v3" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" @@ -28,6 +26,7 @@ import ( "go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/mocks/mockldapconn" + provider2 "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/tlsserver" ) @@ -2152,3 +2151,51 @@ func TestValidUserAccountControl(t *testing.T) { }) } } + +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", + }, + } + + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + err := ValidComputedUserAccountControl(tt.entry, provider2.StoredRefreshAttributes{}) + + if tt.wantErr != "" { + require.Error(t, err) + require.Equal(t, tt.wantErr, err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index cc7beb408..664970527 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -45,6 +45,7 @@ import ( "go.pinniped.dev/test/testlib/browsertest" ) +// nolint:gocyclo func TestSupervisorLogin(t *testing.T) { env := testlib.IntegrationEnv(t) @@ -1024,6 +1025,68 @@ func TestSupervisorLogin(t *testing.T) { }, 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", maybeSkip: func(t *testing.T) { @@ -1153,7 +1216,7 @@ func TestSupervisorLogin(t *testing.T) { "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), ) + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute - wantDownstreamIDTokenUsernameToMatch: func(username string) string { + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, @@ -1788,10 +1851,12 @@ func createFreshADTestUser(t *testing.T, env *testlib.TestEnv) (string, string) 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's password +// deactivate the test user. func deactivateADTestUser(t *testing.T, env *testlib.TestEnv, testUserName string) { conn := dialTLS(t, env) // bind @@ -1803,6 +1868,24 @@ func deactivateADTestUser(t *testing.T, env *testlib.TestEnv, testUserName strin 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) + + for i := 0; i <= 21; i++ { // our password policy allows 20 wrong attempts before locking the account, so do 21. + 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. @@ -1822,13 +1905,14 @@ func changeADTestUserPassword(t *testing.T, env *testlib.TestEnv, testUserName s 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)