github.com/lfch/etcd-io/tests/v3@v3.0.0-20221004140520-eac99acd3e9d/integration/v3_auth_test.go (about)

     1  // Copyright 2017 The etcd Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package integration
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sync"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/lfch/etcd-io/api/v3/authpb"
    25  	pb "github.com/lfch/etcd-io/api/v3/etcdserverpb"
    26  	"github.com/lfch/etcd-io/api/v3/v3rpc/rpctypes"
    27  	"github.com/lfch/etcd-io/client/pkg/v3/testutil"
    28  	"github.com/lfch/etcd-io/client/v3"
    29  	"github.com/lfch/etcd-io/tests/v3/framework/integration"
    30  )
    31  
    32  // TestV3AuthEmptyUserGet ensures that a get with an empty user will return an empty user error.
    33  func TestV3AuthEmptyUserGet(t *testing.T) {
    34  	integration.BeforeTest(t)
    35  	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
    36  	defer clus.Terminate(t)
    37  
    38  	ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
    39  	defer cancel()
    40  
    41  	api := integration.ToGRPC(clus.Client(0))
    42  	authSetupRoot(t, api.Auth)
    43  
    44  	_, err := api.KV.Range(ctx, &pb.RangeRequest{Key: []byte("abc")})
    45  	if !eqErrGRPC(err, rpctypes.ErrUserEmpty) {
    46  		t.Fatalf("got %v, expected %v", err, rpctypes.ErrUserEmpty)
    47  	}
    48  }
    49  
    50  // TestV3AuthEmptyUserPut ensures that a put with an empty user will return an empty user error,
    51  // and the consistent_index should be moved forward even the apply-->Put fails.
    52  func TestV3AuthEmptyUserPut(t *testing.T) {
    53  	integration.BeforeTest(t)
    54  	clus := integration.NewCluster(t, &integration.ClusterConfig{
    55  		Size:          1,
    56  		SnapshotCount: 3,
    57  	})
    58  	defer clus.Terminate(t)
    59  
    60  	ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
    61  	defer cancel()
    62  
    63  	api := integration.ToGRPC(clus.Client(0))
    64  	authSetupRoot(t, api.Auth)
    65  
    66  	// The SnapshotCount is 3, so there must be at least 3 new snapshot files being created.
    67  	// The VERIFY logic will check whether the consistent_index >= last snapshot index on
    68  	// cluster terminating.
    69  	for i := 0; i < 10; i++ {
    70  		_, err := api.KV.Put(ctx, &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
    71  		if !eqErrGRPC(err, rpctypes.ErrUserEmpty) {
    72  			t.Fatalf("got %v, expected %v", err, rpctypes.ErrUserEmpty)
    73  		}
    74  	}
    75  }
    76  
    77  // TestV3AuthTokenWithDisable tests that auth won't crash if
    78  // given a valid token when authentication is disabled
    79  func TestV3AuthTokenWithDisable(t *testing.T) {
    80  	integration.BeforeTest(t)
    81  	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
    82  	defer clus.Terminate(t)
    83  
    84  	authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth)
    85  
    86  	c, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "root", Password: "123"})
    87  	if cerr != nil {
    88  		t.Fatal(cerr)
    89  	}
    90  	defer c.Close()
    91  
    92  	rctx, cancel := context.WithCancel(context.TODO())
    93  	donec := make(chan struct{})
    94  	go func() {
    95  		defer close(donec)
    96  		for rctx.Err() == nil {
    97  			c.Put(rctx, "abc", "def")
    98  		}
    99  	}()
   100  
   101  	time.Sleep(10 * time.Millisecond)
   102  	if _, err := c.AuthDisable(context.TODO()); err != nil {
   103  		t.Fatal(err)
   104  	}
   105  	time.Sleep(10 * time.Millisecond)
   106  
   107  	cancel()
   108  	<-donec
   109  }
   110  
   111  func TestV3AuthRevision(t *testing.T) {
   112  	integration.BeforeTest(t)
   113  	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
   114  	defer clus.Terminate(t)
   115  
   116  	api := integration.ToGRPC(clus.Client(0))
   117  
   118  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   119  	presp, perr := api.KV.Put(ctx, &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
   120  	cancel()
   121  	if perr != nil {
   122  		t.Fatal(perr)
   123  	}
   124  	rev := presp.Header.Revision
   125  
   126  	ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
   127  	aresp, aerr := api.Auth.UserAdd(ctx, &pb.AuthUserAddRequest{Name: "root", Password: "123", Options: &authpb.UserAddOptions{NoPassword: false}})
   128  	cancel()
   129  	if aerr != nil {
   130  		t.Fatal(aerr)
   131  	}
   132  	if aresp.Header.Revision != rev {
   133  		t.Fatalf("revision expected %d, got %d", rev, aresp.Header.Revision)
   134  	}
   135  }
   136  
   137  // TestV3AuthWithLeaseRevokeWithRoot ensures that granted leases
   138  // with root user be revoked after TTL.
   139  func TestV3AuthWithLeaseRevokeWithRoot(t *testing.T) {
   140  	testV3AuthWithLeaseRevokeWithRoot(t, integration.ClusterConfig{Size: 1})
   141  }
   142  
   143  // TestV3AuthWithLeaseRevokeWithRootJWT creates a lease with a JWT-token enabled cluster.
   144  // And tests if server is able to revoke expiry lease item.
   145  func TestV3AuthWithLeaseRevokeWithRootJWT(t *testing.T) {
   146  	testV3AuthWithLeaseRevokeWithRoot(t, integration.ClusterConfig{Size: 1, AuthToken: integration.DefaultTokenJWT})
   147  }
   148  
   149  func testV3AuthWithLeaseRevokeWithRoot(t *testing.T, ccfg integration.ClusterConfig) {
   150  	integration.BeforeTest(t)
   151  
   152  	clus := integration.NewCluster(t, &ccfg)
   153  	defer clus.Terminate(t)
   154  
   155  	api := integration.ToGRPC(clus.Client(0))
   156  	authSetupRoot(t, api.Auth)
   157  
   158  	rootc, cerr := integration.NewClient(t, clientv3.Config{
   159  		Endpoints: clus.Client(0).Endpoints(),
   160  		Username:  "root",
   161  		Password:  "123",
   162  	})
   163  	if cerr != nil {
   164  		t.Fatal(cerr)
   165  	}
   166  	defer rootc.Close()
   167  
   168  	leaseResp, err := rootc.Grant(context.TODO(), 2)
   169  	if err != nil {
   170  		t.Fatal(err)
   171  	}
   172  	leaseID := leaseResp.ID
   173  
   174  	if _, err = rootc.Put(context.TODO(), "foo", "bar", clientv3.WithLease(leaseID)); err != nil {
   175  		t.Fatal(err)
   176  	}
   177  
   178  	// wait for lease expire
   179  	time.Sleep(3 * time.Second)
   180  
   181  	tresp, terr := api.Lease.LeaseTimeToLive(
   182  		context.TODO(),
   183  		&pb.LeaseTimeToLiveRequest{
   184  			ID:   int64(leaseID),
   185  			Keys: true,
   186  		},
   187  	)
   188  	if terr != nil {
   189  		t.Error(terr)
   190  	}
   191  	if len(tresp.Keys) > 0 || tresp.GrantedTTL != 0 {
   192  		t.Errorf("lease %016x should have been revoked, got %+v", leaseID, tresp)
   193  	}
   194  	if tresp.TTL != -1 {
   195  		t.Errorf("lease %016x should have been expired, got %+v", leaseID, tresp)
   196  	}
   197  }
   198  
   199  type user struct {
   200  	name     string
   201  	password string
   202  	role     string
   203  	key      string
   204  	end      string
   205  }
   206  
   207  func TestV3AuthWithLeaseRevoke(t *testing.T) {
   208  	integration.BeforeTest(t)
   209  	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
   210  	defer clus.Terminate(t)
   211  
   212  	users := []user{
   213  		{
   214  			name:     "user1",
   215  			password: "user1-123",
   216  			role:     "role1",
   217  			key:      "k1",
   218  			end:      "k2",
   219  		},
   220  	}
   221  	authSetupUsers(t, integration.ToGRPC(clus.Client(0)).Auth, users)
   222  
   223  	authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth)
   224  
   225  	rootc, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "root", Password: "123"})
   226  	if cerr != nil {
   227  		t.Fatal(cerr)
   228  	}
   229  	defer rootc.Close()
   230  
   231  	leaseResp, err := rootc.Grant(context.TODO(), 90)
   232  	if err != nil {
   233  		t.Fatal(err)
   234  	}
   235  	leaseID := leaseResp.ID
   236  	// permission of k3 isn't granted to user1
   237  	_, err = rootc.Put(context.TODO(), "k3", "val", clientv3.WithLease(leaseID))
   238  	if err != nil {
   239  		t.Fatal(err)
   240  	}
   241  
   242  	userc, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "user1", Password: "user1-123"})
   243  	if cerr != nil {
   244  		t.Fatal(cerr)
   245  	}
   246  	defer userc.Close()
   247  	_, err = userc.Revoke(context.TODO(), leaseID)
   248  	if err == nil {
   249  		t.Fatal("revoking from user1 should be failed with permission denied")
   250  	}
   251  }
   252  
   253  func TestV3AuthWithLeaseAttach(t *testing.T) {
   254  	integration.BeforeTest(t)
   255  	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
   256  	defer clus.Terminate(t)
   257  
   258  	users := []user{
   259  		{
   260  			name:     "user1",
   261  			password: "user1-123",
   262  			role:     "role1",
   263  			key:      "k1",
   264  			end:      "k3",
   265  		},
   266  		{
   267  			name:     "user2",
   268  			password: "user2-123",
   269  			role:     "role2",
   270  			key:      "k2",
   271  			end:      "k4",
   272  		},
   273  	}
   274  	authSetupUsers(t, integration.ToGRPC(clus.Client(0)).Auth, users)
   275  
   276  	authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth)
   277  
   278  	user1c, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "user1", Password: "user1-123"})
   279  	if cerr != nil {
   280  		t.Fatal(cerr)
   281  	}
   282  	defer user1c.Close()
   283  
   284  	user2c, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "user2", Password: "user2-123"})
   285  	if cerr != nil {
   286  		t.Fatal(cerr)
   287  	}
   288  	defer user2c.Close()
   289  
   290  	leaseResp, err := user1c.Grant(context.TODO(), 90)
   291  	if err != nil {
   292  		t.Fatal(err)
   293  	}
   294  	leaseID := leaseResp.ID
   295  	// permission of k2 is also granted to user2
   296  	_, err = user1c.Put(context.TODO(), "k2", "val", clientv3.WithLease(leaseID))
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  
   301  	_, err = user2c.Revoke(context.TODO(), leaseID)
   302  	if err != nil {
   303  		t.Fatal(err)
   304  	}
   305  
   306  	leaseResp, err = user1c.Grant(context.TODO(), 90)
   307  	if err != nil {
   308  		t.Fatal(err)
   309  	}
   310  	leaseID = leaseResp.ID
   311  	// permission of k1 isn't granted to user2
   312  	_, err = user1c.Put(context.TODO(), "k1", "val", clientv3.WithLease(leaseID))
   313  	if err != nil {
   314  		t.Fatal(err)
   315  	}
   316  
   317  	_, err = user2c.Revoke(context.TODO(), leaseID)
   318  	if err == nil {
   319  		t.Fatal("revoking from user2 should be failed with permission denied")
   320  	}
   321  }
   322  
   323  func authSetupUsers(t *testing.T, auth pb.AuthClient, users []user) {
   324  	for _, user := range users {
   325  		if _, err := auth.UserAdd(context.TODO(), &pb.AuthUserAddRequest{Name: user.name, Password: user.password, Options: &authpb.UserAddOptions{NoPassword: false}}); err != nil {
   326  			t.Fatal(err)
   327  		}
   328  		if _, err := auth.RoleAdd(context.TODO(), &pb.AuthRoleAddRequest{Name: user.role}); err != nil {
   329  			t.Fatal(err)
   330  		}
   331  		if _, err := auth.UserGrantRole(context.TODO(), &pb.AuthUserGrantRoleRequest{User: user.name, Role: user.role}); err != nil {
   332  			t.Fatal(err)
   333  		}
   334  
   335  		if len(user.key) == 0 {
   336  			continue
   337  		}
   338  
   339  		perm := &authpb.Permission{
   340  			PermType: authpb.READWRITE,
   341  			Key:      []byte(user.key),
   342  			RangeEnd: []byte(user.end),
   343  		}
   344  		if _, err := auth.RoleGrantPermission(context.TODO(), &pb.AuthRoleGrantPermissionRequest{Name: user.role, Perm: perm}); err != nil {
   345  			t.Fatal(err)
   346  		}
   347  	}
   348  }
   349  
   350  func authSetupRoot(t *testing.T, auth pb.AuthClient) {
   351  	root := []user{
   352  		{
   353  			name:     "root",
   354  			password: "123",
   355  			role:     "root",
   356  			key:      "",
   357  		},
   358  	}
   359  	authSetupUsers(t, auth, root)
   360  	if _, err := auth.AuthEnable(context.TODO(), &pb.AuthEnableRequest{}); err != nil {
   361  		t.Fatal(err)
   362  	}
   363  }
   364  
   365  func TestV3AuthNonAuthorizedRPCs(t *testing.T) {
   366  	integration.BeforeTest(t)
   367  	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
   368  	defer clus.Terminate(t)
   369  
   370  	nonAuthedKV := clus.Client(0).KV
   371  
   372  	key := "foo"
   373  	val := "bar"
   374  	_, err := nonAuthedKV.Put(context.TODO(), key, val)
   375  	if err != nil {
   376  		t.Fatalf("couldn't put key (%v)", err)
   377  	}
   378  
   379  	authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth)
   380  
   381  	respput, err := nonAuthedKV.Put(context.TODO(), key, val)
   382  	if !eqErrGRPC(err, rpctypes.ErrGRPCUserEmpty) {
   383  		t.Fatalf("could put key (%v), it should cause an error of permission denied", respput)
   384  	}
   385  }
   386  
   387  func TestV3AuthOldRevConcurrent(t *testing.T) {
   388  	integration.BeforeTest(t)
   389  	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
   390  	defer clus.Terminate(t)
   391  
   392  	authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth)
   393  
   394  	c, cerr := integration.NewClient(t, clientv3.Config{
   395  		Endpoints:   clus.Client(0).Endpoints(),
   396  		DialTimeout: 5 * time.Second,
   397  		Username:    "root",
   398  		Password:    "123",
   399  	})
   400  	testutil.AssertNil(t, cerr)
   401  	defer c.Close()
   402  
   403  	var wg sync.WaitGroup
   404  	f := func(i int) {
   405  		defer wg.Done()
   406  		role, user := fmt.Sprintf("test-role-%d", i), fmt.Sprintf("test-user-%d", i)
   407  		_, err := c.RoleAdd(context.TODO(), role)
   408  		testutil.AssertNil(t, err)
   409  		_, err = c.RoleGrantPermission(context.TODO(), role, "", clientv3.GetPrefixRangeEnd(""), clientv3.PermissionType(clientv3.PermReadWrite))
   410  		testutil.AssertNil(t, err)
   411  		_, err = c.UserAdd(context.TODO(), user, "123")
   412  		testutil.AssertNil(t, err)
   413  		_, err = c.Put(context.TODO(), "a", "b")
   414  		testutil.AssertNil(t, err)
   415  	}
   416  	// needs concurrency to trigger
   417  	numRoles := 2
   418  	wg.Add(numRoles)
   419  	for i := 0; i < numRoles; i++ {
   420  		go f(i)
   421  	}
   422  	wg.Wait()
   423  }
   424  
   425  func TestV3AuthRestartMember(t *testing.T) {
   426  	integration.BeforeTest(t)
   427  
   428  	// create a cluster with 1 member
   429  	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
   430  	defer clus.Terminate(t)
   431  
   432  	// create a client
   433  	c, cerr := integration.NewClient(t, clientv3.Config{
   434  		Endpoints:   clus.Client(0).Endpoints(),
   435  		DialTimeout: 5 * time.Second,
   436  	})
   437  	testutil.AssertNil(t, cerr)
   438  	defer c.Close()
   439  
   440  	authData := []struct {
   441  		user string
   442  		role string
   443  		pass string
   444  	}{
   445  		{
   446  			user: "root",
   447  			role: "root",
   448  			pass: "123",
   449  		},
   450  		{
   451  			user: "user0",
   452  			role: "role0",
   453  			pass: "123",
   454  		},
   455  	}
   456  
   457  	for _, authObj := range authData {
   458  		// add a role
   459  		_, err := c.RoleAdd(context.TODO(), authObj.role)
   460  		testutil.AssertNil(t, err)
   461  		// add a user
   462  		_, err = c.UserAdd(context.TODO(), authObj.user, authObj.pass)
   463  		testutil.AssertNil(t, err)
   464  		// grant role to user
   465  		_, err = c.UserGrantRole(context.TODO(), authObj.user, authObj.role)
   466  		testutil.AssertNil(t, err)
   467  	}
   468  
   469  	// role grant permission to role0
   470  	_, err := c.RoleGrantPermission(context.TODO(), authData[1].role, "foo", "", clientv3.PermissionType(clientv3.PermReadWrite))
   471  	testutil.AssertNil(t, err)
   472  
   473  	// enable auth
   474  	_, err = c.AuthEnable(context.TODO())
   475  	testutil.AssertNil(t, err)
   476  
   477  	// create another client with ID:Password
   478  	c2, cerr := integration.NewClient(t, clientv3.Config{
   479  		Endpoints:   clus.Client(0).Endpoints(),
   480  		DialTimeout: 5 * time.Second,
   481  		Username:    authData[1].user,
   482  		Password:    authData[1].pass,
   483  	})
   484  	testutil.AssertNil(t, cerr)
   485  	defer c2.Close()
   486  
   487  	// create foo since that is within the permission set
   488  	// expectation is to succeed
   489  	_, err = c2.Put(context.TODO(), "foo", "bar")
   490  	testutil.AssertNil(t, err)
   491  
   492  	clus.Members[0].Stop(t)
   493  	err = clus.Members[0].Restart(t)
   494  	testutil.AssertNil(t, err)
   495  	integration.WaitClientV3WithKey(t, c2.KV, "foo")
   496  
   497  	// nothing has changed, but it fails without refreshing cache after restart
   498  	_, err = c2.Put(context.TODO(), "foo", "bar2")
   499  	testutil.AssertNil(t, err)
   500  }
   501  
   502  func TestV3AuthWatchAndTokenExpire(t *testing.T) {
   503  	integration.BeforeTest(t)
   504  	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1, AuthTokenTTL: 3})
   505  	defer clus.Terminate(t)
   506  
   507  	ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
   508  	defer cancel()
   509  
   510  	authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth)
   511  
   512  	c, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "root", Password: "123"})
   513  	if cerr != nil {
   514  		t.Fatal(cerr)
   515  	}
   516  	defer c.Close()
   517  
   518  	_, err := c.Put(ctx, "key", "val")
   519  	if err != nil {
   520  		t.Fatalf("Unexpected error from Put: %v", err)
   521  	}
   522  
   523  	// The first watch gets a valid auth token through watcher.newWatcherGrpcStream()
   524  	// We should discard the first one by waiting TTL after the first watch.
   525  	wChan := c.Watch(ctx, "key", clientv3.WithRev(1))
   526  	watchResponse := <-wChan
   527  
   528  	time.Sleep(5 * time.Second)
   529  
   530  	wChan = c.Watch(ctx, "key", clientv3.WithRev(1))
   531  	watchResponse = <-wChan
   532  	testutil.AssertNil(t, watchResponse.Err())
   533  }
   534  
   535  func TestV3AuthWatchErrorAndWatchId0(t *testing.T) {
   536  	integration.BeforeTest(t)
   537  	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
   538  	defer clus.Terminate(t)
   539  
   540  	ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
   541  	defer cancel()
   542  
   543  	users := []user{
   544  		{
   545  			name:     "user1",
   546  			password: "user1-123",
   547  			role:     "role1",
   548  			key:      "k1",
   549  			end:      "k2",
   550  		},
   551  	}
   552  	authSetupUsers(t, integration.ToGRPC(clus.Client(0)).Auth, users)
   553  
   554  	authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth)
   555  
   556  	c, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "user1", Password: "user1-123"})
   557  	if cerr != nil {
   558  		t.Fatal(cerr)
   559  	}
   560  	defer c.Close()
   561  
   562  	watchStartCh, watchEndCh := make(chan interface{}), make(chan interface{})
   563  
   564  	go func() {
   565  		wChan := c.Watch(ctx, "k1", clientv3.WithRev(1))
   566  		watchStartCh <- struct{}{}
   567  		watchResponse := <-wChan
   568  		t.Logf("watch response from k1: %v", watchResponse)
   569  		testutil.AssertTrue(t, len(watchResponse.Events) != 0)
   570  		watchEndCh <- struct{}{}
   571  	}()
   572  
   573  	// Chan for making sure that the above goroutine invokes Watch()
   574  	// So the above Watch() can get watch ID = 0
   575  	<-watchStartCh
   576  
   577  	wChan := c.Watch(ctx, "non-allowed-key", clientv3.WithRev(1))
   578  	watchResponse := <-wChan
   579  	testutil.AssertNotNil(t, watchResponse.Err()) // permission denied
   580  
   581  	_, err := c.Put(ctx, "k1", "val")
   582  	if err != nil {
   583  		t.Fatalf("Unexpected error from Put: %v", err)
   584  	}
   585  
   586  	<-watchEndCh
   587  }