github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/instance/instance.go (about) 1 // Package instance is for the instance model, with domain, locale, settings, 2 // etc. 3 package instance 4 5 import ( 6 "encoding/json" 7 "errors" 8 "fmt" 9 "net/http" 10 "net/url" 11 "os" 12 "path" 13 "strings" 14 "time" 15 16 "github.com/cozy/cozy-stack/model/permission" 17 "github.com/cozy/cozy-stack/model/vfs" 18 "github.com/cozy/cozy-stack/model/vfs/vfsafero" 19 "github.com/cozy/cozy-stack/model/vfs/vfsswift" 20 build "github.com/cozy/cozy-stack/pkg/config" 21 "github.com/cozy/cozy-stack/pkg/config/config" 22 "github.com/cozy/cozy-stack/pkg/consts" 23 "github.com/cozy/cozy-stack/pkg/couchdb" 24 "github.com/cozy/cozy-stack/pkg/crypto" 25 "github.com/cozy/cozy-stack/pkg/i18n" 26 "github.com/cozy/cozy-stack/pkg/jsonapi" 27 "github.com/cozy/cozy-stack/pkg/lock" 28 "github.com/cozy/cozy-stack/pkg/logger" 29 "github.com/cozy/cozy-stack/pkg/prefixer" 30 "github.com/cozy/cozy-stack/pkg/realtime" 31 "github.com/golang-jwt/jwt/v5" 32 "github.com/spf13/afero" 33 ) 34 35 // DefaultTemplateTitle represents the default template title. It could be 36 // overrided by configuring it in the instance context parameters 37 const DefaultTemplateTitle = "Cozy" 38 39 // PBKDF2_SHA256 is the value of kdf for using PBKDF2 with SHA256 to hash the 40 // password on client side. 41 // 42 //lint:ignore ST1003 we prefer ALL_CAPS here 43 const PBKDF2_SHA256 = 0 44 45 // An Instance has the informations relatives to the logical cozy instance, 46 // like the domain, the locale or the access to the databases and files storage 47 // It is a couchdb.Doc to be persisted in couchdb. 48 type Instance struct { 49 DocID string `json:"_id,omitempty"` // couchdb _id 50 DocRev string `json:"_rev,omitempty"` // couchdb _rev 51 Domain string `json:"domain"` // The main DNS domain, like example.cozycloud.cc 52 DomainAliases []string `json:"domain_aliases,omitempty"` 53 Prefix string `json:"prefix,omitempty"` // Possible database prefix 54 Locale string `json:"locale"` // The locale used on the server 55 UUID string `json:"uuid,omitempty"` // UUID associated with the instance 56 OIDCID string `json:"oidc_id,omitempty"` // An identifier to check authentication from OIDC 57 FranceConnectID string `json:"franceconnect_id,omitempty"` // An identifier to check authentication from FranceConnect 58 ContextName string `json:"context,omitempty"` // The context attached to the instance 59 Sponsorships []string `json:"sponsorships,omitempty"` // The list of sponsorships for the instance 60 TOSSigned string `json:"tos,omitempty"` // Terms of Service signed version 61 TOSLatest string `json:"tos_latest,omitempty"` // Terms of Service latest version 62 AuthMode AuthMode `json:"auth_mode,omitempty"` // 2 factor authentication 63 MagicLink bool `json:"magic_link,omitempty"` // Authentication via a link sent by email 64 Deleting bool `json:"deleting,omitempty"` 65 Moved bool `json:"moved,omitempty"` // If the instance has been moved to a new place 66 Blocked bool `json:"blocked,omitempty"` // Whether or not the instance is blocked 67 BlockingReason string `json:"blocking_reason,omitempty"` // Why the instance is blocked 68 NoAutoUpdate bool `json:"no_auto_update,omitempty"` // Whether or not the instance has auto updates for its applications 69 70 OnboardingFinished bool `json:"onboarding_finished,omitempty"` // Whether or not the onboarding is complete. 71 PasswordDefined *bool `json:"password_defined"` // 3 possibles states: true, false, and unknown (for legacy reasons) 72 73 BytesDiskQuota int64 `json:"disk_quota,string,omitempty"` // The total size in bytes allowed to the user 74 IndexViewsVersion int `json:"indexes_version,omitempty"` 75 76 // Swift layout number: 77 // - 0 for layout v1 78 // - 1 for layout v2 79 // - 2 for layout v3 80 // It is called swift_cluster in CouchDB and indexed from 0 for legacy reasons. 81 // See model/vfs/vfsswift for more details. 82 SwiftLayout int `json:"swift_cluster,omitempty"` 83 84 CouchCluster int `json:"couch_cluster,omitempty"` 85 86 // PassphraseHash is a hash of a hash of the user's passphrase: the 87 // passphrase is first hashed in client-side to avoid sending it to the 88 // server as it also used for encryption on client-side, and after that, 89 // hashed on the server to ensure robustness. For more informations on the 90 // server-side hashing, see crypto.GenerateFromPassphrase. 91 PassphraseHash []byte `json:"passphrase_hash,omitempty"` 92 PassphraseResetToken []byte `json:"passphrase_reset_token,omitempty"` 93 PassphraseResetTime *time.Time `json:"passphrase_reset_time,omitempty"` 94 95 // Secure assets 96 97 // Register token is used on registration to prevent from stealing instances 98 // waiting for registration. The registerToken secret is only shared (in 99 // clear) with the instance's user. 100 RegisterToken []byte `json:"register_token,omitempty"` 101 // SessSecret is used to authenticate session cookies 102 SessSecret []byte `json:"session_secret,omitempty"` 103 // OAuthSecret is used to authenticate OAuth2 token 104 OAuthSecret []byte `json:"oauth_secret,omitempty"` 105 // CLISecret is used to authenticate request from the CLI 106 CLISecret []byte `json:"cli_secret,omitempty"` 107 108 // FeatureFlags is the feature flags that are specific to this instance 109 FeatureFlags map[string]interface{} `json:"feature_flags,omitempty"` 110 // FeatureSets is a list of feature sets from the manager 111 FeatureSets []string `json:"feature_sets,omitempty"` 112 113 // LastActivityFromDeletedOAuthClients is the date of the last activity for 114 // OAuth clients that have been deleted 115 LastActivityFromDeletedOAuthClients *time.Time `json:"last_activity_from_deleted_oauth_clients,omitempty"` 116 117 vfs vfs.VFS 118 contextualDomain string 119 } 120 121 // DocType implements couchdb.Doc 122 func (i *Instance) DocType() string { return consts.Instances } 123 124 // ID implements couchdb.Doc 125 func (i *Instance) ID() string { return i.DocID } 126 127 // SetID implements couchdb.Doc 128 func (i *Instance) SetID(v string) { i.DocID = v } 129 130 // Rev implements couchdb.Doc 131 func (i *Instance) Rev() string { return i.DocRev } 132 133 // SetRev implements couchdb.Doc 134 func (i *Instance) SetRev(v string) { i.DocRev = v } 135 136 // Clone implements couchdb.Doc 137 func (i *Instance) Clone() couchdb.Doc { 138 cloned := *i 139 140 cloned.DomainAliases = make([]string, len(i.DomainAliases)) 141 copy(cloned.DomainAliases, i.DomainAliases) 142 143 cloned.PassphraseHash = make([]byte, len(i.PassphraseHash)) 144 copy(cloned.PassphraseHash, i.PassphraseHash) 145 146 cloned.PassphraseResetToken = make([]byte, len(i.PassphraseResetToken)) 147 copy(cloned.PassphraseResetToken, i.PassphraseResetToken) 148 149 if i.PassphraseResetTime != nil { 150 tmp := *i.PassphraseResetTime 151 cloned.PassphraseResetTime = &tmp 152 } 153 154 cloned.RegisterToken = make([]byte, len(i.RegisterToken)) 155 copy(cloned.RegisterToken, i.RegisterToken) 156 157 cloned.SessSecret = make([]byte, len(i.SessSecret)) 158 copy(cloned.SessSecret, i.SessSecret) 159 160 cloned.OAuthSecret = make([]byte, len(i.OAuthSecret)) 161 copy(cloned.OAuthSecret, i.OAuthSecret) 162 163 cloned.CLISecret = make([]byte, len(i.CLISecret)) 164 copy(cloned.CLISecret, i.CLISecret) 165 return &cloned 166 } 167 168 // DBCluster returns the index of the CouchDB cluster where the databases for 169 // this instance can be found. 170 func (i *Instance) DBCluster() int { 171 return i.CouchCluster 172 } 173 174 // DBPrefix returns the prefix to use in database naming for the 175 // current instance 176 func (i *Instance) DBPrefix() string { 177 if i.Prefix != "" { 178 return i.Prefix 179 } 180 return i.Domain 181 } 182 183 // DomainName returns the main domain name of the instance. 184 func (i *Instance) DomainName() string { 185 return i.Domain 186 } 187 188 // GetContextName returns the name of the context. 189 func (i *Instance) GetContextName() string { 190 return i.ContextName 191 } 192 193 // SessionSecret returns the session secret. 194 func (i *Instance) SessionSecret() []byte { 195 // The prefix is here to invalidate all the sessions that were created on 196 // an instance where the password was not hashed on client-side. It force 197 // the user to log in again and migrate its passphrase to be hashed on the 198 // client. It is simpler/safer and, in particular, it avoids that he/she 199 // can try to changed its pass in settings (which would fail). 200 secret := make([]byte, 2+len(i.SessSecret)) 201 secret[0] = '2' 202 secret[1] = ':' 203 copy(secret[2:], i.SessSecret) 204 return secret 205 } 206 207 // SlugAndDomain returns the splitted slug and domain of the instance 208 // Ex: foobar.mycozy.cloud => ["foobar", "mycozy.cloud"] 209 func (i *Instance) SlugAndDomain() (string, string) { 210 splitted := strings.SplitN(i.Domain, ".", 2) 211 return splitted[0], splitted[1] 212 } 213 214 // Logger returns the logger associated with the instance 215 func (i *Instance) Logger() *logger.Entry { 216 return logger.WithDomain(i.Domain) 217 } 218 219 // VFS returns the storage provider where the binaries for the current instance 220 // are persisted 221 func (i *Instance) VFS() vfs.VFS { 222 if i.vfs == nil { 223 panic("instance: calling VFS() before makeVFS()") 224 } 225 return i.vfs 226 } 227 228 // MakeVFS is used to initialize the VFS linked to this instance 229 func (i *Instance) MakeVFS() error { 230 if i.vfs != nil { 231 return nil 232 } 233 fsURL := config.FsURL() 234 mutex := config.Lock().ReadWrite(i, "vfs") 235 index := vfs.NewCouchdbIndexer(i) 236 disk := vfs.DiskThresholder(i) 237 var err error 238 switch fsURL.Scheme { 239 case config.SchemeFile, config.SchemeMem: 240 i.vfs, err = vfsafero.New(i, index, disk, mutex, fsURL, i.DirName()) 241 case config.SchemeSwift, config.SchemeSwiftSecure: 242 switch i.SwiftLayout { 243 case 2: 244 i.vfs, err = vfsswift.NewV3(i, index, disk, mutex) 245 default: 246 err = ErrInvalidSwiftLayout 247 } 248 default: 249 err = fmt.Errorf("instance: unknown storage provider %s", fsURL.Scheme) 250 } 251 return err 252 } 253 254 // ThumbsFS returns the hidden filesystem for storing the thumbnails of the 255 // photos/image 256 func (i *Instance) ThumbsFS() vfs.Thumbser { 257 fsURL := config.FsURL() 258 switch fsURL.Scheme { 259 case config.SchemeFile: 260 baseFS := afero.NewBasePathFs(afero.NewOsFs(), 261 path.Join(fsURL.Path, i.DirName(), vfs.ThumbsDirName)) 262 return vfsafero.NewThumbsFs(baseFS) 263 case config.SchemeMem: 264 baseFS := vfsafero.GetMemFS(i.DomainName() + "-thumbs") 265 return vfsafero.NewThumbsFs(baseFS) 266 case config.SchemeSwift, config.SchemeSwiftSecure: 267 switch i.SwiftLayout { 268 case 2: 269 return vfsswift.NewThumbsFsV3(config.GetSwiftConnection(), i) 270 default: 271 panic(ErrInvalidSwiftLayout) 272 } 273 default: 274 panic(fmt.Sprintf("instance: unknown storage provider %s", fsURL.Scheme)) 275 } 276 } 277 278 // EnsureSharedDrivesDir returns the Shared Drives directory, and creates it if 279 // it doesn't exist 280 func (i *Instance) EnsureSharedDrivesDir() (*vfs.DirDoc, error) { 281 fs := i.VFS() 282 dir, err := fs.DirByID(consts.SharedDrivesDirID) 283 if err != nil && !errors.Is(err, os.ErrNotExist) { 284 return nil, err 285 } 286 if dir != nil { 287 return dir, nil 288 } 289 290 name := i.Translate("Tree Shared Drives") 291 dir, err = vfs.NewDirDocWithPath(name, consts.RootDirID, "/", nil) 292 if err != nil { 293 return nil, err 294 } 295 dir.DocID = consts.SharedDrivesDirID 296 dir.CozyMetadata = vfs.NewCozyMetadata(i.PageURL("/", nil)) 297 err = fs.CreateDir(dir) 298 if errors.Is(err, os.ErrExist) { 299 dir, err = fs.DirByPath(dir.Fullpath) 300 } 301 if err != nil { 302 return nil, err 303 } 304 return dir, nil 305 } 306 307 // NotesLock returns a mutex for the notes on this instance. 308 func (i *Instance) NotesLock() lock.ErrorRWLocker { 309 return config.Lock().ReadWrite(i, "notes") 310 } 311 312 func (i *Instance) SetPasswordDefined(defined bool) { 313 if (i.PasswordDefined == nil || !*i.PasswordDefined) && defined { 314 doc := couchdb.JSONDoc{ 315 Type: consts.Settings, 316 M: map[string]interface{}{"_id": consts.PassphraseParametersID}, 317 } 318 realtime.GetHub().Publish(i, realtime.EventCreate, &doc, nil) 319 } 320 321 i.PasswordDefined = &defined 322 } 323 324 // SettingsDocument returns the document with the settings of this instance 325 func (i *Instance) SettingsDocument() (*couchdb.JSONDoc, error) { 326 doc := &couchdb.JSONDoc{} 327 err := couchdb.GetDoc(i, consts.Settings, consts.InstanceSettingsID, doc) 328 if err != nil { 329 return nil, err 330 } 331 doc.Type = consts.Settings 332 return doc, nil 333 } 334 335 // SettingsEMail returns the email address defined in the settings of this 336 // instance. 337 func (i *Instance) SettingsEMail() (string, error) { 338 settings, err := i.SettingsDocument() 339 if err != nil { 340 return "", err 341 } 342 email, _ := settings.M["email"].(string) 343 return email, nil 344 } 345 346 // SettingsPublicName returns the public name defined in the settings of this 347 // instance. 348 func (i *Instance) SettingsPublicName() (string, error) { 349 settings, err := i.SettingsDocument() 350 if err != nil { 351 return "", err 352 } 353 name, _ := settings.M["public_name"].(string) 354 return name, nil 355 } 356 357 // GetFromContexts returns the parameters specific to the instance context 358 func (i *Instance) GetFromContexts(contexts map[string]interface{}) (interface{}, bool) { 359 if contexts == nil { 360 return nil, false 361 } 362 363 if i.ContextName != "" { 364 context, ok := contexts[i.ContextName] 365 if ok { 366 return context, true 367 } 368 } 369 370 context, ok := contexts[config.DefaultInstanceContext] 371 if ok && context != nil { 372 return context, ok 373 } 374 375 return nil, false 376 } 377 378 // SettingsContext returns the map from the config that matches the context of 379 // this instance 380 func (i *Instance) SettingsContext() (map[string]interface{}, bool) { 381 contexts := config.GetConfig().Contexts 382 context, ok := i.GetFromContexts(contexts) 383 if !ok { 384 return nil, false 385 } 386 settings := context.(map[string]interface{}) 387 return settings, true 388 } 389 390 // SupportEmailAddress returns the email address that can be used to contact 391 // the support. 392 func (i *Instance) SupportEmailAddress() string { 393 if ctxSettings, ok := i.SettingsContext(); ok { 394 if email, ok := ctxSettings["support_address"].(string); ok { 395 return email 396 } 397 } 398 return "contact@cozycloud.cc" 399 } 400 401 // TemplateTitle returns the specific-context instance template title (if there 402 // is one). Otherwise, returns the default one 403 func (i *Instance) TemplateTitle() string { 404 ctxSettings, ok := i.SettingsContext() 405 if !ok { 406 return DefaultTemplateTitle 407 } 408 if title, ok := ctxSettings["templates_title"].(string); ok && title != "" { 409 return title 410 } 411 return DefaultTemplateTitle 412 } 413 414 // MoveURL returns URL for move wizard. 415 func (i *Instance) MoveURL() string { 416 moveURL := config.GetConfig().Move.URL 417 if settings, ok := i.SettingsContext(); ok { 418 if u, ok := settings["move_url"].(string); ok { 419 moveURL = u 420 } 421 } 422 return moveURL 423 } 424 425 // Registries returns the list of registries associated with the instance. 426 func (i *Instance) Registries() []*url.URL { 427 contexts := config.GetConfig().Registries 428 var context []*url.URL 429 var ok bool 430 if i.ContextName != "" { 431 context, ok = contexts[i.ContextName] 432 } 433 if !ok { 434 context, ok = contexts[config.DefaultInstanceContext] 435 if !ok { 436 context = make([]*url.URL, 0) 437 } 438 } 439 return context 440 } 441 442 // HasForcedOIDC returns true only if the instance is in a context where the 443 // config says that the stack shouldn't allow to authenticate with the 444 // password. 445 func (i *Instance) HasForcedOIDC() bool { 446 if i.ContextName == "" { 447 return false 448 } 449 auth, ok := config.GetConfig().Authentication[i.ContextName].(map[string]interface{}) 450 if !ok { 451 return false 452 } 453 disabled, ok := auth["disable_password_authentication"].(bool) 454 if !ok { 455 return false 456 } 457 return disabled 458 } 459 460 // PassphraseSalt computes the salt for the client-side hashing of the master 461 // password. The rule for computing the salt is to create a fake email address 462 // "me@<domain>". 463 func (i *Instance) PassphraseSalt() []byte { 464 domain := strings.Split(i.Domain, ":")[0] // Skip the optional port 465 return []byte("me@" + domain) 466 } 467 468 // DiskQuota returns the number of bytes allowed on the disk to the user. 469 func (i *Instance) DiskQuota() int64 { 470 return i.BytesDiskQuota 471 } 472 473 // WithContextualDomain the current instance context with the given hostname. 474 func (i *Instance) WithContextualDomain(domain string) *Instance { 475 if i.HasDomain(domain) { 476 i.contextualDomain = domain 477 } 478 return i 479 } 480 481 // Scheme returns the scheme used for URLs. It is https by default and http 482 // for development instances. 483 func (i *Instance) Scheme() string { 484 if build.IsDevRelease() { 485 return "http" 486 } 487 return "https" 488 } 489 490 // ContextualDomain returns the domain with regard to the current domain 491 // request. 492 func (i *Instance) ContextualDomain() string { 493 if i.contextualDomain != "" { 494 return i.contextualDomain 495 } 496 return i.Domain 497 } 498 499 // HasDomain returns whether or not the given domain name is owned by this 500 // instance, as part of its main domain name or its aliases. 501 func (i *Instance) HasDomain(domain string) bool { 502 if domain == i.Domain { 503 return true 504 } 505 for _, alias := range i.DomainAliases { 506 if domain == alias { 507 return true 508 } 509 } 510 return false 511 } 512 513 // SubDomain returns the full url for a subdomain of this instance 514 // useful with apps slugs 515 func (i *Instance) SubDomain(s string) *url.URL { 516 domain := i.ContextualDomain() 517 if config.GetConfig().Subdomains == config.NestedSubdomains { 518 domain = s + "." + domain 519 } else { 520 parts := strings.SplitN(domain, ".", 2) 521 domain = parts[0] + "-" + s + "." + parts[1] 522 } 523 return &url.URL{ 524 Scheme: i.Scheme(), 525 Host: domain, 526 Path: "/", 527 } 528 } 529 530 // ChangePasswordURL returns the URL of the settings page that can be used by 531 // the user to change their password. 532 func (i *Instance) ChangePasswordURL() string { 533 u := i.SubDomain(consts.SettingsSlug) 534 u.Fragment = "/profile/password" 535 return u.String() 536 } 537 538 // FromURL normalizes a given url with the scheme and domain of the instance. 539 func (i *Instance) FromURL(u *url.URL) string { 540 u2 := url.URL{ 541 Scheme: i.Scheme(), 542 Host: i.ContextualDomain(), 543 Path: u.Path, 544 RawQuery: u.RawQuery, 545 Fragment: u.Fragment, 546 } 547 return u2.String() 548 } 549 550 // PageURL returns the full URL for a path on the cozy stack 551 func (i *Instance) PageURL(path string, queries url.Values) string { 552 var query string 553 if queries != nil { 554 query = queries.Encode() 555 } 556 u := url.URL{ 557 Scheme: i.Scheme(), 558 Host: i.ContextualDomain(), 559 Path: path, 560 RawQuery: query, 561 } 562 return u.String() 563 } 564 565 func (i *Instance) parseRedirectAppAndRoute(redirect string) *url.URL { 566 splits := strings.SplitN(redirect, "#", 2) 567 parts := strings.SplitN(splits[0], "/", 2) 568 u := i.SubDomain(parts[0]) 569 if len(parts) == 2 { 570 u.Path = parts[1] 571 } 572 if len(splits) == 2 { 573 u.Fragment = splits[1] 574 } 575 return u 576 } 577 578 // DefaultAppAndPath returns the default_redirection from the context, in the 579 // slug+path format (or use the home as the default application). 580 func (i *Instance) DefaultAppAndPath() string { 581 context, ok := i.SettingsContext() 582 if !ok { 583 return consts.HomeSlug + "/" 584 } 585 redirect, ok := context["default_redirection"].(string) 586 if !ok { 587 return consts.HomeSlug + "/" 588 } 589 return redirect 590 } 591 592 func (i *Instance) redirection(key, defaultSlug string) *url.URL { 593 context, ok := i.SettingsContext() 594 if !ok { 595 return i.SubDomain(defaultSlug) 596 } 597 redirect, ok := context[key].(string) 598 if !ok { 599 return i.SubDomain(defaultSlug) 600 } 601 return i.parseRedirectAppAndRoute(redirect) 602 } 603 604 // DefaultRedirection returns the URL where to redirect the user afer login 605 // (and in most other cases where we need a redirection URL) 606 func (i *Instance) DefaultRedirection() *url.URL { 607 if doc, err := i.SettingsDocument(); err == nil { 608 // XXX we had a bug where the default_redirection was filled by a full URL 609 // instead of slug+path, and we should ignore the bad format here. 610 if redirect, ok := doc.M["default_redirection"].(string); ok && !strings.HasPrefix(redirect, "http") { 611 return i.parseRedirectAppAndRoute(redirect) 612 } 613 } 614 615 return i.redirection("default_redirection", consts.HomeSlug) 616 } 617 618 // DefaultRedirectionFromContext returns the URL where to redirect the user 619 // after login from the context parameters. It can be overloaded by instance 620 // via the "default_redirection" setting. 621 func (i *Instance) DefaultRedirectionFromContext() *url.URL { 622 return i.redirection("default_redirection", consts.HomeSlug) 623 } 624 625 // OnboardedRedirection returns the URL where to redirect the user after 626 // onboarding 627 func (i *Instance) OnboardedRedirection() *url.URL { 628 return i.redirection("onboarded_redirection", consts.HomeSlug) 629 } 630 631 // Translate is used to translate a string to the locale used on this instance 632 func (i *Instance) Translate(key string, vars ...interface{}) string { 633 return i18n.Translate(key, i.Locale, i.ContextName, vars...) 634 } 635 636 // List returns the list of declared instances. 637 func List() ([]*Instance, error) { 638 var all []*Instance 639 err := ForeachInstances(func(doc *Instance) error { 640 all = append(all, doc) 641 return nil 642 }) 643 if err != nil { 644 return nil, err 645 } 646 return all, nil 647 } 648 649 // ForeachInstances execute the given callback for each instances. 650 func ForeachInstances(fn func(*Instance) error) error { 651 return couchdb.ForeachDocsWithCustomPagination(prefixer.GlobalPrefixer, consts.Instances, 10000, func(_ string, data json.RawMessage) error { 652 var doc *Instance 653 if err := json.Unmarshal(data, &doc); err != nil { 654 return err 655 } 656 return fn(doc) 657 }) 658 } 659 660 // PaginatedList can be used to list the instances, with pagination. 661 func PaginatedList(limit int, startKey string, skip int) ([]*Instance, string, error) { 662 var docs []*Instance 663 req := &couchdb.AllDocsRequest{ 664 // Also get the following document for the next key, 665 // and a few more because of the design docs 666 Limit: limit + 10, 667 StartKey: startKey, 668 Skip: skip, 669 } 670 err := couchdb.GetAllDocs(prefixer.GlobalPrefixer, consts.Instances, req, &docs) 671 if err != nil { 672 return nil, "", err 673 } 674 675 if len(docs) > limit { // There are still documents to fetch 676 nextDoc := docs[limit] 677 docs = docs[:limit] 678 return docs, nextDoc.ID(), nil 679 } 680 return docs, "", nil 681 } 682 683 // PickKey choose which of the Instance keys to use depending on token audience 684 func (i *Instance) PickKey(audience string) ([]byte, error) { 685 switch audience { 686 case consts.AppAudience, consts.KonnectorAudience: 687 return i.SessionSecret(), nil 688 case consts.RefreshTokenAudience, consts.AccessTokenAudience, consts.ShareAudience: 689 return i.OAuthSecret, nil 690 case consts.CLIAudience: 691 return i.CLISecret, nil 692 } 693 return nil, permission.ErrInvalidAudience 694 } 695 696 // MakeJWT is a shortcut to create a JWT 697 func (i *Instance) MakeJWT(audience, subject, scope, sessionID string, issuedAt time.Time) (string, error) { 698 secret, err := i.PickKey(audience) 699 if err != nil { 700 return "", err 701 } 702 return crypto.NewJWT(secret, permission.Claims{ 703 RegisteredClaims: jwt.RegisteredClaims{ 704 Audience: jwt.ClaimStrings{audience}, 705 Issuer: i.Domain, 706 IssuedAt: jwt.NewNumericDate(issuedAt), 707 Subject: subject, 708 }, 709 Scope: scope, 710 SessionID: sessionID, 711 }) 712 } 713 714 // BuildAppToken is used to build a token to identify the app for requests made 715 // to the stack 716 func (i *Instance) BuildAppToken(slug, sessionID string) string { 717 scope := "" // apps tokens don't have a scope 718 now := time.Now() 719 token, err := i.MakeJWT(consts.AppAudience, slug, scope, sessionID, now) 720 if err != nil { 721 return "" 722 } 723 return token 724 } 725 726 // BuildKonnectorToken is used to build a token to identify the konnector for 727 // requests made to the stack 728 func (i *Instance) BuildKonnectorToken(slug string) string { 729 scope := "" // apps tokens don't have a scope 730 token, err := i.MakeJWT(consts.KonnectorAudience, slug, scope, "", time.Now()) 731 if err != nil { 732 return "" 733 } 734 return token 735 } 736 737 // CreateShareCode returns a new sharecode to put the codes field of a 738 // permissions document 739 func (i *Instance) CreateShareCode(subject string) (string, error) { 740 scope := "" 741 sessionID := "" 742 return i.MakeJWT(consts.ShareAudience, subject, scope, sessionID, time.Now()) 743 } 744 745 // MovedError is used to return an error when the instance has been moved to a 746 // new domain/hoster. 747 func (i *Instance) MovedError() *jsonapi.Error { 748 if !i.Moved { 749 return nil 750 } 751 jerr := jsonapi.Error{ 752 Status: http.StatusGone, 753 Title: "Cozy has been moved", 754 Code: "moved", 755 Detail: i.Translate("The Cozy has been moved to a new address"), 756 } 757 doc, err := i.SettingsDocument() 758 if err == nil { 759 if to, ok := doc.M["moved_to"].(string); ok { 760 jerr.Links = &jsonapi.LinksList{Related: to} 761 } 762 } 763 return &jerr 764 } 765 766 func (i *Instance) HasPremiumLinksEnabled() bool { 767 if ctxSettings, ok := i.SettingsContext(); ok { 768 if enabled, ok := ctxSettings["enable_premium_links"].(bool); ok { 769 return enabled 770 } 771 } 772 return false 773 } 774 775 // ensure Instance implements couchdb.Doc 776 var ( 777 _ couchdb.Doc = &Instance{} 778 )