diff --git a/pkg/acl/types.go b/pkg/acl/types.go new file mode 100644 index 0000000..8bae41c --- /dev/null +++ b/pkg/acl/types.go @@ -0,0 +1,7 @@ +package acl + +const ( + PublicBasicRule = 0x1FFFFFFF + PrivateBasicRule = 0x18888888 + ReadOnlyBasicRule = 0x1FFF88FF +) diff --git a/pkg/client/container.go b/pkg/client/container.go new file mode 100644 index 0000000..f121b7f --- /dev/null +++ b/pkg/client/container.go @@ -0,0 +1,319 @@ +package client + +import ( + "context" + + "github.com/nspcc-dev/neofs-api-go/pkg/container" + "github.com/nspcc-dev/neofs-api-go/pkg/refs" + "github.com/nspcc-dev/neofs-api-go/util/signature" + "github.com/nspcc-dev/neofs-api-go/v2/client" + v2container "github.com/nspcc-dev/neofs-api-go/v2/container" + v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" + v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature" + "github.com/pkg/errors" +) + +func (c Client) PutContainer(ctx context.Context, cnr *container.Container, opts ...CallOption) (refs.ContainerID, error) { + switch c.remoteNode.Version.Major { + case 2: + return c.putContainerV2(ctx, cnr, opts...) + default: + return refs.ContainerID{}, unsupportedProtocolErr + } +} + +func (c Client) GetContainer(ctx context.Context, id refs.ContainerID, opts ...CallOption) (*container.Container, error) { + switch c.remoteNode.Version.Major { + case 2: + return c.getContainerV2(ctx, id, opts...) + default: + return nil, unsupportedProtocolErr + } +} + +func (c Client) ListContainers(ctx context.Context, owner refs.NEO3Wallet, opts ...CallOption) ([]refs.ContainerID, error) { + switch c.remoteNode.Version.Major { + case 2: + return c.listContainerV2(ctx, owner, opts...) + default: + return nil, unsupportedProtocolErr + } +} + +func (c Client) ListSelfContainers(ctx context.Context, opts ...CallOption) ([]refs.ContainerID, error) { + owner, err := refs.NEO3WalletFromPublicKey(&c.key.PublicKey) + if err != nil { + return nil, err + } + + return c.ListContainers(ctx, owner, opts...) +} + +func (c Client) DeleteContainer(ctx context.Context, id refs.ContainerID, opts ...CallOption) error { + switch c.remoteNode.Version.Major { + case 2: + panic("not implemented") + default: + return unsupportedProtocolErr + } +} + +// todo: func (c Client) GetExtendedACL +// todo: func (c Client) SetExtendedACL + +func (c Client) putContainerV2(ctx context.Context, cnr *container.Container, opts ...CallOption) (refs.ContainerID, error) { + // apply all available options + callOptions := defaultCallOptions() + for i := range opts { + opts[i].apply(&callOptions) + } + + cid := refs.ContainerID{} + + // set transport version + cnr.SetVersion(c.remoteNode.Version.ToV2Version()) + + // if container owner is not set, then use client key as owner + if cnr.GetOwnerID() == nil { + owner, err := refs.NEO3WalletFromPublicKey(&c.key.PublicKey) + if err != nil { + return cid, err + } + + v2Owner := new(v2refs.OwnerID) + v2Owner.SetValue(owner[:]) + cnr.SetOwnerID(v2Owner) + } + + reqBody := new(v2container.PutRequestBody) + reqBody.SetContainer(&cnr.Container) + + // sign container + signWrapper := v2signature.StableMarshalerWrapper{SM: reqBody.GetContainer()} + err := signature.SignDataWithHandler(c.key, signWrapper, func(key []byte, sig []byte) { + containerSignature := new(v2refs.Signature) + containerSignature.SetKey(key) + containerSignature.SetSign(sig) + reqBody.SetSignature(containerSignature) + }, signature.SignWithRFC6979()) + if err != nil { + return cid, err + } + + req := new(v2container.PutRequest) + req.SetBody(reqBody) + req.SetMetaHeader(v2MetaHeaderFromOpts(callOptions)) + + err = v2signature.SignServiceMessage(c.key, req) + if err != nil { + return cid, err + } + + switch c.remoteNode.Protocol { + case GRPC: + cli, err := v2ContainerClientFromOptions(c.opts) + if err != nil { + return cid, errors.Wrap(err, "can't create grpc client") + } + + resp, err := cli.Put(ctx, req) + if err != nil { + return cid, errors.Wrap(err, "transport error") + } + + err = v2signature.VerifyServiceMessage(resp) + if err != nil { + return cid, errors.Wrap(err, "can't verify response message") + } + + copy(cid[:], resp.GetBody().GetContainerID().GetValue()) + + return cid, nil + default: + return cid, unsupportedProtocolErr + } +} + +func (c Client) getContainerV2(ctx context.Context, id refs.ContainerID, opts ...CallOption) (*container.Container, error) { + // apply all available options + callOptions := defaultCallOptions() + for i := range opts { + opts[i].apply(&callOptions) + } + + cid := new(v2refs.ContainerID) + cid.SetValue(id[:]) + + reqBody := new(v2container.GetRequestBody) + reqBody.SetContainerID(cid) + + req := new(v2container.GetRequest) + req.SetBody(reqBody) + req.SetMetaHeader(v2MetaHeaderFromOpts(callOptions)) + + err := v2signature.SignServiceMessage(c.key, req) + if err != nil { + return nil, err + } + + switch c.remoteNode.Protocol { + case GRPC: + cli, err := v2ContainerClientFromOptions(c.opts) + if err != nil { + return nil, errors.Wrap(err, "can't create grpc client") + } + + resp, err := cli.Get(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "transport error") + } + + err = v2signature.VerifyServiceMessage(resp) + if err != nil { + return nil, errors.Wrap(err, "can't verify response message") + } + + return &container.Container{ + Container: *resp.GetBody().GetContainer(), + }, nil + default: + return nil, unsupportedProtocolErr + } +} + +func (c Client) listContainerV2(ctx context.Context, owner refs.NEO3Wallet, opts ...CallOption) ([]refs.ContainerID, error) { + // apply all available options + callOptions := defaultCallOptions() + for i := range opts { + opts[i].apply(&callOptions) + } + + v2owner := new(v2refs.OwnerID) + v2owner.SetValue(owner[:]) + + reqBody := new(v2container.ListRequestBody) + reqBody.SetOwnerID(v2owner) + + req := new(v2container.ListRequest) + req.SetBody(reqBody) + req.SetMetaHeader(v2MetaHeaderFromOpts(callOptions)) + + err := v2signature.SignServiceMessage(c.key, req) + if err != nil { + return nil, err + } + + switch c.remoteNode.Protocol { + case GRPC: + cli, err := v2ContainerClientFromOptions(c.opts) + if err != nil { + return nil, errors.Wrap(err, "can't create grpc client") + } + + resp, err := cli.List(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "transport error") + } + + err = v2signature.VerifyServiceMessage(resp) + if err != nil { + return nil, errors.Wrap(err, "can't verify response message") + } + + result := make([]refs.ContainerID, len(resp.GetBody().GetContainerIDs())) + for i, cid := range resp.GetBody().GetContainerIDs() { + copy(result[i][:], cid.GetValue()) + } + + return result, nil + default: + return nil, unsupportedProtocolErr + } +} + +func (c Client) delContainerV2(ctx context.Context, id refs.ContainerID, opts ...CallOption) error { + // apply all available options + callOptions := defaultCallOptions() + for i := range opts { + opts[i].apply(&callOptions) + } + + cid := new(v2refs.ContainerID) + cid.SetValue(id[:]) + + reqBody := new(v2container.DeleteRequestBody) + reqBody.SetContainerID(cid) + + // sign container + signWrapper := v2signature.StableMarshalerWrapper{SM: reqBody.GetContainerID()} + err := signature.SignDataWithHandler(c.key, signWrapper, func(key []byte, sig []byte) { + containerSignature := new(v2refs.Signature) + containerSignature.SetKey(key) + containerSignature.SetSign(sig) + reqBody.SetSignature(containerSignature) + }, signature.SignWithRFC6979()) + if err != nil { + return err + } + + req := new(v2container.DeleteRequest) + req.SetBody(reqBody) + req.SetMetaHeader(v2MetaHeaderFromOpts(callOptions)) + + err = v2signature.SignServiceMessage(c.key, req) + if err != nil { + return err + } + + switch c.remoteNode.Protocol { + case GRPC: + cli, err := v2ContainerClientFromOptions(c.opts) + if err != nil { + return errors.Wrap(err, "can't create grpc client") + } + + resp, err := cli.Delete(ctx, req) + if err != nil { + return errors.Wrap(err, "transport error") + } + + err = v2signature.VerifyServiceMessage(resp) + if err != nil { + return errors.Wrap(err, "can't verify response message") + } + + return nil + default: + return unsupportedProtocolErr + } +} + +func v2ContainerClientFromOptions(opts *clientOptions) (cli *v2container.Client, err error) { + switch { + case opts.grpcOpts.v2ContainerClient != nil: + // return value from client cache + return opts.grpcOpts.v2ContainerClient, nil + + case opts.grpcOpts.conn != nil: + cli, err = v2container.NewClient(v2container.WithGlobalOpts( + client.WithGRPCConn(opts.grpcOpts.conn)), + ) + + case opts.addr != "": + cli, err = v2container.NewClient(v2container.WithGlobalOpts( + client.WithNetworkAddress(opts.addr)), + ) + + default: + return nil, errors.New("lack of sdk client options to create accounting client") + } + + // check if client correct and save in cache + if err != nil { + return nil, err + } + + opts.grpcOpts.v2ContainerClient = cli + + return cli, nil +} diff --git a/pkg/container/container.go b/pkg/container/container.go new file mode 100644 index 0000000..d142d3e --- /dev/null +++ b/pkg/container/container.go @@ -0,0 +1,45 @@ +package container + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/container" + "github.com/nspcc-dev/neofs-api-go/v2/refs" +) + +type Container struct { + container.Container +} + +func New(opts ...NewOption) (*Container, error) { + cnrOptions := defaultContainerOptions() + for i := range opts { + opts[i].apply(&cnrOptions) + } + + cnr := new(Container) + cnr.SetNonce(cnrOptions.nonce[:]) + cnr.SetBasicACL(cnrOptions.acl) + + if cnrOptions.policy != "" { + // todo: set placement policy + } + + if cnrOptions.owner != nil { + owner := new(refs.OwnerID) + owner.SetValue(cnrOptions.owner[:]) + + cnr.SetOwnerID(owner) + } + + attributes := make([]*container.Attribute, len(cnrOptions.attributes)) + for i := range cnrOptions.attributes { + attribute := new(container.Attribute) + attribute.SetKey(cnrOptions.attributes[i].key) + attribute.SetValue(cnrOptions.attributes[i].value) + attributes[i] = attribute + } + if len(attributes) > 0 { + cnr.SetAttributes(attributes) + } + + return cnr, nil +} diff --git a/pkg/container/opts.go b/pkg/container/opts.go new file mode 100644 index 0000000..507c988 --- /dev/null +++ b/pkg/container/opts.go @@ -0,0 +1,99 @@ +package container + +import ( + "github.com/google/uuid" + "github.com/nspcc-dev/neofs-api-go/pkg/acl" + "github.com/nspcc-dev/neofs-api-go/pkg/refs" +) + +type ( + NewOption interface { + apply(*containerOptions) + } + + attribute struct { + key string + value string + } + + containerOptions struct { + acl uint32 + policy string + attributes []attribute + owner *refs.NEO3Wallet + nonce uuid.UUID + } +) + +func defaultContainerOptions() containerOptions { + rand, err := uuid.NewRandom() + if err != nil { + panic("can't create new random " + err.Error()) + } + + return containerOptions{ + acl: acl.PrivateBasicRule, + nonce: rand, + } +} + +type funcContainerOption struct { + f func(*containerOptions) +} + +func (fco *funcContainerOption) apply(co *containerOptions) { + fco.f(co) +} + +func newFuncContainerOption(f func(option *containerOptions)) *funcContainerOption { + return &funcContainerOption{ + f: f, + } +} + +func WithPublicBasicACL() NewOption { + return newFuncContainerOption(func(option *containerOptions) { + option.acl = acl.PublicBasicRule + }) +} + +func WithReadOnlyBasicACL() NewOption { + return newFuncContainerOption(func(option *containerOptions) { + option.acl = acl.ReadOnlyBasicRule + }) +} + +func WithCustomBasicACL(acl uint32) NewOption { + return newFuncContainerOption(func(option *containerOptions) { + option.acl = acl + }) +} + +func WithNonce(nonce uuid.UUID) NewOption { + return newFuncContainerOption(func(option *containerOptions) { + option.nonce = nonce + }) +} + +func WithOwner(owner refs.NEO3Wallet) NewOption { + return newFuncContainerOption(func(option *containerOptions) { + option.owner = &owner + }) +} + +func WithPolicy(policy string) NewOption { + return newFuncContainerOption(func(option *containerOptions) { + // todo: make sanity check and store binary structure + option.policy = policy + }) +} + +func WithAttribute(key, value string) NewOption { + return newFuncContainerOption(func(option *containerOptions) { + attr := attribute{ + key: key, + value: value, + } + option.attributes = append(option.attributes, attr) + }) +}