snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
package provision
import (
"context"
"fmt"
"net/http"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
func apigwFilters(ctx context.Context, log *zap.Logger, s store.Storer) (err error) {
log.Info("provision start")
defer log.Info("provision end")
var (
filters types.ApigwFilterSet
)
if filters, _, err = store.SearchApigwFilters(ctx, s, types.ApigwFilterFilter{Ref: "jsonResponse"}); err != nil {
return
}
for _, f := range filters {
h := http.Header{}
h.Add("Content-Type", "application/json")
f.Ref = "response"
f.Params = map[string]interface{}{
"header": h,
"input": &atypes.Expr{Expr: f.Params["input"].(string), Type: "String"},
}
if err = store.UpdateApigwFilter(ctx, s, f); err != nil {
log.Warn(fmt.Sprintf("could not migrate jsonResponse to response: %s", err))
}
}
return
}

View File

@@ -0,0 +1,337 @@
package provision
import (
"context"
"fmt"
"os"
"regexp"
"strings"
internalAuth "github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/handle"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/mail"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/system/service"
"go.uber.org/zap"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/rand"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
var (
nextID = func() uint64 {
return id.Next()
}
)
// Sets email-related settings (if not set) under "auth.internal..."
func emailSettings(ctx context.Context, s store.Storer) error {
var (
val, has = os.LookupEnv("SMTP_HOST")
canSendEmails = has && len(val) > 0
)
// List of name-value pairs we need to iterate and set
ss := types.SettingValueSet{
types.MakeSettingValue(
"auth.internal.signup.email-confirmation-required",
canSendEmails,
),
types.MakeSettingValue(
"auth.internal.password-reset.enabled",
canSendEmails,
),
}
return s.Tx(ctx, func(ctx context.Context, s store.Storer) error {
return ss.Walk(func(setting *types.SettingValue) error {
_, err := store.LookupSettingValueByNameOwnedBy(ctx, s, setting.Name, 0)
if errors.IsNotFound(err) {
setting.UpdatedAt = *now()
return store.CreateSettingValue(ctx, s, setting)
}
return err
})
})
}
// addAuthSuperUsers assigns BYPASS roles to users from AUTH_PROVISION_SUPER_USER value
// When in Production, Corteza should stop and report an error.
func addAuthSuperUsers(ctx context.Context, log *zap.Logger, s store.Storer, authOpt options.AuthOpt) (err error) {
log.Info("provision start")
defer log.Info("provision end")
var (
envOpt = options.Environment()
)
if authOpt.ProvisionSuperUser == "" {
return nil
}
if envOpt.IsProduction() {
log.Warn(fmt.Sprint("when in production environment (ENVIRONMENT=production) you cannot provision " +
"super users; set the environment to dev (ENVIRONMENT=dev) to provision super users"))
return
}
users := strings.Split(authOpt.ProvisionSuperUser, ";")
for _, usr := range users {
u := prepareUser(usr)
//check if the email address is valid
if !mail.IsValidAddress(u.Email) {
log.Warn(fmt.Sprintf("Email address %s is invalid", u.Email))
continue
}
// skip existing email
_, err = s.LookupUserByEmail(ctx, u.Email)
if err != store.ErrNotFound {
log.Warn(fmt.Sprintf("Email address already %s exists", u.Email))
continue
}
// skip existing handle
_, err = s.LookupUserByHandle(ctx, u.Handle)
if err != store.ErrNotFound {
log.Warn(fmt.Sprintf("Handle %s already exists", u.Handle))
continue
}
err = store.Tx(ctx, s, func(ctx context.Context, s store.Storer) (err error) {
if err = store.CreateUser(ctx, s, u); err != nil {
return err
}
if err = service.SetPasswordCredentials(ctx, s, u.ID, u.Email); err != nil {
return err
}
log.Warn(fmt.Sprintf("User {userID: %d, email: %s} created", u.ID, u.Email))
//assign the user a bypass role
for _, r := range internalAuth.BypassRoles() {
m := &types.RoleMember{Resource: fmt.Sprintf("corteza::system:user/%d", u.ID), RoleID: r.ID}
if err = store.CreateRoleMember(ctx, s, m); err != nil {
return err
}
}
return
})
if err != nil {
return err
}
}
return
}
// prepareUser creates and fills a new user depending on the number of arguments provided
func prepareUser(user string) *types.User {
u := &types.User{
ID: nextID(),
CreatedAt: *now(),
}
usr := strings.Split(user, ",")
u.Email = usr[0]
u.EmailConfirmed = true
u.Handle = createUserHandle(u)
if len(usr) > 1 {
u.Handle = usr[1]
}
if len(usr) > 2 {
u.Name = usr[2]
}
return u
}
func createUserHandle(u *types.User) (hdl string) {
hdl, _ = handle.Cast(
func(lookup string) bool {
return true
},
// use email w/o domain
regexp.
MustCompile("(@.*)$").
ReplaceAllString(u.Email, ""),
)
return hdl
}
func defaultUserGroup(ctx context.Context, log *zap.Logger, s store.UserGroups, authOpt options.AuthOpt) error {
log.Info("provision start")
defer log.Info("provision end")
g := &types.UserGroup{
ID: id.Next(),
Handle: authOpt.DefaultUserGroup,
Meta: &types.UserGroupMeta{
Short: "Default User Group",
Description: "The default user group",
},
CreatedAt: *now(),
}
_, err := store.LookupUserGroupByHandle(ctx, s, g.Handle)
if err == nil {
return nil
}
if !errors.IsNotFound(err) {
return err
}
if err = store.CreateUserGroup(ctx, s, g); err != nil {
return err
}
log.Info(
"Added default user group",
zap.String("name", g.Meta.Short),
zap.String("handle", g.Handle),
logger.Uint64("userGroupID", g.ID),
)
g = &types.UserGroup{
ID: id.Next(),
Handle: authOpt.DefaultSubUserGroup,
Config: &types.UserGroupConfig{
Paths: []types.UserGroupPath{{
SelfID: g.ID,
Name: "",
}},
},
Meta: &types.UserGroupMeta{
Short: "Default Sub-User Group",
Description: "The default sub-user group",
},
CreatedAt: *now(),
}
err = store.CreateUserGroup(ctx, s, g)
if err != nil {
return err
}
log.Info(
"Added default sub-user group",
zap.String("name", g.Meta.Short),
zap.String("handle", g.Handle),
logger.Uint64("userGroupID", g.ID),
)
return nil
}
// defaultAuthClient checks if default client exists (handle = AUTH_DEFAULT_CLIENT) and adds it
func defaultAuthClient(ctx context.Context, log *zap.Logger, s store.Storer, authOpt options.AuthOpt) error {
log.Info("provision start")
defer log.Info("provision end")
if authOpt.DefaultClient == "" {
// Default client not set
return nil
}
// Already provisioned; bail out before the user group lookups so a
// deleted group can not fail provisioning on every subsequent boot
if _, err := store.LookupAuthClientByHandle(ctx, s, authOpt.DefaultClient); err == nil {
return nil
} else if !errors.IsNotFound(err) {
return err
}
var (
ug *types.UserGroup
err error
)
if authOpt.DefaultSubUserGroup != "" {
ug, err = store.LookupUserGroupByHandle(ctx, s, authOpt.DefaultSubUserGroup)
if err != nil && !errors.IsNotFound(err) {
return err
}
}
if ug == nil && authOpt.DefaultUserGroup != "" {
ug, err = store.LookupUserGroupByHandle(ctx, s, authOpt.DefaultUserGroup)
if err != nil && !errors.IsNotFound(err) {
return err
}
}
if ug == nil {
// Missing/deleted group; create the client without it instead of aborting
log.Warn(
"default (sub) user group not found or deleted, provisioning default auth client without a security user group",
zap.String("subUserGroup", authOpt.DefaultSubUserGroup),
zap.String("userGroup", authOpt.DefaultUserGroup),
)
}
var ugID uint64
if ug != nil {
ugID = ug.ID
}
c := &types.AuthClient{
ID: id.Next(),
Handle: authOpt.DefaultClient,
Meta: &types.AuthClientMeta{
Name: "Corteza Web Applications",
},
ValidGrant: "authorization_code",
RedirectURI: func() string {
// Disabling protection by redirection URL for now, it caused too much confusion on simple setups
//baseURL, _ := url.Parse(authOpt.BaseURL)
//return fmt.Sprintf("%s://%s", baseURL.Scheme, baseURL.Hostname())
return ""
}(),
Secret: string(rand.Bytes(64)),
Scope: "profile api",
Enabled: true,
Trusted: true,
Security: &types.AuthClientSecurity{
UserGroup: ugID,
},
Labels: nil,
CreatedAt: *now(),
}
_, err = store.LookupAuthClientByHandle(ctx, s, c.Handle)
if err == nil {
return nil
}
if !errors.IsNotFound(err) {
return err
}
if err = store.CreateAuthClient(ctx, s, c); err != nil {
return err
}
log.Info(
"Added OAuth2 client",
zap.String("name", c.Meta.Name),
zap.String("redirectURI", c.RedirectURI),
logger.Uint64("clientId", c.ID),
)
return nil
}

View File

@@ -0,0 +1,178 @@
package provision
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
se "github.com/cortezaproject/corteza/server/system/envoy"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
"go.uber.org/zap"
)
// imports configuration files from path(s)
//
// paths can be colon delimited list of absolute or relative paths and/or with glob pattern
func importConfig(ctx context.Context, log *zap.Logger, s store.Storer, paths string) error {
canImportFull, err := canImportConfig(ctx, s)
if !canImportFull {
log.Info("already provisioned, skipping full config import")
} else if err != nil {
return fmt.Errorf("failed to check if config import can be done: %w", err)
}
var (
nn envoyx.NodeSet
auxN envoyx.NodeSet
evy = envoyx.Global()
sources = make([]string, 0, 16)
)
// verify all paths before doing the actual import
for _, path := range strings.Split(paths, ":") {
if aux, err := filepath.Glob(path); err != nil {
return err
} else {
sources = append(sources, aux...)
}
}
if canImportFull {
log.Info("importing all configs", zap.String("paths", paths))
for _, path := range sources {
log.Info("provisioning from path", zap.String("path", path))
auxN, _, err = evy.Decode(ctx, envoyx.DecodeParams{
Type: envoyx.DecodeTypeURI,
Params: map[string]any{
"uri": "file://" + path,
},
})
if err != nil {
return err
}
nn = append(nn, auxN...)
}
} else {
nn, err = collectUnimportedConfigs(ctx, log, s, sources, evy)
if err != nil {
return err
}
}
// Get potentially missing refs
//
// @todo replace this with getting missing refs and just fetching those.
// For now this is the only scenario so we can get away with this just fine.
rr, _, err := store.SearchRoles(ctx, s, types.RoleFilter{})
if err != nil {
return err
}
for _, r := range rr {
aux, err := se.RoleToEnvoyNode(r)
if err != nil {
return err
}
aux.Placeholder = true
nn = append(nn, aux)
}
// ----------------------------------------------------------------------
return store.Tx(ctx, s, func(ctx context.Context, s store.Storer) (err error) {
ep := envoyx.EncodeParams{
Type: envoyx.EncodeTypeStore,
Params: map[string]any{
"storer": s,
"dal": dal.Service(),
},
Envoy: envoyx.EnvoyConfig{
MergeAlg: envoyx.OnConflictSkip,
},
}
gg, err := evy.Bake(ctx, ep,
nil,
nn...,
)
if err != nil {
return err
}
err = evy.Encode(ctx, ep, gg)
if err != nil {
return err
}
return nil
})
}
// canImportConfig checks state of the store and
// verifies if Corteza should be provisioned (ie config should be imported)
func canImportConfig(ctx context.Context, s store.Storer) (bool, error) {
rr, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{})
return len(rr) == 0, err
}
func collectUnimportedConfigs(ctx context.Context, log *zap.Logger, s store.Storer, sources []string, evy *envoyx.Service) (nn envoyx.NodeSet, err error) {
var (
searchPartialDirectories = []uConfig{
{dir: "000_base", fn: provisionPartialBase},
{dir: "002_templates", fn: provisionPartialTemplates},
{dir: "003_auth", fn: provisionPartialAuthClients},
{dir: "200_federation", fn: nil},
{dir: "300_automation", fn: nil},
}
)
return nn, store.Tx(ctx, s, func(ctx context.Context, s store.Storer) (err error) {
for _, d := range searchPartialDirectories {
// first, check if we need to import at all
if d.fn != nil && !d.fn(ctx, s, log) {
log.Debug("skipping partial config import, no changes", zap.String("dir", d.dir))
continue
}
if list, e := decodeDirectory(ctx, sources, d.dir, evy); e != nil {
return fmt.Errorf("failed to decode configs: %w", err)
} else if len(list) == 0 {
log.Error("failed to execute partial config import, directory not found or no configs", zap.String("dir", d.dir))
return
} else {
log.Debug("partial import ready", zap.String("dir", d.dir))
nn = append(nn, list...)
}
}
return
})
}
func hasSourceDir(sources []string, dir string) (string, bool) {
for _, source := range sources {
if strings.HasSuffix(source, dir) {
return source, true
}
}
return "", false
}
func decodeDirectory(ctx context.Context, sources []string, dir string, evy *envoyx.Service) (res envoyx.NodeSet, err error) {
if source, has := hasSourceDir(sources, dir); has {
res, _, err = evy.Decode(ctx, envoyx.DecodeParams{
Type: envoyx.DecodeTypeURI,
Params: map[string]any{
"uri": "file://" + source,
},
})
}
return
}

View File

@@ -0,0 +1,35 @@
package provision
import (
"context"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/store"
"go.uber.org/zap"
)
func invalidateDedupRules(ctx context.Context, log *zap.Logger, s store.Storer) (err error) {
log.Info("provision start")
defer log.Info("provision end")
var ll types.ModuleSet
if ll, _, err = s.SearchComposeModules(ctx, types.ModuleFilter{}); err != nil {
return
}
// find only the invalid ones and empty the rules, leave the valid ones
// as they were
ll, _ = ll.Filter(func(m *types.Module) (bool, error) {
return m.Config.RecordDeDup.Rules.Validate() != nil, nil
})
ll.Walk(func(m *types.Module) error {
m.Config.RecordDeDup.Rules = types.DeDupRuleSet{}
return nil
})
err = s.UpdateComposeModule(ctx, ll...)
return
}

View File

@@ -0,0 +1,140 @@
package provision
import (
"context"
"fmt"
"os"
"strings"
"github.com/cortezaproject/corteza/server/auth/external"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
// Provisions OIDC providers from PROVISION_OIDC_PROVIDER env variable
//
// Env variable should contains space delimited pairs of providers (<name> <provider> ....)
func oidcAutoDiscovery(ctx context.Context, log *zap.Logger, s store.SettingValues, opt options.AuthOpt) (err error) {
log.Info("provision start")
defer log.Info("provision end")
var provider = strings.TrimSpace(options.EnvString("PROVISION_OIDC_PROVIDER", ""))
log.Debug("OIDC auto discovery provision",
zap.String("envkey", "PROVISION_OIDC_PROVIDER"),
zap.String("providers", provider),
)
if len(provider) == 0 {
return
}
var (
providers = strings.Split(provider, " ")
plen = len(providers)
name, purl string
eap *types.ExternalAuthProvider
)
if plen%2 == 1 {
return fmt.Errorf("expecting even number of providers")
}
for p := 0; p < plen; p = p + 2 {
name, purl = providers[p], providers[p+1]
// force: false
// because we do not want to override the provider every time the system restarts
//
// validate: false
// because at the initial (empty db) start, we cannot actually validate (server is not yet up)
//
// enable: true
// we want provider & the entire external auth to be validated
eap, err = external.RegisterOidcProvider(ctx, log, s, opt, name, purl, false, false, true)
if err != nil {
log.Error(
"could not register OIDC provider",
zap.String("url", purl),
zap.String("name", name),
zap.Error(err))
return
} else if eap == nil {
log.Info("provider already exists",
zap.String("name", name))
} else {
log.Info("provider successfully registered",
zap.String("url", purl),
zap.String("key", eap.Key),
zap.String("name", name))
}
}
return
}
func authAddExternals(ctx context.Context, log *zap.Logger, s store.SettingValues) (err error) {
log.Info("provision start")
defer log.Info("provision end")
var (
kinds = []string{
"github",
"facebook",
"google",
"linkedin",
"oidc",
"nylas",
}
key, name string
pp []string
eap *types.ExternalAuthProvider
)
for _, kind := range kinds {
key = "PROVISION_SETTINGS_AUTH_EXTERNAL_" + strings.ToUpper(kind)
val, has := os.LookupEnv(key)
if !has {
continue
}
if val = strings.TrimSpace(val); val == "" {
log.Warn(fmt.Sprintf("found empty value for %s", key))
continue
} else {
log.Info(fmt.Sprintf("provosioning external auth provider from %s", key))
}
eap = &types.ExternalAuthProvider{Enabled: true}
if kind == "oidc" {
pp = strings.SplitN(val, " ", 4)
if len(pp) < 4 {
log.Warn(fmt.Sprintf("expecting 4 values (name, issuer-url, ket, secret) in %s", key))
}
// Spread name, issuer-url, key and secret from provision string for OIDC provider
name, eap.IssuerUrl, eap.Key, eap.Secret = pp[0], pp[1], pp[2], pp[3]
eap.Handle = external.OIDC_PROVIDER_PREFIX + name
} else {
pp = strings.SplitN(val, " ", 2)
// Spread key and secret from provision string
eap.Key, eap.Secret = pp[0], pp[1]
eap.Handle = kind
}
_ = external.AddProvider(ctx, log, s, eap, false)
}
return
}

View File

@@ -0,0 +1,320 @@
package provision
import (
"context"
"strings"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/service"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cast"
"go.uber.org/zap"
)
func migratePages(ctx context.Context, log *zap.Logger, s store.Storer) (err error) {
layouts, _, err := store.SearchComposePageLayouts(ctx, s, types.PageLayoutFilter{})
if err != nil {
return
}
// Probably already ran, no need to continue
if len(layouts) > 0 {
return
}
var (
crs *filter.PagingCursor
pages types.PageSet
auxf types.PageFilter
translations systemTypes.ResourceTranslationSet
nsRules rbac.RuleSet
pgRules rbac.RuleSet
)
return store.Tx(ctx, s, func(ctx context.Context, s store.Storer) (err error) {
translations, err = getRelevantTranslations(ctx, s)
if err != nil {
return
}
nsRules, pgRules, err = getRelevantRbacRules(ctx, s)
if err != nil {
return
}
for {
pages, auxf, err = store.SearchComposePages(ctx, s, types.PageFilter{
Deleted: filter.StateInclusive,
Paging: filter.Paging{
PageCursor: crs,
},
})
if err != nil {
return
}
if len(pages) == 0 {
break
}
err = migratePageChunk(ctx, s, nsRules, pgRules, translations, pages)
if err != nil {
return
}
crs = auxf.Paging.PageCursor
if crs == nil {
break
}
}
return nil
})
}
func getRelevantTranslations(ctx context.Context, s store.Storer) (out systemTypes.ResourceTranslationSet, err error) {
var (
crs *filter.PagingCursor
ll systemTypes.ResourceTranslationSet
auxf systemTypes.ResourceTranslationFilter
)
for {
ll, auxf, err = store.SearchResourceTranslations(ctx, s, systemTypes.ResourceTranslationFilter{
Paging: filter.Paging{
PageCursor: crs,
},
})
if err != nil {
return
}
if len(ll) == 0 {
break
}
for _, l := range ll {
if !strings.HasPrefix(l.K, "pageBlock.") {
out = append(out, l)
}
}
crs = auxf.Paging.PageCursor
if crs == nil {
break
}
}
return
}
func getRelevantRbacRules(ctx context.Context, s store.Storer) (nsOut, pgOut rbac.RuleSet, err error) {
ll, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{})
if err != nil {
return
}
for _, r := range ll {
if strings.HasPrefix(r.Resource, types.NamespaceResourceType) {
switch r.Operation {
case "pages.search", "page.create":
nsOut = append(nsOut, r)
}
}
if strings.HasPrefix(r.Resource, types.PageResourceType) {
switch r.Operation {
case "read", "update", "delete":
pgOut = append(pgOut, r)
}
}
}
return
}
func migratePageChunk(ctx context.Context, s store.Storer, nsRules, pgRules rbac.RuleSet, translations systemTypes.ResourceTranslationSet, pages types.PageSet) (err error) {
n := now()
for _, p := range pages {
// Base layout
ly := &types.PageLayout{
ID: nextID(),
NamespaceID: p.NamespaceID,
PageID: p.ID,
Handle: "primary",
Primary: true,
Weight: 1,
Meta: types.PageLayoutMeta{
Title: p.Title,
Description: p.Description,
},
Config: types.PageLayoutConfig{
Buttons: extractPageButtons(p),
},
CreatedAt: *n,
UpdatedAt: n,
DeletedAt: p.DeletedAt,
}
// Translations
tt := translations.FilterResource(p.ResourceTranslation())
// Button translations
sr := strings.NewReplacer("recordToolbar", "config.buttons")
for _, t := range tt {
if !strings.HasPrefix(t.K, "recordToolbar.") {
continue
}
t.K = sr.Replace(t.K)
t.Resource = ly.ResourceTranslation()
}
// Title, description
ttAux := tt.FilterKey(types.LocaleKeyPageTitle.Path)
for _, t := range ttAux {
tt = append(tt, &systemTypes.ResourceTranslation{
ID: nextID(),
Resource: ly.ResourceTranslation(),
K: types.LocaleKeyPageLayoutMetaTitle.Path,
Lang: t.Lang,
Message: t.Message,
CreatedAt: *n,
})
}
ttAux = tt.FilterKey(types.LocaleKeyPageDescription.Path)
for _, t := range ttAux {
tt = append(tt, &systemTypes.ResourceTranslation{
ID: nextID(),
Resource: ly.ResourceTranslation(),
K: types.LocaleKeyPageLayoutMetaDescription.Path,
Lang: t.Lang,
Message: t.Message,
CreatedAt: *n,
})
}
// RBAC
rr := make(rbac.RuleSet, 0, len(pgRules)+len(nsRules))
// - from namespace
for _, r := range nsRules {
nsRef := strings.Split(r.Resource, "/")[1]
pgRes := types.PageRbacResource(cast.ToUint64(nsRef), 0)
switch r.Operation {
case "pages.search":
rr = append(rr, &rbac.Rule{
RoleID: r.RoleID,
Resource: pgRes,
Operation: "page-layouts.search",
Access: r.Access,
})
case "page.create":
rr = append(rr, &rbac.Rule{
RoleID: r.RoleID,
Resource: pgRes,
Operation: "page-layout.create",
Access: r.Access,
})
}
}
// - from page
for _, r := range pgRules {
pp := strings.Split(r.Resource, "/")
nsRef := pp[1]
pgRef := pp[2]
lyRes := types.PageLayoutRbacResource(cast.ToUint64(nsRef), cast.ToUint64(pgRef), 0)
switch r.Operation {
case "read":
rr = append(rr, &rbac.Rule{
RoleID: r.RoleID,
Resource: lyRes,
Operation: "read",
Access: r.Access,
})
case "update":
rr = append(rr, &rbac.Rule{
RoleID: r.RoleID,
Resource: lyRes,
Operation: "update",
Access: r.Access,
})
case "delete":
rr = append(rr, &rbac.Rule{
RoleID: r.RoleID,
Resource: lyRes,
Operation: "delete",
Access: r.Access,
})
}
}
// Blocks
for _, b := range p.Blocks {
b.XYWH = adjustBlockScale(b.XYWH, 12, 48)
ly.Blocks = append(ly.Blocks, types.PageLayoutBlock{
BlockID: b.BlockID,
XYWH: b.XYWH,
})
}
err = store.UpsertComposePage(ctx, s, p)
if err != nil {
return
}
err = store.UpsertComposePageLayout(ctx, s, ly)
if err != nil {
return
}
err = store.UpsertResourceTranslation(ctx, s, tt...)
if err != nil {
return
}
err = store.UpsertRbacRule(ctx, s, rr...)
if err != nil {
return
}
}
return
}
func extractPageButtons(p *types.Page) (out types.PageLayoutButtonConfig) {
ss := service.CurrentSettings.Compose.UI.RecordToolbar
if p.ModuleID == 0 {
return
}
out = types.PageLayoutButtonConfig{}
out.New.Enabled = !ss.HideNew
out.Edit.Enabled = !ss.HideEdit
out.Submit.Enabled = !ss.HideSubmit
out.Delete.Enabled = !ss.HideDelete
out.Clone.Enabled = !ss.HideClone
out.Back.Enabled = !ss.HideBack
return
}
func adjustBlockScale(b [4]int, prev, new int) [4]int {
for i, v := range b {
b[i] = v * new / prev
}
return b
}

View File

@@ -0,0 +1,73 @@
package provision
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
type (
uConfigFn func(context.Context, store.Storer, *zap.Logger) bool
uConfig struct {
dir string
fn uConfigFn
}
)
// provisionPartialBase check for roles and permissions
//
// It checks if there are any roles and any RBAC rules. If there are, we assume
// the provision for the base dir was already done.
func provisionPartialBase(ctx context.Context, s store.Storer, log *zap.Logger) bool {
rr, _, err := store.SearchRoles(ctx, s, types.RoleFilter{Deleted: filter.StateInclusive})
if err != nil {
log.Warn("could not make a partial import of base: roles", zap.Error(err))
return false
}
if len(rr) == 0 {
return true
}
pp, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{})
if err != nil {
log.Warn("could not make a partial import of base: permissions", zap.Error(err))
return false
}
if len(pp) == 0 {
return true
}
return false
}
// provisionPartialAuthClients checks for a specific set of auth client rbac rules
func provisionPartialAuthClients(ctx context.Context, s store.Storer, log *zap.Logger) bool {
set, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{})
if err != nil {
log.Warn("could not make a partial import of templates", zap.Error(err))
return false
}
for _, r := range set {
if rbac.ResourceType(r.Resource) == types.AuthClientResourceType {
return false
}
}
return true
}
// provisionPartialTemplates checks if any templates are in the store at all
func provisionPartialTemplates(ctx context.Context, s store.Storer, log *zap.Logger) bool {
set, _, err := store.SearchTemplates(ctx, s, types.TemplateFilter{Deleted: filter.StateInclusive})
if err != nil {
log.Warn("could not make a partial import of templates", zap.Error(err))
}
return err != nil || len(set) == 0
}

View File

@@ -0,0 +1,57 @@
package provision
import (
"context"
"time"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/store"
"go.uber.org/zap"
)
var (
// wrapper around time.Now() that will aid service testing
now = func() *time.Time {
c := time.Now().Round(time.Second)
return &c
}
)
func Run(ctx context.Context, log *zap.Logger, s store.Storer, provisionOpt options.ProvisionOpt, authOpt options.AuthOpt) error {
log = log.Named("provision")
// Note,
ffn := []func() error{
// Migrations:
// (placeholder for all post 2022.9.x modifications)
func() error { return migratePages(ctx, log.Named("pages"), s) },
// *************************************************************************************************************
// Config (full & partial)
func() error { return importConfig(ctx, log.Named("config"), s, provisionOpt.Path) },
// *************************************************************************************************************
// Auto-discoveries and other parts that cannot be imported from static files
func() error { return emailSettings(ctx, s) },
func() error { return apigwFilters(ctx, log.Named("apigw.filters"), s) },
func() error { return authAddExternals(ctx, log.Named("auth.externals"), s) },
func() error { return oidcAutoDiscovery(ctx, log.Named("auth.oidc-auto-discovery"), s, authOpt) },
func() error { return defaultUserGroup(ctx, log.Named("user-groups"), s, authOpt) },
func() error { return defaultAuthClient(ctx, log.Named("auth.clients"), s, authOpt) },
func() error { return addAuthSuperUsers(ctx, log.Named("auth.super-users"), s, authOpt) },
func() error { return invalidateDedupRules(ctx, log.Named("compose.deduplication"), s) },
func() error { return setUsersTheme(ctx, log.Named("users.theme"), s) },
func() error { return updateWebappTheme(ctx, log.Named("webapp.themes"), s) },
func() error { return setDefaultUserGroupRefs(ctx, log.Named("user-group.references"), s, authOpt) },
}
for _, fn := range ffn {
if err := fn(); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,76 @@
package provision
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
func migrateReports(ctx context.Context, log *zap.Logger, s store.Storer) (err error) {
reports, _, err := store.SearchReports(ctx, s, types.ReportFilter{
Deleted: filter.StateExcluded,
})
if err != nil {
return
}
mustMigrate := false
for _, r := range reports {
for _, s := range r.Sources {
mustMigrate = mustMigrate || (s.Step != nil && s.Step.Group_legacy != nil && s.Step.Link == nil)
}
for _, b := range r.Blocks {
for _, s := range b.Sources {
mustMigrate = mustMigrate || (s != nil && s.Group_legacy != nil && s.Link == nil)
}
}
}
if !mustMigrate {
return
}
ds := func(step *types.ReportStep) {
if step.Join != nil {
step.Kind = "link"
step.Link = &types.ReportStepLink{
Name: step.Join.Name,
LocalSource: step.Join.LocalSource,
LocalColumn: step.Join.LocalColumn,
ForeignSource: step.Join.ForeignSource,
ForeignColumn: step.Join.ForeignColumn,
Filter: step.Join.Filter,
}
step.Join = nil
}
if step.Group_legacy != nil {
step.Kind = "aggregate"
step.Aggregate = &types.ReportStepAggregate{
Name: step.Group_legacy.Name,
Source: step.Group_legacy.Source,
Keys: step.Group_legacy.Keys,
Columns: step.Group_legacy.Columns,
Filter: step.Group_legacy.Filter,
}
step.Group_legacy = nil
}
}
for _, r := range reports {
for _, s := range r.Sources {
ds(s.Step)
}
for _, b := range r.Blocks {
for _, s := range b.Sources {
ds(s)
}
}
}
return store.Tx(ctx, s, func(ctx context.Context, s store.Storer) error {
return store.UpdateReport(ctx, s, reports...)
})
}

View File

@@ -0,0 +1,96 @@
package provision
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
// SystemRoles creates system roles
func SystemRoles(ctx context.Context, log *zap.Logger, s store.Storer) (rr []*types.Role, err error) {
rr = types.RoleSet{
&types.Role{
Name: "Super administrator",
Handle: "super-admin",
Meta: &types.RoleMeta{
Description: "Super admin is a 'bypass' role that auto-allows all operations to it's members",
Context: nil,
},
},
&types.Role{
Name: "Authenticated",
Handle: "authenticated",
Meta: &types.RoleMeta{
Description: "Authenticated role is auto-assigned to all authenticated sessions",
Context: nil,
},
},
&types.Role{
Name: "Anonymous",
Handle: "anonymous",
Meta: &types.RoleMeta{
Description: "Authenticated role is auto-assigned to all non-authenticated sessions",
Context: nil,
}},
}
m, err := loadRoles(ctx, s)
if err != nil {
return
}
for i := range rr {
r := rr[i]
if m[r.Handle] == nil {
// this is a new role
r.ID = id.Next()
r.CreatedAt = *now()
m[r.Handle] = r
log.Info("creating system role", zap.String("handle", r.Handle), logger.Uint64("ID", r.ID))
} else {
// use existing role
rr[i] = m[r.Handle]
// make sure it's not deleted or archived
// and leave other props as they are
r.DeletedAt = nil
r.ArchivedAt = nil
log.Info("updating system role", zap.String("handle", r.Handle), logger.Uint64("ID", r.ID))
}
}
if err := store.UpsertRole(ctx, s, rr...); err != nil {
return nil, fmt.Errorf("failed to provision system roles: %w", err)
}
return
}
func loadRoles(ctx context.Context, s store.Roles) (m map[string]*types.Role, err error) {
var (
f = types.RoleFilter{
Archived: filter.StateInclusive,
Deleted: filter.StateInclusive,
}
)
m = make(map[string]*types.Role)
if set, _, err := store.SearchRoles(ctx, s, f); err == nil {
for _, r := range set {
m[r.Handle] = r
}
}
return
}

View File

@@ -0,0 +1,200 @@
package provision
import (
"context"
"encoding/json"
"strconv"
"time"
"github.com/cortezaproject/corteza/server/pkg/sass"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
// updateWebappTheme is a function that provisions new webapp themes,
// and migrates the old custom css and branding sass settings to new webapp themes setting.
func updateWebappTheme(ctx context.Context, log *zap.Logger, s store.Storer) (err error) {
log.Info("provision start")
defer log.Info("provision end")
vv, _, err := store.SearchSettingValues(ctx, s, types.SettingsFilter{})
if err != nil {
return err
}
oldCustomCSS := vv.FindByName("ui.custom-css")
oldBranding := vv.FindByName("ui.studio.branding-sass")
studioThemes := vv.FindByName("ui.studio.themes")
customCSSThemes := vv.FindByName("ui.studio.custom-css")
//get branding themes setting value
brandingTheme, err := processBrandingTheme(oldBranding)
if err != nil {
return err
}
//get custom css themes setting value
customCSSTheme, err := processCustomCSSTheme(oldCustomCSS)
if err != nil {
return err
}
// provision new themes
if studioThemes.IsNull() {
// provision branding themes setting
err = provisionTheme(ctx, s, "ui.studio.themes", brandingTheme, log)
if err != nil {
return err
}
if !oldBranding.IsNull() {
// delete old branding sass settings from the database
err = store.DeleteSettingValue(ctx, s, oldBranding)
if err != nil {
return err
}
}
}
if customCSSThemes.IsNull() {
// provision custom CSS themes setting
err = provisionTheme(ctx, s, "ui.studio.custom-css", customCSSTheme, log)
if err != nil {
return err
}
if !oldCustomCSS.IsNull() {
// delete old custom css settings from the database
err = store.DeleteSettingValue(ctx, s, oldCustomCSS)
if err != nil {
return err
}
}
}
return nil
}
func provisionTheme(ctx context.Context, s store.Storer, name string, themes []types.Theme, log *zap.Logger) (err error) {
value, err := json.Marshal(themes)
if err != nil {
return err
}
newThemeSetting := &types.SettingValue{
Name: name,
Value: value,
UpdatedAt: time.Now(),
}
err = store.CreateSettingValue(ctx, s, newThemeSetting)
if err != nil {
log.Error("failed to provision webapp themes", zap.Error(err))
return err
}
return nil
}
func processBrandingTheme(oldBranding *types.SettingValue) (themes []types.Theme, err error) {
var brandingMap map[string]string
lightModeMap := map[string]string{
"black": "#0B344E",
"white": "#FFFFFF",
"primary": "#FF9661",
"secondary": "#758D9B",
"success": "#43AA8B",
"warning": "#E27646",
"danger": "#E54122",
"light": "#F3F5F7",
"extra-light": "#E4E9EF",
"body-bg": "#F3F5F7",
"sidebar-bg": "#FFFFFF",
"topbar-bg": "#F3F5F7",
}
// process old branding sass settings and match them with the new branding themes setting
if !oldBranding.IsNull() {
oldBrandingString, err := strconv.Unquote(oldBranding.Value.String())
if err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(oldBrandingString), &brandingMap); err != nil {
return nil, err
}
for key, bmValue := range brandingMap {
if key == "light" || key == "extra-light" {
continue
}
if _, ok := lightModeMap[key]; ok {
lightModeMap[key] = bmValue
}
}
}
darkModeValues := `
{
"black":"#FBF7F4",
"white":"#0B344E",
"primary":"#FF9661",
"secondary":"#758D9B",
"success":"#43AA8B",
"warning":"#E27646",
"danger":"#E54122",
"light":"#23495F",
"extra-light":"#3E5A6F",
"body-bg":"#092B40",
"sidebar-bg": "#0B344E",
"topbar-bg": "#092B40"
}`
lightModeValues, _ := json.Marshal(lightModeMap)
themes = []types.Theme{
{
ID: sass.LightTheme,
Values: string(lightModeValues),
},
{
ID: sass.DarkTheme,
Values: darkModeValues,
},
}
return themes, nil
}
func processCustomCSSTheme(oldValue *types.SettingValue) (themes []types.Theme, err error) {
var generalCSS string
if oldValue.IsNull() {
generalCSS = ""
} else {
generalCSS, err = strconv.Unquote(oldValue.Value.String())
if err != nil {
return nil, err
}
}
themes = []types.Theme{
{
ID: sass.GeneralTheme,
Values: generalCSS,
},
{
ID: sass.LightTheme,
Values: "",
},
{
ID: sass.DarkTheme,
Values: "",
},
}
return themes, nil
}

View File

@@ -0,0 +1,78 @@
package provision
import (
"context"
"os"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/options"
"go.uber.org/zap"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
func setDefaultUserGroupRefs(ctx context.Context, log *zap.Logger, s store.Storer, authOpt options.AuthOpt) (err error) {
log.Info("provision start")
defer log.Info("provision end")
h, ok := os.LookupEnv("AUTH_DEFAULT_USER_GROUP")
if !ok {
h = "default-root"
}
ug, err := store.LookupUserGroupByHandle(ctx, s, h)
if err != nil {
if errors.IsNotFound(err) {
// Missing/deleted group; nothing to backfill, don't abort provisioning
log.Warn("default user group not found or deleted, skipping user/auth-client backfill", zap.String("handle", h))
return nil
}
return
}
// ---
// Users
// ---
uu, _, err := store.SearchUsers(ctx, s, types.UserFilter{
Kind: types.NormalUser,
})
if err != nil {
return
}
for _, u := range uu {
if u.UserGroupID != 0 {
continue
}
u.UserGroupID = ug.ID
err = store.UpdateUser(ctx, s, u)
if err != nil {
return
}
}
// ---
// auth clients
// ---
cc, _, err := store.SearchAuthClients(ctx, s, types.AuthClientFilter{})
if err != nil {
return
}
for _, c := range cc {
if c.Security.UserGroup != 0 {
continue
}
c.Security.UserGroup = ug.ID
err = store.UpdateAuthClient(ctx, s, c)
if err != nil {
return
}
}
return
}

View File

@@ -0,0 +1,135 @@
package provision
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/sass"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
// SystemUsers creates or updates system users
func SystemUsers(ctx context.Context, log *zap.Logger, s store.Users) (uu []*types.User, err error) {
uu = types.UserSet{
&types.User{
Email: "provision@corteza.local",
Name: "Corteza Provisioner",
Handle: auth.ProvisionUserHandle,
Kind: types.SystemUser,
},
&types.User{
Email: "service@corteza.local",
Name: "Corteza Service",
Handle: auth.ServiceUserHandle,
Kind: types.SystemUser,
},
&types.User{
Email: "federation@corteza.local",
Name: "Corteza Federation",
Handle: auth.FederationUserHandle,
Kind: types.SystemUser,
},
}
m, err := loadSystemUsers(ctx, s)
if err != nil {
return
}
for i := range uu {
u := uu[i]
if m[u.Handle] == nil {
// this is a new user
u.ID = id.Next()
u.CreatedAt = *now()
if err := store.UpsertUser(ctx, s, u); err != nil {
return nil, fmt.Errorf("failed to provision system user %s: %w", u.Handle, err)
}
log.Info("creating system user", zap.String("handle", u.Handle), logger.Uint64("ID", u.ID))
} else {
u.ID = m[u.Handle].ID
// There is no need to update system users if they are unchanged
if m[u.Handle].UpdatedAt == nil &&
m[u.Handle].SuspendedAt == nil &&
m[u.Handle].DeletedAt == nil {
continue
}
// Make sure all values are as they should be
u.CreatedAt = m[u.Handle].CreatedAt
u.Email = m[u.Handle].Email
u.Name = m[u.Handle].Name
u.UpdatedAt = nil
u.SuspendedAt = nil
u.DeletedAt = nil
if err := store.UpsertUser(ctx, s, u); err != nil {
return nil, fmt.Errorf("failed to provision system user %s: %w", u.Handle, err)
}
log.Info("updating system user", zap.String("handle", u.Handle), logger.Uint64("ID", u.ID))
}
}
return
}
func loadSystemUsers(ctx context.Context, s store.Users) (m map[string]*types.User, err error) {
var (
f = types.UserFilter{
Suspended: filter.StateInclusive,
Deleted: filter.StateInclusive,
Kind: types.SystemUser,
}
)
m = make(map[string]*types.User)
if set, _, err := store.SearchUsers(ctx, s, f); err == nil {
for _, r := range set {
m[r.Handle] = r
}
}
return
}
func setUsersTheme(ctx context.Context, log *zap.Logger, s store.Users) (err error) {
log.Info("provision start")
defer log.Info("provision end")
var (
f = types.UserFilter{
Deleted: filter.StateInclusive,
}
users []*types.User
)
if set, _, err := store.SearchUsers(ctx, s, f); err == nil {
for _, r := range set {
if r.Meta.Theme == "" {
r.Meta.Theme = sass.LightTheme
users = append(users, r)
}
}
}
err = store.UpdateUser(ctx, s, users...)
if err != nil {
log.Error("error provisioning users theme", zap.Error(err))
return err
}
return nil
}