github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/permission/permissions.go (about) 1 // Package permission is used to store the permissions for each webapp, 2 // konnector, sharing, etc. 3 package permission 4 5 import ( 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "strings" 10 "time" 11 12 build "github.com/cozy/cozy-stack/pkg/config" 13 "github.com/cozy/cozy-stack/pkg/consts" 14 "github.com/cozy/cozy-stack/pkg/couchdb" 15 "github.com/cozy/cozy-stack/pkg/couchdb/mango" 16 "github.com/cozy/cozy-stack/pkg/crypto" 17 "github.com/cozy/cozy-stack/pkg/metadata" 18 "github.com/cozy/cozy-stack/pkg/prefixer" 19 "github.com/labstack/echo/v4" 20 ) 21 22 // DocTypeVersion represents the doctype version. Each time this document 23 // structure is modified, update this value 24 const DocTypeVersion = "1" 25 26 // Permission is a storable object containing a set of rules and 27 // several codes 28 type Permission struct { 29 PID string `json:"_id,omitempty"` 30 PRev string `json:"_rev,omitempty"` 31 Type string `json:"type,omitempty"` 32 SourceID string `json:"source_id,omitempty"` 33 Permissions Set `json:"permissions,omitempty"` 34 ExpiresAt *time.Time `json:"expires_at,omitempty"` 35 Codes map[string]string `json:"codes,omitempty"` 36 ShortCodes map[string]string `json:"shortcodes,omitempty"` 37 Password interface{} `json:"password,omitempty"` 38 39 Client interface{} `json:"-"` // Contains the *oauth.Client client pointer for Oauth permission type 40 Metadata *metadata.CozyMetadata `json:"cozyMetadata,omitempty"` 41 } 42 43 const ( 44 // TypeRegister is the value of Permission.Type for the temporary permissions 45 // allowed by registerToken 46 TypeRegister = "register" 47 48 // TypeWebapp is the value of Permission.Type for an application 49 TypeWebapp = "app" 50 51 // TypeKonnector is the value of Permission.Type for an application 52 TypeKonnector = "konnector" 53 54 // TypeOauth is the value of Permission.Type for a oauth permission doc 55 TypeOauth = "oauth" 56 57 // TypeCLI is the value of Permission.Type for a command-line permission doc 58 TypeCLI = "cli" 59 60 // TypeShareByLink is the value of Permission.Type for a share (by link) permission doc 61 TypeShareByLink = "share" 62 63 // TypeSharePreview is the value of Permission.Type to preview a 64 // cozy-to-cozy sharing 65 TypeSharePreview = "share-preview" 66 67 // TypeShareInteract is the value of Permission.Type for reading and 68 // writing a note in a shared folder. 69 TypeShareInteract = "share-interact" 70 ) 71 72 // ID implements jsonapi.Doc 73 func (p *Permission) ID() string { return p.PID } 74 75 // Rev implements jsonapi.Doc 76 func (p *Permission) Rev() string { return p.PRev } 77 78 // DocType implements jsonapi.Doc 79 func (p *Permission) DocType() string { return consts.Permissions } 80 81 // Clone implements couchdb.Doc 82 func (p *Permission) Clone() couchdb.Doc { 83 cloned := *p 84 cloned.Codes = make(map[string]string) 85 cloned.ShortCodes = make(map[string]string) 86 if p.Metadata != nil { 87 cloned.Metadata = p.Metadata.Clone() 88 } 89 for k, v := range p.Codes { 90 cloned.Codes[k] = v 91 } 92 for k, v := range p.ShortCodes { 93 cloned.ShortCodes[k] = v 94 } 95 cloned.Permissions = make([]Rule, len(p.Permissions)) 96 for i, r := range p.Permissions { 97 vals := r.Values 98 r.Values = make([]string, len(r.Values)) 99 copy(r.Values, vals) 100 cloned.Permissions[i] = r 101 } 102 return &cloned 103 } 104 105 // SetID implements jsonapi.Doc 106 func (p *Permission) SetID(id string) { p.PID = id } 107 108 // SetRev implements jsonapi.Doc 109 func (p *Permission) SetRev(rev string) { p.PRev = rev } 110 111 // Expired returns true if the permissions are no longer valid 112 func (p *Permission) Expired() bool { 113 if p.ExpiresAt == nil { 114 return false 115 } 116 return p.ExpiresAt.Before(time.Now()) 117 } 118 119 // AddRules add some rules to the permission doc 120 func (p *Permission) AddRules(rules ...Rule) { 121 newperms := append(p.Permissions, rules...) 122 p.Permissions = newperms 123 } 124 125 // RemoveRule remove a rule from the permission doc 126 func (p *Permission) RemoveRule(rule Rule) { 127 newperms := p.Permissions[:0] 128 for _, r := range p.Permissions { 129 if r.Title != rule.Title { 130 newperms = append(newperms, r) 131 } 132 } 133 p.Permissions = newperms 134 } 135 136 // PatchCodes replace the permission docs codes 137 func (p *Permission) PatchCodes(codes map[string]string) { 138 p.Codes = codes 139 140 // Removing associated shortcodes 141 if p.ShortCodes != nil { 142 updatedShortcodes := map[string]string{} 143 144 for codeName := range codes { 145 for shortcodeName, v := range p.ShortCodes { 146 if shortcodeName == codeName { 147 updatedShortcodes[shortcodeName] = v 148 } 149 } 150 } 151 p.ShortCodes = updatedShortcodes 152 } 153 } 154 155 // Revoke destroy a Permission 156 func (p *Permission) Revoke(db prefixer.Prefixer) error { 157 return couchdb.DeleteDoc(db, p) 158 } 159 160 // CanUpdateShareByLink check if the child permissions can be updated by p 161 // (p can be the parent or it has a superset of the permissions). 162 func (p *Permission) CanUpdateShareByLink(child *Permission) bool { 163 if child.Type != TypeShareByLink { 164 return false 165 } 166 if p.Type != TypeWebapp && p.Type != TypeOauth { 167 return false 168 } 169 return child.SourceID == p.SourceID || child.Permissions.IsSubSetOf(p.Permissions) 170 } 171 172 // GetByID fetch a permission by its ID 173 func GetByID(db prefixer.Prefixer, id string) (*Permission, error) { 174 perm := &Permission{} 175 if err := couchdb.GetDoc(db, consts.Permissions, id, perm); err != nil { 176 return nil, err 177 } 178 if perm.Expired() { 179 return nil, ErrExpiredToken 180 } 181 return perm, nil 182 } 183 184 // GetForRegisterToken create a non-persisted permissions doc with hard coded 185 // registerToken permissions set 186 func GetForRegisterToken() *Permission { 187 return &Permission{ 188 Type: TypeRegister, 189 Permissions: Set{ 190 Rule{ 191 Verbs: Verbs(GET), 192 Type: consts.Settings, 193 Values: []string{consts.InstanceSettingsID}, 194 }, 195 }, 196 } 197 } 198 199 // GetForCLI create a non-persisted permissions doc for the command-line 200 func GetForCLI(claims *Claims) (*Permission, error) { 201 set, err := UnmarshalScopeString(claims.Scope) 202 if err != nil { 203 return nil, err 204 } 205 pdoc := &Permission{ 206 Type: TypeCLI, 207 Permissions: set, 208 } 209 return pdoc, nil 210 } 211 212 // GetForWebapp retrieves the Permission doc for a given webapp 213 func GetForWebapp(db prefixer.Prefixer, slug string) (*Permission, error) { 214 return getFromSource(db, TypeWebapp, consts.Apps, slug) 215 } 216 217 // GetForKonnector retrieves the Permission doc for a given konnector 218 func GetForKonnector(db prefixer.Prefixer, slug string) (*Permission, error) { 219 return getFromSource(db, TypeKonnector, consts.Konnectors, slug) 220 } 221 222 // GetForSharePreview retrieves the Permission doc for a given sharing preview 223 func GetForSharePreview(db prefixer.Prefixer, sharingID string) (*Permission, error) { 224 return getFromSource(db, TypeSharePreview, consts.Sharings, sharingID) 225 } 226 227 // GetForShareInteract retrieves the Permission doc for a given sharing to 228 // read/write a note 229 func GetForShareInteract(db prefixer.Prefixer, sharingID string) (*Permission, error) { 230 return getFromSource(db, TypeShareInteract, consts.Sharings, sharingID) 231 } 232 233 func getFromSource(db prefixer.Prefixer, permType, docType, slug string) (*Permission, error) { 234 var res []Permission 235 req := couchdb.FindRequest{ 236 UseIndex: "by-source-and-type", 237 Selector: mango.And( 238 mango.Equal("type", permType), 239 mango.Equal("source_id", docType+"/"+slug), 240 ), 241 Limit: 1, 242 } 243 err := couchdb.FindDocs(db, consts.Permissions, &req, &res) 244 if err != nil { 245 // With a cluster of couchdb, we can have a race condition where we 246 // query an index before it has been updated for an app that has 247 // just been created. 248 // Cf https://issues.apache.org/jira/browse/COUCHDB-3336 249 time.Sleep(1 * time.Second) 250 err = couchdb.FindDocs(db, consts.Permissions, &req, &res) 251 if err != nil { 252 return nil, err 253 } 254 } 255 if len(res) == 0 { 256 return nil, &couchdb.Error{ 257 StatusCode: http.StatusNotFound, 258 Name: "not_found", 259 Reason: fmt.Sprintf("no permission doc for %v", slug), 260 } 261 } 262 perm := &res[0] 263 if perm.Expired() { 264 return nil, ErrExpiredToken 265 } 266 return perm, nil 267 } 268 269 // GetForShareCode retrieves the Permission doc for a given sharing code 270 func GetForShareCode(db prefixer.Prefixer, tokenCode string) (*Permission, error) { 271 var res couchdb.ViewResponse 272 err := couchdb.ExecView(db, couchdb.PermissionsShareByCView, &couchdb.ViewRequest{ 273 Key: tokenCode, 274 IncludeDocs: true, 275 }, &res) 276 if err != nil { 277 return nil, err 278 } 279 280 if len(res.Rows) == 0 { 281 msg := fmt.Sprintf("no permission doc for token %v", tokenCode) 282 return nil, echo.NewHTTPError(http.StatusForbidden, msg) 283 } 284 285 if len(res.Rows) > 1 { 286 msg := fmt.Sprintf("Bad state: several permission docs for token %v", tokenCode) 287 return nil, echo.NewHTTPError(http.StatusBadRequest, msg) 288 } 289 290 perm := &Permission{} 291 err = json.Unmarshal(res.Rows[0].Doc, perm) 292 if err != nil { 293 return nil, err 294 } 295 296 if perm.Expired() { 297 return nil, ErrExpiredToken 298 } 299 300 // Check for sharing made by a webapp/konnector that the app is still 301 // present (but not for OAuth). It is not checked in development release, 302 // since the --appdir does not create the expected document. 303 if !build.IsDevRelease() { 304 parts := strings.SplitN(perm.SourceID, "/", 2) 305 if len(parts) == 2 { 306 var doc couchdb.JSONDoc 307 docID := parts[0] + "/" + parts[1] 308 if parts[0] == consts.Sharings { 309 docID = parts[1] 310 } 311 if err := couchdb.GetDoc(db, parts[0], docID, &doc); err != nil { 312 return nil, ErrExpiredToken 313 } 314 } 315 } 316 return perm, nil 317 } 318 319 // GetTokenFromShortcode retrieves the token doc for a given sharing shortcode 320 func GetTokenFromShortcode(db prefixer.Prefixer, shortcode string) (string, error) { 321 token, _, err := GetTokenAndPermissionsFromShortcode(db, shortcode) 322 return token, err 323 } 324 325 // GetTokenAndPermissionsFromShortcode retrieves the token and permissions doc for a given sharing shortcode 326 func GetTokenAndPermissionsFromShortcode(db prefixer.Prefixer, shortcode string) (string, *Permission, error) { 327 var res couchdb.ViewResponse 328 329 err := couchdb.ExecView(db, couchdb.PermissionsShareByShortcodeView, &couchdb.ViewRequest{ 330 Key: shortcode, 331 IncludeDocs: true, 332 }, &res) 333 if err != nil { 334 return "", nil, err 335 } 336 337 if len(res.Rows) == 0 { 338 msg := fmt.Sprintf("no permission doc for shortcode %v", shortcode) 339 return "", nil, echo.NewHTTPError(http.StatusForbidden, msg) 340 } 341 342 if len(res.Rows) > 1 { 343 msg := fmt.Sprintf("Bad state: several permission docs for shortcode %v", shortcode) 344 return "", nil, echo.NewHTTPError(http.StatusBadRequest, msg) 345 } 346 347 perm := Permission{} 348 err = json.Unmarshal(res.Rows[0].Doc, &perm) 349 350 if err != nil { 351 return "", nil, err 352 } 353 354 for mail, code := range perm.Codes { 355 if mail == res.Rows[0].Value { 356 return code, &perm, nil 357 } 358 } 359 360 return "", nil, fmt.Errorf("Cannot find token for shortcode %s", res.Rows[0].Key) 361 } 362 363 // CreateWebappSet creates a Permission doc for an app 364 func CreateWebappSet(db prefixer.Prefixer, slug string, set Set, version string) (*Permission, error) { 365 existing, _ := GetForWebapp(db, slug) 366 if existing != nil { 367 return nil, fmt.Errorf("There is already a permission doc for %v", slug) 368 } 369 // Add metadata 370 md, err := metadata.NewWithApp(slug, version, DocTypeVersion) 371 if err != nil { 372 return nil, err 373 } 374 return createAppSet(db, TypeWebapp, consts.Apps, slug, set, md) 375 } 376 377 // CreateKonnectorSet creates a Permission doc for a konnector 378 func CreateKonnectorSet(db prefixer.Prefixer, slug string, set Set, version string) (*Permission, error) { 379 existing, _ := GetForKonnector(db, slug) 380 if existing != nil { 381 return nil, fmt.Errorf("There is already a permission doc for %v", slug) 382 } 383 // Add metadata 384 md, err := metadata.NewWithApp(slug, version, DocTypeVersion) 385 if err != nil { 386 return nil, err 387 } 388 return createAppSet(db, TypeKonnector, consts.Konnectors, slug, set, md) 389 } 390 391 func createAppSet(db prefixer.Prefixer, typ, docType, slug string, set Set, md *metadata.CozyMetadata) (*Permission, error) { 392 doc := &Permission{ 393 Type: typ, 394 SourceID: docType + "/" + slug, 395 Permissions: set, 396 Metadata: md, 397 } 398 err := couchdb.CreateDoc(db, doc) 399 if err != nil { 400 return nil, err 401 } 402 return doc, nil 403 } 404 405 // MergeExtraPermissions merges rules from "extraPermissions" set by adding them 406 // in the "perms" one 407 func MergeExtraPermissions(perms, extraPermissions Set) (Set, error) { 408 var permissions Set 409 410 // Appending the extraPermissions which are not in the target permissions 411 for _, ep := range extraPermissions { 412 found := false 413 for _, p := range perms { 414 if ep.Title == p.Title { 415 found = true 416 break 417 } 418 } 419 if !found { 420 permissions = append(permissions, ep) 421 } 422 } 423 424 // Merging the rules already existing 425 for _, rule := range perms { 426 found := false 427 for _, newRule := range extraPermissions { 428 if rule.Title == newRule.Title { 429 mergedRule, err := rule.Merge(newRule) 430 if err != nil { 431 continue 432 } 433 permissions = append(permissions, *mergedRule) 434 found = true 435 break 436 } 437 } 438 if !found { 439 permissions = append(permissions, rule) 440 } 441 } 442 443 return permissions, nil 444 } 445 446 // UpdateWebappSet creates a Permission doc for an app 447 func UpdateWebappSet(db prefixer.Prefixer, slug string, set Set) (*Permission, error) { 448 doc, err := GetForWebapp(db, slug) 449 if err != nil { 450 return nil, err 451 } 452 return updateAppSet(db, doc, TypeWebapp, consts.Apps, slug, set) 453 } 454 455 // UpdateKonnectorSet creates a Permission doc for a konnector 456 func UpdateKonnectorSet(db prefixer.Prefixer, slug string, set Set) (*Permission, error) { 457 doc, err := GetForKonnector(db, slug) 458 if err != nil { 459 return nil, err 460 } 461 return updateAppSet(db, doc, TypeKonnector, consts.Konnectors, slug, set) 462 } 463 464 func updateAppSet(db prefixer.Prefixer, doc *Permission, typ, docType, slug string, set Set) (*Permission, error) { 465 doc.Permissions = set 466 if doc.Metadata == nil { 467 doc.Metadata, _ = metadata.NewWithApp(slug, "", DocTypeVersion) 468 } else { 469 doc.Metadata.ChangeUpdatedAt() 470 } 471 err := couchdb.UpdateDoc(db, doc) 472 if err != nil { 473 return nil, err 474 } 475 return doc, nil 476 } 477 478 func checkSetPermissions(set Set, parent *Permission) error { 479 if parent.Type != TypeWebapp && parent.Type != TypeKonnector && parent.Type != TypeOauth { 480 return ErrOnlyAppCanCreateSubSet 481 } 482 if !set.IsSubSetOf(parent.Permissions) { 483 return ErrNotSubset 484 } 485 for _, rule := range set { 486 // XXX io.cozy.files is allowed and handled with specific code for sharings 487 if MatchType(rule, consts.Files) { 488 continue 489 } 490 if err := CheckWritable(rule.Type); err != nil { 491 return err 492 } 493 } 494 return nil 495 } 496 497 // CreateShareSet creates a Permission doc for sharing by link 498 func CreateShareSet(db prefixer.Prefixer, parent *Permission, sourceID string, codes, shortcodes map[string]string, subdoc Permission, expiresAt *time.Time) (*Permission, error) { 499 set := subdoc.Permissions 500 if err := checkSetPermissions(set, parent); err != nil { 501 return nil, err 502 } 503 // SourceID stays the same, allow quick destruction of all children permissions 504 doc := &Permission{ 505 Type: TypeShareByLink, 506 SourceID: sourceID, 507 Permissions: set, 508 Codes: codes, 509 ShortCodes: shortcodes, 510 ExpiresAt: expiresAt, 511 Metadata: subdoc.Metadata, 512 } 513 514 if pass, ok := subdoc.Password.(string); ok && len(pass) > 0 { 515 hash, err := crypto.GenerateFromPassphrase([]byte(pass)) 516 if err != nil { 517 return nil, err 518 } 519 doc.Password = hash 520 } 521 522 err := couchdb.CreateDoc(db, doc) 523 if err != nil { 524 return nil, err 525 } 526 527 return doc, nil 528 } 529 530 // CreateSharePreviewSet creates a Permission doc for previewing a sharing 531 func CreateSharePreviewSet(db prefixer.Prefixer, sharingID string, codes, shortcodes map[string]string, subdoc Permission) (*Permission, error) { 532 doc := &Permission{ 533 Type: TypeSharePreview, 534 Permissions: subdoc.Permissions, 535 Codes: codes, 536 ShortCodes: shortcodes, 537 SourceID: consts.Sharings + "/" + sharingID, 538 Metadata: subdoc.Metadata, 539 } 540 err := couchdb.CreateDoc(db, doc) 541 if err != nil { 542 return nil, err 543 } 544 return doc, nil 545 } 546 547 // CreateShareInteractSet creates a Permission doc for reading/writing a note 548 // inside a sharing 549 func CreateShareInteractSet(db prefixer.Prefixer, sharingID string, codes map[string]string, subdoc Permission) (*Permission, error) { 550 doc := &Permission{ 551 Type: TypeShareInteract, 552 Permissions: subdoc.Permissions, 553 Codes: codes, 554 SourceID: consts.Sharings + "/" + sharingID, 555 Metadata: subdoc.Metadata, 556 } 557 err := couchdb.CreateDoc(db, doc) 558 if err != nil { 559 return nil, err 560 } 561 return doc, nil 562 } 563 564 // ForceWebapp creates or updates a Permission doc for a given webapp 565 func ForceWebapp(db prefixer.Prefixer, slug string, set Set) error { 566 existing, _ := GetForWebapp(db, slug) 567 doc := &Permission{ 568 Type: TypeWebapp, 569 SourceID: consts.Apps + "/" + slug, 570 Permissions: set, 571 } 572 if existing == nil { 573 return couchdb.CreateDoc(db, doc) 574 } 575 576 doc.SetID(existing.ID()) 577 doc.SetRev(existing.Rev()) 578 return couchdb.UpdateDoc(db, doc) 579 } 580 581 // ForceKonnector creates or updates a Permission doc for a given konnector 582 func ForceKonnector(db prefixer.Prefixer, slug string, set Set) error { 583 existing, _ := GetForKonnector(db, slug) 584 doc := &Permission{ 585 Type: TypeKonnector, 586 SourceID: consts.Konnectors + "/" + slug, 587 Permissions: set, 588 } 589 if existing == nil { 590 return couchdb.CreateDoc(db, doc) 591 } 592 593 doc.SetID(existing.ID()) 594 doc.SetRev(existing.Rev()) 595 return couchdb.UpdateDoc(db, doc) 596 } 597 598 // DestroyWebapp remove all Permission docs for a given app 599 func DestroyWebapp(db prefixer.Prefixer, slug string) error { 600 return destroyApp(db, TypeWebapp, consts.Apps, slug) 601 } 602 603 // DestroyKonnector remove all Permission docs for a given konnector 604 func DestroyKonnector(db prefixer.Prefixer, slug string) error { 605 return destroyApp(db, TypeKonnector, consts.Konnectors, slug) 606 } 607 608 func destroyApp(db prefixer.Prefixer, permType, docType, slug string) error { 609 var res []Permission 610 err := couchdb.FindDocs(db, consts.Permissions, &couchdb.FindRequest{ 611 UseIndex: "by-source-and-type", 612 Selector: mango.And( 613 mango.Equal("source_id", docType+"/"+slug), 614 mango.Equal("type", permType), 615 ), 616 Limit: 1000, 617 }, &res) 618 if err != nil { 619 return err 620 } 621 for _, p := range res { 622 err := couchdb.DeleteDoc(db, &p) 623 if err != nil { 624 return err 625 } 626 } 627 return nil 628 } 629 630 // GetPermissionsForIDs gets permissions for several IDs 631 // returns for every id the combined allowed verbset 632 func GetPermissionsForIDs(db prefixer.Prefixer, doctype string, ids []string) (map[string]*VerbSet, error) { 633 var res struct { 634 Rows []struct { 635 ID string `json:"id"` 636 Key []string `json:"key"` 637 Value *VerbSet `json:"value"` 638 } `json:"rows"` 639 } 640 641 keys := make([]interface{}, len(ids)) 642 for i, id := range ids { 643 keys[i] = []string{doctype, "_id", id} 644 } 645 646 err := couchdb.ExecView(db, couchdb.PermissionsShareByDocView, &couchdb.ViewRequest{ 647 Keys: keys, 648 }, &res) 649 if err != nil { 650 return nil, err 651 } 652 653 result := make(map[string]*VerbSet) 654 for _, row := range res.Rows { 655 if _, ok := result[row.Key[2]]; ok { 656 result[row.Key[2]].Merge(row.Value) 657 } else { 658 result[row.Key[2]] = row.Value 659 } 660 } 661 662 return result, nil 663 } 664 665 // GetPermissionsByDoctype returns the list of all permissions of the given 666 // type (shared-with-me by example) that have at least one rule for the given 667 // doctype. The cursor will be modified in place. 668 func GetPermissionsByDoctype(db prefixer.Prefixer, permType, doctype string, cursor couchdb.Cursor) ([]Permission, error) { 669 req := &couchdb.ViewRequest{ 670 Key: [2]interface{}{doctype, permType}, 671 IncludeDocs: true, 672 } 673 cursor.ApplyTo(req) 674 675 var res couchdb.ViewResponse 676 err := couchdb.ExecView(db, couchdb.PermissionsByDoctype, req, &res) 677 if err != nil { 678 return nil, err 679 } 680 cursor.UpdateFrom(&res) 681 682 result := make([]Permission, len(res.Rows)) 683 684 for i, row := range res.Rows { 685 var doc Permission 686 err := json.Unmarshal(row.Doc, &doc) 687 if err != nil { 688 return nil, err 689 } 690 result[i] = doc 691 } 692 693 return result, nil 694 }