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 }