From e82a2d86ef823f9dde19b7065b63fadadd5da7e5 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 16 Jun 2022 09:17:41 +0300 Subject: [PATCH] [#225] container: Refactor and document basic ACL Replace basic ACL functionality from `acl` package to the `container` one. Create `BasicACL` type and provide convenient interface to work with it. Signed-off-by: Leonard Lyubich --- acl/doc.go | 40 ---- acl/types.go | 107 ---------- acl/types_test.go | 82 -------- container/acl.go | 81 ++++++++ container/acl_basic.go | 272 +++++++++++++++++++++++++ container/acl_basic_test.go | 384 ++++++++++++++++++++++++++++++++++++ container/container.go | 12 +- container/container_test.go | 9 +- container/init.go | 30 +++ container/opts.go | 11 +- container/test/generate.go | 2 +- container/util.go | 45 +++++ container/util_test.go | 133 +++++++++++++ 13 files changed, 961 insertions(+), 247 deletions(-) delete mode 100644 acl/doc.go delete mode 100644 acl/types.go delete mode 100644 acl/types_test.go create mode 100644 container/acl.go create mode 100644 container/acl_basic.go create mode 100644 container/acl_basic_test.go create mode 100644 container/init.go create mode 100644 container/util.go create mode 100644 container/util_test.go diff --git a/acl/doc.go b/acl/doc.go deleted file mode 100644 index 0a366086..00000000 --- a/acl/doc.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -Package acl provides primitives to perform handling basic ACL management in NeoFS. - -BasicACL type provides functionality for managing container basic access-control list. -For example, setting public basic ACL that could not be extended with any eACL rules: - - import "github.com/nspcc-dev/neofs-sdk-go/container" - ... - c := container.New() - c.SetBasicACL(acl.PublicBasicRule) - -Using package types in an application is recommended to potentially work with -different protocol versions with which these types are compatible. - - Basic ACL bits meaning: - - ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ - │31│30│29│28│27│26│25│24│23│22│21│20│19│18│17│16│ <- Bit - ├──┼──┼──┼──┼──┴──┴──┴──┼──┴──┴──┴──┼──┴──┴──┴──┤ - │ │ │ │ │ RANGEHASH │ RANGE │ SEARCH │ <- Object service method - │ │ │ │ ├──┬──┬──┬──┼──┬──┬──┬──┼──┬──┬──┬──┤ - │ │ │ X│ F│ U│ S│ O│ B│ U│ S│ O│ B│ U│ S│ O│ B│ <- Rule - ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ - │15│14│13│12│11│10│09│08│07│06│05│04│03│02│01│00│ <- Bit - ├──┴──┴──┴──┼──┴──┴──┴──┼──┴──┴──┴──┼──┴──┴──┴──┤ - │ DELETE │ PUT │ HEAD │ GET │ <- Object service method - ├──┬──┬──┬──┼──┬──┬──┬──┼──┬──┬──┬──┼──┬──┬──┬──┤ - │ U│ S│ O│ B│ U│ S│ O│ B│ U│ S│ O│ B│ U│ S│ O│ B│ <- Rule - └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ - - U - Allows access to the owner of the container. - S - Allows access to Inner Ring and container nodes in the current version of network map. - O - Clients that do not match any of the categories above. - B - Allows using Bear Token ACL rules to replace eACL rules. - F - Flag denying Extended ACL. If set Extended ACL is ignored. - X - Flag denying different owners of the request and the object. - - Remaining bits are reserved and are not used. -*/ -package acl diff --git a/acl/types.go b/acl/types.go deleted file mode 100644 index 7fa20195..00000000 --- a/acl/types.go +++ /dev/null @@ -1,107 +0,0 @@ -package acl - -import ( - "fmt" - "strconv" - "strings" -) - -// BasicACL is Access Control List that defines who can interact with containers and what exactly they can do. -type BasicACL uint32 - -// String returns BasicACL string representation -// in hexadecimal form with 0x prefix. -func (a BasicACL) String() string { - return fmt.Sprintf("0x%08x", uint32(a)) -} - -const ( - // PublicBasicRule is a basic ACL value for final public-read-write container for which extended ACL CANNOT be set. - PublicBasicRule BasicACL = 0x1FBFBFFF - - // PrivateBasicRule is a basic ACL value for final private container for which extended ACL CANNOT be set. - PrivateBasicRule BasicACL = 0x1C8C8CCC - - // ReadOnlyBasicRule is a basic ACL value for final public-read container for which extended ACL CANNOT be set. - ReadOnlyBasicRule BasicACL = 0x1FBF8CFF - - // PublicAppendRule is a basic ACL value for final public-append container for which extended ACL CANNOT be set. - PublicAppendRule BasicACL = 0x1FBF9FFF - - // EACLPublicBasicRule is a basic ACL value for non-final public-read-write container for which extended ACL CAN be set. - EACLPublicBasicRule BasicACL = 0x0FBFBFFF - - // EACLPrivateBasicRule is a basic ACL value for non-final private container for which extended ACL CAN be set. - EACLPrivateBasicRule BasicACL = 0x0C8C8CCC - - // EACLReadOnlyBasicRule is a basic ACL value for non-final public-read container for which extended ACL CAN be set. - EACLReadOnlyBasicRule BasicACL = 0x0FBF8CFF - - // EACLPublicAppendRule is a basic ACL value for non-final public-append container for which extended ACL CAN be set. - EACLPublicAppendRule BasicACL = 0x0FBF9FFF -) - -const ( - // PublicBasicName is a well-known name for 0x1FBFBFFF basic ACL. - // It represents fully-public container without eACL. - PublicBasicName = "public-read-write" - - // PrivateBasicName is a well-known name for 0x1C8C8CCC basic ACL. - // It represents fully-private container without eACL. - PrivateBasicName = "private" - - // ReadOnlyBasicName is a well-known name for 0x1FBF8CFF basic ACL. - // It represents public read-only container without eACL. - ReadOnlyBasicName = "public-read" - - // PublicAppendName is a well-known name for 0x1FBF9FFF basic ACL. - // It represents fully-public container without eACL except DELETE operation is only allowed on the owner. - PublicAppendName = "public-append" - - // EACLPublicBasicName is a well-known name for 0x0FBFBFFF basic ACL. - // It represents fully-public container that allows eACL. - EACLPublicBasicName = "eacl-public-read-write" - - // EACLPrivateBasicName is a well-known name for 0x0C8C8CCC basic ACL. - // It represents fully-private container that allows eACL. - EACLPrivateBasicName = "eacl-private" - - // EACLReadOnlyBasicName is a well-known name for 0x0FBF8CFF basic ACL. - // It represents public read-only container that allows eACL. - EACLReadOnlyBasicName = "eacl-public-read" - - // EACLPublicAppendName is a well-known name for 0x0FBF9FFF basic ACL. - // It represents fully-public container that allows eACL except DELETE operation is only allowed on the owner. - EACLPublicAppendName = "eacl-public-append" -) - -// ParseBasicACL parse string ACL (well-known names or hex representation). -func ParseBasicACL(basicACL string) (BasicACL, error) { - switch basicACL { - case PublicBasicName: - return PublicBasicRule, nil - case PrivateBasicName: - return PrivateBasicRule, nil - case ReadOnlyBasicName: - return ReadOnlyBasicRule, nil - case PublicAppendName: - return PublicAppendRule, nil - case EACLPublicBasicName: - return EACLPublicBasicRule, nil - case EACLPrivateBasicName: - return EACLPrivateBasicRule, nil - case EACLReadOnlyBasicName: - return EACLReadOnlyBasicRule, nil - case EACLPublicAppendName: - return EACLPublicAppendRule, nil - default: - basicACL = strings.TrimPrefix(strings.ToLower(basicACL), "0x") - - value, err := strconv.ParseUint(basicACL, 16, 32) - if err != nil { - return 0, fmt.Errorf("can't parse basic ACL: %s", basicACL) - } - - return BasicACL(value), nil - } -} diff --git a/acl/types_test.go b/acl/types_test.go deleted file mode 100644 index 9bf0494f..00000000 --- a/acl/types_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package acl - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestParser(t *testing.T) { - for _, tc := range []struct { - acl string - expected BasicACL - err bool - }{ - { - acl: PublicBasicName, - expected: PublicBasicRule, - }, - { - acl: PrivateBasicName, - expected: PrivateBasicRule, - }, - { - acl: ReadOnlyBasicName, - expected: ReadOnlyBasicRule, - }, - { - acl: PublicAppendName, - expected: PublicAppendRule, - }, - { - acl: EACLPublicBasicName, - expected: EACLPublicBasicRule, - }, - { - acl: EACLPrivateBasicName, - expected: EACLPrivateBasicRule, - }, - { - acl: EACLReadOnlyBasicName, - expected: EACLReadOnlyBasicRule, - }, - { - acl: EACLPublicAppendName, - expected: EACLPublicAppendRule, - }, - { - acl: "0x1C8C8CCC", - expected: 0x1C8C8CCC, - }, - { - acl: "1C8C8CCC", - expected: 0x1C8C8CCC, - }, - { - acl: "123456789", - err: true, - }, - { - acl: "0x1C8C8CCG", - err: true, - }, - } { - actual, err := ParseBasicACL(tc.acl) - if tc.err { - require.Error(t, err) - continue - } - - require.NoError(t, err) - require.Equal(t, tc.expected, actual) - } -} - -func TestString(t *testing.T) { - acl := BasicACL(0x1fbfbfff) - require.Equal(t, "0x1fbfbfff", acl.String()) - - acl2, err := ParseBasicACL(PrivateBasicName) - require.NoError(t, err) - require.Equal(t, "0x1c8c8ccc", acl2.String()) -} diff --git a/container/acl.go b/container/acl.go new file mode 100644 index 00000000..362c42ed --- /dev/null +++ b/container/acl.go @@ -0,0 +1,81 @@ +package container + +import "strconv" + +// ACLOp enumerates operations under access control inside container. +// Non-positive values are reserved and depend on context (e.g. unsupported op). +// +// Note that type conversion from- and to numerical types is not recommended, +// use corresponding constants and/or methods instead. +type ACLOp uint32 + +const ( + aclOpZero ACLOp = iota // extreme value for testing + + ACLOpObjectGet // Object.Get rpc + ACLOpObjectHead // Object.Head rpc + ACLOpObjectPut // Object.Put rpc + ACLOpObjectDelete // Object.Delete rpc + ACLOpObjectSearch // Object.Search rpc + ACLOpObjectRange // Object.GetRange rpc + ACLOpObjectHash // Object.GetRangeHash rpc + + aclOpLast // extreme value for testing +) + +// String implements fmt.Stringer. +func (x ACLOp) String() string { + switch x { + default: + return "UNKNOWN#" + strconv.FormatUint(uint64(x), 10) + case ACLOpObjectGet: + return "OBJECT_GET" + case ACLOpObjectHead: + return "OBJECT_HEAD" + case ACLOpObjectPut: + return "OBJECT_PUT" + case ACLOpObjectDelete: + return "OBJECT_DELETE" + case ACLOpObjectSearch: + return "OBJECT_SEARCH" + case ACLOpObjectRange: + return "OBJECT_RANGE" + case ACLOpObjectHash: + return "OBJECT_HASH" + } +} + +// ACLRole enumerates roles covered by container ACL. Each role represents +// some party which can be authenticated during container op execution. +// Non-positive values are reserved and depend on context (e.g. unsupported role). +// +// Note that type conversion from- and to numerical types is not recommended, +// use corresponding constants and/or methods instead. +type ACLRole uint32 + +const ( + aclRoleZero ACLRole = iota // extreme value for testing + + ACLRoleOwner // container owner + ACLRoleContainer // nodes of the related container + ACLRoleInnerRing // Inner Ring nodes + ACLRoleOthers // all others + + aclRoleLast // extreme value for testing +) + +// String implements fmt.Stringer. +func (x ACLRole) String() string { + switch x { + default: + return "UNKNOWN#" + strconv.FormatUint(uint64(x), 10) + case ACLRoleOwner: + return "OWNER" + case ACLRoleContainer: + return "CONTAINER" + case ACLRoleInnerRing: + return "INNER_RING" + case ACLRoleOthers: + return "OTHERS" + } +} diff --git a/container/acl_basic.go b/container/acl_basic.go new file mode 100644 index 00000000..10a2a087 --- /dev/null +++ b/container/acl_basic.go @@ -0,0 +1,272 @@ +package container + +import ( + "fmt" + "strconv" + "strings" +) + +// BasicACL represents basic part of the NeoFS container's ACL. It includes +// common (pretty simple) access rules for operations inside the container. +// See NeoFS Specification for details. +// +// One can find some similarities with the traditional Unix permission, such as +// division into scopes: user, group, others +// op-permissions: read, write, etc. +// sticky bit +// However, these similarities should only be used for better understanding, +// in general these mechanisms are different. +// +// Instances can be created using built-in var declaration, but look carefully +// at the default values, and how individual permissions are regulated. +// Some frequently used values are presented in BasicACL* values. +// +// BasicACL instances are comparable: values can be compared directly using +// == operator. +type BasicACL struct { + bits uint32 +} + +func (x *BasicACL) fromUint32(num uint32) { + x.bits = num +} + +func (x BasicACL) toUint32() uint32 { + return x.bits +} + +// common bit sections. +const ( + opAmount = 7 + bitsPerOp = 4 + + bitPosFinal = opAmount * bitsPerOp + bitPosSticky = bitPosFinal + 1 +) + +// per-op bit order. +const ( + opBitPosBearer uint8 = iota + opBitPosOthers + opBitPosContainer + opBitPosOwner +) + +// DisableExtension makes BasicACL FINAL. FINAL indicates the ACL non-extendability +// in the related container. +// +// See also Extendable. +func (x *BasicACL) DisableExtension() { + setBit(&x.bits, bitPosFinal) +} + +// Extendable checks if BasicACL is NOT made FINAL using DisableExtension. +// +// Zero BasicACL is NOT FINAL or extendable. +func (x BasicACL) Extendable() bool { + return !isBitSet(x.bits, bitPosFinal) +} + +// MakeSticky makes BasicACL STICKY. STICKY indicates that only the owner of any +// particular object is allowed to operate on it. +// +// See also Sticky. +func (x *BasicACL) MakeSticky() { + setBit(&x.bits, bitPosSticky) +} + +// Sticky checks if BasicACL is made STICKY using MakeSticky. +// +// Zero BasicACL is NOT STICKY. +func (x BasicACL) Sticky() bool { + return isBitSet(x.bits, bitPosSticky) +} + +// checks if op is used by the storage nodes within replication mechanism. +func isReplicationOp(op ACLOp) bool { + //nolint:exhaustive + switch op { + case + ACLOpObjectGet, + ACLOpObjectHead, + ACLOpObjectPut, + ACLOpObjectSearch, + ACLOpObjectHash: + return true + } + + return false +} + +// AllowOp allows the parties with the given role to the given operation. +// Op MUST be one of the ACLOp enumeration. Role MUST be one of: +// ACLRoleOwner +// ACLRoleContainer +// ACLRoleOthers +// and if role is ACLRoleContainer, op MUST NOT be: +// ACLOpObjectGet +// ACLOpObjectHead +// ACLOpObjectPut +// ACLOpObjectSearch +// ACLOpObjectHash +// +// See also IsOpAllowed. +func (x *BasicACL) AllowOp(op ACLOp, role ACLRole) { + var bitPos uint8 + + switch role { + default: + panic(fmt.Sprintf("unable to set rules for unsupported role %v", role)) + case ACLRoleInnerRing: + panic("basic ACL MUST NOT be modified for Inner Ring") + case ACLRoleOwner: + bitPos = opBitPosOwner + case ACLRoleContainer: + if isReplicationOp(op) { + panic("basic ACL for container replication ops MUST NOT be modified") + } + + bitPos = opBitPosContainer + case ACLRoleOthers: + bitPos = opBitPosOthers + } + + setOpBit(&x.bits, op, bitPos) +} + +// IsOpAllowed checks if parties with the given role are allowed to the given op +// according to the BasicACL rules. Op MUST be one of the ACLOp enumeration. +// Role MUST be one of the ACLRole enumeration. +// +// Members with ACLRoleContainer role have exclusive default access to the +// operations of the data replication mechanism: +// ACLOpObjectGet +// ACLOpObjectHead +// ACLOpObjectPut +// ACLOpObjectSearch +// ACLOpObjectHash +// +// ACLRoleInnerRing members are allowed to data audit ops only: +// ACLOpObjectGet +// ACLOpObjectHead +// ACLOpObjectHash +// ACLOpObjectSearch +// +// Zero BasicACL prevents any role from accessing any operation in the absence +// of default rights. +// +// See also AllowOp. +func (x BasicACL) IsOpAllowed(op ACLOp, role ACLRole) bool { + var bitPos uint8 + + switch role { + default: + panic(fmt.Sprintf("role is unsupported %v", role)) + case ACLRoleInnerRing: + switch op { + case + ACLOpObjectGet, + ACLOpObjectHead, + ACLOpObjectHash, + ACLOpObjectSearch: + return true + default: + return false + } + case ACLRoleOwner: + bitPos = opBitPosOwner + case ACLRoleContainer: + if isReplicationOp(op) { + return true + } + + bitPos = opBitPosContainer + case ACLRoleOthers: + bitPos = opBitPosOthers + } + + return isOpBitSet(x.bits, op, bitPos) +} + +// AllowBearerRules allows bearer to provide extended ACL rules for the given +// operation. Bearer rules doesn't depend on container ACL +// // extensibility. +// +// See also AllowedBearerRules. +func (x *BasicACL) AllowBearerRules(op ACLOp) { + setOpBit(&x.bits, op, opBitPosBearer) +} + +// AllowedBearerRules checks if bearer rules are allowed using AllowBearerRules. +// Op MUST be one of the ACLOp enumeration. +// +// Zero BasicACL disallows bearer rules for any op. +func (x BasicACL) AllowedBearerRules(op ACLOp) bool { + return isOpBitSet(x.bits, op, opBitPosBearer) +} + +// EncodeToString encodes BasicACL into hexadecimal string. +// +// See also DecodeString. +func (x BasicACL) EncodeToString() string { + return strconv.FormatUint(uint64(x.bits), 16) +} + +// Names of the frequently used BasicACL values. +const ( + BasicACLNamePrivate = "private" + BasicACLNamePrivateExtended = "eacl-private" + BasicACLNamePublicRO = "public-read" + BasicACLNamePublicROExtended = "eacl-public-read" + BasicACLNamePublicRW = "public-read-write" + BasicACLNamePublicRWExtended = "eacl-public-read-write" + BasicACLNamePublicAppend = "public-append" + BasicACLNamePublicAppendExtended = "eacl-public-append" +) + +// Frequently used BasicACL values (each value MUST NOT be modified, make a +// copy instead). +var ( + BasicACLPrivate BasicACL // private + BasicACLPrivateExtended BasicACL // eacl-private + BasicACLPublicRO BasicACL // public-read + BasicACLPublicROExtended BasicACL // eacl-public-read + BasicACLPublicRW BasicACL // public-read-write + BasicACLPublicRWExtended BasicACL // eacl-public-read-write + BasicACLPublicAppend BasicACL // public-append + BasicACLPublicAppendExtended BasicACL // eacl-public-append +) + +// DecodeString decodes string calculated using EncodeToString. Also supports +// human-readable names (BasicACLName* constants). +func (x *BasicACL) DecodeString(s string) error { + switch s { + case BasicACLNamePrivate: + *x = BasicACLPrivate + case BasicACLNamePrivateExtended: + *x = BasicACLPrivateExtended + case BasicACLNamePublicRO: + *x = BasicACLPublicRO + case BasicACLNamePublicROExtended: + *x = BasicACLPublicROExtended + case BasicACLNamePublicRW: + *x = BasicACLPublicRW + case BasicACLNamePublicRWExtended: + *x = BasicACLPublicRWExtended + case BasicACLNamePublicAppend: + *x = BasicACLPublicAppend + case BasicACLNamePublicAppendExtended: + *x = BasicACLPublicAppendExtended + default: + s = strings.TrimPrefix(strings.ToLower(s), "0x") + + v, err := strconv.ParseUint(s, 16, 32) + if err != nil { + return fmt.Errorf("parse hex: %w", err) + } + + x.bits = uint32(v) + } + + return nil +} diff --git a/container/acl_basic_test.go b/container/acl_basic_test.go new file mode 100644 index 00000000..595d2f19 --- /dev/null +++ b/container/acl_basic_test.go @@ -0,0 +1,384 @@ +package container + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBasicACL_DisableExtension(t *testing.T) { + var val, val2 BasicACL + + require.True(t, val.Extendable()) + val2.fromUint32(val.toUint32()) + require.True(t, val2.Extendable()) + + val.DisableExtension() + + require.False(t, val.Extendable()) + val2.fromUint32(val.toUint32()) + require.False(t, val2.Extendable()) +} + +func TestBasicACL_MakeSticky(t *testing.T) { + var val, val2 BasicACL + + require.False(t, val.Sticky()) + val2.fromUint32(val.toUint32()) + require.False(t, val2.Sticky()) + + val.MakeSticky() + + require.True(t, val.Sticky()) + val2.fromUint32(val.toUint32()) + require.True(t, val2.Sticky()) +} + +func TestBasicACL_AllowBearerRules(t *testing.T) { + var val BasicACL + + require.Panics(t, func() { val.AllowBearerRules(aclOpZero) }) + require.Panics(t, func() { val.AllowBearerRules(aclOpLast) }) + + require.Panics(t, func() { val.AllowedBearerRules(aclOpZero) }) + require.Panics(t, func() { val.AllowedBearerRules(aclOpLast) }) + + for op := aclOpZero + 1; op < aclOpLast; op++ { + val := val + + require.False(t, val.AllowedBearerRules(op)) + + val.AllowBearerRules(op) + + for j := aclOpZero + 1; j < aclOpLast; j++ { + if j == op { + require.True(t, val.AllowedBearerRules(j), op) + } else { + require.False(t, val.AllowedBearerRules(j), op) + } + } + } +} + +func TestBasicACL_AllowOp(t *testing.T) { + var val, val2 BasicACL + + require.Panics(t, func() { val.IsOpAllowed(aclOpZero, aclRoleZero+1) }) + require.Panics(t, func() { val.IsOpAllowed(aclOpLast, aclRoleZero+1) }) + require.Panics(t, func() { val.IsOpAllowed(aclOpZero+1, aclRoleZero) }) + require.Panics(t, func() { val.IsOpAllowed(aclOpZero+1, aclRoleLast) }) + + for op := aclOpZero + 1; op < aclOpLast; op++ { + require.Panics(t, func() { val.AllowOp(op, ACLRoleInnerRing) }) + + if isReplicationOp(op) { + require.Panics(t, func() { val.AllowOp(op, ACLRoleContainer) }) + require.True(t, val.IsOpAllowed(op, ACLRoleContainer)) + } + } + + require.True(t, val.IsOpAllowed(ACLOpObjectGet, ACLRoleInnerRing)) + require.True(t, val.IsOpAllowed(ACLOpObjectHead, ACLRoleInnerRing)) + require.True(t, val.IsOpAllowed(ACLOpObjectSearch, ACLRoleInnerRing)) + require.True(t, val.IsOpAllowed(ACLOpObjectHash, ACLRoleInnerRing)) + + const op = aclOpZero + 1 + const role = ACLRoleOthers + + require.False(t, val.IsOpAllowed(op, role)) + val2.fromUint32(val.toUint32()) + require.False(t, val2.IsOpAllowed(op, role)) + + val.AllowOp(op, role) + + require.True(t, val.IsOpAllowed(op, role)) + val2.fromUint32(val.toUint32()) + require.True(t, val2.IsOpAllowed(op, role)) +} + +type opsExpected struct { + owner, container, innerRing, others, bearer bool +} + +func testOp(t *testing.T, v BasicACL, op ACLOp, exp opsExpected) { + require.Equal(t, exp.owner, v.IsOpAllowed(op, ACLRoleOwner), op) + require.Equal(t, exp.container, v.IsOpAllowed(op, ACLRoleContainer), op) + require.Equal(t, exp.innerRing, v.IsOpAllowed(op, ACLRoleInnerRing), op) + require.Equal(t, exp.others, v.IsOpAllowed(op, ACLRoleOthers), op) + require.Equal(t, exp.bearer, v.AllowedBearerRules(op), op) +} + +type expected struct { + extendable, sticky bool + + mOps map[ACLOp]opsExpected +} + +func testBasicACLPredefined(t *testing.T, val BasicACL, name string, exp expected) { + require.Equal(t, exp.sticky, val.Sticky()) + require.Equal(t, exp.extendable, val.Extendable()) + + for op, exp := range exp.mOps { + testOp(t, val, op, exp) + } + + s := val.EncodeToString() + + var val2 BasicACL + + require.NoError(t, val2.DecodeString(s)) + require.Equal(t, val, val2) + + require.NoError(t, val2.DecodeString(name)) + require.Equal(t, val, val2) +} + +func TestBasicACLPredefined(t *testing.T) { + t.Run("private", func(t *testing.T) { + exp := expected{ + extendable: false, + sticky: false, + mOps: map[ACLOp]opsExpected{ + ACLOpObjectHash: { + owner: true, + container: true, + innerRing: true, + others: false, + bearer: false, + }, + ACLOpObjectRange: { + owner: true, + container: false, + innerRing: false, + others: false, + bearer: false, + }, + ACLOpObjectSearch: { + owner: true, + container: true, + innerRing: true, + others: false, + bearer: false, + }, + ACLOpObjectDelete: { + owner: true, + container: false, + innerRing: false, + others: false, + bearer: false, + }, + ACLOpObjectPut: { + owner: true, + container: true, + innerRing: false, + others: false, + bearer: false, + }, + ACLOpObjectHead: { + owner: true, + container: true, + innerRing: true, + others: false, + bearer: false, + }, + ACLOpObjectGet: { + owner: true, + container: true, + innerRing: true, + others: false, + bearer: false, + }, + }, + } + + testBasicACLPredefined(t, BasicACLPrivate, BasicACLNamePrivate, exp) + exp.extendable = true + testBasicACLPredefined(t, BasicACLPrivateExtended, BasicACLNamePrivateExtended, exp) + }) + + t.Run("public-read", func(t *testing.T) { + exp := expected{ + extendable: false, + sticky: false, + mOps: map[ACLOp]opsExpected{ + ACLOpObjectHash: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + ACLOpObjectRange: { + owner: true, + container: false, + innerRing: false, + others: true, + bearer: true, + }, + ACLOpObjectSearch: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + ACLOpObjectDelete: { + owner: true, + container: false, + innerRing: false, + others: false, + bearer: false, + }, + ACLOpObjectPut: { + owner: true, + container: true, + innerRing: false, + others: false, + bearer: false, + }, + ACLOpObjectHead: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + ACLOpObjectGet: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + }, + } + + testBasicACLPredefined(t, BasicACLPublicRO, BasicACLNamePublicRO, exp) + exp.extendable = true + testBasicACLPredefined(t, BasicACLPublicROExtended, BasicACLNamePublicROExtended, exp) + }) + + t.Run("public-read-write", func(t *testing.T) { + exp := expected{ + extendable: false, + sticky: false, + mOps: map[ACLOp]opsExpected{ + ACLOpObjectHash: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + ACLOpObjectRange: { + owner: true, + container: false, + innerRing: false, + others: true, + bearer: true, + }, + ACLOpObjectSearch: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + ACLOpObjectDelete: { + owner: true, + container: false, + innerRing: false, + others: true, + bearer: true, + }, + ACLOpObjectPut: { + owner: true, + container: true, + innerRing: false, + others: true, + bearer: true, + }, + ACLOpObjectHead: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + ACLOpObjectGet: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + }, + } + + testBasicACLPredefined(t, BasicACLPublicRW, BasicACLNamePublicRW, exp) + exp.extendable = true + testBasicACLPredefined(t, BasicACLPublicRWExtended, BasicACLNamePublicRWExtended, exp) + }) + + t.Run("public-append", func(t *testing.T) { + exp := expected{ + extendable: false, + sticky: false, + mOps: map[ACLOp]opsExpected{ + ACLOpObjectHash: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + ACLOpObjectRange: { + owner: true, + container: false, + innerRing: false, + others: true, + bearer: true, + }, + ACLOpObjectSearch: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + ACLOpObjectDelete: { + owner: true, + container: false, + innerRing: false, + others: false, + bearer: true, + }, + ACLOpObjectPut: { + owner: true, + container: true, + innerRing: false, + others: true, + bearer: true, + }, + ACLOpObjectHead: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + ACLOpObjectGet: { + owner: true, + container: true, + innerRing: true, + others: true, + bearer: true, + }, + }, + } + + testBasicACLPredefined(t, BasicACLPublicAppend, BasicACLNamePublicAppend, exp) + exp.extendable = true + testBasicACLPredefined(t, BasicACLPublicAppendExtended, BasicACLNamePublicAppendExtended, exp) + }) +} diff --git a/container/container.go b/container/container.go index d26ce12d..40075e23 100644 --- a/container/container.go +++ b/container/container.go @@ -7,7 +7,6 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/container" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-sdk-go/acl" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/user" @@ -23,7 +22,7 @@ type Container struct { // Defaults: // - token: nil; // - sig: nil; -// - basicACL: acl.PrivateBasicRule; +// - basicACL: BasicACLPrivate; // - version: version.Current; // - nonce: random UUID; // - attr: nil; @@ -136,12 +135,13 @@ func (c *Container) SetNonceUUID(v uuid.UUID) { c.v2.SetNonce(data) } -func (c *Container) BasicACL() uint32 { - return c.v2.GetBasicACL() +func (c *Container) BasicACL() (res BasicACL) { + res.fromUint32(c.v2.GetBasicACL()) + return } -func (c *Container) SetBasicACL(v acl.BasicACL) { - c.v2.SetBasicACL(uint32(v)) +func (c *Container) SetBasicACL(v BasicACL) { + c.v2.SetBasicACL(v.toUint32()) } func (c *Container) Attributes() Attributes { diff --git a/container/container_test.go b/container/container_test.go index 2b2a04aa..a5d63cfe 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -5,7 +5,6 @@ import ( "github.com/google/uuid" "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-sdk-go/acl" "github.com/nspcc-dev/neofs-sdk-go/container" containertest "github.com/nspcc-dev/neofs-sdk-go/container/test" netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test" @@ -23,7 +22,7 @@ func TestNewContainer(t *testing.T) { ownerID := usertest.ID() policy := netmaptest.PlacementPolicy() - c.SetBasicACL(acl.PublicBasicRule) + c.SetBasicACL(container.BasicACLPublicRW) attrs := containertest.Attributes() c.SetAttributes(attrs) @@ -40,7 +39,7 @@ func TestNewContainer(t *testing.T) { require.EqualValues(t, newContainer.PlacementPolicy(), &policy) require.EqualValues(t, newContainer.Attributes(), attrs) - require.EqualValues(t, newContainer.BasicACL(), acl.PublicBasicRule) + require.EqualValues(t, newContainer.BasicACL(), container.BasicACLPublicRW) newNonce, err := newContainer.NonceUUID() require.NoError(t, err) @@ -89,7 +88,7 @@ func TestContainer_ToV2(t *testing.T) { require.Nil(t, cnt.PlacementPolicy()) require.Nil(t, cnt.OwnerID()) - require.EqualValues(t, acl.PrivateBasicRule, cnt.BasicACL()) + require.EqualValues(t, container.BasicACLPrivate, cnt.BasicACL()) require.Equal(t, version.Current(), *cnt.Version()) nonce, err := cnt.NonceUUID() @@ -108,7 +107,7 @@ func TestContainer_ToV2(t *testing.T) { require.Nil(t, cntV2.GetPlacementPolicy()) require.Nil(t, cntV2.GetOwnerID()) - require.Equal(t, uint32(acl.PrivateBasicRule), cntV2.GetBasicACL()) + require.EqualValues(t, 0x1C8C8CCC, cntV2.GetBasicACL()) var verV2 refs.Version version.Current().WriteToV2(&verV2) diff --git a/container/init.go b/container/init.go new file mode 100644 index 00000000..e06949ed --- /dev/null +++ b/container/init.go @@ -0,0 +1,30 @@ +package container + +func init() { + // left-to-right order of the object operations + orderedOps := [...]ACLOp{ + ACLOpObjectGet, + ACLOpObjectHead, + ACLOpObjectPut, + ACLOpObjectDelete, + ACLOpObjectSearch, + ACLOpObjectRange, + ACLOpObjectHash, + } + + mOrder = make(map[ACLOp]uint8, len(orderedOps)) + + for i := range orderedOps { + mOrder[orderedOps[i]] = uint8(i) + } + + // numbers are taken from NeoFS Specification + BasicACLPrivate.fromUint32(0x1C8C8CCC) + BasicACLPrivateExtended.fromUint32(0x0C8C8CCC) + BasicACLPublicRO.fromUint32(0x1FBF8CFF) + BasicACLPublicROExtended.fromUint32(0x0FBF8CFF) + BasicACLPublicRW.fromUint32(0x1FBFBFFF) + BasicACLPublicRWExtended.fromUint32(0x0FBFBFFF) + BasicACLPublicAppend.fromUint32(0x1FBF9FFF) + BasicACLPublicAppendExtended.fromUint32(0x0FBF9FFF) +} diff --git a/container/opts.go b/container/opts.go index 276aa8ff..2fe7f3e8 100644 --- a/container/opts.go +++ b/container/opts.go @@ -4,7 +4,6 @@ import ( "crypto/ecdsa" "github.com/google/uuid" - "github.com/nspcc-dev/neofs-sdk-go/acl" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/user" ) @@ -13,7 +12,7 @@ type ( Option func(*containerOptions) containerOptions struct { - acl acl.BasicACL + acl BasicACL policy *netmap.PlacementPolicy attributes Attributes owner *user.ID @@ -28,24 +27,24 @@ func defaultContainerOptions() containerOptions { } return containerOptions{ - acl: acl.PrivateBasicRule, + acl: BasicACLPrivate, nonce: rand, } } func WithPublicBasicACL() Option { return func(option *containerOptions) { - option.acl = acl.PublicBasicRule + option.acl = BasicACLPublicRW } } func WithReadOnlyBasicACL() Option { return func(option *containerOptions) { - option.acl = acl.ReadOnlyBasicRule + option.acl = BasicACLPublicRO } } -func WithCustomBasicACL(acl acl.BasicACL) Option { +func WithCustomBasicACL(acl BasicACL) Option { return func(option *containerOptions) { option.acl = acl } diff --git a/container/test/generate.go b/container/test/generate.go index 4ee76d48..e733365a 100644 --- a/container/test/generate.go +++ b/container/test/generate.go @@ -31,7 +31,7 @@ func Container() *container.Container { x.SetVersion(&ver) x.SetAttributes(Attributes()) x.SetOwnerID(usertest.ID()) - x.SetBasicACL(123) + x.SetBasicACL(container.BasicACLPublicRW) p := netmaptest.PlacementPolicy() x.SetPlacementPolicy(&p) diff --git a/container/util.go b/container/util.go new file mode 100644 index 00000000..025d2eb3 --- /dev/null +++ b/container/util.go @@ -0,0 +1,45 @@ +package container + +import "fmt" + +// sets n-th bit in num (starting at 0). +func setBit(num *uint32, n uint8) { + *num |= 1 << n +} + +// resets n-th bit in num (starting at 0). +func resetBit(num *uint32, n uint8) { + var mask uint32 + setBit(&mask, n) + + *num &= ^mask +} + +// checks if n-th bit in num is set (starting at 0). +func isBitSet(num uint32, n uint8) bool { + mask := uint32(1 << n) + return mask != 0 && num&mask == mask +} + +// maps ACLOp to op-section index in BasicACL. Filled on init. +var mOrder map[ACLOp]uint8 + +// sets n-th bit in num for the given op. Panics if op is unsupported. +func setOpBit(num *uint32, op ACLOp, opBitPos uint8) { + n, ok := mOrder[op] + if !ok { + panic(fmt.Sprintf("op is unsupported %v", op)) + } + + setBit(num, n*bitsPerOp+opBitPos) +} + +// checks if n-th bit in num for the given op is set. Panics if op is unsupported. +func isOpBitSet(num uint32, op ACLOp, n uint8) bool { + off, ok := mOrder[op] + if !ok { + panic(fmt.Sprintf("op is unsupported %v", op)) + } + + return isBitSet(num, bitsPerOp*off+n) +} diff --git a/container/util_test.go b/container/util_test.go new file mode 100644 index 00000000..1b3cdd67 --- /dev/null +++ b/container/util_test.go @@ -0,0 +1,133 @@ +package container + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBits(t *testing.T) { + num := uint32(0b10110) + + require.False(t, isBitSet(num, 0)) + require.True(t, isBitSet(num, 1)) + require.True(t, isBitSet(num, 2)) + require.False(t, isBitSet(num, 3)) + require.True(t, isBitSet(num, 4)) + require.False(t, isBitSet(num, 5)) + + setBit(&num, 3) + require.EqualValues(t, 0b11110, num) + + setBit(&num, 6) + require.EqualValues(t, 0b1011110, num) + + resetBit(&num, 1) + require.EqualValues(t, 0b1011100, num) +} + +func TestOpBits(t *testing.T) { + num := uint32(0b_1001_0101_1100_0011_0110_0111_1000_1111) + + require.Panics(t, func() { isOpBitSet(num, aclOpZero, 0) }) + require.Panics(t, func() { isOpBitSet(num, aclOpLast, 0) }) + + cpNum := num + + require.Panics(t, func() { setOpBit(&num, aclOpZero, 0) }) + require.EqualValues(t, cpNum, num) + require.Panics(t, func() { setOpBit(&num, aclOpLast, 0) }) + require.EqualValues(t, cpNum, num) + + for _, tc := range []struct { + op ACLOp + set [4]bool // is bit set (left-to-right) + bits [4]uint32 // result of setting i-th bit (left-to-right) to zero num + }{ + { + op: ACLOpObjectHash, + set: [4]bool{false, true, false, true}, + bits: [4]uint32{ + 0b_0000_1000_0000_0000_0000_0000_0000_0000, + 0b_0000_0100_0000_0000_0000_0000_0000_0000, + 0b_0000_0010_0000_0000_0000_0000_0000_0000, + 0b_0000_0001_0000_0000_0000_0000_0000_0000, + }, + }, + { + op: ACLOpObjectRange, + set: [4]bool{true, true, false, false}, + bits: [4]uint32{ + 0b_0000_0000_1000_0000_0000_0000_0000_0000, + 0b_0000_0000_0100_0000_0000_0000_0000_0000, + 0b_0000_0000_0010_0000_0000_0000_0000_0000, + 0b_0000_0000_0001_0000_0000_0000_0000_0000, + }, + }, + { + op: ACLOpObjectSearch, + set: [4]bool{false, false, true, true}, + bits: [4]uint32{ + 0b_0000_0000_0000_1000_0000_0000_0000_0000, + 0b_0000_0000_0000_0100_0000_0000_0000_0000, + 0b_0000_0000_0000_0010_0000_0000_0000_0000, + 0b_0000_0000_0000_0001_0000_0000_0000_0000, + }, + }, + { + op: ACLOpObjectDelete, + set: [4]bool{false, true, true, false}, + bits: [4]uint32{ + 0b_0000_0000_0000_0000_1000_0000_0000_0000, + 0b_0000_0000_0000_0000_0100_0000_0000_0000, + 0b_0000_0000_0000_0000_0010_0000_0000_0000, + 0b_0000_0000_0000_0000_0001_0000_0000_0000, + }, + }, + { + op: ACLOpObjectPut, + set: [4]bool{false, true, true, true}, + bits: [4]uint32{ + 0b_0000_0000_0000_0000_0000_1000_0000_0000, + 0b_0000_0000_0000_0000_0000_0100_0000_0000, + 0b_0000_0000_0000_0000_0000_0010_0000_0000, + 0b_0000_0000_0000_0000_0000_0001_0000_0000, + }, + }, + { + op: ACLOpObjectHead, + set: [4]bool{true, false, false, false}, + bits: [4]uint32{ + 0b_0000_0000_0000_0000_0000_0000_1000_0000, + 0b_0000_0000_0000_0000_0000_0000_0100_0000, + 0b_0000_0000_0000_0000_0000_0000_0010_0000, + 0b_0000_0000_0000_0000_0000_0000_0001_0000, + }, + }, + { + op: ACLOpObjectGet, + set: [4]bool{true, true, true, true}, + bits: [4]uint32{ + 0b_0000_0000_0000_0000_0000_0000_0000_1000, + 0b_0000_0000_0000_0000_0000_0000_0000_0100, + 0b_0000_0000_0000_0000_0000_0000_0000_0010, + 0b_0000_0000_0000_0000_0000_0000_0000_0001, + }, + }, + } { + for i := range tc.set { + require.EqualValues(t, tc.set[i], isOpBitSet(num, tc.op, uint8(len(tc.set)-1-i)), + fmt.Sprintf("op %s, left bit #%d", tc.op, i), + ) + } + + for i := range tc.bits { + num := uint32(0) + + setOpBit(&num, tc.op, uint8(len(tc.bits)-1-i)) + + require.EqualValues(t, tc.bits[i], num) + } + } +}