Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Broken relay of objects prepared on the client with session #2460

Closed
cthulhu-rider opened this issue Jul 26, 2023 · 0 comments · Fixed by #2461
Closed

Broken relay of objects prepared on the client with session #2460

cthulhu-rider opened this issue Jul 26, 2023 · 0 comments · Fixed by #2461
Assignees
Labels
bug Something isn't working neofs-storage Storage node application issues

Comments

@cthulhu-rider
Copy link
Contributor

Context

object may be prepared (formed and signed) on gateway side acting on behalf of the particular user within opened session. When gateway streams ready objects, it attaches session token to the request meta header as a power of attorney. The system must interpret such an operation as being performed by the session author (user). In particular, access control is carried out specifically for the user and not the gateway.

when out-of-container node receives PUT request, it forwards the stream into in-container nodes that must store the object. At the same time, the intermediate node supplements the request with its own metadata, retaining the original. Thus, the stream received by the container nodes should be perceived as essentially sent by the user.

Steps to reproduce

i tested described scenario with currently WIP implementation of object Slicer, and got denial of service

  1. run NeoFS Dev Env. WIth make prepare.ir it fills services/chain/node-wallet.json account with GAS
  2. create container create a container so that at least one of the nodes is outside it
CLI commands
$ cat cli.yaml
rpc-endpoint: s04.neofs.devenv:8080
wallet: ../devenv/services/chain/node-wallet.json
password: 'one'
$
$ neofs-cli -c cli.yaml container create --policy 'REP 1 CBF 1' --await
container ID: 2XSyCDKH1p6XrYot781sgp8kVDk3uePT2k7cdvqxDuQw
awaiting...
container has been persisted on sidechain
$
$ neofs-cli -c cli.yaml container nodes --cid 2XSyCDKH1p6XrYot781sgp8kVDk3uePT2k7cdvqxDuQw
Descriptor #1, REP 1:
	Node 1: 02ac920cd7df0b61b289072e6b946e2da4e1a31b9ab1c621bb475e30fa4ab102c3 ONLINE /dns4/s03.neofs.devenv/tcp/8080
  1. select any node other than in container nodes output, i chose s04
  2. slice data into objects and try to put them into the container. At the moment, no utility can do this, so I sketched:
Test script
type objectWriter struct {
	context context.Context
	client  *client.Client
	session *session.Object
}

func (x *objectWriter) InitDataStream(header object.Object, signer neofscrypto.Signer) (io.Writer, error) {
	var prm client.PrmObjectPutInit
	prm.WithinSession(*x.session)

	stream, err := x.client.ObjectPutInit(x.context, header, signer, prm)
	if err != nil {
		return nil, fmt.Errorf("init object stream: %w", err)
	}

	return &payloadWriter{
		stream: stream,
	}, nil
}

type payloadWriter struct {
	stream *client.ObjectWriter
}

func (x *payloadWriter) Write(p []byte) (int, error) {
	if !x.stream.WritePayloadChunk(p) {
		return 0, x.Close()
	}

	return len(p), nil
}

func (x *payloadWriter) Close() error {
	_, err := x.stream.Close()
	if err != nil {
		return err
	}

	return nil
}

func TestSlicerRelay(t *testing.T) {
	const endpoint = "s04.neofs.devenv:8080"
	const strCnr = "2XSyCDKH1p6XrYot781sgp8kVDk3uePT2k7cdvqxDuQw"
	payload := strings.NewReader("Hello, world!")
	ctx := context.Background()

	gateAcc, err := wallet.NewAccount()
	require.NoError(t, err)

	usrWallet, err := wallet.NewWalletFromFile("/home/ll/projects/neo/devenv/services/chain/node-wallet.json")
	require.NoError(t, err)

	usrAcc := usrWallet.Accounts[0]

	err = usrAcc.Decrypt("one", keys.NEP2ScryptParams())
	require.NoError(t, err)

	var gateSigner neofscrypto.Signer = neofsecdsa.SignerRFC6979(gateAcc.PrivateKey().PrivateKey)
	var usrSigner neofscrypto.Signer = neofsecdsa.Signer(usrAcc.PrivateKey().PrivateKey)

	var cnr cid.ID
	require.NoError(t, cnr.DecodeString(strCnr))

	var prmInit client.PrmInit

	c, err := client.New(prmInit)
	require.NoError(t, err)

	var prmDial client.PrmDial
	prmDial.SetServerURI(endpoint)

	require.NoError(t, c.Dial(prmDial))

	t.Cleanup(func() {
		_ = c.Close()
	})

	netInfo, err := c.NetworkInfo(ctx, client.PrmNetworkInfo{})
	require.NoError(t, err)

	var opts slicer.Options
	opts.SetObjectPayloadLimit(netInfo.MaxObjectSize())
	opts.SetCurrentNeoFSEpoch(netInfo.CurrentEpoch())
	if !netInfo.HomomorphicHashingDisabled() {
		opts.CalculateHomomorphicChecksum()
	}

	var sessionToken session.Object
	sessionToken.SetAuthKey(gateSigner.Public())
	sessionToken.SetID(uuid.New())
	sessionToken.SetIat(netInfo.CurrentEpoch())
	sessionToken.SetNbf(netInfo.CurrentEpoch())
	sessionToken.SetExp(netInfo.CurrentEpoch() + 100) // or particular exp value
	sessionToken.BindContainer(cnr)
	sessionToken.ForVerb(session.VerbObjectPut)

	require.NoError(t, sessionToken.Sign(usrSigner))

	objWriter := &objectWriter{
		context: ctx,
		client:  c,
		session: &sessionToken,
	}

	_slicer := slicer.NewSession(gateSigner, cnr, sessionToken, objWriter, opts)

	id, err := _slicer.Slice(payload, object.AttributeFileName, "alex-testing-something")
	require.NoError(t, err)
	log.Println("object:", id)
}

Script failed:

Error:      	Received unexpected error:
        	            	write single root object: finish object stream: status: code = 1024 message = incomplete object PUT by placement: closing the stream failed: rpc error: code = Unknown desc = (*response.ClientMessageStreamer) could not send the request: status: code = 2048 message = access to object operation denied

Log message in out-of-container node:

error	util/log.go:11	object service error	{"node": "/dns4/s03.neofs.devenv/tcp/8080", "request": "PUT", "error": "closing the stream failed: rpc error: code = Unknown desc = (*response.ClientMessageStreamer) could not send the request: status: code = 2048 message = access to object operation denied"}

Conclusion

according to the log, out-of-container node did not receive permission to write the object when forwarding the request.

i researched and found that when checking write permissions on an object, the node only considers the session token attached by the last client in the request chain, but ignores the original one. This is a bug, the node must process the original request.

Versions

  • neofs-dev-env 8fe3a55d8b1ccd079b595ce8d08a3674b7f993c5
  • neofs-sdk-go 1300860c8ce5cd37e20528dc591e190836891fa9
$ neofs-cli --version
NeoFS CLI
Version: v0.37.0
GoVersion: go1.20.3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working neofs-storage Storage node application issues
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant