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
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:
42
server/pkg/provision/apigw.go
Normal file
42
server/pkg/provision/apigw.go
Normal 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
|
||||
}
|
||||
337
server/pkg/provision/auth.go
Normal file
337
server/pkg/provision/auth.go
Normal 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
|
||||
}
|
||||
178
server/pkg/provision/config.go
Normal file
178
server/pkg/provision/config.go
Normal 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
|
||||
}
|
||||
35
server/pkg/provision/dedup.go
Normal file
35
server/pkg/provision/dedup.go
Normal 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
|
||||
}
|
||||
140
server/pkg/provision/oidc.go
Normal file
140
server/pkg/provision/oidc.go
Normal 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
|
||||
}
|
||||
320
server/pkg/provision/pages.go
Normal file
320
server/pkg/provision/pages.go
Normal 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
|
||||
}
|
||||
73
server/pkg/provision/partial.go
Normal file
73
server/pkg/provision/partial.go
Normal 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
|
||||
}
|
||||
57
server/pkg/provision/provision.go
Normal file
57
server/pkg/provision/provision.go
Normal 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
|
||||
}
|
||||
76
server/pkg/provision/reports.go
Normal file
76
server/pkg/provision/reports.go
Normal 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...)
|
||||
})
|
||||
}
|
||||
96
server/pkg/provision/roles.go
Normal file
96
server/pkg/provision/roles.go
Normal 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
|
||||
}
|
||||
200
server/pkg/provision/stylesheet.go
Normal file
200
server/pkg/provision/stylesheet.go
Normal 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
|
||||
}
|
||||
78
server/pkg/provision/user_groups.go
Normal file
78
server/pkg/provision/user_groups.go
Normal 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
|
||||
}
|
||||
135
server/pkg/provision/users.go
Normal file
135
server/pkg/provision/users.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user