[#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 <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2022-06-16 09:17:41 +03:00 committed by fyrchik
parent af7e20073b
commit e82a2d86ef
13 changed files with 961 additions and 247 deletions

View file

@ -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:
31302928272625242322212019181716 <- Bit
RANGEHASH RANGE SEARCH <- Object service method
X F U S O B U S O B U S O B <- Rule
15141312111009080706050403020100 <- 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

View file

@ -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
}
}

View file

@ -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())
}

81
container/acl.go Normal file
View file

@ -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"
}
}

272
container/acl_basic.go Normal file
View file

@ -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
}

384
container/acl_basic_test.go Normal file
View file

@ -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)
})
}

View file

@ -7,7 +7,6 @@ import (
"github.com/nspcc-dev/neofs-api-go/v2/container" "github.com/nspcc-dev/neofs-api-go/v2/container"
v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
"github.com/nspcc-dev/neofs-api-go/v2/refs" "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" 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/netmap"
"github.com/nspcc-dev/neofs-sdk-go/user" "github.com/nspcc-dev/neofs-sdk-go/user"
@ -23,7 +22,7 @@ type Container struct {
// Defaults: // Defaults:
// - token: nil; // - token: nil;
// - sig: nil; // - sig: nil;
// - basicACL: acl.PrivateBasicRule; // - basicACL: BasicACLPrivate;
// - version: version.Current; // - version: version.Current;
// - nonce: random UUID; // - nonce: random UUID;
// - attr: nil; // - attr: nil;
@ -136,12 +135,13 @@ func (c *Container) SetNonceUUID(v uuid.UUID) {
c.v2.SetNonce(data) c.v2.SetNonce(data)
} }
func (c *Container) BasicACL() uint32 { func (c *Container) BasicACL() (res BasicACL) {
return c.v2.GetBasicACL() res.fromUint32(c.v2.GetBasicACL())
return
} }
func (c *Container) SetBasicACL(v acl.BasicACL) { func (c *Container) SetBasicACL(v BasicACL) {
c.v2.SetBasicACL(uint32(v)) c.v2.SetBasicACL(v.toUint32())
} }
func (c *Container) Attributes() Attributes { func (c *Container) Attributes() Attributes {

View file

@ -5,7 +5,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/nspcc-dev/neofs-api-go/v2/refs" "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" "github.com/nspcc-dev/neofs-sdk-go/container"
containertest "github.com/nspcc-dev/neofs-sdk-go/container/test" containertest "github.com/nspcc-dev/neofs-sdk-go/container/test"
netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test" netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test"
@ -23,7 +22,7 @@ func TestNewContainer(t *testing.T) {
ownerID := usertest.ID() ownerID := usertest.ID()
policy := netmaptest.PlacementPolicy() policy := netmaptest.PlacementPolicy()
c.SetBasicACL(acl.PublicBasicRule) c.SetBasicACL(container.BasicACLPublicRW)
attrs := containertest.Attributes() attrs := containertest.Attributes()
c.SetAttributes(attrs) c.SetAttributes(attrs)
@ -40,7 +39,7 @@ func TestNewContainer(t *testing.T) {
require.EqualValues(t, newContainer.PlacementPolicy(), &policy) require.EqualValues(t, newContainer.PlacementPolicy(), &policy)
require.EqualValues(t, newContainer.Attributes(), attrs) require.EqualValues(t, newContainer.Attributes(), attrs)
require.EqualValues(t, newContainer.BasicACL(), acl.PublicBasicRule) require.EqualValues(t, newContainer.BasicACL(), container.BasicACLPublicRW)
newNonce, err := newContainer.NonceUUID() newNonce, err := newContainer.NonceUUID()
require.NoError(t, err) require.NoError(t, err)
@ -89,7 +88,7 @@ func TestContainer_ToV2(t *testing.T) {
require.Nil(t, cnt.PlacementPolicy()) require.Nil(t, cnt.PlacementPolicy())
require.Nil(t, cnt.OwnerID()) 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()) require.Equal(t, version.Current(), *cnt.Version())
nonce, err := cnt.NonceUUID() nonce, err := cnt.NonceUUID()
@ -108,7 +107,7 @@ func TestContainer_ToV2(t *testing.T) {
require.Nil(t, cntV2.GetPlacementPolicy()) require.Nil(t, cntV2.GetPlacementPolicy())
require.Nil(t, cntV2.GetOwnerID()) require.Nil(t, cntV2.GetOwnerID())
require.Equal(t, uint32(acl.PrivateBasicRule), cntV2.GetBasicACL()) require.EqualValues(t, 0x1C8C8CCC, cntV2.GetBasicACL())
var verV2 refs.Version var verV2 refs.Version
version.Current().WriteToV2(&verV2) version.Current().WriteToV2(&verV2)

30
container/init.go Normal file
View file

@ -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)
}

View file

@ -4,7 +4,6 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"github.com/google/uuid" "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/netmap"
"github.com/nspcc-dev/neofs-sdk-go/user" "github.com/nspcc-dev/neofs-sdk-go/user"
) )
@ -13,7 +12,7 @@ type (
Option func(*containerOptions) Option func(*containerOptions)
containerOptions struct { containerOptions struct {
acl acl.BasicACL acl BasicACL
policy *netmap.PlacementPolicy policy *netmap.PlacementPolicy
attributes Attributes attributes Attributes
owner *user.ID owner *user.ID
@ -28,24 +27,24 @@ func defaultContainerOptions() containerOptions {
} }
return containerOptions{ return containerOptions{
acl: acl.PrivateBasicRule, acl: BasicACLPrivate,
nonce: rand, nonce: rand,
} }
} }
func WithPublicBasicACL() Option { func WithPublicBasicACL() Option {
return func(option *containerOptions) { return func(option *containerOptions) {
option.acl = acl.PublicBasicRule option.acl = BasicACLPublicRW
} }
} }
func WithReadOnlyBasicACL() Option { func WithReadOnlyBasicACL() Option {
return func(option *containerOptions) { 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) { return func(option *containerOptions) {
option.acl = acl option.acl = acl
} }

View file

@ -31,7 +31,7 @@ func Container() *container.Container {
x.SetVersion(&ver) x.SetVersion(&ver)
x.SetAttributes(Attributes()) x.SetAttributes(Attributes())
x.SetOwnerID(usertest.ID()) x.SetOwnerID(usertest.ID())
x.SetBasicACL(123) x.SetBasicACL(container.BasicACLPublicRW)
p := netmaptest.PlacementPolicy() p := netmaptest.PlacementPolicy()
x.SetPlacementPolicy(&p) x.SetPlacementPolicy(&p)

45
container/util.go Normal file
View file

@ -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)
}

133
container/util_test.go Normal file
View file

@ -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)
}
}
}