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  )