diff --git a/tests/common/auth_test.go b/tests/common/auth_test.go index 4722a1a47fb..cd543905198 100644 --- a/tests/common/auth_test.go +++ b/tests/common/auth_test.go @@ -28,8 +28,9 @@ import ( "github.com/stretchr/testify/require" ) -var defaultAuthToken = fmt.Sprintf("jwt,pub-key=%s,priv-key=%s,sign-method=RS256,ttl=1s", - mustAbsPath("../fixtures/server.crt"), mustAbsPath("../fixtures/server.key.insecure")) +var tokenTTL = time.Second +var defaultAuthToken = fmt.Sprintf("jwt,pub-key=%s,priv-key=%s,sign-method=RS256,ttl=%s", + mustAbsPath("../fixtures/server.crt"), mustAbsPath("../fixtures/server.key.insecure"), tokenTTL) const ( PermissionDenied = "etcdserver: permission denied" @@ -736,6 +737,121 @@ func TestAuthRoleList(t *testing.T) { }) } +func TestAuthJWTExpire(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken})) + defer clus.Close() + cc := testutils.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + require.NoErrorf(t, setupAuth(cc, []authRole{testRole}, []authUser{rootUser, testUser}), "failed to enable auth") + testUserAuthClient := testutils.MustClient(clus.Client(WithAuth(testUserName, testPassword))) + + require.NoError(t, testUserAuthClient.Put(ctx, "foo", "bar", config.PutOptions{})) + // wait an expiration of my JWT token + <-time.After(3 * tokenTTL) + + // e2e test will generate a new token while + // integration test that re-uses the same etcd client will refresh the token on server failure. + require.NoError(t, testUserAuthClient.Put(ctx, "foo", "bar", config.PutOptions{})) + }) +} + +// TestAuthRevisionConsistency ensures auth revision is the same after member restarts +func TestAuthRevisionConsistency(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken})) + defer clus.Close() + cc := testutils.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + require.NoErrorf(t, setupAuth(cc, []authRole{}, []authUser{rootUser}), "failed to enable auth") + rootAuthClient := testutils.MustClient(clus.Client(WithAuth(rootUserName, rootPassword))) + + // add user + _, err := rootAuthClient.UserAdd(ctx, testUserName, testPassword, config.UserAddOptions{}) + require.NoError(t, err) + // delete the same user + _, err = rootAuthClient.UserDelete(ctx, testUserName) + require.NoError(t, err) + + // get node0 auth revision + aresp, err := rootAuthClient.AuthStatus(ctx) + require.NoError(t, err) + oldAuthRevision := aresp.AuthRevision + + // restart the node + clus.Members()[0].Stop() + require.NoError(t, clus.Members()[0].Start(ctx)) + + aresp2, err := rootAuthClient.AuthStatus(ctx) + require.NoError(t, err) + newAuthRevision := aresp2.AuthRevision + + require.Equal(t, oldAuthRevision, newAuthRevision) + }) +} + +// TestAuthTestCacheReload ensures permissions are persisted and will be reloaded after member restarts +func TestAuthTestCacheReload(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken})) + defer clus.Close() + cc := testutils.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + require.NoErrorf(t, setupAuth(cc, []authRole{testRole}, []authUser{rootUser, testUser}), "failed to enable auth") + testUserAuthClient := testutils.MustClient(clus.Client(WithAuth(testUserName, testPassword))) + + // create foo since that is within the permission set + // expectation is to succeed + require.NoError(t, testUserAuthClient.Put(ctx, "foo", "bar", config.PutOptions{})) + + // restart the node + clus.Members()[0].Stop() + require.NoError(t, clus.Members()[0].Start(ctx)) + + // nothing has changed, but it fails without refreshing cache after restart + require.NoError(t, testUserAuthClient.Put(ctx, "foo", "bar2", config.PutOptions{})) + }) +} + +// TestAuthLeaseTimeToLive gated lease time to live with RBAC control +func TestAuthLeaseTimeToLive(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken})) + defer clus.Close() + cc := testutils.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + require.NoErrorf(t, setupAuth(cc, []authRole{testRole}, []authUser{rootUser, testUser}), "failed to enable auth") + testUserAuthClient := testutils.MustClient(clus.Client(WithAuth(testUserName, testPassword))) + + gresp, err := testUserAuthClient.Grant(ctx, 10) + require.NoError(t, err) + leaseID := gresp.ID + + require.NoError(t, testUserAuthClient.Put(ctx, "foo", "bar", config.PutOptions{LeaseID: leaseID})) + _, err = testUserAuthClient.TimeToLive(ctx, leaseID, config.LeaseOption{WithAttachedKeys: true}) + require.NoError(t, err) + + rootAuthClient := testutils.MustClient(clus.Client(WithAuth(rootUserName, rootPassword))) + require.NoError(t, rootAuthClient.Put(ctx, "bar", "foo", config.PutOptions{LeaseID: leaseID})) + + // the lease is attached to bar, which test-user cannot access + _, err = testUserAuthClient.TimeToLive(ctx, leaseID, config.LeaseOption{WithAttachedKeys: true}) + require.Errorf(t, err, "test-user must not be able to access to the lease, because it's attached to the key bar") + + // without --keys, access should be allowed + _, err = testUserAuthClient.TimeToLive(ctx, leaseID, config.LeaseOption{WithAttachedKeys: false}) + require.NoError(t, err) + }) +} + func mustAbsPath(path string) string { abs, err := filepath.Abs(path) if err != nil { diff --git a/tests/e2e/ctl_v3_auth_test.go b/tests/e2e/ctl_v3_auth_test.go index e5b65dcb23f..1b01303e9ca 100644 --- a/tests/e2e/ctl_v3_auth_test.go +++ b/tests/e2e/ctl_v3_auth_test.go @@ -15,15 +15,12 @@ package e2e import ( - "context" "fmt" "os" "testing" - "time" "github.com/stretchr/testify/require" - clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/tests/v3/framework/e2e" ) @@ -34,20 +31,16 @@ func TestCtlV3AuthFromKeyPerm(t *testing.T) { testCtl(t, authTestFromKeyPerm) } func TestCtlV3AuthAndWatch(t *testing.T) { testCtl(t, authTestWatch) } func TestCtlV3AuthAndWatchJWT(t *testing.T) { testCtl(t, authTestWatch, withCfg(*e2e.NewConfigJWT())) } -func TestCtlV3AuthDefrag(t *testing.T) { testCtl(t, authTestDefrag) } +// TestCtlV3AuthEndpointHealth https://github.com/etcd-io/etcd/pull/13774#discussion_r1189118815 is the blocker of migration to common/auth_test.go func TestCtlV3AuthEndpointHealth(t *testing.T) { testCtl(t, authTestEndpointHealth, withQuorum()) } + +// TestCtlV3AuthSnapshot TODO fill up common/maintenance_auth_test.go when Snapshot API is added in interfaces.Client func TestCtlV3AuthSnapshot(t *testing.T) { testCtl(t, authTestSnapshot) } func TestCtlV3AuthSnapshotJWT(t *testing.T) { testCtl(t, authTestSnapshot, withCfg(*e2e.NewConfigJWT())) } -func TestCtlV3AuthJWTExpire(t *testing.T) { - testCtl(t, authTestJWTExpire, withCfg(*e2e.NewConfigJWT())) -} -func TestCtlV3AuthRevisionConsistency(t *testing.T) { testCtl(t, authTestRevisionConsistency) } -func TestCtlV3AuthTestCacheReload(t *testing.T) { testCtl(t, authTestCacheReload) } -func TestCtlV3AuthLeaseTimeToLive(t *testing.T) { testCtl(t, authTestLeaseTimeToLive) } func authEnable(cx ctlCtx) error { // create root user with root role @@ -303,29 +296,6 @@ func authTestWatch(cx ctlCtx) { } -func authTestDefrag(cx ctlCtx) { - maintenanceInitKeys(cx) - - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // ordinary user cannot defrag - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3OnlineDefrag(cx); err == nil { - cx.t.Fatal("ordinary user should not be able to issue a defrag request") - } - - // root can defrag - cx.user, cx.pass = "root", "root" - if err := ctlV3OnlineDefrag(cx); err != nil { - cx.t.Fatal(err) - } -} - func authTestSnapshot(cx ctlCtx) { maintenanceInitKeys(cx) @@ -456,75 +426,6 @@ func authTestCertCNAndUsernameNoPassword(cx ctlCtx) { certCNAndUsername(cx, true) } -func authTestJWTExpire(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // try a granted key - if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil { - cx.t.Error(err) - } - - // wait an expiration of my JWT token - <-time.After(3 * time.Second) - - if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil { - cx.t.Error(err) - } -} - -func authTestRevisionConsistency(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - cx.user, cx.pass = "root", "root" - - // add user - if err := ctlV3User(cx, []string{"add", "test-user", "--interactive=false"}, "User test-user created", []string{"pass"}); err != nil { - cx.t.Fatal(err) - } - // delete the same user - if err := ctlV3User(cx, []string{"delete", "test-user"}, "User test-user deleted", []string{}); err != nil { - cx.t.Fatal(err) - } - - // get node0 auth revision - node0 := cx.epc.Procs[0] - endpoint := node0.EndpointsGRPC()[0] - cli, err := clientv3.New(clientv3.Config{Endpoints: []string{endpoint}, Username: cx.user, Password: cx.pass, DialTimeout: 3 * time.Second}) - if err != nil { - cx.t.Fatal(err) - } - defer cli.Close() - - sresp, err := cli.AuthStatus(context.TODO()) - if err != nil { - cx.t.Fatal(err) - } - oldAuthRevision := sresp.AuthRevision - - // restart the node - if err := node0.Restart(context.TODO()); err != nil { - cx.t.Fatal(err) - } - - // get node0 auth revision again - sresp, err = cli.AuthStatus(context.TODO()) - if err != nil { - cx.t.Fatal(err) - } - newAuthRevision := sresp.AuthRevision - - // assert AuthRevision equal - if newAuthRevision != oldAuthRevision { - cx.t.Fatalf("auth revison shouldn't change when restarting etcd, expected: %d, got: %d", oldAuthRevision, newAuthRevision) - } -} - func ctlV3EndpointHealth(cx ctlCtx) error { cmdArgs := append(cx.PrefixArgs(), "endpoint", "health") lines := make([]string, cx.epc.Cfg.ClusterSize) @@ -554,132 +455,3 @@ func ctlV3User(cx ctlCtx, args []string, expStr string, stdIn []string) error { _, err = proc.Expect(expStr) return err } - -// authTestCacheReload tests the permissions when a member restarts -func authTestCacheReload(cx ctlCtx) { - - authData := []struct { - user string - role string - pass string - }{ - { - user: "root", - role: "root", - pass: "123", - }, - { - user: "user0", - role: "role0", - pass: "123", - }, - } - - node0 := cx.epc.Procs[0] - endpoint := node0.EndpointsGRPC()[0] - - // create a client - c, err := clientv3.New(clientv3.Config{Endpoints: []string{endpoint}, DialTimeout: 3 * time.Second}) - if err != nil { - cx.t.Fatal(err) - } - defer c.Close() - - for _, authObj := range authData { - // add role - if _, err = c.RoleAdd(context.TODO(), authObj.role); err != nil { - cx.t.Fatal(err) - } - - // add user - if _, err = c.UserAdd(context.TODO(), authObj.user, authObj.pass); err != nil { - cx.t.Fatal(err) - } - - // grant role to user - if _, err = c.UserGrantRole(context.TODO(), authObj.user, authObj.role); err != nil { - cx.t.Fatal(err) - } - } - - // role grant permission to role0 - if _, err = c.RoleGrantPermission(context.TODO(), authData[1].role, "foo", "", clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { - cx.t.Fatal(err) - } - - // enable auth - if _, err = c.AuthEnable(context.TODO()); err != nil { - cx.t.Fatal(err) - } - - // create another client with ID:Password - c2, err := clientv3.New(clientv3.Config{Endpoints: []string{endpoint}, Username: authData[1].user, Password: authData[1].pass, DialTimeout: 3 * time.Second}) - if err != nil { - cx.t.Fatal(err) - } - defer c2.Close() - - // create foo since that is within the permission set - // expectation is to succeed - if _, err = c2.Put(context.TODO(), "foo", "bar"); err != nil { - cx.t.Fatal(err) - } - - // restart the node - if err := node0.Restart(context.TODO()); err != nil { - cx.t.Fatal(err) - } - - // nothing has changed, but it fails without refreshing cache after restart - if _, err = c2.Put(context.TODO(), "foo", "bar2"); err != nil { - cx.t.Fatal(err) - } -} - -func authTestLeaseTimeToLive(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - cx.user, cx.pass = "root", "root" - - authSetupTestUser(cx) - - cx.user = "test-user" - cx.pass = "pass" - - leaseID, err := ctlV3LeaseGrant(cx, 10) - if err != nil { - cx.t.Fatal(err) - } - - err = ctlV3Put(cx, "foo", "val", leaseID) - if err != nil { - cx.t.Fatal(err) - } - - err = ctlV3LeaseTimeToLive(cx, leaseID, true) - if err != nil { - cx.t.Fatal(err) - } - - cx.user = "root" - cx.pass = "root" - err = ctlV3Put(cx, "bar", "val", leaseID) - if err != nil { - cx.t.Fatal(err) - } - - cx.user = "test-user" - cx.pass = "pass" - // the lease is attached to bar, which test-user cannot access - err = ctlV3LeaseTimeToLive(cx, leaseID, true) - if err == nil { - cx.t.Fatal("test-user must not be able to access to the lease, because it's attached to the key bar") - } - - // without --keys, access should be allowed - err = ctlV3LeaseTimeToLive(cx, leaseID, false) - if err != nil { - cx.t.Fatal(err) - } -} diff --git a/tests/integration/v3_auth_test.go b/tests/integration/v3_auth_test.go index 89bbe2193ec..f6ffe871d71 100644 --- a/tests/integration/v3_auth_test.go +++ b/tests/integration/v3_auth_test.go @@ -420,83 +420,6 @@ func TestV3AuthOldRevConcurrent(t *testing.T) { wg.Wait() } -func TestV3AuthRestartMember(t *testing.T) { - integration.BeforeTest(t) - - // create a cluster with 1 member - clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1}) - defer clus.Terminate(t) - - // create a client - c, cerr := integration.NewClient(t, clientv3.Config{ - Endpoints: clus.Client(0).Endpoints(), - DialTimeout: 5 * time.Second, - }) - testutil.AssertNil(t, cerr) - defer c.Close() - - authData := []struct { - user string - role string - pass string - }{ - { - user: "root", - role: "root", - pass: "123", - }, - { - user: "user0", - role: "role0", - pass: "123", - }, - } - - for _, authObj := range authData { - // add a role - _, err := c.RoleAdd(context.TODO(), authObj.role) - testutil.AssertNil(t, err) - // add a user - _, err = c.UserAdd(context.TODO(), authObj.user, authObj.pass) - testutil.AssertNil(t, err) - // grant role to user - _, err = c.UserGrantRole(context.TODO(), authObj.user, authObj.role) - testutil.AssertNil(t, err) - } - - // role grant permission to role0 - _, err := c.RoleGrantPermission(context.TODO(), authData[1].role, "foo", "", clientv3.PermissionType(clientv3.PermReadWrite)) - testutil.AssertNil(t, err) - - // enable auth - _, err = c.AuthEnable(context.TODO()) - testutil.AssertNil(t, err) - - // create another client with ID:Password - c2, cerr := integration.NewClient(t, clientv3.Config{ - Endpoints: clus.Client(0).Endpoints(), - DialTimeout: 5 * time.Second, - Username: authData[1].user, - Password: authData[1].pass, - }) - testutil.AssertNil(t, cerr) - defer c2.Close() - - // create foo since that is within the permission set - // expectation is to succeed - _, err = c2.Put(context.TODO(), "foo", "bar") - testutil.AssertNil(t, err) - - clus.Members[0].Stop(t) - err = clus.Members[0].Restart(t) - testutil.AssertNil(t, err) - integration.WaitClientV3WithKey(t, c2.KV, "foo") - - // nothing has changed, but it fails without refreshing cache after restart - _, err = c2.Put(context.TODO(), "foo", "bar2") - testutil.AssertNil(t, err) -} - func TestV3AuthWatchAndTokenExpire(t *testing.T) { integration.BeforeTest(t) clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1, AuthTokenTTL: 3}) @@ -583,86 +506,3 @@ func TestV3AuthWatchErrorAndWatchId0(t *testing.T) { <-watchEndCh } - -func TestV3AuthWithLeaseTimeToLive(t *testing.T) { - integration.BeforeTest(t) - clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1}) - defer clus.Terminate(t) - - users := []user{ - { - name: "user1", - password: "user1-123", - role: "role1", - key: "k1", - end: "k3", - }, - { - name: "user2", - password: "user2-123", - role: "role2", - key: "k2", - end: "k4", - }, - } - authSetupUsers(t, integration.ToGRPC(clus.Client(0)).Auth, users) - - authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth) - - user1c, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "user1", Password: "user1-123"}) - if cerr != nil { - t.Fatal(cerr) - } - defer user1c.Close() - - user2c, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "user2", Password: "user2-123"}) - if cerr != nil { - t.Fatal(cerr) - } - defer user2c.Close() - - leaseResp, err := user1c.Grant(context.TODO(), 90) - if err != nil { - t.Fatal(err) - } - leaseID := leaseResp.ID - _, err = user1c.Put(context.TODO(), "k1", "val", clientv3.WithLease(leaseID)) - if err != nil { - t.Fatal(err) - } - // k2 can be accessed from both user1 and user2 - _, err = user1c.Put(context.TODO(), "k2", "val", clientv3.WithLease(leaseID)) - if err != nil { - t.Fatal(err) - } - - _, err = user1c.TimeToLive(context.TODO(), leaseID) - if err != nil { - t.Fatal(err) - } - - _, err = user2c.TimeToLive(context.TODO(), leaseID) - if err != nil { - t.Fatal(err) - } - - _, err = user2c.TimeToLive(context.TODO(), leaseID, clientv3.WithAttachedKeys()) - if err == nil { - t.Fatal("timetolive from user2 should be failed with permission denied") - } - - rootc, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "root", Password: "123"}) - if cerr != nil { - t.Fatal(cerr) - } - defer rootc.Close() - - if _, err := rootc.RoleRevokePermission(context.TODO(), "role1", "k1", "k3"); err != nil { - t.Fatal(err) - } - - _, err = user1c.TimeToLive(context.TODO(), leaseID, clientv3.WithAttachedKeys()) - if err == nil { - t.Fatal("timetolive from user2 should be failed with permission denied") - } -}