Semaphore/util/config.go
2020-12-04 13:46:02 +05:00

540 lines
13 KiB
Go

package util
import (
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
log "github.com/Sirupsen/logrus"
"os"
"path"
"net/url"
"io"
"strings"
"github.com/gorilla/securecookie"
"golang.org/x/crypto/bcrypt"
)
// Cookie is a runtime generated secure cookie used for authentication
var Cookie *securecookie.SecureCookie
// Migration indicates that the user wishes to run database migrations, deprecated
var Migration bool
// InteractiveSetup indicates that the cli should perform interactive setup mode
var InteractiveSetup bool
// Upgrade indicates that we should perform an upgrade action
var Upgrade bool
// WebHostURL is the public route to the semaphore server
var WebHostURL *url.URL
const (
longPos = "yes"
shortPos = "y"
)
type DbDriver int
const (
DbDriverMySQL DbDriver = iota
DbDriverSQLite
)
type DbConfig struct {
Dialect DbDriver `json:"-"`
Hostname string `json:"host"`
Username string `json:"user"`
Password string `json:"pass"`
DbName string `json:"name"`
}
type ldapMappings struct {
DN string `json:"dn"`
Mail string `json:"mail"`
UID string `json:"uid"`
CN string `json:"cn"`
}
//ConfigType mapping between Config and the json file that sets it
type ConfigType struct {
MySQL DbConfig `json:"mysql"`
SQLite DbConfig `json:"sqlite"`
// Format `:port_num` eg, :3000
// if : is missing it will be corrected
Port string `json:"port"`
// Interface ip, put in front of the port.
// defaults to empty
Interface string `json:"interface"`
// semaphore stores ephemeral projects here
TmpPath string `json:"tmp_path"`
// cookie hashing & encryption
CookieHash string `json:"cookie_hash"`
CookieEncryption string `json:"cookie_encryption"`
// email alerting
EmailSender string `json:"email_sender"`
EmailHost string `json:"email_host"`
EmailPort string `json:"email_port"`
// web host
WebHost string `json:"web_host"`
// ldap settings
LdapBindDN string `json:"ldap_binddn"`
LdapBindPassword string `json:"ldap_bindpassword"`
LdapServer string `json:"ldap_server"`
LdapSearchDN string `json:"ldap_searchdn"`
LdapSearchFilter string `json:"ldap_searchfilter"`
LdapMappings ldapMappings `json:"ldap_mappings"`
// telegram alerting
TelegramChat string `json:"telegram_chat"`
TelegramToken string `json:"telegram_token"`
// task concurrency
ConcurrencyMode string `json:"concurrency_mode"`
MaxParallelTasks int `json:"max_parallel_tasks"`
// configType field ordering with bools at end reduces struct size
// (maligned check)
// feature switches
EmailAlert bool `json:"email_alert"`
TelegramAlert bool `json:"telegram_alert"`
LdapEnable bool `json:"ldap_enable"`
LdapNeedTLS bool `json:"ldap_needtls"`
OldFrontend bool `json:"old_frontend"`
}
//Config exposes the application configuration storage for use in the application
var Config *ConfigType
var confPath *string
// NewConfig returns a reference to a new blank configType
// nolint: golint
func NewConfig() *ConfigType {
return &ConfigType{}
}
// ScanErrorChecker deals with errors encountered while scanning lines
// since we do not fail on these errors currently we can simply note them
// and move on
func ScanErrorChecker(n int, err error) {
if err != nil {
log.Warn("An input error occurred:" + err.Error())
}
}
// ConfigInit reads in cli flags, and switches actions appropriately on them
func ConfigInit() {
flag.BoolVar(&InteractiveSetup, "setup", false, "perform interactive setup")
flag.BoolVar(&Migration, "migrate", false, "execute migrations")
flag.BoolVar(&Upgrade, "upgrade", false, "upgrade semaphore")
confPath = flag.String("config", "", "config path")
var unhashedPwd string
flag.StringVar(&unhashedPwd, "hash", "", "generate hash of given password")
var printConfig bool
flag.BoolVar(&printConfig, "printConfig", false, "print example configuration")
var printVersion bool
flag.BoolVar(&printVersion, "version", false, "print the semaphore version")
flag.Parse()
if InteractiveSetup {
return
}
if printVersion {
fmt.Println(Version)
os.Exit(0)
}
if printConfig {
cfg := &ConfigType{
MySQL: DbConfig{
Hostname: "127.0.0.1:3306",
Username: "root",
DbName: "semaphore",
},
Port: ":3000",
TmpPath: "/tmp/semaphore",
}
cfg.GenerateCookieSecrets()
b, _ := json.MarshalIndent(cfg, "", "\t")
fmt.Println(string(b))
os.Exit(0)
}
if len(unhashedPwd) > 0 {
password, _ := bcrypt.GenerateFromPassword([]byte(unhashedPwd), 11)
fmt.Println("Generated password: ", string(password))
os.Exit(0)
}
loadConfig()
validateConfig()
var encryption []byte
hash, _ := base64.StdEncoding.DecodeString(Config.CookieHash)
if len(Config.CookieEncryption) > 0 {
encryption, _ = base64.StdEncoding.DecodeString(Config.CookieEncryption)
}
Cookie = securecookie.New(hash, encryption)
WebHostURL, _ = url.Parse(Config.WebHost)
if len(WebHostURL.String()) == 0 {
WebHostURL = nil
}
}
func loadConfig() {
//If the confPath option has been set try to load and decode it
if confPath != nil && len(*confPath) > 0 {
file, err := os.Open(*confPath)
exitOnConfigError(err)
decodeConfig(file)
} else {
// if no confPath look in the cwd
cwd, err := os.Getwd()
exitOnConfigError(err)
cwd = cwd + "/config.json"
confPath = &cwd
file, err := os.Open(*confPath)
exitOnConfigError(err)
decodeConfig(file)
}
fmt.Println("Using config file: " + *confPath)
}
func validateConfig() {
validatePort()
if len(Config.TmpPath) == 0 {
Config.TmpPath = "/tmp/semaphore"
}
if Config.MaxParallelTasks < 1 {
Config.MaxParallelTasks = 10
}
}
func validatePort() {
//TODO - why do we do this only with this variable?
if len(os.Getenv("PORT")) > 0 {
Config.Port = ":" + os.Getenv("PORT")
}
if len(Config.Port) == 0 {
Config.Port = ":3000"
}
if !strings.HasPrefix(Config.Port, ":") {
Config.Port = ":" + Config.Port
}
}
func exitOnConfigError(err error) {
if err != nil {
fmt.Println("Cannot Find configuration! Use -c parameter to point to a JSON file generated by -setup.\n\n Hint: have you run `-setup` ?")
os.Exit(1)
}
}
func decodeConfig(file io.Reader) {
if err := json.NewDecoder(file).Decode(&Config); err != nil {
fmt.Println("Could not decode configuration!")
panic(err)
}
}
func (d DbDriver) String() string {
return [...]string{"mysql", "sqlite3"}[d]
}
func (d *DbConfig) isPresent() bool {
return d.Hostname != ""
}
func (d *DbConfig) HasSupportMultipleDatabases() bool {
return d.Dialect != DbDriverSQLite
}
func (d *DbConfig) GetConnectionString(includeDbName bool) (connectionString string, err error) {
switch d.Dialect {
case DbDriverMySQL:
if includeDbName {
connectionString = fmt.Sprintf(
"%s:%s@tcp(%s)/%s?parseTime=true&interpolateParams=true",
d.Username,
d.Password,
d.Hostname,
d.DbName)
} else {
connectionString = fmt.Sprintf(
"%s:%s@tcp(%s)?parseTime=true&interpolateParams=true",
d.Username,
d.Password,
d.Hostname)
}
case DbDriverSQLite:
if includeDbName {
connectionString = fmt.Sprintf("file://%s?&cache=shared", d.Hostname)
} else {
err = errors.New("sqlite3 not support multiple databases and argument includeDbName can not be false")
}
default:
err = fmt.Errorf("unsupported database driver: %s", d.Dialect)
}
return
}
func (conf *ConfigType) GetDBConfig() (dbConfig DbConfig, err error) {
switch {
case conf.MySQL.isPresent():
dbConfig = conf.MySQL
dbConfig.Dialect = DbDriverMySQL
case conf.SQLite.isPresent():
dbConfig = conf.SQLite
dbConfig.Dialect = DbDriverSQLite
default:
err = errors.New("database configuration not found")
}
return
}
//GenerateCookieSecrets generates cookie secret during setup
func (conf *ConfigType) GenerateCookieSecrets() {
hash := securecookie.GenerateRandomKey(32)
encryption := securecookie.GenerateRandomKey(32)
conf.CookieHash = base64.StdEncoding.EncodeToString(hash)
conf.CookieEncryption = base64.StdEncoding.EncodeToString(encryption)
}
func (conf *ConfigType) ScanMySQL() {
fmt.Print(" > DB Hostname (default 127.0.0.1:3306): ")
ScanErrorChecker(fmt.Scanln(&conf.MySQL.Hostname))
if len(conf.MySQL.Hostname) == 0 {
conf.MySQL.Hostname = "127.0.0.1:3306"
}
fmt.Print(" > DB User (default root): ")
ScanErrorChecker(fmt.Scanln(&conf.MySQL.Username))
if len(conf.MySQL.Username) == 0 {
conf.MySQL.Username = "root"
}
fmt.Print(" > DB Password: ")
ScanErrorChecker(fmt.Scanln(&conf.MySQL.Password))
fmt.Print(" > DB Name (default semaphore): ")
ScanErrorChecker(fmt.Scanln(&conf.MySQL.DbName))
if len(conf.MySQL.DbName) == 0 {
conf.MySQL.DbName = "semaphore"
}
}
func (conf *ConfigType) ScanSQLite() {
homeDir, err := os.UserHomeDir()
if err == nil {
fmt.Print(" > DB file path (default " + homeDir + "/semaphore.sqlite): ")
ScanErrorChecker(fmt.Scanln(&conf.SQLite.Hostname))
if len(conf.SQLite.Hostname) == 0 {
conf.SQLite.Hostname = homeDir + "/semaphore.sqlite"
}
} else {
fmt.Print(" > DB file path (for example /path/to/the/file.sqlite): ")
ScanErrorChecker(fmt.Scanln(&conf.SQLite.Hostname))
}
}
//nolint: gocyclo
func (conf *ConfigType) Scan() {
db := 1
fmt.Println(" > DB")
fmt.Println(" 1 - MySQL")
fmt.Println(" 2 - SQLite")
fmt.Print(" (default 1): ")
ScanErrorChecker(fmt.Scanln(&db))
switch db {
case 1:
conf.ScanMySQL()
case 2:
conf.ScanSQLite()
}
fmt.Print(" > Playbook path (default /tmp/semaphore): ")
ScanErrorChecker(fmt.Scanln(&conf.TmpPath))
if len(conf.TmpPath) == 0 {
conf.TmpPath = "/tmp/semaphore"
}
conf.TmpPath = path.Clean(conf.TmpPath)
fmt.Print(" > Web root URL (optional, example http://localhost:8010/): ")
ScanErrorChecker(fmt.Scanln(&conf.WebHost))
var EmailAlertAnswer string
fmt.Print(" > Enable email alerts (y/n, default n): ")
ScanErrorChecker(fmt.Scanln(&EmailAlertAnswer))
if EmailAlertAnswer == longPos || EmailAlertAnswer == shortPos {
conf.EmailAlert = true
fmt.Print(" > Mail server host (default localhost): ")
ScanErrorChecker(fmt.Scanln(&conf.EmailHost))
if len(conf.EmailHost) == 0 {
conf.EmailHost = "localhost"
}
fmt.Print(" > Mail server port (default 25): ")
ScanErrorChecker(fmt.Scanln(&conf.EmailPort))
if len(conf.EmailPort) == 0 {
conf.EmailPort = "25"
}
fmt.Print(" > Mail sender address (default semaphore@localhost): ")
ScanErrorChecker(fmt.Scanln(&conf.EmailSender))
if len(conf.EmailSender) == 0 {
conf.EmailSender = "semaphore@localhost"
}
} else {
conf.EmailAlert = false
}
var TelegramAlertAnswer string
fmt.Print(" > Enable telegram alerts (y/n, default n): ")
ScanErrorChecker(fmt.Scanln(&TelegramAlertAnswer))
if TelegramAlertAnswer == longPos || TelegramAlertAnswer == shortPos {
conf.TelegramAlert = true
fmt.Print(" > Telegram bot token (you can get it from @BotFather) (default ''): ")
ScanErrorChecker(fmt.Scanln(&conf.TelegramToken))
if len(conf.TelegramToken) == 0 {
conf.TelegramToken = ""
}
fmt.Print(" > Telegram chat ID (default ''): ")
ScanErrorChecker(fmt.Scanln(&conf.TelegramChat))
if len(conf.TelegramChat) == 0 {
conf.TelegramChat = ""
}
} else {
conf.TelegramAlert = false
}
var LdapAnswer string
fmt.Print(" > Enable LDAP authentication (y/n, default n): ")
ScanErrorChecker(fmt.Scanln(&LdapAnswer))
if LdapAnswer == longPos || LdapAnswer == shortPos {
conf.LdapEnable = true
fmt.Print(" > LDAP server host (default localhost:389): ")
ScanErrorChecker(fmt.Scanln(&conf.LdapServer))
if len(conf.LdapServer) == 0 {
conf.LdapServer = "localhost:389"
}
var LdapTLSAnswer string
fmt.Print(" > Enable LDAP TLS connection (y/n, default n): ")
ScanErrorChecker(fmt.Scanln(&LdapTLSAnswer))
if LdapTLSAnswer == longPos || LdapTLSAnswer == shortPos {
conf.LdapNeedTLS = true
} else {
conf.LdapNeedTLS = false
}
fmt.Print(" > LDAP DN for bind (default cn=user,ou=users,dc=example): ")
ScanErrorChecker(fmt.Scanln(&conf.LdapBindDN))
if len(conf.LdapBindDN) == 0 {
conf.LdapBindDN = "cn=user,ou=users,dc=example"
}
fmt.Print(" > Password for LDAP bind user (default pa55w0rd): ")
ScanErrorChecker(fmt.Scanln(&conf.LdapBindPassword))
if len(conf.LdapBindPassword) == 0 {
conf.LdapBindPassword = "pa55w0rd"
}
fmt.Print(" > LDAP DN for user search (default ou=users,dc=example): ")
ScanErrorChecker(fmt.Scanln(&conf.LdapSearchDN))
if len(conf.LdapSearchDN) == 0 {
conf.LdapSearchDN = "ou=users,dc=example"
}
fmt.Print(" > LDAP search filter (default (uid=" + "%" + "s)): ")
ScanErrorChecker(fmt.Scanln(&conf.LdapSearchFilter))
if len(conf.LdapSearchFilter) == 0 {
conf.LdapSearchFilter = "(uid=%s)"
}
fmt.Print(" > LDAP mapping for DN field (default dn): ")
ScanErrorChecker(fmt.Scanln(&conf.LdapMappings.DN))
if len(conf.LdapMappings.DN) == 0 {
conf.LdapMappings.DN = "dn"
}
fmt.Print(" > LDAP mapping for username field (default uid): ")
ScanErrorChecker(fmt.Scanln(&conf.LdapMappings.UID))
if len(conf.LdapMappings.UID) == 0 {
conf.LdapMappings.UID = "uid"
}
fmt.Print(" > LDAP mapping for full name field (default cn): ")
ScanErrorChecker(fmt.Scanln(&conf.LdapMappings.CN))
if len(conf.LdapMappings.CN) == 0 {
conf.LdapMappings.CN = "cn"
}
fmt.Print(" > LDAP mapping for email field (default mail): ")
ScanErrorChecker(fmt.Scanln(&conf.LdapMappings.Mail))
if len(conf.LdapMappings.Mail) == 0 {
conf.LdapMappings.Mail = "mail"
}
} else {
conf.LdapEnable = false
}
}