From 7ddf2cc2e899fa26a64669ff42b943975bbf6bfd Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Fri, 8 Mar 2024 09:05:31 -0800 Subject: [PATCH] feat: posix read permission check wip --- backend/acl/acl.go | 272 +++++++++++++++++++++++++++++++++++++++++ backend/acl/perm.go | 63 ++++++++++ backend/posix/posix.go | 26 +++- 3 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 backend/acl/acl.go create mode 100644 backend/acl/perm.go diff --git a/backend/acl/acl.go b/backend/acl/acl.go new file mode 100644 index 0000000..547914a --- /dev/null +++ b/backend/acl/acl.go @@ -0,0 +1,272 @@ +// Copyright 2020 the authors. +// +// Licensed under the Apache License, Version 2.0 (the LICENSE-APACHE file) or +// the MIT license (the LICENSE-MIT file) at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +// code modified from https://github.com/joshlf/go-acl + +package acl + +import ( + "encoding/binary" + "fmt" + "math" + "os" + "strings" + "syscall" +) + +// ACL represents an access control list as defined +// in the POSIX.1e draft standard. If an ACL is not +// valid (see the IsValid method), the behavior of +// the functions and methods of this package is +// undefined. +type ACL []Entry + +// ToUnix returns the unix permissions bitmask +// encoded by a. If a is not valid as defined +// by a.IsValid, the behavior of ToUnix is +// undefined. +func ToUnix(a ACL) os.FileMode { + var perms os.FileMode + for _, e := range a { + switch e.Tag { + case TagUserObj: + perms |= (e.perms() << 6) + case TagGroupObj: + perms |= (e.perms() << 3) + case TagOther: + perms |= e.perms() + } + } + return perms +} + +// String implements the POSIX.1e short text form. +// For example: +// +// u::rwx,g::r-x,o::---,u:dvader:r--,m::r-- +// +// This output is produced by an ACL in which the file owner +// has read, write, and execute; the file group has read and +// execute; other has no permissions; the user dvader has +// read; and the mask is read. +func (a ACL) String() string { + strs := make([]string, len(a)) + for i, e := range a { + strs[i] = e.String() + } + return strings.Join(strs, ",") +} + +// StringLong implements the POSIX.1e long text form. +// The long text form of the example given above is: +// +// user::rwx +// group::r-x +// other::--- +// user:dvader:r-- +// mask::r-- +func (a ACL) StringLong() string { + lines := make([]string, len(a)) + mask := os.FileMode(7) + for _, e := range a { + if e.Tag == TagMask { + mask = e.perms() + break + } + } + for i, e := range a { + if (e.Tag == TagUser || e.Tag == TagGroupObj || e.Tag == TagGroup) && + mask|e.perms() != mask { + effective := mask & e.perms() + lines[i] = fmt.Sprintf("%-20s#effective:%s", e.StringLong(), permString(effective)) + } else { + lines[i] = e.StringLong() + } + } + return strings.Join(lines, "\n") +} + +// Tag is the type of an ACL entry tag. +type Tag tag + +const ( + TagUserObj Tag = tagUserObj // Permissions of the file owner + TagUser = tagUser // Permissions of a specified user + TagGroupObj = tagGroupObj // Permissions of the file group + TagGroup = tagGroup // Permissions of a specified group + + // Maximum allowed access rights of any entry + // with the tag TagUser, TagGroupObj, or TagGroup + TagMask = tagMask + TagOther = tagOther // Permissions of a process not matching any other entry +) + +// String implements the POSIX.1e short text form. +func (t Tag) String() string { + switch t { + case TagUser, TagUserObj: + return "u" + case TagGroup, TagGroupObj: + return "g" + case TagOther: + return "o" + case TagMask: + return "m" + default: + // TODO(joshlf): what to do in this case? + return "?" // non-standard, but not specified in POSIX.1e + } +} + +// StringLong implements the POSIX.1e long text form. +func (t Tag) StringLong() string { + switch t { + case TagUser, TagUserObj: + return "user" + case TagGroup, TagGroupObj: + return "group" + case TagOther: + return "other" + case TagMask: + return "mask" + default: + // TODO(joshlf): what to do in this case? + return "????" // non-standard, but not specified in POSIX.1e + } +} + +// Entry represents an entry in an ACL. +type Entry struct { + Tag Tag + + // TODO(joshlf): it would be nice if we could handle + // the UID/user name or GID/group name transition + // transparently under the hood rather than pushing + // the responsibility to the user. However, there are + // some subtle considerations: + // - It must be valid to provide a UID/GID for a + // user or group that does not exist (setfactl + // supports this) + // - If the qualifier can be either a UID/GID or + // a user name/group name, there should probably + // be a better way of encoding it (that is, + // better than just setting it to one or the + // other and letting the user implement custom + // logic to tell the difference) + + // The Qualifier specifies what entity (user or group) + // this entry applies to. If the Tag is TagUser, it is + // a UID; if the Tag is TagGroup, it is a GID; otherwise + // the field is ignored. Note that the qualifier must + // be a UID or GID - it cannot be, for example, a user name. + Qualifier string + + // ACL permissions are taken from a traditional rwx + // (read/write/execute) permissions vector. The Perms + // field stores these as the lowest three bits - + // the bits in any higher positions are ignored. + Perms os.FileMode +} + +// Use e.perms() to make sure that only +// the lowest three bits are set - some +// algorithms may inadvertently break +// otherwise (including libacl itself). +func (e Entry) perms() os.FileMode { return 7 & e.Perms } + +var permStrings = []string{ + 0: "---", + 1: "--x", + 2: "-w-", + 3: "-wx", + 4: "r--", + 5: "r-x", + 6: "rw-", + 7: "rwx", +} + +// assumes perm has only lowest three bits set +func permString(perm os.FileMode) string { + return permStrings[int(perm)] +} + +// String implements the POSIX.1e short text form. +func (e Entry) String() string { + middle := "::" + if e.Tag == TagUser || e.Tag == TagGroup { + middle = ":" + formatQualifier(e.Qualifier, e.Tag) + ":" + } + return fmt.Sprintf("%s%s%s", e.Tag, middle, permString(e.perms())) +} + +// StringLong implements the POSIX.1e long text form. +func (e Entry) StringLong() string { + middle := "::" + if e.Tag == TagUser || e.Tag == TagGroup { + middle = ":" + formatQualifier(e.Qualifier, e.Tag) + ":" + } + return fmt.Sprintf("%s%s%s", e.Tag.StringLong(), middle, permString(e.perms())) +} + +// overwrite in other files to implement platform-specific behavior +var formatQualifier = func(q string, _ Tag) string { return q } + +/* + NOTE: This implementation is largely based on Linux's libacl. +*/ + +type tag int + +const ( + // defined in sys/acl.h + tagUndefined Tag = 0x00 + tagUserObj Tag = 0x01 + tagUser Tag = 0x02 + tagGroupObj Tag = 0x04 + tagGroup Tag = 0x08 + tagMask Tag = 0x10 + tagOther Tag = 0x20 + + // defined in include/acl_ea.h (see libacl source) + aclEAAccess = "system.posix_acl_access" + aclEADefault = "system.posix_acl_default" + aclEAVersion = 2 + aclEAEntrySize = 8 + aclUndefinedID = math.MaxUint32 // defined in sys/acl.h +) + +func AclFromXattr(xattr []byte) (acl ACL, err error) { + if len(xattr) < 4 { + return nil, syscall.EINVAL + } + version := binary.LittleEndian.Uint32(xattr) + xattr = xattr[4:] + if version != aclEAVersion { + return nil, syscall.EINVAL + } + if len(xattr)%aclEAEntrySize != 0 { + return nil, syscall.EINVAL + } + + for len(xattr) > 0 { + etag := binary.LittleEndian.Uint16(xattr) + sperm := binary.LittleEndian.Uint16(xattr[2:]) + qid := binary.LittleEndian.Uint32(xattr[4:]) + + ent := Entry{ + Tag: Tag(etag), + Perms: os.FileMode(sperm), + } + if ent.Tag == TagUser || ent.Tag == TagGroup { + ent.Qualifier = fmt.Sprint(qid) + } + + acl = append(acl, ent) + xattr = xattr[8:] + } + + return acl, nil +} diff --git a/backend/acl/perm.go b/backend/acl/perm.go new file mode 100644 index 0000000..ec8b731 --- /dev/null +++ b/backend/acl/perm.go @@ -0,0 +1,63 @@ +package acl + +import ( + "fmt" + "os" + "syscall" +) + +const ( + Read os.FileMode = 1 << iota +) + +func (a ACL) IsReadAllowed(uid, gid uint32) bool { + return a.isAllowed(uid, gid, Read) +} + +func (a ACL) isAllowed(uid, gid uint32, perm os.FileMode) bool { + for _, e := range a { + if e.matches(uid, gid) { + return e.isAllowed(perm) + } + } + return false +} + +func (e Entry) matches(uid, gid uint32) bool { + switch e.Tag { + case TagUserObj: + return e.Qualifier == fmt.Sprintf("%v", uid) + case TagGroupObj: + return e.Qualifier == fmt.Sprintf("%v", gid) + case TagMask: + return true + case TagOther: + return true + } + return false +} + +func (e Entry) isAllowed(perm os.FileMode) bool { + return e.Perms&perm != 0 +} + +func IsReadAllowed(fi os.FileInfo, uid, gid uint32) bool { + fiuser := fi.Sys().(*syscall.Stat_t).Uid + figroup := fi.Sys().(*syscall.Stat_t).Gid + + switch { + case fiuser == uid: + if fi.Mode()&0400 != 0 { + return true + } + case figroup == gid: + if fi.Mode()&0040 != 0 { + return true + } + default: + if fi.Mode()&0004 != 0 { + return true + } + } + return false +} diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 8ff09eb..e7d5a5a 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -37,6 +37,7 @@ import ( "github.com/pkg/xattr" "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" + "github.com/versity/versitygw/backend/acl" "github.com/versity/versitygw/s3err" "github.com/versity/versitygw/s3response" ) @@ -1224,7 +1225,7 @@ func (p *Posix) DeleteObjects(ctx context.Context, input *s3.DeleteObjectsInput) }, nil } -func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput, writer io.Writer) (*s3.GetObjectOutput, error) { +func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput, writer io.Writer) (*s3.GetObjectOutput, error) { if input.Bucket == nil { return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -1235,6 +1236,8 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput, writer io return nil, s3err.GetAPIError(s3err.ErrInvalidRange) } + account := ctx.Value("account").(auth.Account) + bucket := *input.Bucket _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { @@ -1254,6 +1257,27 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput, writer io return nil, fmt.Errorf("stat object: %w", err) } + var permDenied bool + // check posix permssions for this file + if account.UserID != 0 { + permDenied = !acl.IsReadAllowed(fi, uint32(account.UserID), uint32(account.GroupID)) + } + if permDenied { + // check posix acl access for this file + b, err := xattr.Get(objPath, "system.posix_acl_access") + if err != nil { + // no acl set, return permission denied due to failed posix permissions check + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + } + acl, err := acl.AclFromXattr(b) + if err != nil { + return nil, fmt.Errorf("parse acl: %w", err) + } + if !acl.IsReadAllowed(uint32(account.UserID), uint32(account.GroupID)) { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + } + } + acceptRange := *input.Range startOffset, length, err := backend.ParseRange(fi, acceptRange) if err != nil {