mirror of
https://github.com/versity/versitygw.git
synced 2026-01-04 02:53:55 +00:00
feat: posix read permission check wip
This commit is contained in:
272
backend/acl/acl.go
Normal file
272
backend/acl/acl.go
Normal file
@@ -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
|
||||
}
|
||||
63
backend/acl/perm.go
Normal file
63
backend/acl/perm.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user