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 }