1 package config
2
3 import (
4 "bytes"
5 "crypto/rsa"
6
7 "errors"
8 "fmt"
9 "os"
10 "strconv"
11 "strings"
12
13 "time"
14
15 "github.com/go-logr/logr"
16 "github.com/google/uuid"
17 "github.com/launchdarkly/go-sdk-common/v3/ldcontext"
18 "github.com/ory/fosite/token/hmac"
19 "github.com/ory/fosite/token/jwt"
20
21 ldredis "github.com/launchdarkly/go-server-sdk-redis-redigo/v2"
22 ldclient "github.com/launchdarkly/go-server-sdk/v6"
23 "github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
24
25 "edge-infra.dev/pkg/edge/iam/crypto"
26 ff "edge-infra.dev/pkg/lib/featureflag"
27 )
28
29 var (
30 privateKey *rsa.PrivateKey
31 privateKeyID string
32 bslInfo *BslInfo
33 edgeInfo *EdgeInfo
34 logger logr.Logger
35 disruptWAN bool
36 )
37
38 const (
39 Okta = "okta"
40 trueString = "true"
41 )
42
43 func SetLogger(l logr.Logger) {
44 logger = l
45 logger.Info("config's logger is set and ready to be used")
46 }
47
48 func Verify() error {
49 result := make([]string, 0)
50
51 if !isLDReachable() {
52 logger.Error(nil, "launchdarkly is currently not reachable. will use default values based on environment variables for now")
53 }
54
55 if Issuer() == "" {
56 result = append(result, "IAM_ISSUER")
57 }
58
59 if IssuerURL() == "" {
60 result = append(result, "IAM_ISSUER_URL")
61 }
62
63 if OrganizationID() == "" {
64 result = append(result, "IAM_ORGANIZATION_ID")
65 }
66
67 if OrganizationName() == "" {
68 result = append(result, "IAM_ORGANIZATION_NAME")
69 }
70
71 if SiteID() == "" {
72 result = append(result, "IAM_SITE_ID")
73 }
74
75 if OktaEnabled() {
76 if OktaClientID() == "" {
77 result = append(result, "OKTA_CLIENT_ID")
78 }
79 if OktaClientSecret() == "" {
80 result = append(result, "OKTA_CLIENT_SECRET")
81 }
82 if OktaIssuer() == "" {
83 result = append(result, "OKTA_ISSUER")
84 }
85 }
86
87 if len(result) > 0 {
88
89 resultAsString := strings.Join(result, ", ")
90 if OktaEnabled() && strings.Contains(resultAsString, "OKTA_CLIENT_ID") {
91 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")
92 }
93 return fmt.Errorf("missing the following configuration: %v", resultAsString)
94 }
95
96 return nil
97 }
98
99 func ensureBslInfo() {
100 if bslInfo == nil {
101 var err error
102 bslInfo, err = getBslInfo()
103 if err != nil {
104 panic(err.Error())
105 }
106 }
107 }
108
109 func ensureEdgeInfo() {
110 if edgeInfo == nil {
111 var err error
112 edgeInfo, err = getEdgeInfo()
113 if err != nil {
114 panic(err.Error())
115 }
116 }
117 }
118
119 func Info() {
120 logger.Info("configuration",
121 "ISSUER", Issuer(),
122 "ISSUER_URL", IssuerURL(),
123 "ADDR", Addr(),
124 "IAM_ORGANIZATION_NAME", OrganizationName(),
125 "IAM_ORGANIZATION_ID", OrganizationID(),
126 "IAM_SITE_ID", SiteID(),
127 )
128 }
129
130 func IsProduction() bool {
131 return os.Getenv("IAM_MODE") != "debug"
132 }
133
134 func GetBarcodeKeyLength() uint8 {
135 return 5
136 }
137 func GetBarcodeLength() uint8 {
138 return 20
139 }
140
141 func GetBarcodeLifeSpan() time.Duration {
142 defaultValue := 8 * time.Hour
143 duration, err := time.ParseDuration(os.Getenv("IAM_BARCODE_LIFESPAN"))
144 if err != nil {
145 return defaultValue
146 }
147
148 return duration
149 }
150 func GetJWTStrategy() *jwt.RS256JWTStrategy {
151 return &jwt.RS256JWTStrategy{
152 PrivateKey: PrivateKey(),
153 }
154 }
155 func GetEmergencyBarcodeLifeSpan() time.Duration {
156 defaultVaule := 8 * time.Hour
157 duration, err := time.ParseDuration(os.Getenv("IAM_EBC_LIFESPAN"))
158 if err != nil {
159 return defaultVaule
160 }
161
162 return duration
163 }
164
165 func GetPINLifeSpan() time.Duration {
166 defaultValue := 6 * 30 * 24 * time.Hour
167 duration, err := time.ParseDuration(os.Getenv("IAM_PIN_LIFESPAN"))
168 if err != nil {
169 return defaultValue
170 }
171
172 return duration
173 }
174
175 func GetPINTTL() time.Duration {
176 return 2 * GetPINLifeSpan()
177 }
178
179 func GetAccessTokenTTL() time.Duration {
180 return 2 * AccessTokenLifespan()
181 }
182
183 func GetRefreshTokenTTL() time.Duration {
184 return 2 * RefreshTokenLifespan()
185 }
186
187 func InitLaunchDarkly() {
188 redisAddr := fmt.Sprintf("redis://%s", RedisAddress())
189 c := ldclient.Config{
190 DataStore: ldcomponents.PersistentDataStore(
191 ldredis.DataStore().URL(redisAddr)),
192 }
193
194 ff.SetConfig(c)
195 logger.Info("setting launchdarkly to use redis as data store")
196 }
197
198 func isLDReachable() bool {
199
200 ldctx := ldcontext.NewMulti(
201 ldcontext.NewWithKind("siteID", SiteID()),
202 ldcontext.NewWithKind("orgID", OrganizationID()),
203 )
204
205
206 _, err := ff.FeatureEnabledForLDContext(ff.OktaEnabled, ldctx, os.Getenv("FF_OKTA_ENABLED") == trueString)
207 return err == nil
208 }
209
210
211 func BarcodeEnabled(clientID string) bool {
212
213 barcodectx := ldcontext.NewMulti(
214 ldcontext.NewWithKind("clientID", clientID),
215 ldcontext.NewWithKind("siteID", SiteID()),
216 ldcontext.NewWithKind("orgID", OrganizationID()),
217 )
218
219 return isFeatureEnabled(ff.BarcodeEnabled, os.Getenv("FF_BARCODE_ENABLED") == trueString, barcodectx)
220 }
221
222
223 func TestClientEnabled() bool {
224
225 testclientctx := ldcontext.NewMulti(
226 ldcontext.NewWithKind("siteID", SiteID()),
227 ldcontext.NewWithKind("orgID", OrganizationID()),
228 )
229
230 return isFeatureEnabled(ff.EdgeIDTestClient, os.Getenv("FF_CREATE_TEST_CLIENT") == trueString, testclientctx)
231 }
232
233 func EnforcePermissions() bool {
234 ldCtx := ldcontext.NewMulti(
235 ldcontext.NewWithKind("siteID", SiteID()),
236 ldcontext.NewWithKind("orgID", OrganizationID()),
237 )
238
239 return isFeatureEnabled(ff.EnforceEdgeIDPermissions, os.Getenv("FF_ENFORCE_PERMISSIONS") == trueString, ldCtx)
240 }
241
242
243 func EmergencyBarcodeEnabled(clientID string) bool {
244 barcodectx := ldcontext.NewMulti(
245 ldcontext.NewWithKind("clientID", clientID),
246 ldcontext.NewWithKind("siteID", SiteID()),
247 ldcontext.NewWithKind("orgID", OrganizationID()),
248 )
249 return isFeatureEnabled(ff.EmergencyBarcodeEnabled, os.Getenv("FF_EBC_ENABLED") == trueString, barcodectx)
250 }
251
252 func DeviceLoginEnabled() bool {
253 deviceLoginctx := ldcontext.NewMulti(
254 ldcontext.NewWithKind("siteID", SiteID()),
255 ldcontext.NewWithKind("orgID", OrganizationID()),
256 )
257 return isFeatureEnabled(ff.DeviceLoginEnabled, os.Getenv("FF_DEVICE_ENABLED") == trueString, deviceLoginctx)
258 }
259
260
261 func GetBarcodeUserTTL() time.Duration {
262 return GetBarcodeLifeSpan() + 30*24*time.Hour
263 }
264
265 func GetBarcodePrintPermission() string {
266 return "EDGE_IAM_ENABLE_BARCODE"
267 }
268
269 func GetEBCPrintPermission() string {
270 return "EDGE_IAM_ENABLE_EBC"
271 }
272
273
274 func GetProfileLifespan() time.Duration {
275 defaultValue := 24 * time.Hour
276 duration, err := time.ParseDuration(os.Getenv("IAM_PROFILE_LIFESPAN"))
277 if err != nil {
278 return defaultValue
279 }
280
281 return duration
282 }
283
284
285 func GetProfileTTL() time.Duration {
286 return 90 * GetProfileLifespan()
287 }
288
289 func GetPINPermission() string {
290 return "EDGE_IAM_ENABLE_PIN"
291 }
292
293 func GetPINRetryThreshold() int8 {
294 defaultValue := 5
295 _, exist := os.LookupEnv("IAM_PIN_RETRY_THRESHOLD")
296 if !exist {
297 return int8(defaultValue)
298 }
299
300 val, _ := strconv.Atoi(os.Getenv("IAM_PIN_RETRY_THRESHOLD"))
301 return int8(val)
302 }
303
304 func GetDeviceRetryThreshold() int8 {
305 defaultValue := 10
306 _, exist := os.LookupEnv("IAM_DEVICE_RETRY_THRESHOLD")
307 if !exist {
308 return int8(defaultValue)
309 }
310
311 val, _ := strconv.Atoi(os.Getenv("IAM_DEVICE_RETRY_THRESHOLD"))
312 return int8(val)
313 }
314
315 func RetriesExceededTimeout() int8 {
316 defaultValue := 30
317 _, exist := os.LookupEnv("IAM_DEVICE_RETRY_TIMEOUT")
318 if !exist {
319 return int8(defaultValue)
320 }
321
322 val, _ := strconv.Atoi(os.Getenv("IAM_DEVICE_RETRY_TIMEOUT"))
323 return int8(val)
324 }
325
326 func PINHistoryLength() int8 {
327 defaultValue := 3
328
329 _, exist := os.LookupEnv("IAM_PIN_HISTORY_LENGTH")
330 if !exist {
331 return int8(defaultValue)
332 }
333
334 val, _ := strconv.Atoi(os.Getenv("IAM_PIN_HISTORY_LENGTH"))
335 return int8(val)
336 }
337
338
339
340
341 func GetBarcodeCodeLifespan() time.Duration {
342 return 1 * time.Minute
343 }
344
345
346 func GetBarcodeCodeTTL() time.Duration {
347 return 3 * 24 * time.Hour
348 }
349
350
351 func GetLoginHintTTL() time.Duration {
352 return 3 * time.Hour
353 }
354
355 func GetAuthCodeTTL() time.Duration {
356 return 2 * GetAuthCodeLifeSpan()
357 }
358
359 func GetAuthCodeLifeSpan() time.Duration {
360 return 15 * time.Minute
361 }
362
363 func GetBarcodeCharset() string {
364 return "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
365 }
366
367 func HMACStrategy() *hmac.HMACStrategy {
368 return &hmac.HMACStrategy{
369 GlobalSecret: Secret(),
370 RotatedGlobalSecrets: nil,
371 TokenEntropy: 32,
372 }
373 }
374
375 func OrganizationID() string {
376 organizationID := os.Getenv("IAM_ORGANIZATION_ID")
377 if organizationID == "" {
378 ensureBslInfo()
379 return bslInfo.organizationID
380 }
381 return organizationID
382 }
383
384 func BarcodePrefix() string {
385 return "412"
386 }
387
388 func OrganizationName() string {
389 organizationName := os.Getenv("IAM_ORGANIZATION_NAME")
390 if organizationName == "" {
391 ensureBslInfo()
392 return bslInfo.organizationName
393 }
394 return organizationName
395 }
396
397 func SiteID() string {
398 siteID := os.Getenv("IAM_SITE_ID")
399 if siteID == "" {
400 ensureBslInfo()
401 return bslInfo.siteID
402 }
403 return siteID
404 }
405
406 func TimeZone() string {
407 timeZone := os.Getenv("IAM_TIME_ZONE")
408 if timeZone == "" {
409 ensureBslInfo()
410 return bslInfo.timeZone
411 }
412 return timeZone
413 }
414
415 func ClusterID() string {
416 clusterID := os.Getenv("IAM_CLUSTER_ID")
417 if clusterID == "" {
418 ensureEdgeInfo()
419 return edgeInfo.ClusterEdgeID
420 }
421 return clusterID
422 }
423
424 func BslSecurityURL() string {
425 return os.Getenv("IAM_BSL_BASE_URL") + "/security/authorization"
426 }
427
428 func DeviceBaseURL() string {
429 return os.Getenv("IAM_BSL_BASE_URL") + "/site-security"
430 }
431
432 func GetLeeWayForDeviceTokenValidation() time.Duration {
433 return 5 * time.Minute
434 }
435
436 func BslIntrospectURL() string {
437 return os.Getenv("IAM_BSL_BASE_URL") + "/users/introspect"
438 }
439
440 func ProvisioningUserProfilesURL() string {
441 return os.Getenv("IAM_BSL_BASE_URL") + "/provisioning/user-profiles"
442 }
443
444 func Issuer() string {
445 return os.Getenv("IAM_ISSUER")
446 }
447
448 func IssuerURL() string {
449 return os.Getenv("IAM_ISSUER_URL")
450 }
451
452 func Addr() string {
453 addr, found := os.LookupEnv("IAM_ADDR")
454 if !found {
455 return ":8080"
456 }
457
458 return addr
459 }
460
461 func CouchDBAddress() string {
462 return os.Getenv("IAM_COUCHDB_ADDRESS")
463 }
464
465 func CouchDBUser() string {
466 return os.Getenv("IAM_COUCHDB_USER")
467 }
468
469 func CouchDBPassword() string {
470 return os.Getenv("IAM_COUCHDB_PASSWORD")
471 }
472
473 func CouchDBAddressLocal() string {
474 return os.Getenv("IAM_COUCHDB_ADDRESS_LOCAL")
475 }
476
477 func CouchDBUserLocal() (string, error) {
478 username, ok := os.LookupEnv("COUCHDB_USER")
479 if !ok {
480 username, ok = os.LookupEnv("IAM_COUCHDB_USER_LOCAL")
481 if !ok {
482 return "", errors.New("cannot find local couchdb user")
483 }
484 }
485 return username, nil
486 }
487
488 func CouchDBPasswordLocal() (string, error) {
489 password, ok := os.LookupEnv("COUCHDB_PASSWORD")
490 if !ok {
491 password, ok = os.LookupEnv("IAM_COUCHDB_PASSWORD_LOCAL")
492 if !ok {
493 return "", errors.New("cannot find local couchdb password")
494 }
495 }
496 return password, nil
497 }
498 func RedisAddress() string {
499 return os.Getenv("IAM_REDIS_ADDRESS")
500 }
501
502 func NodeName() string {
503 return os.Getenv("POD_NODE_NAME")
504 }
505
506 func IsTouchpoint() bool {
507 return os.Getenv("IAM_PROVIDER_TYPE") == "touchpoint"
508 }
509
510 func SessionCookieMaxAge() int {
511 defaultValue := 900
512
513 str := os.Getenv("IAM_SESSION_COOKIE_MAX_AGE")
514
515 maxAge, err := strconv.Atoi(str)
516 if err != nil {
517 return defaultValue
518 }
519
520 return maxAge
521 }
522
523 func BcryptCost() int {
524 defaultValue := 10
525
526 str := os.Getenv("IAM_BCRYPT_COST")
527
528 bcryptCost, err := strconv.Atoi(str)
529 if err != nil {
530 return defaultValue
531 }
532
533 return bcryptCost
534 }
535
536 func FositeBcryptCost() int {
537 defaultValue := 10
538
539 str := os.Getenv("IAM_FOSITE_BCRYPT_COST")
540
541 bcryptCost, err := strconv.Atoi(str)
542 if err != nil {
543 return defaultValue
544 }
545
546 return bcryptCost
547 }
548
549 func PrivateKey() *rsa.PrivateKey {
550
551 if privateKey == nil {
552 if pk, found := os.LookupEnv("IAM_PRIVATE_KEY"); found {
553 privateKey = crypto.Deserialize(pk)
554 }
555 }
556
557
558 if privateKey == nil {
559 if !IsProduction() {
560 privateKey = crypto.CreatePrivateKey()
561 }
562 }
563
564 return privateKey
565 }
566
567 func PrivateKeyID() string {
568
569 if privateKeyID == "" {
570 if id, found := os.LookupEnv("IAM_PRIVATE_KEY_ID"); found {
571 privateKeyID = id
572 }
573 }
574
575
576 if privateKeyID == "" {
577 if !IsProduction() {
578 privateKeyID = uuid.NewString()
579 }
580 }
581
582 return privateKeyID
583 }
584
585 func Secret() []byte {
586 if id, found := os.LookupEnv("IAM_CHALLENGE_SECRET"); found {
587 return []byte(id)
588 }
589
590 return []byte("some-secret-that-is-32-bytes-123")
591 }
592
593 func ProxyToWeb() bool {
594 v := os.Getenv("IAM_PROXY_TO_WEB")
595 flag, err := strconv.ParseBool(v)
596 if err != nil {
597 return false
598 }
599 return flag
600 }
601
602 func Namespace() string {
603 return os.Getenv("IAM_WATCH_NAMESPACE")
604 }
605
606 func Domain() string {
607 return "iam.edge.ncr.com"
608 }
609
610 func FieldManager(ctlname string) string {
611 return fmt.Sprintf("%s/%s", Domain(), ctlname)
612 }
613
614 func AccessTokenLifespan() time.Duration {
615 return 15 * time.Minute
616 }
617
618 func RefreshTokenLifespan() time.Duration {
619 return 7 * 24 * time.Hour
620 }
621
622 func CloudIDPTimeout() time.Duration {
623 return 6 * time.Second
624 }
625
626 func OktaClientID() string {
627 return os.Getenv("OKTA_CLIENT_ID")
628 }
629
630 func OktaClientSecret() string {
631 return os.Getenv("OKTA_CLIENT_SECRET")
632 }
633
634 func OktaIssuer() string {
635 return os.Getenv("OKTA_ISSUER")
636 }
637
638 func OktaEnabled() bool {
639 ldctx := ldcontext.NewMulti(
640 ldcontext.NewWithKind("siteID", SiteID()),
641 ldcontext.NewWithKind("orgID", OrganizationID()),
642 )
643
644 return isFeatureEnabled(ff.OktaEnabled, os.Getenv("FF_OKTA_ENABLED") == trueString, ldctx)
645 }
646
647 func ToggleDisruptWAN() {
648 disruptWAN = !disruptWAN
649 }
650
651 func IsWANDisrupted() bool {
652 return disruptWAN
653 }
654
655 func StrongAuthDisabled() bool {
656 ldctx := ldcontext.NewMulti(
657 ldcontext.NewWithKind("siteID", SiteID()),
658 ldcontext.NewWithKind("orgID", OrganizationID()),
659 )
660
661 return isFeatureEnabled("disable-strong-auth", os.Getenv("FF_STRONG_AUTH_DISABLED") == trueString, ldctx)
662 }
663
664 func isFeatureEnabled(feature string, defaultVal bool, ldCtx ldcontext.Context) bool {
665 logging := logger.WithName("Feature Flag")
666
667 featureEnabled, err := ff.FeatureEnabledForLDContext(feature, ldCtx, defaultVal)
668 if err != nil {
669 logging.Info("failed to check if LD feature is enabled. using default value instead", feature, featureEnabled, "error", err)
670 return featureEnabled
671 }
672 if featureEnabled != defaultVal {
673 logging.Info("using value set on launchdarkly which is different than the default", "feature", feature, "val", featureEnabled, "default", defaultVal)
674 }
675
676 return featureEnabled
677 }
678
679 func EncryptionKey() []byte {
680 key, found := os.LookupEnv("IAM_ENCRYPTION_KEY")
681 if found {
682 return []byte(key)
683 }
684
685 return nil
686 }
687
688
689 func EncryptionEnabled() bool {
690 return !bytes.Equal(EncryptionKey(), nil)
691 }
692
693 func IsTest() bool {
694 return os.Getenv("IAM_ENV_TEST") == trueString
695 }
696
697 func RevertEncryption() bool {
698 logging := logger.WithName("Revert Encryption")
699 revert := (os.Getenv("IAM_REVERT_ENCRYPTION") == trueString)
700
701 if revert && EncryptionEnabled() {
702 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.")
703 }
704 return revert
705 }
706
View as plain text