github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/instance/lifecycle/create.go (about)

     1  package lifecycle
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"encoding/hex"
     7  	"errors"
     8  	"math"
     9  	"math/rand"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/cozy/cozy-stack/model/contact"
    15  	"github.com/cozy/cozy-stack/model/instance"
    16  	"github.com/cozy/cozy-stack/pkg/config/config"
    17  	"github.com/cozy/cozy-stack/pkg/consts"
    18  	"github.com/cozy/cozy-stack/pkg/couchdb"
    19  	"github.com/cozy/cozy-stack/pkg/crypto"
    20  	"github.com/cozy/cozy-stack/pkg/logger"
    21  	"github.com/cozy/cozy-stack/pkg/prefixer"
    22  	"github.com/cozy/cozy-stack/pkg/utils"
    23  	"golang.org/x/sync/errgroup"
    24  )
    25  
    26  // Options holds the parameters to create a new instance.
    27  type Options struct {
    28  	Domain             string
    29  	DomainAliases      []string
    30  	Locale             string
    31  	UUID               string
    32  	OIDCID             string
    33  	FranceConnectID    string
    34  	TOSSigned          string
    35  	TOSLatest          string
    36  	Timezone           string
    37  	ContextName        string
    38  	Sponsorships       []string
    39  	FeatureSets        []string
    40  	Email              string
    41  	PublicName         string
    42  	Settings           string
    43  	SettingsObj        *couchdb.JSONDoc
    44  	AuthMode           string
    45  	Passphrase         string
    46  	Key                string
    47  	KdfIterations      int
    48  	SwiftLayout        int
    49  	CouchCluster       int
    50  	DiskQuota          int64
    51  	Apps               []string
    52  	AutoUpdate         *bool
    53  	MagicLink          *bool
    54  	Debug              *bool
    55  	Traced             *bool
    56  	OnboardingFinished *bool
    57  	Blocked            *bool
    58  	BlockingReason     string
    59  	FromCloudery       bool // Do not call the cloudery when the changes come from it
    60  }
    61  
    62  func (opts *Options) trace(name string, do func()) {
    63  	if opts.Traced != nil && *opts.Traced {
    64  		t := time.Now()
    65  		defer func() {
    66  			elapsed := time.Since(t)
    67  			logger.
    68  				WithDomain("admin").
    69  				WithNamespace("trace").
    70  				Infof("%s: %v", name, elapsed)
    71  		}()
    72  	}
    73  	do()
    74  }
    75  
    76  // Create builds an instance and initializes it
    77  func Create(opts *Options) (*instance.Instance, error) {
    78  	domain := opts.Domain
    79  	var err error
    80  	opts.trace("validate domain", func() {
    81  		domain, err = validateDomain(domain)
    82  	})
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	opts.trace("check if instance already exist", func() {
    87  		_, err = instance.GetFromCouch(domain)
    88  	})
    89  	if !errors.Is(err, instance.ErrNotFound) {
    90  		if err == nil {
    91  			err = instance.ErrExists
    92  		}
    93  		return nil, err
    94  	}
    95  
    96  	locale := opts.Locale
    97  	if locale == "" {
    98  		locale = consts.DefaultLocale
    99  	}
   100  
   101  	if opts.SettingsObj == nil {
   102  		opts.SettingsObj = &couchdb.JSONDoc{M: make(map[string]interface{})}
   103  	}
   104  
   105  	settings, err := buildSettings(nil, opts)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	prefix := sha256.Sum256([]byte(domain))
   110  	i := &instance.Instance{}
   111  	i.Domain = domain
   112  	i.DomainAliases, err = checkAliases(i, opts.DomainAliases)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	i.Prefix = "cozy" + hex.EncodeToString(prefix[:16])
   117  	i.Locale = locale
   118  	i.UUID = opts.UUID
   119  	i.OIDCID = opts.OIDCID
   120  	i.FranceConnectID = opts.FranceConnectID
   121  	i.TOSSigned = opts.TOSSigned
   122  	i.TOSLatest = opts.TOSLatest
   123  	i.ContextName = opts.ContextName
   124  	i.Sponsorships = opts.Sponsorships
   125  	i.FeatureSets = opts.FeatureSets
   126  	i.BytesDiskQuota = opts.DiskQuota
   127  	i.IndexViewsVersion = couchdb.IndexViewsVersion
   128  	opts.trace("generate secrets", func() {
   129  		i.RegisterToken = crypto.GenerateRandomBytes(instance.RegisterTokenLen)
   130  		i.SessSecret = crypto.GenerateRandomBytes(instance.SessionSecretLen)
   131  		i.OAuthSecret = crypto.GenerateRandomBytes(instance.OauthSecretLen)
   132  		i.CLISecret = crypto.GenerateRandomBytes(instance.OauthSecretLen)
   133  	})
   134  
   135  	switch config.FsURL().Scheme {
   136  	case config.SchemeSwift, config.SchemeSwiftSecure:
   137  		switch opts.SwiftLayout {
   138  		case 0:
   139  			return nil, errors.New("Swift layout v1 disabled for instance creation")
   140  		case 1, 2:
   141  			i.SwiftLayout = opts.SwiftLayout
   142  		default:
   143  			i.SwiftLayout = config.GetConfig().Fs.DefaultLayout
   144  		}
   145  	}
   146  
   147  	if opts.CouchCluster >= 0 {
   148  		i.CouchCluster = opts.CouchCluster
   149  	} else {
   150  		clusters := config.GetConfig().CouchDB.Clusters
   151  		i.CouchCluster, err = ChooseCouchCluster(clusters)
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  	}
   156  
   157  	if opts.AuthMode != "" {
   158  		var authMode instance.AuthMode
   159  		if authMode, err = instance.StringToAuthMode(opts.AuthMode); err == nil {
   160  			i.AuthMode = authMode
   161  		}
   162  	}
   163  
   164  	opts.trace("init couchdb", func() {
   165  		g, _ := errgroup.WithContext(context.Background())
   166  		g.Go(func() error { return couchdb.CreateDB(i, consts.Files) })
   167  		g.Go(func() error { return couchdb.CreateDB(i, consts.Apps) })
   168  		g.Go(func() error { return couchdb.CreateDB(i, consts.Konnectors) })
   169  		g.Go(func() error { return couchdb.CreateDB(i, consts.OAuthClients) })
   170  		g.Go(func() error { return couchdb.CreateDB(i, consts.Jobs) })
   171  		g.Go(func() error { return couchdb.CreateDB(i, consts.Triggers) })
   172  		g.Go(func() error { return couchdb.CreateDB(i, consts.Permissions) })
   173  		g.Go(func() error { return couchdb.CreateDB(i, consts.Sharings) })
   174  		g.Go(func() error { return couchdb.CreateDB(i, consts.BitwardenCiphers) })
   175  		g.Go(func() error { return couchdb.CreateDB(i, consts.SessionsLogins) })
   176  		g.Go(func() error { return couchdb.CreateDB(i, consts.Notifications) })
   177  		g.Go(func() error {
   178  			var errg error
   179  			if errg = couchdb.CreateNamedDocWithDB(i, settings); errg != nil {
   180  				return errg
   181  			}
   182  			_, errg = contact.CreateMyself(i, settings)
   183  			return errg
   184  		})
   185  		err = g.Wait()
   186  	})
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	opts.trace("define views and indexes", func() {
   192  		err = DefineViewsAndIndex(i)
   193  	})
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	if magicLink := opts.MagicLink; magicLink != nil {
   199  		i.MagicLink = *magicLink
   200  	}
   201  
   202  	passwordDefined := opts.Passphrase != ""
   203  
   204  	// If the password authentication is disabled, we force a random password.
   205  	// It won't be known by the user and cannot be used to authenticate. It
   206  	// will only be used if the configuration is changed later: the user will
   207  	// be able to reset the passphrase. Same when the user has used
   208  	// FranceConnect to create their instance.
   209  	if i.HasForcedOIDC() || i.FranceConnectID != "" || i.MagicLink {
   210  		opts.Passphrase = utils.RandomString(instance.RegisterTokenLen)
   211  		opts.KdfIterations = crypto.DefaultPBKDF2Iterations
   212  	}
   213  
   214  	if opts.Passphrase != "" {
   215  		opts.trace("register passphrase", func() {
   216  			err = registerPassphrase(i, i.RegisterToken, PassParameters{
   217  				Pass:       []byte(opts.Passphrase),
   218  				Iterations: opts.KdfIterations,
   219  				Key:        opts.Key,
   220  			})
   221  		})
   222  		if err != nil {
   223  			return nil, err
   224  		}
   225  		// set the onboarding finished when specifying a passphrase. we totally
   226  		// skip the onboarding in that case.
   227  		i.OnboardingFinished = true
   228  	}
   229  
   230  	i.SetPasswordDefined(passwordDefined)
   231  	if onboardingFinished := opts.OnboardingFinished; onboardingFinished != nil {
   232  		i.OnboardingFinished = *onboardingFinished
   233  	}
   234  
   235  	if autoUpdate := opts.AutoUpdate; autoUpdate != nil {
   236  		i.NoAutoUpdate = !(*opts.AutoUpdate)
   237  	}
   238  
   239  	if err = couchdb.CreateDoc(prefixer.GlobalPrefixer, i); err != nil {
   240  		return nil, err
   241  	}
   242  
   243  	opts.trace("init VFS", func() {
   244  		if err = i.MakeVFS(); err != nil {
   245  			return
   246  		}
   247  		if err = i.VFS().InitFs(); err != nil {
   248  			return
   249  		}
   250  		err = createDefaultFilesTree(i)
   251  	})
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	opts.trace("install apps", func() {
   257  		done := make(chan struct{})
   258  		for _, app := range opts.Apps {
   259  			go func(app string) {
   260  				if err := installApp(i, app); err != nil {
   261  					i.Logger().Errorf("Failed to install %s: %s", app, err)
   262  				}
   263  				done <- struct{}{}
   264  			}(app)
   265  		}
   266  		for range opts.Apps {
   267  			<-done
   268  		}
   269  	})
   270  
   271  	return i, nil
   272  }
   273  
   274  func ChooseCouchCluster(clusters []config.CouchDBCluster) (int, error) {
   275  	index := -1
   276  	var count uint32 = 0
   277  	for i, cluster := range clusters {
   278  		if !cluster.Creation {
   279  			continue
   280  		}
   281  		count++
   282  		if rand.Uint32() <= math.MaxUint32/count {
   283  			index = i
   284  		}
   285  	}
   286  	if index < 0 {
   287  		return index, errors.New("no CouchDB cluster available for creation")
   288  	}
   289  	return index, nil
   290  }
   291  
   292  func buildSettings(inst *instance.Instance, opts *Options) (*couchdb.JSONDoc, error) {
   293  	var settings *couchdb.JSONDoc
   294  	if opts.SettingsObj != nil {
   295  		settings = opts.SettingsObj
   296  	} else {
   297  		var err error
   298  		settings, err = inst.SettingsDocument()
   299  		if err != nil {
   300  			return nil, err
   301  		}
   302  	}
   303  
   304  	settings.Type = consts.Settings
   305  	settings.SetID(consts.InstanceSettingsID)
   306  
   307  	for _, s := range strings.Split(opts.Settings, ",") {
   308  		if parts := strings.SplitN(s, ":", 2); len(parts) == 2 {
   309  			settings.M[parts[0]] = parts[1]
   310  		}
   311  	}
   312  
   313  	// Handling global/instance settings
   314  	if contextName, ok := settings.M["context"].(string); ok {
   315  		opts.ContextName = contextName
   316  		delete(settings.M, "context")
   317  	}
   318  	if sponsorships, ok := settings.M["sponsorships"].([]string); ok {
   319  		opts.Sponsorships = sponsorships
   320  		delete(settings.M, "sponsorships")
   321  	}
   322  	if featureSets, ok := settings.M["feature_sets"].([]string); ok {
   323  		opts.FeatureSets = featureSets
   324  		delete(settings.M, "feature_sets")
   325  	}
   326  	if locale, ok := settings.M["locale"].(string); ok {
   327  		opts.Locale = locale
   328  		delete(settings.M, "locale")
   329  	}
   330  	if onboardingFinished, ok := settings.M["onboarding_finished"].(bool); ok {
   331  		opts.OnboardingFinished = &onboardingFinished
   332  		delete(settings.M, "onboarding_finished")
   333  	}
   334  	if uuid, ok := settings.M["uuid"].(string); ok {
   335  		opts.UUID = uuid
   336  		delete(settings.M, "uuid")
   337  	}
   338  	if oidcID, ok := settings.M["oidc_id"].(string); ok {
   339  		opts.OIDCID = oidcID
   340  		delete(settings.M, "oidc_id")
   341  	}
   342  	if tos, ok := settings.M["tos"].(string); ok {
   343  		opts.TOSSigned = tos
   344  		delete(settings.M, "tos")
   345  	}
   346  	if tos, ok := settings.M["tos_latest"].(string); ok {
   347  		opts.TOSLatest = tos
   348  		delete(settings.M, "tos_latest")
   349  	}
   350  	if autoUpdate, ok := settings.M["auto_update"].(string); ok {
   351  		if b, err := strconv.ParseBool(autoUpdate); err == nil {
   352  			opts.AutoUpdate = &b
   353  		}
   354  		delete(settings.M, "auto_update")
   355  	}
   356  	if authMode, ok := settings.M["auth_mode"].(string); ok {
   357  		opts.AuthMode = authMode
   358  		delete(settings.M, "auth_mode")
   359  	}
   360  
   361  	// Handling instance settings document
   362  	if tz := opts.Timezone; tz != "" {
   363  		settings.M["tz"] = tz
   364  	}
   365  	if email := opts.Email; email != "" {
   366  		settings.M["email"] = email
   367  	}
   368  	if name := opts.PublicName; name != "" {
   369  		settings.M["public_name"] = name
   370  	}
   371  
   372  	if len(opts.TOSSigned) == 8 {
   373  		opts.TOSSigned = "1.0.0-" + opts.TOSSigned
   374  	}
   375  
   376  	return settings, nil
   377  }