From a26b0dd606bbe52a2a06c9e6adb51b8a26fc0d4d Mon Sep 17 00:00:00 2001 From: kinyaelgrande Date: Mon, 12 Dec 2022 18:09:11 +0300 Subject: [PATCH] Add support for helper env-variable that create super users --- server/.env.example | 97 +++++++++-------- server/app/options/auth.cue | 8 ++ server/pkg/options/options.gen.go | 1 + server/pkg/provision/auth.go | 127 +++++++++++++++++++++- server/pkg/provision/provision.go | 1 + server/system/service/auth.go | 2 +- server/system/service/auth_credentials.go | 37 ++++--- 7 files changed, 208 insertions(+), 65 deletions(-) diff --git a/server/.env.example b/server/.env.example index eaabe8d08..0caa484c3 100644 --- a/server/.env.example +++ b/server/.env.example @@ -39,7 +39,7 @@ ############################################################################### # Allow insecure (invalid, expired TLS/SSL certificates) connections. -# +# # [IMPORTANT] # ==== # We strongly recommend keeping this value set to false except for local development or demos. @@ -209,7 +209,7 @@ ############################################################################### # Password for the web console endpoint. When running in dev environment, password is not required. -# +# # Corteza intentionally sets default password to random chars to prevent security incidents. # Type: string # Default: @@ -296,7 +296,7 @@ # Email sending # # Configure your local SMTP server or use one of the available providers. -# +# # These values are copied to settings when the server starts and can be managed from the administration console. # We recommend you remove these values after they are copied to settings. # If server detects difference between these options and settings, it shows a warning in the log on server start. @@ -436,7 +436,7 @@ ############################################################################### # Password security allows you to disable constraints to which passwords must conform to. -# +# # [CAUTION] # ==== # Disabling password security can be useful for development environments as it removes the need for complex passwords. @@ -448,12 +448,12 @@ ############################################################################### # Algoritm to be use for JWT signature. -# +# # Supported valus: # - HS256, HS384, HS512 # - PS256, PS384, PS512, # - RS256, RS384, RS512 -# +# # Provide shared secret string for HS256, HS384, HS512 and full private key or path to the file PS* and RS* algorithms. # Type: string # Default: HS512 @@ -462,7 +462,7 @@ ############################################################################### # Secret used for signing JWT tokens. # Value is used only when HS256, HS384 or HS512 algorithm is used. -# +# # [IMPORTANT] # ==== # If secret is not set, system auto-generates one from DB_DSN and HOSTNAME environment variables. @@ -486,7 +486,7 @@ ############################################################################### # Lifetime of the refresh token. Should be much longer than lifetime of the access token. -# +# # Refresh tokens are used to exchange expired access tokens with new ones. # Type: time.Duration # Default: 72h @@ -494,7 +494,7 @@ ############################################################################### # Redirect URL to be sent with OAuth2 authentication request to provider -# +# # `provider` placeholder is replaced with the actual value when used. # Type: string # Default: @@ -502,7 +502,7 @@ ############################################################################### # Secret used for securing cookies -# +# # [IMPORTANT] # ==== # If secret is not set, system auto-generates one from DB_DSN and HOSTNAME environment variables. @@ -545,21 +545,21 @@ ############################################################################### # Maximum time user is allowed to stay idle when logged in without "remember-me" option and before session is expired. -# +# # Recomended value is between an hour and a day. -# +# # [IMPORTANT] # ==== # This affects only profile (/auth) pages. Using applications (admin, compose, ...) does not prolong the session. # ==== -# +# # Type: time.Duration # Default: 24h # AUTH_SESSION_LIFETIME=24h ############################################################################### # Duration of the session in /auth lasts when user logs-in with "remember-me" option. -# +# # If set to 0, "remember-me" option is removed. # Type: time.Duration # Default: 8640h @@ -586,7 +586,7 @@ ############################################################################### # Secret used for securing CSRF protection -# +# # [IMPORTANT] # ==== # If secret is not set, system auto-generates one from DB_DSN and HOSTNAME environment variables. @@ -616,19 +616,19 @@ ############################################################################### # Handle for OAuth2 client used for automatic redirect from /auth/oauth2/go endpoint. -# +# # This simplifies configuration for OAuth2 flow for Corteza Web applications as it removes # the need to suply redirection URL and client ID (oauth2/go endpoint does that internally) -# +# # Type: string # Default: corteza-webapp # AUTH_DEFAULT_CLIENT=corteza-webapp ############################################################################### # Path to js, css, images and template source files -# +# # When corteza starts, if path exists it tries to load template files from it. -# +# # When empty path is set (default value), embedded files are used. # Type: string # Default: @@ -637,12 +637,21 @@ ############################################################################### # When enabled, corteza reloads template before every execution. # Enable this for debugging or when developing auth templates. -# +# # Should be disabled in production where templates do not change between server restarts. # Type: bool # Default: # AUTH_DEVELOPMENT_MODE= +############################################################################### +# When set, Corteza creates one or more users with the configured values using provided email as a password. +# It skips existing (email, handle). All new users are assigned to all bypass roles. +# +# When set in production, Corteza stops and reports an error +# Type: string +# Default: +# AUTH_PROVISION_SUPER_USER= + ############################################################################### ############################################################################### # Connection to Corredor @@ -815,17 +824,15 @@ ############################################################################### # List of compa delimited languages (language tags) to enable. # In case when an enabled language can not be loaded, error is logged. -# +# # When loading language configurations (config.xml) from the configured path(s). -# +# # Type: string # Default: en # LOCALE_LANGUAGES=en ############################################################################### -# One or more paths to locale config and translation files, separated by colon -# -# When with LOCALE_DEVELOPMENT_MODE=true, default value for path is ../../locale +# One or more paths to locale config and translation files, separated by colon # Type: string # Default: # LOCALE_PATH= @@ -834,7 +841,7 @@ # Name of the query string parameter used to pass the language tag (it overrides Accept-Language header). # Set it to empty string to disable detection from the query string. # This parameter is ignored if only one language is enabled -# +# # Type: string # Default: lng # LOCALE_QUERY_STRING_PARAM=lng @@ -865,9 +872,9 @@ ############################################################################### # Disables json format for logging and enables more human-readable output with colors. -# +# # Disable for production. -# +# # Type: bool # Default: # LOG_DEBUG= @@ -875,11 +882,11 @@ ############################################################################### # Minimum logging level. If set to "warn", # Levels warn, error, dpanic panic and fatal will be logged. -# +# # Recommended value for production: warn -# +# # Possible values: debug, info, warn, error, dpanic, panic, fatal -# +# # Type: string # Default: warn # LOG_LEVEL=warn @@ -887,25 +894,25 @@ ############################################################################### # Log filtering rules by level and name (log-level:log-namespace). # Please note that level (LOG_LEVEL) is applied before filter and it affects the final output! -# +# # Leave unset for production. -# +# # Example: # `warn+:* *:auth,workflow.*` # Log warnings, errors, panic, fatals. Everything from auth and workflow is logged. -# -# +# +# # See more examples and documentation here: https://github.com/moul/zapfilter -# +# # Type: string # Default: # LOG_FILTER= ############################################################################### # Set to true to see where the logging was called from. -# +# # Disable for production. -# +# # Type: bool # Default: # LOG_INCLUDE_CALLER= @@ -913,9 +920,9 @@ ############################################################################### # Include stack-trace when logging at a specified level or below. # Disable for production. -# +# # Possible values: debug, info, warn, error, dpanic, panic, fatal -# +# # Type: string # Default: dpanic # LOG_STACKTRACE_LEVEL=dpanic @@ -1010,11 +1017,11 @@ # # Provisioning allows you to configure a {PRODUCT_NAME} instance when deployed. # It occurs automatically after the {PRODUCT_NAME} server starts. -# +# # [IMPORTANT] # ==== # We recommend you to keep provisioning enabled as it simplifies version updates by updating the database and updating settings. -# +# # If you're doing local development or some debugging, you can disable this. # ==== # @@ -1040,7 +1047,7 @@ # ==== # These parameters help in the development and testing process. # When you are deploying to production, these should be disabled to improve performance and reduce storage usage. -# +# # You should configure external services such as Sentry or ELK to keep track of logs and error reports. # ==== # @@ -1146,7 +1153,7 @@ # Delay system startup # # You can configure these options to defer API execution until another external (HTTP) service is up and running. -# +# # [ TIP ] # ==== # Delaying API execution can come in handy in complex setups where execution order is important. @@ -1170,7 +1177,7 @@ ############################################################################### # Space delimited list of hosts and/or URLs to probe. # Host format: `host` or `host:443` (port will default to 80). -# +# # [NOTE] # ==== # Services are probed in parallel. diff --git a/server/app/options/auth.cue b/server/app/options/auth.cue index ec94e7d4a..dcc118a95 100644 --- a/server/app/options/auth.cue +++ b/server/app/options/auth.cue @@ -231,6 +231,14 @@ auth: schema.#optionsGroup & { Should be disabled in production where templates do not change between server restarts. """ } + provision_super_user: { + description: """ + When set, Corteza creates one or more users with the configured values using provided email as a password. + It skips existing (email, handle). All new users are assigned to all bypass roles. + + When set in production, Corteza stops and reports an error + """ + } } title: "Authentication" } diff --git a/server/pkg/options/options.gen.go b/server/pkg/options/options.gen.go index 88b1be063..adbcc21ea 100644 --- a/server/pkg/options/options.gen.go +++ b/server/pkg/options/options.gen.go @@ -123,6 +123,7 @@ type ( DefaultClient string `env:"AUTH_DEFAULT_CLIENT"` AssetsPath string `env:"AUTH_ASSETS_PATH"` DevelopmentMode bool `env:"AUTH_DEVELOPMENT_MODE"` + ProvisionSuperUser string `env:"AUTH_PROVISION_SUPER_USER"` } CorredorOpt struct { diff --git a/server/pkg/provision/auth.go b/server/pkg/provision/auth.go index 21e17fb46..b7ad80b7c 100644 --- a/server/pkg/provision/auth.go +++ b/server/pkg/provision/auth.go @@ -2,16 +2,30 @@ package provision import ( "context" + "fmt" + 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/mail" + "github.com/cortezaproject/corteza/server/pkg/options" + "github.com/cortezaproject/corteza/server/system/service" + "go.uber.org/zap" "os" + "regexp" + "strings" "github.com/cortezaproject/corteza/server/pkg/errors" "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") @@ -43,3 +57,112 @@ func emailSettings(ctx context.Context, s store.Storer) error { }) }) } + +// 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) { + 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{UserID: 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 +} diff --git a/server/pkg/provision/provision.go b/server/pkg/provision/provision.go index 055a758e2..a24b2060d 100644 --- a/server/pkg/provision/provision.go +++ b/server/pkg/provision/provision.go @@ -42,6 +42,7 @@ func Run(ctx context.Context, log *zap.Logger, s store.Storer, provisionOpt opti 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 defaultAuthClient(ctx, log.Named("auth.clients"), s, authOpt) }, + func() error { return addAuthSuperUsers(ctx, log.Named("auth.super-users"), s, authOpt) }, } for _, fn := range ffn { diff --git a/server/system/service/auth.go b/server/system/service/auth.go index 0e9db6fa0..27c8c6635 100644 --- a/server/system/service/auth.go +++ b/server/system/service/auth.go @@ -93,7 +93,7 @@ func Auth(opt AuthOptions) *auth { // 2.3. create credentials for that social login // // External login/signup does not: -// - validate provider on profile, only uses it for matching credentials +// - validate provider on profile, only uses it for matching credentials func (svc *auth) External(ctx context.Context, profile types.ExternalAuthUser) (u *types.User, err error) { var ( authProvider = &types.AuthProvider{Provider: profile.Provider} diff --git a/server/system/service/auth_credentials.go b/server/system/service/auth_credentials.go index 6c7a353b3..d72b9bcaf 100644 --- a/server/system/service/auth_credentials.go +++ b/server/system/service/auth_credentials.go @@ -522,7 +522,7 @@ func (svc *auth) ChangePassword(ctx context.Context, userID uint64, oldPassword, return svc.recordAction(ctx, aam, AuthActionChangePassword, err) } -func (svc *auth) hashPassword(password string) (hash []byte, err error) { +func hashPassword(password string) (hash []byte, err error) { return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) } @@ -532,20 +532,25 @@ func (svc *auth) CheckPasswordStrength(password string) bool { // SetPasswordCredentials (soft) deletes old password entry and creates a new entry with new password on every change // -// This way we can implement more strict password-change policies in the future +// # This way we can implement more strict password-change policies in the future // // This method is used by auth and user procedures to unify password hashing and updating // credentials func (svc *auth) SetPasswordCredentials(ctx context.Context, userID uint64, password string) (err error) { + if err = svc.removePasswordCredentials(ctx, userID); err != nil { + return + } + + return SetPasswordCredentials(ctx, svc.store, userID, password) +} + +// SetPasswordCredentials creates a new password entry +func SetPasswordCredentials(ctx context.Context, s store.Storer, userID uint64, password string) (err error) { var ( hash []byte ) - if hash, err = svc.hashPassword(password); err != nil { - return - } - - if err = svc.removePasswordCredentials(ctx, userID); err != nil { + if hash, err = hashPassword(password); err != nil { return } @@ -558,7 +563,7 @@ func (svc *auth) SetPasswordCredentials(ctx context.Context, userID uint64, pass Credentials: string(hash), } - return store.CreateCredential(ctx, svc.store, c) + return store.CreateCredential(ctx, s, c) } //CheckPassword verifies if password matches any of the valid credentials @@ -568,18 +573,17 @@ func (svc *auth) SetPasswordCredentials(ctx context.Context, userID uint64, pass // SetPassword sets new password for a user // -// This function also records an action +// # This function also records an action // // this method is used in 2 scenarios: // -// SELF: -// user forgot the password and needs to reset it -// there should be protocols prior to this point that -// authenticate and validate users -// -// USER MANAGEMENT: -// administrator is resetting password for another user +// SELF: +// user forgot the password and needs to reset it +// there should be protocols prior to this point that +// authenticate and validate users // +// USER MANAGEMENT: +// administrator is resetting password for another user func (svc *auth) SetPassword(ctx context.Context, userID uint64, password string) (err error) { var ( u *types.User @@ -1066,7 +1070,6 @@ func skipInvalid(c *types.Credential) bool { return c.Valid() } -// func compareHashedCredentials(password string) func(c *types.Credential) bool { var ( p = []byte(password)