github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/ace/ace.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package ace
    20  
    21  import (
    22  	"bytes"
    23  	"encoding/csv"
    24  	"fmt"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
    30  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    31  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    32  	typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    33  	"github.com/cs3org/reva/v2/pkg/storage/utils/grants"
    34  )
    35  
    36  /*
    37  ACE represents an Access Control Entry, mimicing NFSv4 ACLs
    38  The difference is tht grant ACEs are not propagated down the tree when being set on a dir.
    39  The tradeoff is that every read has to check the permissions of all path segments up to the root,
    40  to determine the permissions. But reads can be scaled better than writes, so here we are.
    41  See https://github.com/cs3org/reva/pull/1170#issuecomment-700526118 for more details.
    42  
    43  The following is taken from the nfs4_acl man page,
    44  see https://linux.die.net/man/5/nfs4_acl:
    45  the extended attributes will look like this
    46  "user.oc.grant.<type>:<flags>:<principal>:<permissions>"
    47  
    48  *type*: will be limited to A for now
    49  
    50    - A: Allow
    51  
    52      allow *principal* to perform actions requiring *permissions*
    53      In the future we can use:
    54  
    55    - U: aUdit
    56  
    57      log any attempted access by principal which requires
    58      permissions.
    59  
    60    - L: aLarm
    61  
    62      generate a system alarm at any attempted access by
    63      principal which requires permissions
    64  
    65    - D: for Deny is not recommended
    66  
    67  *flags*: for now empty or g for group, no inheritance yet
    68  
    69    - d directory-inherit
    70  
    71      newly-created subdirectories will inherit the
    72      ACE.
    73  
    74    - f file-inherit
    75  
    76      newly-created files will inherit the ACE, minus its
    77      inheritance flags. Newly-created subdirectories
    78      will inherit the ACE; if directory-inherit is not
    79      also specified in the parent ACE, inherit-only will
    80      be added to the inherited ACE.
    81  
    82    - n no-propagate-inherit
    83  
    84      newly-created subdirectories will inherit
    85      the ACE, minus its inheritance flags.
    86  
    87    - i inherit-only
    88  
    89      the ACE is not considered in permissions checks,
    90      but it is heritable; however, the inherit-only
    91      flag is stripped from inherited ACEs.
    92  
    93  *principal* a named user, group or special principal
    94  
    95    - the oidc sub@iss maps nicely to this
    96  
    97    - 'OWNER@', 'GROUP@', and 'EVERYONE@', which are, respectively, analogous to the POSIX user/group/other
    98  
    99  *permissions*
   100  
   101    - r read-data (files) / list-directory (directories)
   102  
   103    - w write-data (files) / create-file (directories)
   104  
   105    - a append-data (files) / create-subdirectory (directories)
   106  
   107    - x execute (files) / change-directory (directories)
   108  
   109    - d delete - delete the file/directory. Some servers will allow a delete to occur if either this permission is set in the file/directory or if the delete-child permission is set in its parent directory.
   110  
   111    - D delete-child - remove a file or subdirectory from within the given directory (directories only)
   112  
   113    - t read-attributes - read the attributes of the file/directory.
   114  
   115    - T write-attributes - write the attributes of the file/directory.
   116  
   117    - n read-named-attributes - read the named attributes of the file/directory.
   118  
   119    - N write-named-attributes - write the named attributes of the file/directory.
   120  
   121    - c read-ACL - read the file/directory NFSv4 ACL.
   122  
   123    - C write-ACL - write the file/directory NFSv4 ACL.
   124  
   125    - o write-owner - change ownership of the file/directory.
   126  
   127    - y synchronize - allow clients to use synchronous I/O with the server.
   128  
   129  *TODO*
   130  
   131    - implement OWNER@ as "user.oc.grant.A::OWNER@:rwaDxtTnNcCy"
   132  
   133  *Limitations*
   134  
   135  	attribute names are limited to 255 chars by the linux kernel vfs, values to 64 kb
   136  	ext3 extended attributes must fit inside a single filesystem block ... 4096 bytes
   137  	that leaves us with "user.oc.grant.A::someonewithaslightlylongersubject@whateverissuer:rwaDxtTnNcCy" ~80 chars
   138  	4096/80 = 51 shares ... with luck we might move the actual permissions to the value, saving ~15 chars
   139  	4096/64 = 64 shares ... still meh ... we can do better by using ints instead of strings for principals
   140  
   141  	"user.oc.grant.u:100000" is pretty neat, but we can still do better: base64 encode the int
   142  	"user.oc.grant.u:6Jqg" but base64 always has at least 4 chars, maybe hex is better for smaller numbers
   143  	well use 4 chars in addition to the ace: "user.oc.grant.u:////" = 65535 -> 18 chars
   144  
   145  	4096/18 = 227 shares
   146  	still ... ext attrs for this are not infinite scale ...
   147  	so .. attach shares via fileid.
   148  	<userhome>/metadata/<fileid>/shares, similar to <userhome>/files
   149  	<userhome>/metadata/<fileid>/shares/u/<issuer>/<subject>/A:fdi:rwaDxtTnNcCy permissions as filename to keep them in the stat cache?
   150  
   151  	whatever ... 50 shares is good enough. If more is needed we can delegate to the metadata
   152  	if "user.oc.grant.M" is present look inside the metadata app.
   153  
   154  *Notes*
   155  
   156    - if we cannot set an ace we might get an io error.
   157      in that case convert all shares to metadata and try to set "user.oc.grant.m"
   158  
   159      what about metadata like share creator, share time, expiry?
   160  
   161    - creator is same as owner, but can be set
   162  
   163    - share date, or abbreviated st is a unix timestamp
   164  
   165    - expiry is a unix timestamp
   166  
   167    - can be put inside the value
   168  
   169    - we need to reorder the fields:
   170      "user.oc.grant.<u|g|o>:<principal>" -> "kv:t=<type>:f=<flags>:p=<permissions>:st=<share time>:c=<creator>:e=<expiry>:pw=<password>:n=<name>"
   171      "user.oc.grant.<u|g|o>:<principal>" -> "v1:<type>:<flags>:<permissions>:<share time>:<creator>:<expiry>:<password>:<name>"
   172      or the first byte determines the format
   173      0x00 = key value
   174      0x01 = v1 ...
   175  */
   176  type ACE struct {
   177  	// NFSv4 acls
   178  	_type       string // t
   179  	flags       string // f
   180  	principal   string // im key
   181  	permissions string // p
   182  
   183  	// sharing specific
   184  	shareTime int    // s
   185  	creator   string // c
   186  	expires   int64  // e
   187  	password  string // w passWord TODO h = hash
   188  	label     string // l
   189  }
   190  
   191  // FromGrant creates an ACE from a CS3 grant
   192  func FromGrant(g *provider.Grant) *ACE {
   193  	t := "A"
   194  	// Currently we only deny the full permission set
   195  	if grants.PermissionsEqual(&provider.ResourcePermissions{}, g.Permissions) {
   196  		t = "D"
   197  	}
   198  	e := &ACE{
   199  		_type:       t,
   200  		permissions: getACEPerm(g.Permissions),
   201  		creator:     userIDToString(g.Creator),
   202  	}
   203  	if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP {
   204  		e.flags = "g"
   205  		e.principal = "g:" + g.Grantee.GetGroupId().OpaqueId
   206  	} else {
   207  		e.principal = UserAce(g.Grantee.GetUserId())
   208  	}
   209  
   210  	if g.Expiration != nil {
   211  		e.expires = int64(g.Expiration.Seconds)*int64(time.Second) + int64(g.Expiration.Nanos)
   212  	}
   213  
   214  	return e
   215  }
   216  
   217  func UserAce(id *userpb.UserId) string {
   218  	return "u:" + id.OpaqueId
   219  }
   220  
   221  // Principal returns the principal of the ACE, eg. `u:<userid>` or `g:<groupid>`
   222  func (e *ACE) Principal() string {
   223  	return e.principal
   224  }
   225  
   226  // Marshal renders a principal and byte[] that can be used to persist the ACE as an extended attribute
   227  func (e *ACE) Marshal() (string, []byte) {
   228  	// NOTE: first byte will be replaced after converting to byte array
   229  	var b bytes.Buffer
   230  	w := csv.NewWriter(&b)
   231  	w.Comma = ':'
   232  	if err := w.Write([]string{
   233  		fmt.Sprintf("_t=%s", e._type),
   234  		fmt.Sprintf("f=%s", e.flags),
   235  		fmt.Sprintf("p=%s", e.permissions),
   236  		fmt.Sprintf("c=%s", e.creator),
   237  		fmt.Sprintf("e=%d", e.expires),
   238  	}); err != nil {
   239  		return "", nil
   240  	}
   241  	w.Flush()
   242  
   243  	bs := b.Bytes()
   244  	bs[0] = 0 // indicate key value
   245  	return e.principal, bs
   246  }
   247  
   248  // Unmarshal parses a principal string and byte[] into an ACE
   249  func Unmarshal(principal string, v []byte) (e *ACE, err error) {
   250  	// first byte indicates type of value
   251  	switch v[0] {
   252  	case 0: // = ':' separated key=value pairs
   253  		s := string(v[1:])
   254  		if e, err = unmarshalKV(s); err == nil {
   255  			e.principal = principal
   256  		}
   257  		// check consistency of Flags and principal type
   258  		if strings.Contains(e.flags, "g") {
   259  			if principal[:1] != "g" {
   260  				return nil, fmt.Errorf("inconsistent ace: expected group")
   261  			}
   262  		} else {
   263  			if principal[:1] != "u" {
   264  				return nil, fmt.Errorf("inconsistent ace: expected user")
   265  			}
   266  		}
   267  	default:
   268  		return nil, fmt.Errorf("unknown ace encoding")
   269  	}
   270  	return
   271  }
   272  
   273  // Grant returns a CS3 grant
   274  func (e *ACE) Grant() *provider.Grant {
   275  	// if type equals "D" we have a full denial which means an empty permission set
   276  	permissions := &provider.ResourcePermissions{}
   277  	if e._type == "A" {
   278  		permissions = e.grantPermissionSet()
   279  	}
   280  	g := &provider.Grant{
   281  		Grantee: &provider.Grantee{
   282  			Type: e.granteeType(),
   283  		},
   284  		Permissions: permissions,
   285  		Creator:     userIDFromString(e.creator),
   286  	}
   287  	id := e.principal[2:]
   288  	if e.granteeType() == provider.GranteeType_GRANTEE_TYPE_GROUP {
   289  		g.Grantee.Id = &provider.Grantee_GroupId{GroupId: &grouppb.GroupId{OpaqueId: id}}
   290  	} else if e.granteeType() == provider.GranteeType_GRANTEE_TYPE_USER {
   291  		g.Grantee.Id = &provider.Grantee_UserId{UserId: &userpb.UserId{OpaqueId: id}}
   292  	}
   293  
   294  	if e.expires != 0 {
   295  		g.Expiration = &typesv1beta1.Timestamp{
   296  			Seconds: uint64(e.expires / int64(time.Second)),
   297  			Nanos:   uint32(e.expires % int64(time.Second)),
   298  		}
   299  	}
   300  
   301  	return g
   302  }
   303  
   304  // granteeType returns the CS3 grantee type
   305  func (e *ACE) granteeType() provider.GranteeType {
   306  	if strings.Contains(e.flags, "g") {
   307  		return provider.GranteeType_GRANTEE_TYPE_GROUP
   308  	}
   309  	return provider.GranteeType_GRANTEE_TYPE_USER
   310  }
   311  
   312  // grantPermissionSet returns the set of CS3 resource permissions representing the ACE
   313  func (e *ACE) grantPermissionSet() *provider.ResourcePermissions {
   314  	p := &provider.ResourcePermissions{}
   315  	// t
   316  	if strings.Contains(e.permissions, "t") {
   317  		p.Stat = true
   318  		p.GetPath = true
   319  	}
   320  	// r
   321  	if strings.Contains(e.permissions, "r") {
   322  		p.Stat = true    // currently assumed
   323  		p.GetPath = true // currently assumed
   324  		p.InitiateFileDownload = true
   325  		p.ListContainer = true
   326  	}
   327  	// w
   328  	if strings.Contains(e.permissions, "w") {
   329  		p.InitiateFileUpload = true
   330  		if p.InitiateFileDownload {
   331  			p.Move = true
   332  		}
   333  	}
   334  	// a
   335  	if strings.Contains(e.permissions, "a") {
   336  		// TODO append data to file permission?
   337  		p.CreateContainer = true
   338  	}
   339  	// x
   340  	if strings.Contains(e.permissions, "x") {
   341  		p.ListContainer = true
   342  	}
   343  	// d
   344  	if strings.Contains(e.permissions, "d") {
   345  		p.Delete = true
   346  	}
   347  	// D ?
   348  
   349  	// sharing
   350  	if strings.Contains(e.permissions, "C") {
   351  		p.AddGrant = true
   352  	}
   353  	if strings.Contains(e.permissions, "c") {
   354  		p.ListGrants = true
   355  	}
   356  	if strings.Contains(e.permissions, "o") { // missuse o = write-owner
   357  		p.RemoveGrant = true
   358  		p.UpdateGrant = true
   359  	}
   360  	if strings.Contains(e.permissions, "O") {
   361  		p.DenyGrant = true
   362  	}
   363  
   364  	// trash
   365  	if strings.Contains(e.permissions, "u") { // u = undelete
   366  		p.ListRecycle = true
   367  	}
   368  	if strings.Contains(e.permissions, "U") {
   369  		p.RestoreRecycleItem = true
   370  	}
   371  	if strings.Contains(e.permissions, "P") {
   372  		p.PurgeRecycle = true
   373  	}
   374  
   375  	// versions
   376  	if strings.Contains(e.permissions, "v") {
   377  		p.ListFileVersions = true
   378  	}
   379  	if strings.Contains(e.permissions, "V") {
   380  		p.RestoreFileVersion = true
   381  	}
   382  
   383  	// ?
   384  	if strings.Contains(e.permissions, "q") {
   385  		p.GetQuota = true
   386  	}
   387  	// TODO set quota permission?
   388  	return p
   389  }
   390  
   391  func unmarshalKV(s string) (*ACE, error) {
   392  	e := &ACE{}
   393  	r := csv.NewReader(strings.NewReader(s))
   394  	r.Comma = ':'
   395  	r.Comment = 0
   396  	r.FieldsPerRecord = -1
   397  	r.LazyQuotes = false
   398  	r.TrimLeadingSpace = false
   399  	records, err := r.ReadAll()
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  	if len(records) != 1 {
   404  		return nil, fmt.Errorf("more than one row of ace kvs")
   405  	}
   406  	for i := range records[0] {
   407  		kv := strings.Split(records[0][i], "=")
   408  		switch kv[0] {
   409  		case "t":
   410  			e._type = kv[1]
   411  		case "f":
   412  			e.flags = kv[1]
   413  		case "p":
   414  			e.permissions = kv[1]
   415  		case "s":
   416  			v, err := strconv.Atoi(kv[1])
   417  			if err != nil {
   418  				return nil, err
   419  			}
   420  			e.shareTime = v
   421  		case "c":
   422  			e.creator = kv[1]
   423  		case "e":
   424  			v, err := strconv.ParseInt(kv[1], 10, 64)
   425  			if err != nil {
   426  				return nil, err
   427  			}
   428  			e.expires = v
   429  		case "w":
   430  			e.password = kv[1]
   431  		case "l":
   432  			e.label = kv[1]
   433  			// TODO default ... log unknown keys? or add as opaque? hm we need that for tagged shares ...
   434  		}
   435  	}
   436  	return e, nil
   437  }
   438  
   439  // getACEPerm produces an NFSv4.x inspired permission string from a CS3 resource permissions set
   440  func getACEPerm(set *provider.ResourcePermissions) string {
   441  	var b strings.Builder
   442  
   443  	if set.Stat || set.GetPath {
   444  		b.WriteString("t")
   445  	}
   446  	if set.ListContainer { // we have no dedicated traversal permission, but to listing a container allows traversing it
   447  		b.WriteString("x")
   448  	}
   449  	if set.InitiateFileDownload {
   450  		b.WriteString("r")
   451  	}
   452  	if set.InitiateFileUpload || set.Move {
   453  		b.WriteString("w")
   454  	}
   455  	if set.CreateContainer {
   456  		b.WriteString("a")
   457  	}
   458  	if set.Delete {
   459  		b.WriteString("d")
   460  	}
   461  
   462  	// sharing
   463  	if set.AddGrant {
   464  		b.WriteString("C")
   465  	}
   466  	if set.ListGrants {
   467  		b.WriteString("c")
   468  	}
   469  	if set.RemoveGrant || set.UpdateGrant {
   470  		b.WriteString("o")
   471  	}
   472  	if set.DenyGrant {
   473  		b.WriteString("O")
   474  	}
   475  
   476  	// trash
   477  	if set.ListRecycle {
   478  		b.WriteString("u")
   479  	}
   480  	if set.RestoreRecycleItem {
   481  		b.WriteString("U")
   482  	}
   483  	if set.PurgeRecycle {
   484  		b.WriteString("P")
   485  	}
   486  
   487  	// versions
   488  	if set.ListFileVersions {
   489  		b.WriteString("v")
   490  	}
   491  	if set.RestoreFileVersion {
   492  		b.WriteString("V")
   493  	}
   494  
   495  	// quota
   496  	if set.GetQuota {
   497  		b.WriteString("q")
   498  	}
   499  	// TODO set quota permission?
   500  	// TODO GetPath
   501  	return b.String()
   502  }
   503  
   504  func userIDToString(u *userpb.UserId) string {
   505  	return u.GetOpaqueId()
   506  }
   507  
   508  func userIDFromString(uid string) *userpb.UserId {
   509  	s := strings.SplitN(uid, "!", 2)
   510  	return &userpb.UserId{
   511  		OpaqueId: s[0],
   512  	}
   513  }