3
0

Add support for helper env-variable that create super users

This commit is contained in:
kinyaelgrande
2022-12-12 18:09:11 +03:00
committed by Mumbi Francis
parent 84fe246ea9
commit a26b0dd606
7 changed files with 208 additions and 65 deletions

View File

@@ -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: <no value>
@@ -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: <no value>
@@ -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: <no value>
@@ -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: <no value>
# AUTH_DEVELOPMENT_MODE=<no value>
###############################################################################
# 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: <no value>
# AUTH_PROVISION_SUPER_USER=<no value>
###############################################################################
###############################################################################
# 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: <no value>
# LOCALE_PATH=<no value>
@@ -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: <no value>
# LOG_DEBUG=<no value>
@@ -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: <no value>
# LOG_FILTER=<no value>
###############################################################################
# Set to true to see where the logging was called from.
#
#
# Disable for production.
#
#
# Type: bool
# Default: <no value>
# LOG_INCLUDE_CALLER=<no value>
@@ -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.

View File

@@ -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"
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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}

View File

@@ -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)