diff --git a/container/acl.go b/container/acl.go deleted file mode 100644 index 362c42ed..00000000 --- a/container/acl.go +++ /dev/null @@ -1,81 +0,0 @@ -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/acl.go b/container/acl/acl.go new file mode 100644 index 00000000..1df713f7 --- /dev/null +++ b/container/acl/acl.go @@ -0,0 +1,81 @@ +package acl + +import "strconv" + +// Op 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 Op uint32 + +const ( + opZero Op = iota // extreme value for testing + + OpObjectGet // Object.Get rpc + OpObjectHead // Object.Head rpc + OpObjectPut // Object.Put rpc + OpObjectDelete // Object.Delete rpc + OpObjectSearch // Object.Search rpc + OpObjectRange // Object.GetRange rpc + OpObjectHash // Object.GetRangeHash rpc + + opLast // extreme value for testing +) + +// String implements fmt.Stringer. +func (x Op) String() string { + switch x { + default: + return "UNKNOWN#" + strconv.FormatUint(uint64(x), 10) + case OpObjectGet: + return "OBJECT_GET" + case OpObjectHead: + return "OBJECT_HEAD" + case OpObjectPut: + return "OBJECT_PUT" + case OpObjectDelete: + return "OBJECT_DELETE" + case OpObjectSearch: + return "OBJECT_SEARCH" + case OpObjectRange: + return "OBJECT_RANGE" + case OpObjectHash: + return "OBJECT_HASH" + } +} + +// Role 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 Role uint32 + +const ( + roleZero Role = iota // extreme value for testing + + RoleOwner // container owner + RoleContainer // nodes of the related container + RoleInnerRing // Inner Ring nodes + RoleOthers // all others + + roleLast // extreme value for testing +) + +// String implements fmt.Stringer. +func (x Role) String() string { + switch x { + default: + return "UNKNOWN#" + strconv.FormatUint(uint64(x), 10) + case RoleOwner: + return "OWNER" + case RoleContainer: + return "CONTAINER" + case RoleInnerRing: + return "INNER_RING" + case RoleOthers: + return "OTHERS" + } +} diff --git a/container/acl/acl_basic.go b/container/acl/acl_basic.go new file mode 100644 index 00000000..f45ff012 --- /dev/null +++ b/container/acl/acl_basic.go @@ -0,0 +1,278 @@ +package acl + +import ( + "fmt" + "strconv" + "strings" +) + +// Basic 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 exported variables. +// +// Basic instances are comparable: values can be compared directly using +// == operator. +type Basic struct { + bits uint32 +} + +// FromBits decodes Basic from the numerical representation. +// +// See also Bits. +func (x *Basic) FromBits(bits uint32) { + x.bits = bits +} + +// Bits returns numerical encoding of Basic. +// +// See also FromBits. +func (x Basic) Bits() 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 Basic FINAL. FINAL indicates the ACL non-extendability +// in the related container. +// +// See also Extendable. +func (x *Basic) DisableExtension() { + setBit(&x.bits, bitPosFinal) +} + +// Extendable checks if Basic is NOT made FINAL using DisableExtension. +// +// Zero Basic is NOT FINAL or extendable. +func (x Basic) Extendable() bool { + return !isBitSet(x.bits, bitPosFinal) +} + +// MakeSticky makes Basic STICKY. STICKY indicates that only the owner of any +// particular object is allowed to operate on it. +// +// See also Sticky. +func (x *Basic) MakeSticky() { + setBit(&x.bits, bitPosSticky) +} + +// Sticky checks if Basic is made STICKY using MakeSticky. +// +// Zero Basic is NOT STICKY. +func (x Basic) Sticky() bool { + return isBitSet(x.bits, bitPosSticky) +} + +// checks if op is used by the storage nodes within replication mechanism. +func isReplicationOp(op Op) bool { + //nolint:exhaustive + switch op { + case + OpObjectGet, + OpObjectHead, + OpObjectPut, + OpObjectSearch, + OpObjectHash: + return true + } + + return false +} + +// AllowOp allows the parties with the given role to the given operation. +// Op MUST be one of the Op enumeration. Role MUST be one of: +// RoleOwner +// RoleContainer +// RoleOthers +// and if role is RoleContainer, op MUST NOT be: +// OpObjectGet +// OpObjectHead +// OpObjectPut +// OpObjectSearch +// OpObjectHash +// +// See also IsOpAllowed. +func (x *Basic) AllowOp(op Op, role Role) { + var bitPos uint8 + + switch role { + default: + panic(fmt.Sprintf("unable to set rules for unsupported role %v", role)) + case RoleInnerRing: + panic("basic ACL MUST NOT be modified for Inner Ring") + case RoleOwner: + bitPos = opBitPosOwner + case RoleContainer: + if isReplicationOp(op) { + panic("basic ACL for container replication ops MUST NOT be modified") + } + + bitPos = opBitPosContainer + case RoleOthers: + bitPos = opBitPosOthers + } + + setOpBit(&x.bits, op, bitPos) +} + +// IsOpAllowed checks if parties with the given role are allowed to the given op +// according to the Basic rules. Op MUST be one of the Op enumeration. +// Role MUST be one of the Role enumeration. +// +// Members with RoleContainer role have exclusive default access to the +// operations of the data replication mechanism: +// OpObjectGet +// OpObjectHead +// OpObjectPut +// OpObjectSearch +// OpObjectHash +// +// RoleInnerRing members are allowed to data audit ops only: +// OpObjectGet +// OpObjectHead +// OpObjectHash +// OpObjectSearch +// +// Zero Basic prevents any role from accessing any operation in the absence +// of default rights. +// +// See also AllowOp. +func (x Basic) IsOpAllowed(op Op, role Role) bool { + var bitPos uint8 + + switch role { + default: + panic(fmt.Sprintf("role is unsupported %v", role)) + case RoleInnerRing: + switch op { + case + OpObjectGet, + OpObjectHead, + OpObjectHash, + OpObjectSearch: + return true + default: + return false + } + case RoleOwner: + bitPos = opBitPosOwner + case RoleContainer: + if isReplicationOp(op) { + return true + } + + bitPos = opBitPosContainer + case RoleOthers: + 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 *Basic) AllowBearerRules(op Op) { + setOpBit(&x.bits, op, opBitPosBearer) +} + +// AllowedBearerRules checks if bearer rules are allowed using AllowBearerRules. +// Op MUST be one of the Op enumeration. +// +// Zero Basic disallows bearer rules for any op. +func (x Basic) AllowedBearerRules(op Op) bool { + return isOpBitSet(x.bits, op, opBitPosBearer) +} + +// EncodeToString encodes Basic into hexadecimal string. +// +// See also DecodeString. +func (x Basic) EncodeToString() string { + return strconv.FormatUint(uint64(x.bits), 16) +} + +// Names of the frequently used Basic values. +const ( + NamePrivate = "private" + NamePrivateExtended = "eacl-private" + NamePublicRO = "public-read" + NamePublicROExtended = "eacl-public-read" + NamePublicRW = "public-read-write" + NamePublicRWExtended = "eacl-public-read-write" + NamePublicAppend = "public-append" + NamePublicAppendExtended = "eacl-public-append" +) + +// Frequently used Basic values (each value MUST NOT be modified, make a +// copy instead). +var ( + Private Basic // private + PrivateExtended Basic // eacl-private + PublicRO Basic // public-read + PublicROExtended Basic // eacl-public-read + PublicRW Basic // public-read-write + PublicRWExtended Basic // eacl-public-read-write + PublicAppend Basic // public-append + PublicAppendExtended Basic // eacl-public-append +) + +// DecodeString decodes string calculated using EncodeToString. Also supports +// human-readable names (Name* constants). +func (x *Basic) DecodeString(s string) (e error) { + switch s { + case NamePrivate: + *x = Private + case NamePrivateExtended: + *x = PrivateExtended + case NamePublicRO: + *x = PublicRO + case NamePublicROExtended: + *x = PublicROExtended + case NamePublicRW: + *x = PublicRW + case NamePublicRWExtended: + *x = PublicRWExtended + case NamePublicAppend: + *x = PublicAppend + case NamePublicAppendExtended: + *x = PublicAppendExtended + 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/acl_basic_test.go similarity index 59% rename from container/acl_basic_test.go rename to container/acl/acl_basic_test.go index 595d2f19..89107647 100644 --- a/container/acl_basic_test.go +++ b/container/acl/acl_basic_test.go @@ -1,4 +1,4 @@ -package container +package acl import ( "testing" @@ -6,51 +6,51 @@ import ( "github.com/stretchr/testify/require" ) -func TestBasicACL_DisableExtension(t *testing.T) { - var val, val2 BasicACL +func TestBasic_DisableExtension(t *testing.T) { + var val, val2 Basic require.True(t, val.Extendable()) - val2.fromUint32(val.toUint32()) + val2.FromBits(val.Bits()) require.True(t, val2.Extendable()) val.DisableExtension() require.False(t, val.Extendable()) - val2.fromUint32(val.toUint32()) + val2.FromBits(val.Bits()) require.False(t, val2.Extendable()) } -func TestBasicACL_MakeSticky(t *testing.T) { - var val, val2 BasicACL +func TestBasic_MakeSticky(t *testing.T) { + var val, val2 Basic require.False(t, val.Sticky()) - val2.fromUint32(val.toUint32()) + val2.FromBits(val.Bits()) require.False(t, val2.Sticky()) val.MakeSticky() require.True(t, val.Sticky()) - val2.fromUint32(val.toUint32()) + val2.FromBits(val.Bits()) require.True(t, val2.Sticky()) } -func TestBasicACL_AllowBearerRules(t *testing.T) { - var val BasicACL +func TestBasic_AllowBearerRules(t *testing.T) { + var val Basic - require.Panics(t, func() { val.AllowBearerRules(aclOpZero) }) - require.Panics(t, func() { val.AllowBearerRules(aclOpLast) }) + require.Panics(t, func() { val.AllowBearerRules(opZero) }) + require.Panics(t, func() { val.AllowBearerRules(opLast) }) - require.Panics(t, func() { val.AllowedBearerRules(aclOpZero) }) - require.Panics(t, func() { val.AllowedBearerRules(aclOpLast) }) + require.Panics(t, func() { val.AllowedBearerRules(opZero) }) + require.Panics(t, func() { val.AllowedBearerRules(opLast) }) - for op := aclOpZero + 1; op < aclOpLast; op++ { + for op := opZero + 1; op < opLast; op++ { val := val require.False(t, val.AllowedBearerRules(op)) val.AllowBearerRules(op) - for j := aclOpZero + 1; j < aclOpLast; j++ { + for j := opZero + 1; j < opLast; j++ { if j == op { require.True(t, val.AllowedBearerRules(j), op) } else { @@ -60,39 +60,39 @@ func TestBasicACL_AllowBearerRules(t *testing.T) { } } -func TestBasicACL_AllowOp(t *testing.T) { - var val, val2 BasicACL +func TestBasic_AllowOp(t *testing.T) { + var val, val2 Basic - 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) }) + require.Panics(t, func() { val.IsOpAllowed(opZero, roleZero+1) }) + require.Panics(t, func() { val.IsOpAllowed(opLast, roleZero+1) }) + require.Panics(t, func() { val.IsOpAllowed(opZero+1, roleZero) }) + require.Panics(t, func() { val.IsOpAllowed(opZero+1, roleLast) }) - for op := aclOpZero + 1; op < aclOpLast; op++ { - require.Panics(t, func() { val.AllowOp(op, ACLRoleInnerRing) }) + for op := opZero + 1; op < opLast; op++ { + require.Panics(t, func() { val.AllowOp(op, RoleInnerRing) }) if isReplicationOp(op) { - require.Panics(t, func() { val.AllowOp(op, ACLRoleContainer) }) - require.True(t, val.IsOpAllowed(op, ACLRoleContainer)) + require.Panics(t, func() { val.AllowOp(op, RoleContainer) }) + require.True(t, val.IsOpAllowed(op, RoleContainer)) } } - 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)) + require.True(t, val.IsOpAllowed(OpObjectGet, RoleInnerRing)) + require.True(t, val.IsOpAllowed(OpObjectHead, RoleInnerRing)) + require.True(t, val.IsOpAllowed(OpObjectSearch, RoleInnerRing)) + require.True(t, val.IsOpAllowed(OpObjectHash, RoleInnerRing)) - const op = aclOpZero + 1 - const role = ACLRoleOthers + const op = opZero + 1 + const role = RoleOthers require.False(t, val.IsOpAllowed(op, role)) - val2.fromUint32(val.toUint32()) + val2.FromBits(val.Bits()) require.False(t, val2.IsOpAllowed(op, role)) val.AllowOp(op, role) require.True(t, val.IsOpAllowed(op, role)) - val2.fromUint32(val.toUint32()) + val2.FromBits(val.Bits()) require.True(t, val2.IsOpAllowed(op, role)) } @@ -100,21 +100,21 @@ 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) +func testOp(t *testing.T, v Basic, op Op, exp opsExpected) { + require.Equal(t, exp.owner, v.IsOpAllowed(op, RoleOwner), op) + require.Equal(t, exp.container, v.IsOpAllowed(op, RoleContainer), op) + require.Equal(t, exp.innerRing, v.IsOpAllowed(op, RoleInnerRing), op) + require.Equal(t, exp.others, v.IsOpAllowed(op, RoleOthers), op) require.Equal(t, exp.bearer, v.AllowedBearerRules(op), op) } type expected struct { extendable, sticky bool - mOps map[ACLOp]opsExpected + mOps map[Op]opsExpected } -func testBasicACLPredefined(t *testing.T, val BasicACL, name string, exp expected) { +func testBasicPredefined(t *testing.T, val Basic, name string, exp expected) { require.Equal(t, exp.sticky, val.Sticky()) require.Equal(t, exp.extendable, val.Extendable()) @@ -124,7 +124,7 @@ func testBasicACLPredefined(t *testing.T, val BasicACL, name string, exp expecte s := val.EncodeToString() - var val2 BasicACL + var val2 Basic require.NoError(t, val2.DecodeString(s)) require.Equal(t, val, val2) @@ -133,55 +133,55 @@ func testBasicACLPredefined(t *testing.T, val BasicACL, name string, exp expecte require.Equal(t, val, val2) } -func TestBasicACLPredefined(t *testing.T) { +func TestBasicPredefined(t *testing.T) { t.Run("private", func(t *testing.T) { exp := expected{ extendable: false, sticky: false, - mOps: map[ACLOp]opsExpected{ - ACLOpObjectHash: { + mOps: map[Op]opsExpected{ + OpObjectHash: { owner: true, container: true, innerRing: true, others: false, bearer: false, }, - ACLOpObjectRange: { + OpObjectRange: { owner: true, container: false, innerRing: false, others: false, bearer: false, }, - ACLOpObjectSearch: { + OpObjectSearch: { owner: true, container: true, innerRing: true, others: false, bearer: false, }, - ACLOpObjectDelete: { + OpObjectDelete: { owner: true, container: false, innerRing: false, others: false, bearer: false, }, - ACLOpObjectPut: { + OpObjectPut: { owner: true, container: true, innerRing: false, others: false, bearer: false, }, - ACLOpObjectHead: { + OpObjectHead: { owner: true, container: true, innerRing: true, others: false, bearer: false, }, - ACLOpObjectGet: { + OpObjectGet: { owner: true, container: true, innerRing: true, @@ -191,59 +191,59 @@ func TestBasicACLPredefined(t *testing.T) { }, } - testBasicACLPredefined(t, BasicACLPrivate, BasicACLNamePrivate, exp) + testBasicPredefined(t, Private, NamePrivate, exp) exp.extendable = true - testBasicACLPredefined(t, BasicACLPrivateExtended, BasicACLNamePrivateExtended, exp) + testBasicPredefined(t, PrivateExtended, NamePrivateExtended, exp) }) t.Run("public-read", func(t *testing.T) { exp := expected{ extendable: false, sticky: false, - mOps: map[ACLOp]opsExpected{ - ACLOpObjectHash: { + mOps: map[Op]opsExpected{ + OpObjectHash: { owner: true, container: true, innerRing: true, others: true, bearer: true, }, - ACLOpObjectRange: { + OpObjectRange: { owner: true, container: false, innerRing: false, others: true, bearer: true, }, - ACLOpObjectSearch: { + OpObjectSearch: { owner: true, container: true, innerRing: true, others: true, bearer: true, }, - ACLOpObjectDelete: { + OpObjectDelete: { owner: true, container: false, innerRing: false, others: false, bearer: false, }, - ACLOpObjectPut: { + OpObjectPut: { owner: true, container: true, innerRing: false, others: false, bearer: false, }, - ACLOpObjectHead: { + OpObjectHead: { owner: true, container: true, innerRing: true, others: true, bearer: true, }, - ACLOpObjectGet: { + OpObjectGet: { owner: true, container: true, innerRing: true, @@ -253,59 +253,59 @@ func TestBasicACLPredefined(t *testing.T) { }, } - testBasicACLPredefined(t, BasicACLPublicRO, BasicACLNamePublicRO, exp) + testBasicPredefined(t, PublicRO, NamePublicRO, exp) exp.extendable = true - testBasicACLPredefined(t, BasicACLPublicROExtended, BasicACLNamePublicROExtended, exp) + testBasicPredefined(t, PublicROExtended, NamePublicROExtended, exp) }) t.Run("public-read-write", func(t *testing.T) { exp := expected{ extendable: false, sticky: false, - mOps: map[ACLOp]opsExpected{ - ACLOpObjectHash: { + mOps: map[Op]opsExpected{ + OpObjectHash: { owner: true, container: true, innerRing: true, others: true, bearer: true, }, - ACLOpObjectRange: { + OpObjectRange: { owner: true, container: false, innerRing: false, others: true, bearer: true, }, - ACLOpObjectSearch: { + OpObjectSearch: { owner: true, container: true, innerRing: true, others: true, bearer: true, }, - ACLOpObjectDelete: { + OpObjectDelete: { owner: true, container: false, innerRing: false, others: true, bearer: true, }, - ACLOpObjectPut: { + OpObjectPut: { owner: true, container: true, innerRing: false, others: true, bearer: true, }, - ACLOpObjectHead: { + OpObjectHead: { owner: true, container: true, innerRing: true, others: true, bearer: true, }, - ACLOpObjectGet: { + OpObjectGet: { owner: true, container: true, innerRing: true, @@ -315,59 +315,59 @@ func TestBasicACLPredefined(t *testing.T) { }, } - testBasicACLPredefined(t, BasicACLPublicRW, BasicACLNamePublicRW, exp) + testBasicPredefined(t, PublicRW, NamePublicRW, exp) exp.extendable = true - testBasicACLPredefined(t, BasicACLPublicRWExtended, BasicACLNamePublicRWExtended, exp) + testBasicPredefined(t, PublicRWExtended, NamePublicRWExtended, exp) }) t.Run("public-append", func(t *testing.T) { exp := expected{ extendable: false, sticky: false, - mOps: map[ACLOp]opsExpected{ - ACLOpObjectHash: { + mOps: map[Op]opsExpected{ + OpObjectHash: { owner: true, container: true, innerRing: true, others: true, bearer: true, }, - ACLOpObjectRange: { + OpObjectRange: { owner: true, container: false, innerRing: false, others: true, bearer: true, }, - ACLOpObjectSearch: { + OpObjectSearch: { owner: true, container: true, innerRing: true, others: true, bearer: true, }, - ACLOpObjectDelete: { + OpObjectDelete: { owner: true, container: false, innerRing: false, others: false, bearer: true, }, - ACLOpObjectPut: { + OpObjectPut: { owner: true, container: true, innerRing: false, others: true, bearer: true, }, - ACLOpObjectHead: { + OpObjectHead: { owner: true, container: true, innerRing: true, others: true, bearer: true, }, - ACLOpObjectGet: { + OpObjectGet: { owner: true, container: true, innerRing: true, @@ -377,8 +377,8 @@ func TestBasicACLPredefined(t *testing.T) { }, } - testBasicACLPredefined(t, BasicACLPublicAppend, BasicACLNamePublicAppend, exp) + testBasicPredefined(t, PublicAppend, NamePublicAppend, exp) exp.extendable = true - testBasicACLPredefined(t, BasicACLPublicAppendExtended, BasicACLNamePublicAppendExtended, exp) + testBasicPredefined(t, PublicAppendExtended, NamePublicAppendExtended, exp) }) } diff --git a/container/acl/doc.go b/container/acl/doc.go new file mode 100644 index 00000000..568f31aa --- /dev/null +++ b/container/acl/doc.go @@ -0,0 +1,9 @@ +/* +Package acl provides functionality to control access to data and operations on them in NeoFS containers. + +Type Basic represents basic ACL of the NeoFS container which specifies the general order of data access. +Basic provides interface of rule composition. Package acl also exports some frequently used settings like +private or public. + +*/ +package acl diff --git a/container/acl/init.go b/container/acl/init.go new file mode 100644 index 00000000..94c1ce10 --- /dev/null +++ b/container/acl/init.go @@ -0,0 +1,30 @@ +package acl + +func init() { + // left-to-right order of the object operations + orderedOps := [...]Op{ + OpObjectGet, + OpObjectHead, + OpObjectPut, + OpObjectDelete, + OpObjectSearch, + OpObjectRange, + OpObjectHash, + } + + mOrder = make(map[Op]uint8, len(orderedOps)) + + for i := range orderedOps { + mOrder[orderedOps[i]] = uint8(i) + } + + // numbers are taken from NeoFS Specification + Private.FromBits(0x1C8C8CCC) + PrivateExtended.FromBits(0x0C8C8CCC) + PublicRO.FromBits(0x1FBF8CFF) + PublicROExtended.FromBits(0x0FBF8CFF) + PublicRW.FromBits(0x1FBFBFFF) + PublicRWExtended.FromBits(0x0FBFBFFF) + PublicAppend.FromBits(0x1FBF9FFF) + PublicAppendExtended.FromBits(0x0FBF9FFF) +} diff --git a/container/util.go b/container/acl/util.go similarity index 78% rename from container/util.go rename to container/acl/util.go index 025d2eb3..91c4b216 100644 --- a/container/util.go +++ b/container/acl/util.go @@ -1,4 +1,4 @@ -package container +package acl import "fmt" @@ -21,11 +21,11 @@ func isBitSet(num uint32, n uint8) bool { return mask != 0 && num&mask == mask } -// maps ACLOp to op-section index in BasicACL. Filled on init. -var mOrder map[ACLOp]uint8 +// maps Op to op-section index in Basic. Filled on init. +var mOrder map[Op]uint8 // sets n-th bit in num for the given op. Panics if op is unsupported. -func setOpBit(num *uint32, op ACLOp, opBitPos uint8) { +func setOpBit(num *uint32, op Op, opBitPos uint8) { n, ok := mOrder[op] if !ok { panic(fmt.Sprintf("op is unsupported %v", op)) @@ -35,7 +35,7 @@ func setOpBit(num *uint32, op ACLOp, opBitPos uint8) { } // 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 { +func isOpBitSet(num uint32, op Op, n uint8) bool { off, ok := mOrder[op] if !ok { panic(fmt.Sprintf("op is unsupported %v", op)) diff --git a/container/util_test.go b/container/acl/util_test.go similarity index 87% rename from container/util_test.go rename to container/acl/util_test.go index 1b3cdd67..7881c175 100644 --- a/container/util_test.go +++ b/container/acl/util_test.go @@ -1,4 +1,4 @@ -package container +package acl import ( "fmt" @@ -30,23 +30,23 @@ func TestBits(t *testing.T) { 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) }) + require.Panics(t, func() { isOpBitSet(num, opZero, 0) }) + require.Panics(t, func() { isOpBitSet(num, opLast, 0) }) cpNum := num - require.Panics(t, func() { setOpBit(&num, aclOpZero, 0) }) + require.Panics(t, func() { setOpBit(&num, opZero, 0) }) require.EqualValues(t, cpNum, num) - require.Panics(t, func() { setOpBit(&num, aclOpLast, 0) }) + require.Panics(t, func() { setOpBit(&num, opLast, 0) }) require.EqualValues(t, cpNum, num) for _, tc := range []struct { - op ACLOp + op Op 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, + op: OpObjectHash, set: [4]bool{false, true, false, true}, bits: [4]uint32{ 0b_0000_1000_0000_0000_0000_0000_0000_0000, @@ -56,7 +56,7 @@ func TestOpBits(t *testing.T) { }, }, { - op: ACLOpObjectRange, + op: OpObjectRange, set: [4]bool{true, true, false, false}, bits: [4]uint32{ 0b_0000_0000_1000_0000_0000_0000_0000_0000, @@ -66,7 +66,7 @@ func TestOpBits(t *testing.T) { }, }, { - op: ACLOpObjectSearch, + op: OpObjectSearch, set: [4]bool{false, false, true, true}, bits: [4]uint32{ 0b_0000_0000_0000_1000_0000_0000_0000_0000, @@ -76,7 +76,7 @@ func TestOpBits(t *testing.T) { }, }, { - op: ACLOpObjectDelete, + op: OpObjectDelete, set: [4]bool{false, true, true, false}, bits: [4]uint32{ 0b_0000_0000_0000_0000_1000_0000_0000_0000, @@ -86,7 +86,7 @@ func TestOpBits(t *testing.T) { }, }, { - op: ACLOpObjectPut, + op: OpObjectPut, set: [4]bool{false, true, true, true}, bits: [4]uint32{ 0b_0000_0000_0000_0000_0000_1000_0000_0000, @@ -96,7 +96,7 @@ func TestOpBits(t *testing.T) { }, }, { - op: ACLOpObjectHead, + op: OpObjectHead, set: [4]bool{true, false, false, false}, bits: [4]uint32{ 0b_0000_0000_0000_0000_0000_0000_1000_0000, @@ -106,7 +106,7 @@ func TestOpBits(t *testing.T) { }, }, { - op: ACLOpObjectGet, + op: OpObjectGet, set: [4]bool{true, true, true, true}, bits: [4]uint32{ 0b_0000_0000_0000_0000_0000_0000_0000_1000, diff --git a/container/acl_basic.go b/container/acl_basic.go deleted file mode 100644 index 10a2a087..00000000 --- a/container/acl_basic.go +++ /dev/null @@ -1,272 +0,0 @@ -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/container.go b/container/container.go index 40075e23..3834cdd7 100644 --- a/container/container.go +++ b/container/container.go @@ -7,6 +7,7 @@ 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/container/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" @@ -22,7 +23,7 @@ type Container struct { // Defaults: // - token: nil; // - sig: nil; -// - basicACL: BasicACLPrivate; +// - basicACL: Private; // - version: version.Current; // - nonce: random UUID; // - attr: nil; @@ -135,13 +136,13 @@ func (c *Container) SetNonceUUID(v uuid.UUID) { c.v2.SetNonce(data) } -func (c *Container) BasicACL() (res BasicACL) { - res.fromUint32(c.v2.GetBasicACL()) +func (c *Container) BasicACL() (res acl.Basic) { + res.FromBits(c.v2.GetBasicACL()) return } -func (c *Container) SetBasicACL(v BasicACL) { - c.v2.SetBasicACL(v.toUint32()) +func (c *Container) SetBasicACL(v acl.Basic) { + c.v2.SetBasicACL(v.Bits()) } func (c *Container) Attributes() Attributes { diff --git a/container/container_test.go b/container/container_test.go index a5d63cfe..1d5ecef2 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/uuid" "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-sdk-go/container" + "github.com/nspcc-dev/neofs-sdk-go/container/acl" containertest "github.com/nspcc-dev/neofs-sdk-go/container/test" netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" @@ -22,7 +23,7 @@ func TestNewContainer(t *testing.T) { ownerID := usertest.ID() policy := netmaptest.PlacementPolicy() - c.SetBasicACL(container.BasicACLPublicRW) + c.SetBasicACL(acl.PublicRW) attrs := containertest.Attributes() c.SetAttributes(attrs) @@ -39,7 +40,7 @@ func TestNewContainer(t *testing.T) { require.EqualValues(t, newContainer.PlacementPolicy(), &policy) require.EqualValues(t, newContainer.Attributes(), attrs) - require.EqualValues(t, newContainer.BasicACL(), container.BasicACLPublicRW) + require.EqualValues(t, newContainer.BasicACL(), acl.PublicRW) newNonce, err := newContainer.NonceUUID() require.NoError(t, err) @@ -88,7 +89,7 @@ func TestContainer_ToV2(t *testing.T) { require.Nil(t, cnt.PlacementPolicy()) require.Nil(t, cnt.OwnerID()) - require.EqualValues(t, container.BasicACLPrivate, cnt.BasicACL()) + require.EqualValues(t, acl.Private, cnt.BasicACL()) require.Equal(t, version.Current(), *cnt.Version()) nonce, err := cnt.NonceUUID() diff --git a/container/init.go b/container/init.go deleted file mode 100644 index e06949ed..00000000 --- a/container/init.go +++ /dev/null @@ -1,30 +0,0 @@ -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 2fe7f3e8..52fdebd0 100644 --- a/container/opts.go +++ b/container/opts.go @@ -4,6 +4,7 @@ import ( "crypto/ecdsa" "github.com/google/uuid" + "github.com/nspcc-dev/neofs-sdk-go/container/acl" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/user" ) @@ -12,7 +13,7 @@ type ( Option func(*containerOptions) containerOptions struct { - acl BasicACL + acl acl.Basic policy *netmap.PlacementPolicy attributes Attributes owner *user.ID @@ -27,24 +28,24 @@ func defaultContainerOptions() containerOptions { } return containerOptions{ - acl: BasicACLPrivate, + acl: acl.Private, nonce: rand, } } func WithPublicBasicACL() Option { return func(option *containerOptions) { - option.acl = BasicACLPublicRW + option.acl = acl.PublicRW } } func WithReadOnlyBasicACL() Option { return func(option *containerOptions) { - option.acl = BasicACLPublicRO + option.acl = acl.PublicRO } } -func WithCustomBasicACL(acl BasicACL) Option { +func WithCustomBasicACL(acl acl.Basic) Option { return func(option *containerOptions) { option.acl = acl } diff --git a/container/test/generate.go b/container/test/generate.go index e733365a..036a65f8 100644 --- a/container/test/generate.go +++ b/container/test/generate.go @@ -2,6 +2,7 @@ package containertest import ( "github.com/nspcc-dev/neofs-sdk-go/container" + "github.com/nspcc-dev/neofs-sdk-go/container/acl" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" @@ -31,7 +32,7 @@ func Container() *container.Container { x.SetVersion(&ver) x.SetAttributes(Attributes()) x.SetOwnerID(usertest.ID()) - x.SetBasicACL(container.BasicACLPublicRW) + x.SetBasicACL(BasicACL()) p := netmaptest.PlacementPolicy() x.SetPlacementPolicy(&p) @@ -48,3 +49,9 @@ func UsedSpaceAnnouncement() *container.UsedSpaceAnnouncement { return x } + +// BasicACL returns random acl.Basic. +func BasicACL() (x acl.Basic) { + x.FromBits(0xffffffff) + return +}