-
Notifications
You must be signed in to change notification settings - Fork 1
/
config.go
416 lines (382 loc) · 12.7 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
package common
import (
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/APTrust/registry/constants"
"github.com/asaskevich/govalidator"
"github.com/gorilla/securecookie"
"github.com/rs/zerolog"
"github.com/spf13/viper"
"github.com/stretchr/stew/slice"
)
var allowedConfigs = []string{
"ci",
"dev",
"docker",
"integration",
"production",
"staging",
"test",
"travis",
}
// DBConfig contains info for connecting to the Postgres database.
type DBConfig struct {
Host string
Name string
User string
Password string
Port int
Driver string
UseSSL bool
}
type CookieConfig struct {
Secure *securecookie.SecureCookie
Domain string
HTTPSOnly bool
MaxAge int
SessionCookie string
FlashCookie string
PrefsCookie string
}
type LoggingConfig struct {
File string
Level zerolog.Level
LogCaller bool
LogToConsole bool
LogSql bool
}
// TwoFactorConfig contains info for sending push messages
// through Authy and SMS text messages through AWS SNS.
// If SNSEndpoint is empty, we'll use the default public
// SNS endpoint for the specified region. If non-empty,
// we'll use the explicit SNSEndpoint. Should be non-empty
// if we're on a private subnet without a NAT gateway.
type TwoFactorConfig struct {
AuthyEnabled bool
AuthyAPIKey string
AWSRegion string
SMSEnabled bool
OTPExpiration time.Duration
SNSUser string
SNSPassword string
SNSEndpoint string
}
// EmailConfig describes how to connect to Amazon SES or
// another SMTP service. If SesEndpoint is empty, we'll use
// the default public SES endpoint for the specified region.
// If non-empty, we'll use the explicit SesEndpoint. Should
// be non-empty if we're on a private subnet without a NAT gateway.
type EmailConfig struct {
AWSRegion string
Enabled bool
FromAddress string
SesUser string
SesPassword string
SesEndpoint string
}
type RedisConfig struct {
URL string
Password string
DefaultDB int
}
// RetentionMinimum describes the minimum number of days items
// must remain in preservation storage before they can be deleted.
// For S3 and APTrust Standard storage, this is zero. We can
// delete those items at any time. All other storage types have
// restrictions. We prevent depositors from deleting items that
// have not met the minimum retention period because we have to
// pay for the minimum retention period no matter what, and we
// need to pass those costs through to depositors.
type RetentionMinimum struct {
Glacier int
GlacierDeep int
Wasabi int
Standard int
}
// For returns the minimum number of days an object
// or file must be stored in the specified storage option.
// (RetentionMinimum.For(option) makes for readable code.)
func (rm *RetentionMinimum) For(storageOption string) int {
days := 0
switch storageOption {
case constants.StorageOptionGlacierDeepOH, constants.StorageOptionGlacierDeepOR, constants.StorageOptionGlacierDeepVA:
days = rm.GlacierDeep
case constants.StorageOptionGlacierOH, constants.StorageOptionGlacierOR, constants.StorageOptionGlacierVA:
days = rm.Glacier
case constants.StorageOptionWasabiOR, constants.StorageOptionWasabiVA:
days = rm.Wasabi
case constants.StorageOptionStandard:
days = rm.Standard
default:
days = 0
}
return days
}
type Config struct {
Cookies *CookieConfig
DB *DBConfig
EnvName string
Logging *LoggingConfig
NsqUrl string
TwoFactor *TwoFactorConfig
Email *EmailConfig
Redis *RedisConfig
RetentionMinimum *RetentionMinimum
// BatchDeletionKey is a secret loaded from parameter store.
// Batch deletion requests must include this as an extra security token.
BatchDeletionKey string
// MaintenanceMode indicates whether we're currently doing maintenance
// on the system. If this is true, all requests will be redirected to
// the /maintenance page, which will render HTML or JSON as necessary.
// Also, when this is true, the cron jobs in application/cron.go will
// not be initialized, so that the DB will receive no writes from Registry
// and will be free to run migrations.
MaintenanceMode bool
// EmailServiceType describes which email service to use in the current
// environment. This should be "SMTP" if we're running on a private
// subnet with no NAT gateway. Otherwise, it should be "SES". If this is
// not set, or if it's set to an invalid value, it defaults to SMTP.
EmailServiceType string
}
// Returns a new config based on APT_ENV
func NewConfig() *Config {
config := loadConfig()
config.expandPaths()
config.makeDirs()
return config
}
// This returns the default config directory and file.
// In most cases, that will be the .env file in the
// current working directory. When running automated tests,
// however, go changes into the subdirectories that contain
// the test files, so this resolves configDir to the project
// root directory.
func configDirAndFile() (configDir string, configFile string) {
configDir, _ = os.Getwd()
envName := os.Getenv("APT_ENV")
if !slice.Contains(allowedConfigs, envName) {
PrintAndExit(fmt.Sprintf("Set APT_ENV to one of %s", strings.Join(allowedConfigs, ",")))
}
configFile = ".env"
if envName != "" {
configFile = ".env." + envName
}
if TestsAreRunning() {
configDir = ProjectRoot()
}
return configDir, configFile
}
func loadConfig() *Config {
configDir, configFile := configDirAndFile()
v := viper.New()
v.AddConfigPath(configDir)
v.SetConfigName(configFile)
v.SetConfigType("env")
v.AutomaticEnv() // override config file vars with ENV vars
err := v.ReadInConfig()
if err != nil {
PrintAndExit(fmt.Sprintf("Fatal error config file: %v \n", err))
}
hashKey := []byte(v.GetString("COOKIE_HASH_KEY"))
blockKey := []byte(v.GetString("COOKIE_BLOCK_KEY"))
if len(hashKey) < 32 || len(blockKey) < 32 {
PrintAndExit("COOKIE_HASH_KEY and COOKIE_BLOCK_KEY must each be >= 32 bytes")
}
var secureCookie = securecookie.New(hashKey, blockKey)
nsqUrl := v.GetString("NSQ_URL")
if !govalidator.IsURL(nsqUrl) {
PrintAndExit("NSQ_URL is missing or invalid")
}
sesUser := v.GetString("AWS_SES_USER")
sesPassword := v.GetString("AWS_SES_PWD")
if sesUser == "" {
fmt.Fprintln(os.Stderr, "AWS_SES_USER not set. Defaulting to AWS_ACCESS_KEY_ID for sending email.")
sesUser = v.GetString("AWS_ACCESS_KEY_ID")
}
if sesPassword == "" {
fmt.Fprintln(os.Stderr, "AWS_SES_PWD not set. Defaulting to AWS_SECRET_ACCESS_KEY for sending email.")
sesPassword = v.GetString("AWS_SECRET_ACCESS_KEY")
}
snsUser := v.GetString("AWS_SNS_USER")
snsPassword := v.GetString("AWS_SNS_PWD")
if snsUser == "" {
fmt.Fprintln(os.Stderr, "AWS_SNS_USER not set. Defaulting to AWS_ACCESS_KEY_ID for sending text messages.")
snsUser = v.GetString("AWS_ACCESS_KEY_ID")
}
if snsPassword == "" {
fmt.Fprintln(os.Stderr, "AWS_SNS_PWD not set. Defaulting to AWS_SECRET_ACCESS_KEY for sending text messages.")
snsPassword = v.GetString("AWS_SECRET_ACCESS_KEY")
}
emailServiceType := strings.ToUpper(v.GetString("EMAIL_SERVICE_TYPE"))
if emailServiceType != constants.EmailServiceSES && emailServiceType != constants.EmailServiceSMTP {
fmt.Fprintf(os.Stderr, "EMAIL_SERVICE_TYPE %s is not valid. Defaulting to %s.", emailServiceType, constants.EmailServiceSMTP)
emailServiceType = constants.EmailServiceSMTP
}
return &Config{
Logging: &LoggingConfig{
File: v.GetString("LOG_FILE"),
Level: getLogLevel(v.GetInt("LOG_LEVEL")),
LogCaller: v.GetBool("LOG_CALLER"),
LogToConsole: v.GetBool("LOG_TO_CONSOLE"),
LogSql: v.GetBool("LOG_SQL"),
},
DB: &DBConfig{
Host: v.GetString("DB_HOST"),
Name: v.GetString("DB_NAME"),
User: v.GetString("DB_USER"),
Password: v.GetString("DB_PASSWORD"),
Port: v.GetInt("DB_PORT"),
Driver: v.GetString("DB_DRIVER"),
UseSSL: v.GetBool("DB_USE_SSL"),
},
EnvName: os.Getenv("APT_ENV"),
Cookies: &CookieConfig{
Secure: secureCookie,
Domain: v.GetString("COOKIE_DOMAIN"),
HTTPSOnly: v.GetBool("HTTPS_COOKIES"),
MaxAge: v.GetInt("SESSION_MAX_AGE"),
SessionCookie: v.GetString("SESSION_COOKIE_NAME"),
FlashCookie: v.GetString("FLASH_COOKIE_NAME"),
PrefsCookie: v.GetString("PREFS_COOKIE_NAME"),
},
NsqUrl: nsqUrl,
BatchDeletionKey: v.GetString("BATCH_DELETION_KEY"),
EmailServiceType: emailServiceType,
MaintenanceMode: v.GetBool("MAINTENANCE_MODE"),
TwoFactor: &TwoFactorConfig{
AuthyAPIKey: v.GetString("AUTHY_API_KEY"),
AuthyEnabled: v.GetBool("ENABLE_TWO_FACTOR_AUTHY"),
AWSRegion: v.GetString("AWS_REGION"),
SMSEnabled: v.GetBool("ENABLE_TWO_FACTOR_SMS"),
OTPExpiration: v.GetDuration("OTP_EXPIRATION"),
SNSUser: snsUser,
SNSPassword: snsPassword,
SNSEndpoint: v.GetString("SNS_ENDPOINT"),
},
Email: &EmailConfig{
AWSRegion: v.GetString("AWS_REGION"),
Enabled: v.GetBool("EMAIL_ENABLED"),
FromAddress: v.GetString("EMAIL_FROM_ADDRESS"),
SesUser: sesUser,
SesPassword: sesPassword,
SesEndpoint: v.GetString("SES_ENDPOINT"),
},
Redis: &RedisConfig{
DefaultDB: v.GetInt("REDIS_DEFAULT_DB"),
Password: v.GetString("REDIS_PASSWORD"),
URL: v.GetString("REDIS_URL"),
},
RetentionMinimum: &RetentionMinimum{
Glacier: v.GetInt("RETENTION_MINIMUM_GLACIER"),
GlacierDeep: v.GetInt("RETENTION_MINIMUM_GLACIER_DEEP"),
Wasabi: v.GetInt("RETENTION_MINIMUM_WASABI"),
Standard: v.GetInt("RETENTION_MINIMUM_STANDARD"),
},
}
}
func getLogLevel(level int) zerolog.Level {
return zerolog.Level(int8(level))
}
// Expand ~ to home dir in path settings.
func (config *Config) expandPaths() {
config.Logging.File = expandPath(config.Logging.File)
}
func expandPath(dirName string) string {
dir, err := ExpandTilde(dirName)
if err != nil {
PrintAndExit(err.Error())
}
if dir == dirName && strings.HasPrefix(dirName, ".") {
// dirName didn't change
absPath, err := filepath.Abs(path.Join(ProjectRoot(), dirName))
if err == nil && absPath != "" {
dir = absPath
}
}
return dir
}
func (config *Config) makeDirs() error {
dirs := []string{
path.Dir(config.Logging.File),
}
for _, dir := range dirs {
err := os.MkdirAll(dir, 0755)
if err == nil || os.IsExist(err) {
return nil
} else {
PrintAndExit(err.Error())
}
}
return nil
}
// BucketQualifier returns the S3 bucket qualifier for the current
// config. We could set this in the .env file, but we want to avoid
// the possibility of a config pointing to the wrong buckets. (For
// example, by someone carelessly copying and pasting config settings.)
// Our restrictive IAM permissions prevent the wrong environments
// from accessing the wrong buckets, but this is an extra layer of
// protection. This defaults to ".test", so if anything is misconfigured,
// we'll be reading from and writing to buckets in which we explicitly
// guarantee no permanance.
func (config *Config) BucketQualifier() string {
if config.Cookies.Domain == "repo.aptrust.org" {
return ""
} else if config.Cookies.Domain == "staging.aptrust.org" {
return ".staging"
}
return ".test"
}
// ToJSON serializes the config to JSON for logging purposes.
// It omits some sensitive data, such as the Pharos API key and
// AWS credentials.
func (config *Config) ToJSON() (string, error) {
// Quick and dirty copy
data, err := json.Marshal(config)
if err != nil {
return "", err
}
copyOfConfig := &Config{}
err = json.Unmarshal(data, copyOfConfig)
if err != nil {
return "", err
}
// Mask sensitive data
copyOfConfig.BatchDeletionKey = maskString(config.BatchDeletionKey)
copyOfConfig.DB.Password = maskString(config.DB.Password)
copyOfConfig.DB.User = maskString(config.DB.User)
copyOfConfig.Email.SesUser = maskString(config.Email.SesUser)
copyOfConfig.Email.SesPassword = maskString(config.Email.SesPassword)
copyOfConfig.Redis.Password = maskString(config.Redis.Password)
copyOfConfig.TwoFactor.AuthyAPIKey = maskString(config.TwoFactor.AuthyAPIKey)
copyOfConfig.TwoFactor.SNSUser = maskString(config.TwoFactor.SNSUser)
copyOfConfig.TwoFactor.SNSPassword = maskString(config.TwoFactor.SNSPassword)
safeJson, err := json.MarshalIndent(copyOfConfig, "", " ")
return string(safeJson), err
}
// Returns true if we're in a test or dev environment.
func (config *Config) IsTestOrDevEnv() bool {
switch config.EnvName {
case "dev", "test", "ci", "travis", "integration":
return true
}
return false
}
// HTTPScheme returns "http" for the dev, test, ci, and travis
// environments. It returns "https" for all other environments.
func (config *Config) HTTPScheme() string {
if config.IsTestOrDevEnv() {
return "http"
}
return "https"
}
func maskString(s string) string {
if len(s) < 10 {
return "****"
}
return fmt.Sprintf("****%s", s[len(s)-3:])
}