package config import ( "bytes" "crypto/rsa" "errors" "fmt" "os" "strconv" "strings" "time" "github.com/go-logr/logr" "github.com/google/uuid" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/ory/fosite/token/hmac" "github.com/ory/fosite/token/jwt" ldredis "github.com/launchdarkly/go-server-sdk-redis-redigo/v2" ldclient "github.com/launchdarkly/go-server-sdk/v6" "github.com/launchdarkly/go-server-sdk/v6/ldcomponents" "edge-infra.dev/pkg/edge/iam/crypto" ff "edge-infra.dev/pkg/lib/featureflag" ) var ( privateKey *rsa.PrivateKey privateKeyID string bslInfo *BslInfo edgeInfo *EdgeInfo logger logr.Logger disruptWAN bool ) const ( Okta = "okta" trueString = "true" ) func SetLogger(l logr.Logger) { logger = l logger.Info("config's logger is set and ready to be used") } func Verify() error { result := make([]string, 0) if !isLDReachable() { logger.Error(nil, "launchdarkly is currently not reachable. will use default values based on environment variables for now") } if Issuer() == "" { result = append(result, "IAM_ISSUER") } if IssuerURL() == "" { result = append(result, "IAM_ISSUER_URL") } if OrganizationID() == "" { result = append(result, "IAM_ORGANIZATION_ID") } if OrganizationName() == "" { result = append(result, "IAM_ORGANIZATION_NAME") } if SiteID() == "" { result = append(result, "IAM_SITE_ID") } if OktaEnabled() { if OktaClientID() == "" { result = append(result, "OKTA_CLIENT_ID") } if OktaClientSecret() == "" { result = append(result, "OKTA_CLIENT_SECRET") } if OktaIssuer() == "" { result = append(result, "OKTA_ISSUER") } } if len(result) > 0 { // if okta is enabled but missing the values for the okta creds, store might not be registered w okta resultAsString := strings.Join(result, ", ") if OktaEnabled() && strings.Contains(resultAsString, "OKTA_CLIENT_ID") { logger.Info("Value for OKTA_CLIENT_ID is missing. Please confirm the okta-registration feature flag is enabled for this cluster to create okta creds") } return fmt.Errorf("missing the following configuration: %v", resultAsString) } return nil } func ensureBslInfo() { if bslInfo == nil { var err error bslInfo, err = getBslInfo() if err != nil { panic(err.Error()) } } } func ensureEdgeInfo() { if edgeInfo == nil { var err error edgeInfo, err = getEdgeInfo() if err != nil { panic(err.Error()) } } } func Info() { logger.Info("configuration", "ISSUER", Issuer(), "ISSUER_URL", IssuerURL(), "ADDR", Addr(), "IAM_ORGANIZATION_NAME", OrganizationName(), "IAM_ORGANIZATION_ID", OrganizationID(), "IAM_SITE_ID", SiteID(), ) } func IsProduction() bool { return os.Getenv("IAM_MODE") != "debug" } func GetBarcodeKeyLength() uint8 { return 5 } func GetBarcodeLength() uint8 { return 20 } func GetBarcodeLifeSpan() time.Duration { defaultValue := 8 * time.Hour // 8 hours duration, err := time.ParseDuration(os.Getenv("IAM_BARCODE_LIFESPAN")) if err != nil { return defaultValue } return duration } func GetJWTStrategy() *jwt.RS256JWTStrategy { return &jwt.RS256JWTStrategy{ PrivateKey: PrivateKey(), } } func GetEmergencyBarcodeLifeSpan() time.Duration { defaultVaule := 8 * time.Hour // 8 hours duration, err := time.ParseDuration(os.Getenv("IAM_EBC_LIFESPAN")) if err != nil { return defaultVaule } return duration } func GetPINLifeSpan() time.Duration { defaultValue := 6 * 30 * 24 * time.Hour // 180 days duration, err := time.ParseDuration(os.Getenv("IAM_PIN_LIFESPAN")) if err != nil { return defaultValue } return duration } func GetPINTTL() time.Duration { return 2 * GetPINLifeSpan() } func GetAccessTokenTTL() time.Duration { return 2 * AccessTokenLifespan() } func GetRefreshTokenTTL() time.Duration { return 2 * RefreshTokenLifespan() } func InitLaunchDarkly() { redisAddr := fmt.Sprintf("redis://%s", RedisAddress()) c := ldclient.Config{ DataStore: ldcomponents.PersistentDataStore( ldredis.DataStore().URL(redisAddr)), } ff.SetConfig(c) logger.Info("setting launchdarkly to use redis as data store") } func isLDReachable() bool { //define ld context ldctx := ldcontext.NewMulti( ldcontext.NewWithKind("siteID", SiteID()), ldcontext.NewWithKind("orgID", OrganizationID()), ) // check if okta is enabled _, err := ff.FeatureEnabledForLDContext(ff.OktaEnabled, ldctx, os.Getenv("FF_OKTA_ENABLED") == trueString) return err == nil } // barcode is enabled in LD depending on organization id, site id, & client id func BarcodeEnabled(clientID string) bool { //define barcode context barcodectx := ldcontext.NewMulti( ldcontext.NewWithKind("clientID", clientID), ldcontext.NewWithKind("siteID", SiteID()), ldcontext.NewWithKind("orgID", OrganizationID()), ) return isFeatureEnabled(ff.BarcodeEnabled, os.Getenv("FF_BARCODE_ENABLED") == trueString, barcodectx) } // Test Client is enabled in LD depending on organization id and site id func TestClientEnabled() bool { //define test client context testclientctx := ldcontext.NewMulti( ldcontext.NewWithKind("siteID", SiteID()), ldcontext.NewWithKind("orgID", OrganizationID()), ) return isFeatureEnabled(ff.EdgeIDTestClient, os.Getenv("FF_CREATE_TEST_CLIENT") == trueString, testclientctx) } func EnforcePermissions() bool { ldCtx := ldcontext.NewMulti( ldcontext.NewWithKind("siteID", SiteID()), ldcontext.NewWithKind("orgID", OrganizationID()), ) return isFeatureEnabled(ff.EnforceEdgeIDPermissions, os.Getenv("FF_ENFORCE_PERMISSIONS") == trueString, ldCtx) } // Emergency barcode is enabled in LD depending on organization id, site id, & client id func EmergencyBarcodeEnabled(clientID string) bool { barcodectx := ldcontext.NewMulti( ldcontext.NewWithKind("clientID", clientID), ldcontext.NewWithKind("siteID", SiteID()), ldcontext.NewWithKind("orgID", OrganizationID()), ) return isFeatureEnabled(ff.EmergencyBarcodeEnabled, os.Getenv("FF_EBC_ENABLED") == trueString, barcodectx) } func DeviceLoginEnabled() bool { deviceLoginctx := ldcontext.NewMulti( ldcontext.NewWithKind("siteID", SiteID()), ldcontext.NewWithKind("orgID", OrganizationID()), ) return isFeatureEnabled(ff.DeviceLoginEnabled, os.Getenv("FF_DEVICE_ENABLED") == trueString, deviceLoginctx) } // GetBarcodeUserTTL: expiration of barcode user = barcode lifespan + 30 days func GetBarcodeUserTTL() time.Duration { return GetBarcodeLifeSpan() + 30*24*time.Hour } func GetBarcodePrintPermission() string { return "EDGE_IAM_ENABLE_BARCODE" } func GetEBCPrintPermission() string { return "EDGE_IAM_ENABLE_EBC" } // GetProfileLifespan: make sure that the user has to sign in with cloud at least once every 24 hours func GetProfileLifespan() time.Duration { defaultValue := 24 * time.Hour // 1 day duration, err := time.ParseDuration(os.Getenv("IAM_PROFILE_LIFESPAN")) if err != nil { return defaultValue } return duration } // GetProfileTTL: make sure we get rid of the profile after 90 days func GetProfileTTL() time.Duration { return 90 * GetProfileLifespan() } func GetPINPermission() string { return "EDGE_IAM_ENABLE_PIN" } func GetPINRetryThreshold() int8 { defaultValue := 5 _, exist := os.LookupEnv("IAM_PIN_RETRY_THRESHOLD") if !exist { return int8(defaultValue) /* #nosec G115 value from ENV and ENV is considered trusted */ } val, _ := strconv.Atoi(os.Getenv("IAM_PIN_RETRY_THRESHOLD")) return int8(val) /* #nosec G115 value from ENV and ENV is considered trusted */ } func GetDeviceRetryThreshold() int8 { defaultValue := 10 _, exist := os.LookupEnv("IAM_DEVICE_RETRY_THRESHOLD") if !exist { return int8(defaultValue) /* #nosec G115 value from ENV and ENV is considered trusted */ } val, _ := strconv.Atoi(os.Getenv("IAM_DEVICE_RETRY_THRESHOLD")) return int8(val) /* #nosec G115 value from ENV and ENV is considered trusted */ } func RetriesExceededTimeout() int8 { defaultValue := 30 _, exist := os.LookupEnv("IAM_DEVICE_RETRY_TIMEOUT") if !exist { return int8(defaultValue) /* #nosec G115 value from ENV and ENV is considered trusted */ } val, _ := strconv.Atoi(os.Getenv("IAM_DEVICE_RETRY_TIMEOUT")) return int8(val) /* #nosec G115 value from ENV and ENV is considered trusted */ } func PINHistoryLength() int8 { defaultValue := 3 _, exist := os.LookupEnv("IAM_PIN_HISTORY_LENGTH") if !exist { return int8(defaultValue) /* #nosec G115 value from ENV and ENV is considered trusted */ } val, _ := strconv.Atoi(os.Getenv("IAM_PIN_HISTORY_LENGTH")) return int8(val) /* #nosec G115 value from ENV and ENV is considered trusted */ } // GetBarcodeCodeLifespan is the time we give a client to exchange the code for a barcode // Which should be pretty small, cause it should happen directly after they request // to print func GetBarcodeCodeLifespan() time.Duration { return 1 * time.Minute } // GetBarcodeCodeTTL: expiration of barcode-code from Redis DB = 3 days func GetBarcodeCodeTTL() time.Duration { return 3 * 24 * time.Hour } // GetLoginHintTTL: expiration of login-hint from Redis DB = 3 hours func GetLoginHintTTL() time.Duration { return 3 * time.Hour } func GetAuthCodeTTL() time.Duration { return 2 * GetAuthCodeLifeSpan() } func GetAuthCodeLifeSpan() time.Duration { return 15 * time.Minute } func GetBarcodeCharset() string { return "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" } func HMACStrategy() *hmac.HMACStrategy { return &hmac.HMACStrategy{ GlobalSecret: Secret(), RotatedGlobalSecrets: nil, TokenEntropy: 32, } } func OrganizationID() string { organizationID := os.Getenv("IAM_ORGANIZATION_ID") if organizationID == "" { ensureBslInfo() return bslInfo.organizationID } return organizationID } func BarcodePrefix() string { return "412" } func OrganizationName() string { organizationName := os.Getenv("IAM_ORGANIZATION_NAME") if organizationName == "" { ensureBslInfo() return bslInfo.organizationName } return organizationName } func SiteID() string { siteID := os.Getenv("IAM_SITE_ID") if siteID == "" { ensureBslInfo() return bslInfo.siteID } return siteID } func TimeZone() string { timeZone := os.Getenv("IAM_TIME_ZONE") if timeZone == "" { ensureBslInfo() return bslInfo.timeZone } return timeZone } func ClusterID() string { clusterID := os.Getenv("IAM_CLUSTER_ID") if clusterID == "" { ensureEdgeInfo() return edgeInfo.ClusterEdgeID } return clusterID } func BslSecurityURL() string { return os.Getenv("IAM_BSL_BASE_URL") + "/security/authorization" } func DeviceBaseURL() string { return os.Getenv("IAM_BSL_BASE_URL") + "/site-security" } func GetLeeWayForDeviceTokenValidation() time.Duration { return 5 * time.Minute } func BslIntrospectURL() string { return os.Getenv("IAM_BSL_BASE_URL") + "/users/introspect" } func ProvisioningUserProfilesURL() string { return os.Getenv("IAM_BSL_BASE_URL") + "/provisioning/user-profiles" } func Issuer() string { return os.Getenv("IAM_ISSUER") } func IssuerURL() string { return os.Getenv("IAM_ISSUER_URL") } func Addr() string { addr, found := os.LookupEnv("IAM_ADDR") if !found { return ":8080" } return addr } func CouchDBAddress() string { return os.Getenv("IAM_COUCHDB_ADDRESS") } func CouchDBUser() string { return os.Getenv("IAM_COUCHDB_USER") } func CouchDBPassword() string { return os.Getenv("IAM_COUCHDB_PASSWORD") } func CouchDBAddressLocal() string { return os.Getenv("IAM_COUCHDB_ADDRESS_LOCAL") } func CouchDBUserLocal() (string, error) { username, ok := os.LookupEnv("COUCHDB_USER") if !ok { username, ok = os.LookupEnv("IAM_COUCHDB_USER_LOCAL") if !ok { return "", errors.New("cannot find local couchdb user") } } return username, nil } func CouchDBPasswordLocal() (string, error) { password, ok := os.LookupEnv("COUCHDB_PASSWORD") if !ok { password, ok = os.LookupEnv("IAM_COUCHDB_PASSWORD_LOCAL") if !ok { return "", errors.New("cannot find local couchdb password") } } return password, nil } func RedisAddress() string { return os.Getenv("IAM_REDIS_ADDRESS") } func NodeName() string { return os.Getenv("POD_NODE_NAME") } func IsTouchpoint() bool { return os.Getenv("IAM_PROVIDER_TYPE") == "touchpoint" } func SessionCookieMaxAge() int { defaultValue := 900 // 15 mintues * 60 str := os.Getenv("IAM_SESSION_COOKIE_MAX_AGE") maxAge, err := strconv.Atoi(str) if err != nil { return defaultValue } return maxAge } func BcryptCost() int { defaultValue := 10 str := os.Getenv("IAM_BCRYPT_COST") bcryptCost, err := strconv.Atoi(str) if err != nil { return defaultValue } return bcryptCost } func FositeBcryptCost() int { defaultValue := 10 str := os.Getenv("IAM_FOSITE_BCRYPT_COST") bcryptCost, err := strconv.Atoi(str) if err != nil { return defaultValue } return bcryptCost } func PrivateKey() *rsa.PrivateKey { // try and get from ENV if privateKey == nil { if pk, found := os.LookupEnv("IAM_PRIVATE_KEY"); found { privateKey = crypto.Deserialize(pk) } } // we'll just make one for you in debug if privateKey == nil { if !IsProduction() { privateKey = crypto.CreatePrivateKey() // } } return privateKey } func PrivateKeyID() string { // try and get it from env if privateKeyID == "" { if id, found := os.LookupEnv("IAM_PRIVATE_KEY_ID"); found { privateKeyID = id } } // if there's none in the env and we're debugging we'll give you one if privateKeyID == "" { if !IsProduction() { privateKeyID = uuid.NewString() } } return privateKeyID } func Secret() []byte { if id, found := os.LookupEnv("IAM_CHALLENGE_SECRET"); found { return []byte(id) } return []byte("some-secret-that-is-32-bytes-123") } func ProxyToWeb() bool { v := os.Getenv("IAM_PROXY_TO_WEB") flag, err := strconv.ParseBool(v) if err != nil { return false } return flag } func Namespace() string { return os.Getenv("IAM_WATCH_NAMESPACE") } func Domain() string { return "iam.edge.ncr.com" } func FieldManager(ctlname string) string { return fmt.Sprintf("%s/%s", Domain(), ctlname) } func AccessTokenLifespan() time.Duration { return 15 * time.Minute } func RefreshTokenLifespan() time.Duration { return 7 * 24 * time.Hour } func CloudIDPTimeout() time.Duration { return 6 * time.Second } func OktaClientID() string { return os.Getenv("OKTA_CLIENT_ID") } func OktaClientSecret() string { return os.Getenv("OKTA_CLIENT_SECRET") } func OktaIssuer() string { return os.Getenv("OKTA_ISSUER") } func OktaEnabled() bool { ldctx := ldcontext.NewMulti( ldcontext.NewWithKind("siteID", SiteID()), ldcontext.NewWithKind("orgID", OrganizationID()), ) return isFeatureEnabled(ff.OktaEnabled, os.Getenv("FF_OKTA_ENABLED") == trueString, ldctx) } func ToggleDisruptWAN() { disruptWAN = !disruptWAN } func IsWANDisrupted() bool { return disruptWAN } func StrongAuthDisabled() bool { ldctx := ldcontext.NewMulti( ldcontext.NewWithKind("siteID", SiteID()), ldcontext.NewWithKind("orgID", OrganizationID()), ) return isFeatureEnabled("disable-strong-auth", os.Getenv("FF_STRONG_AUTH_DISABLED") == trueString, ldctx) } func isFeatureEnabled(feature string, defaultVal bool, ldCtx ldcontext.Context) bool { logging := logger.WithName("Feature Flag") featureEnabled, err := ff.FeatureEnabledForLDContext(feature, ldCtx, defaultVal) if err != nil { logging.Info("failed to check if LD feature is enabled. using default value instead", feature, featureEnabled, "error", err) return featureEnabled } if featureEnabled != defaultVal { logging.Info("using value set on launchdarkly which is different than the default", "feature", feature, "val", featureEnabled, "default", defaultVal) } return featureEnabled } func EncryptionKey() []byte { key, found := os.LookupEnv("IAM_ENCRYPTION_KEY") if found { return []byte(key) } return nil } // returns true if we have an Encryption key func EncryptionEnabled() bool { return !bytes.Equal(EncryptionKey(), nil) } func IsTest() bool { return os.Getenv("IAM_ENV_TEST") == trueString } func RevertEncryption() bool { logging := logger.WithName("Revert Encryption") revert := (os.Getenv("IAM_REVERT_ENCRYPTION") == trueString) if revert && EncryptionEnabled() { logging.Info("Encryption was set to be reverted but the encryption env is still currently enabled. IAM_ENCRYPTION_ENABLED needs to be set to false.") } return revert }