diff --git a/pkg/acl/acl.go b/pkg/acl/acl.go new file mode 100644 index 000000000000..f2ffc97d7cab --- /dev/null +++ b/pkg/acl/acl.go @@ -0,0 +1,211 @@ +/* + * JuiceFS, Copyright 2024 Juicedata, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package acl + +import ( + "fmt" + "hash/crc32" + + "github.com/juicedata/juicefs/pkg/utils" +) + +const Version uint8 = 2 + +type Entry struct { + Id uint32 + Perm uint16 +} + +type Entries []Entry + +func (es *Entries) Len() int { return len(*es) } +func (es *Entries) Less(i, j int) bool { return (*es)[i].Id < (*es)[j].Id } +func (es *Entries) Swap(i, j int) { (*es)[i], (*es)[j] = (*es)[j], (*es)[i] } + +func (es *Entries) IsEqual(other *Entries) bool { + if es.Len() != other.Len() { + return false + } + for i := 0; i < es.Len(); i++ { + if (*es)[i].Id != (*other)[i].Id || (*es)[i].Perm != (*other)[i].Perm { + return false + } + } + return true +} + +func (es *Entries) Encode() []byte { + w := utils.NewBuffer(uint32(es.Len() * 6)) + for _, e := range *es { + w.Put32(e.Id) + w.Put16(e.Perm) + } + return w.Bytes() +} + +func (es *Entries) Decode(data []byte) { + r := utils.ReadBuffer(data) + for r.HasMore() { + *es = append(*es, Entry{ + Id: r.Get32(), + Perm: r.Get16(), + }) + } +} + +// Rule acl rule +type Rule struct { + Owner uint16 + Group uint16 + Mask uint16 + Other uint16 + NamedUsers Entries + NamedGroups Entries +} + +func (r *Rule) String() string { + return fmt.Sprintf("owner %o, group %o, mask %o, other %o, named users: %+v, named group %+v", + r.Owner, r.Group, r.Mask, r.Other, r.NamedUsers, r.NamedGroups) +} + +func (r *Rule) Encode() []byte { + w := utils.NewBuffer(uint32(16 + (len(r.NamedUsers)+len(r.NamedGroups))*6)) + w.Put16(r.Owner) + w.Put16(r.Group) + w.Put16(r.Mask) + w.Put16(r.Other) + w.Put32(uint32(len(r.NamedUsers))) + for _, entry := range r.NamedUsers { + w.Put32(entry.Id) + w.Put16(entry.Perm) + } + w.Put32(uint32(len(r.NamedGroups))) + for _, entry := range r.NamedGroups { + w.Put32(entry.Id) + w.Put16(entry.Perm) + } + return w.Bytes() +} + +func (r *Rule) Decode(buf []byte) { + rb := utils.ReadBuffer(buf) + r.Owner = rb.Get16() + r.Group = rb.Get16() + r.Mask = rb.Get16() + r.Other = rb.Get16() + uCnt := rb.Get32() + r.NamedUsers = make([]Entry, uCnt) + for i := 0; i < int(uCnt); i++ { + r.NamedUsers[i].Id = rb.Get32() + r.NamedUsers[i].Perm = rb.Get16() + } + + gCnt := rb.Get32() + r.NamedGroups = make([]Entry, gCnt) + for i := 0; i < int(gCnt); i++ { + r.NamedGroups[i].Id = rb.Get32() + r.NamedGroups[i].Perm = rb.Get16() + } +} + +func EmptyRule() *Rule { + return &Rule{ + Owner: 0xFFFF, + Group: 0xFFFF, + Other: 0xFFFF, + Mask: 0xFFFF, + } +} + +func (r *Rule) IsEmpty() bool { + return len(r.NamedUsers)+len(r.NamedGroups) == 0 && + r.Owner&r.Group&r.Other&r.Mask == 0xFFFF +} + +// IsMinimal just like normal permission +func (r *Rule) IsMinimal() bool { + return len(r.NamedGroups)+len(r.NamedUsers) == 0 && r.Mask == 0xFFFF +} + +func (r *Rule) IsEqual(other *Rule) bool { + if r.Owner != other.Owner || r.Group != other.Group || r.Mask != other.Mask || r.Other != other.Other { + return false + } + + return r.NamedUsers.IsEqual(&other.NamedUsers) && + r.NamedGroups.IsEqual(&other.NamedGroups) +} + +// InheritPerms from normal permission +func (r *Rule) InheritPerms(mode uint16) { + if r.Owner == 0xFFFF { + r.Owner = (mode >> 6) & 7 + } + if r.Group == 0xFFFF { + r.Group = (mode >> 3) & 7 + } + if r.Other == 0xFFFF { + r.Other = mode & 7 + } +} + +func (r *Rule) SetMode(mode uint16) { + r.Owner &= 0xFFF8 + r.Owner |= (mode >> 6) & 7 + + if r.IsMinimal() { + r.Group &= 0xFFF8 + r.Group |= (mode >> 3) & 7 + } else { + r.Mask &= 0xFFF8 + r.Mask |= (mode >> 3) & 7 + } + r.Other &= 0xFFF8 + r.Other |= mode & 7 +} + +func (r *Rule) GetMode() uint16 { + if r.IsMinimal() { + return ((r.Owner & 7) << 6) | ((r.Group & 7) << 3) | (r.Other & 7) + } + return ((r.Owner & 7) << 6) | ((r.Mask & 7) << 3) | (r.Other & 7) +} + +// ChildAccessACL return the child node access acl with this default acl +func (r *Rule) ChildAccessACL(mode uint16) *Rule { + cRule := &Rule{} + cRule.Owner = (mode >> 6) & 7 & r.Owner + cRule.Mask = (mode >> 3) & 7 & r.Mask + cRule.Other = mode & 7 & r.Other + + cRule.Group = r.Group + cRule.NamedUsers = r.NamedUsers + cRule.NamedGroups = r.NamedGroups + return cRule +} + +var crc32c = crc32.MakeTable(crc32.Castagnoli) + +func (r *Rule) Checksum() uint32 { + return crc32.Checksum(r.Encode(), crc32c) +} + +const ( + TypeNone = iota + TypeAccess + TypeDefault +) diff --git a/pkg/acl/cache.go b/pkg/acl/cache.go new file mode 100644 index 000000000000..d39fbc82b1f9 --- /dev/null +++ b/pkg/acl/cache.go @@ -0,0 +1,146 @@ +/* + * JuiceFS, Copyright 2024 Juicedata, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package acl + +import ( + "sort" + "sync" +) + +const None = 0 + +// Cache all rules +// - cache all rules when meta init. +// - on getfacl failure, read and cache rule from meta. +// - on setfacl success, read and cache all missed rules from meta. (considered as a low-frequency operation) +// - concurrent mounts may result in duplicate rules. +type Cache interface { + Put(id uint32, r *Rule) + Get(id uint32) *Rule + GetId(r *Rule) uint32 + Size() int + GetMissIds(maxId uint32) []uint32 +} + +func NewCache() Cache { + return &cache{ + lock: sync.RWMutex{}, + maxId: None, + id2Rule: make(map[uint32]*Rule), + cksum2Id: make(map[uint32][]uint32), + } +} + +type cache struct { + lock sync.RWMutex + maxId uint32 + id2Rule map[uint32]*Rule + cksum2Id map[uint32][]uint32 +} + +// GetMissIds return all miss ids from 1 to max(maxId, c.maxId) +func (c *cache) GetMissIds(maxId uint32) []uint32 { + c.lock.RLock() + defer c.lock.RUnlock() + + if c.maxId == maxId && uint32(len(c.id2Rule)) == maxId { + return nil + } + + if c.maxId > maxId { + maxId = c.maxId + } + + n := maxId + 1 + mark := make([]bool, n) + for i := uint32(1); i < n; i++ { + if _, ok := c.id2Rule[i]; ok { + mark[i] = true + } + } + + var ret []uint32 + for i := uint32(1); i < n; i++ { + if !mark[i] { + ret = append(ret, i) + } + } + return ret +} + +func (c *cache) Size() int { + c.lock.RLock() + defer c.lock.RUnlock() + return len(c.id2Rule) +} + +func (c *cache) Get(id uint32) *Rule { + c.lock.RLock() + defer c.lock.RUnlock() + if r, ok := c.id2Rule[id]; ok { + return r + } + return nil +} + +func (c *cache) Put(id uint32, r *Rule) { + c.lock.Lock() + defer c.lock.Unlock() + + if _, ok := c.id2Rule[id]; ok { + return + } + + if id > c.maxId { + c.maxId = id + } + + c.id2Rule[id] = r + + // empty slot + if r == nil { + return + } + + sort.Sort(&c.id2Rule[id].NamedUsers) + sort.Sort(&c.id2Rule[id].NamedGroups) + + cksum := r.Checksum() + if _, ok := c.cksum2Id[cksum]; ok { + c.cksum2Id[cksum] = append(c.cksum2Id[cksum], id) + } else { + c.cksum2Id[r.Checksum()] = []uint32{id} + } +} + +func (c *cache) GetId(r *Rule) uint32 { + c.lock.RLock() + defer c.lock.RUnlock() + + if r == nil { + return None + } + + if ids, ok := c.cksum2Id[r.Checksum()]; ok { + for _, id := range ids { + if r.IsEqual(c.id2Rule[id]) { + return id + } + } + } + return None +} diff --git a/pkg/acl/cache_test.go b/pkg/acl/cache_test.go new file mode 100644 index 000000000000..760d5d529185 --- /dev/null +++ b/pkg/acl/cache_test.go @@ -0,0 +1,75 @@ +/* + * JuiceFS, Copyright 2024 Juicedata, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package acl + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCache(t *testing.T) { + rule := &Rule{ + Owner: 6, + Group: 4, + Mask: 4, + Other: 4, + NamedUsers: Entries{ + { + Id: 2, + Perm: 2, + }, + { + Id: 1, + Perm: 1, + }, + }, + NamedGroups: Entries{ + { + Id: 4, + Perm: 4, + }, + { + Id: 3, + Perm: 3, + }, + }, + } + + c := NewCache() + c.Put(1, rule) + c.Put(2, rule) + assert.True(t, rule.IsEqual(c.Get(1))) + assert.True(t, rule.IsEqual(c.Get(2))) + assert.Equal(t, uint32(1), c.GetId(rule)) + assert.Equal(t, uint32(1), c.Get(1).NamedUsers[0].Id) // sorted + + rule2 := &Rule{} + *rule2 = *rule + rule2.Owner = 4 + + c.Put(3, rule2) + assert.Equal(t, uint32(3), c.GetId(rule2)) + + c.Put(8, rule2) + assert.Equal(t, []uint32{4, 5, 6, 7, 9, 10}, c.GetMissIds(10)) + assert.Equal(t, []uint32{4, 5, 6, 7}, c.GetMissIds(6)) + + assert.NotPanics(t, func() { + c.Put(10, nil) + }) +}