diff --git a/Dockerfile b/Dockerfile index 5dc0c46e..f8f62a3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,15 @@ -FROM alpine +FROM alpine:3.5 -RUN apk add --no-cache git ansible mysql-client curl openssh-client -RUN curl -L https://github.com/ansible-semaphore/semaphore/releases/download/v2.2.0/semaphore_linux_amd64 > /usr/bin/semaphore && chmod +x /usr/bin/semaphore && mkdir -p /etc/semaphore/playbooks +ENV SEMAPHORE_VERSION="2.2.0" SEMAPHORE_ARCH="linux_amd64" -ADD ./scripts/docker-startup.sh /usr/bin/semaphore-startup.sh -RUN chmod +x /usr/bin/semaphore-startup.sh +RUN apk add --no-cache git ansible mysql-client curl openssh-client && \ + curl -sSfL "https://github.com/ansible-semaphore/semaphore/releases/download/v$SEMAPHORE_VERSION/semaphore_$SEMAPHORE_ARCH" > /usr/bin/semaphore && \ + chmod +x /usr/bin/semaphore && mkdir -p /etc/semaphore/playbooks EXPOSE 3000 + +ADD semaphore-startup.sh /usr/bin/semaphore-startup.sh + ENTRYPOINT ["/usr/bin/semaphore-startup.sh"] CMD ["/usr/bin/semaphore", "-config", "/etc/semaphore/semaphore_config.json"] diff --git a/api-docs.yml b/api-docs.yml index 21ea24ce..ade480a7 100644 --- a/api-docs.yml +++ b/api-docs.yml @@ -53,6 +53,8 @@ definitions: created: type: string format: date-time + alert: + type: boolean APIToken: type: object properties: @@ -75,6 +77,8 @@ definitions: created: type: string format: date-time + alert: + type: boolean AccessKey: type: object properties: diff --git a/api/login.go b/api/login.go index 9a5c581e..05e00abd 100644 --- a/api/login.go +++ b/api/login.go @@ -1,19 +1,105 @@ package api import ( + "crypto/tls" "database/sql" + "fmt" "net/http" "net/mail" "strings" "time" + log "github.com/Sirupsen/logrus" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" "github.com/castawaylabs/mulekick" sq "github.com/masterminds/squirrel" "golang.org/x/crypto/bcrypt" + "gopkg.in/ldap.v2" ) +func ldapAuthentication(auth, password string) (error, models.User) { + + if util.Config.LdapEnable != true { + return fmt.Errorf("LDAP not configured"), models.User{} + } + + bindusername := util.Config.LdapBindDN + bindpassword := util.Config.LdapBindPassword + + l, err := ldap.Dial("tcp", util.Config.LdapServer) + if err != nil { + return err, models.User{} + } + defer l.Close() + + // Reconnect with TLS if needed + if util.Config.LdapNeedTLS == true { + err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err, models.User{} + } + } + + // First bind with a read only user + err = l.Bind(bindusername, bindpassword) + if err != nil { + return err, models.User{} + } + + // Search for the given username + searchRequest := ldap.NewSearchRequest( + util.Config.LdapSearchDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf(util.Config.LdapSearchFilter, auth), + []string{util.Config.LdapMappings.DN}, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + return err, models.User{} + } + + if len(sr.Entries) != 1 { + return fmt.Errorf("User does not exist or too many entries returned"), models.User{} + } + + // Bind as the user to verify their password + userdn := sr.Entries[0].DN + err = l.Bind(userdn, password) + if err != nil { + return err, models.User{} + } + + // Get user info and ensure authentication in case LDAP supports unauthenticated bind + searchRequest = ldap.NewSearchRequest( + util.Config.LdapSearchDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf(util.Config.LdapSearchFilter, auth), + []string{util.Config.LdapMappings.DN, util.Config.LdapMappings.Mail, util.Config.LdapMappings.Uid, util.Config.LdapMappings.CN}, + nil, + ) + + sr, err = l.Search(searchRequest) + if err != nil { + return err, models.User{} + } + + ldapUser := models.User{ + Username: sr.Entries[0].GetAttributeValue(util.Config.LdapMappings.Uid), + Created: time.Now(), + Name: sr.Entries[0].GetAttributeValue(util.Config.LdapMappings.CN), + Email: sr.Entries[0].GetAttributeValue(util.Config.LdapMappings.Mail), + External: true, + Alert: false, + } + + log.Info("User " + ldapUser.Name + " with email " + ldapUser.Email + " authorized via LDAP correctly") + return nil, ldapUser + +} + func login(w http.ResponseWriter, r *http.Request) { var login struct { Auth string `json:"auth" binding:"required"` @@ -26,30 +112,59 @@ func login(w http.ResponseWriter, r *http.Request) { login.Auth = strings.ToLower(login.Auth) - q := sq.Select("*").From("user") + ldapErr, ldapUser := ldapAuthentication(login.Auth, login.Password) - _, err := mail.ParseAddress(login.Auth) - if err == nil { - q = q.Where("email=?", login.Auth) - } else { - q = q.Where("username=?", login.Auth) + if util.Config.LdapEnable == true && ldapErr != nil { + log.Info(ldapErr.Error()) } - query, args, _ := q.ToSql() + q := sq.Select("*"). + From("user") - var user db.User - if err := db.Mysql.SelectOne(&user, query, args...); err != nil { - if err == sql.ErrNoRows { + var user models.User + if ldapErr != nil { + // Perform normal authorization + _, err := mail.ParseAddress(login.Auth) + if err == nil { + q = q.Where("email=?", login.Auth) + } else { + q = q.Where("username=?", login.Auth) + } + + query, args, _ := q.ToSql() + + if err := db.Mysql.SelectOne(&user, query, args...); err != nil { + if err == sql.ErrNoRows { + c.AbortWithStatus(400) + return + } + + panic(err) + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(login.Password)); err != nil { + c.AbortWithStatus(400) w.WriteHeader(http.StatusBadRequest) return } + } else { + // Check if that user already exist in database + q = q.Where("username=? and external=true", ldapUser.Username) - panic(err) - } + query, args, _ := q.ToSql() - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(login.Password)); err != nil { - w.WriteHeader(http.StatusBadRequest) - return + if err := database.Mysql.SelectOne(&user, query, args...); err != nil { + if err == sql.ErrNoRows { + //Create new user + user = ldapUser + if err := database.Mysql.Insert(&user); err != nil { + panic(err) + } + } else if err != nil { + panic(err) + } + + } } session := db.Session{ diff --git a/api/projects/environment.go b/api/projects/environment.go index 0675422b..67ce1227 100644 --- a/api/projects/environment.go +++ b/api/projects/environment.go @@ -49,7 +49,8 @@ func GetEnvironment(w http.ResponseWriter, r *http.Request) { } q := squirrel.Select("*"). - From("project__environment pe") + From("project__environment pe"). + Where("project_id=?", project.ID) switch sort { case "name": @@ -76,6 +77,14 @@ func UpdateEnvironment(w http.ResponseWriter, r *http.Request) { return } + var js map[string]interface{} + if json.Unmarshal([]byte(env.JSON), &js) != nil { + c.JSON(400, map[string]string{ + "error": "JSON is not valid", + }) + return + } + if _, err := db.Mysql.Exec("update project__environment set name=?, json=? where id=?", env.Name, env.JSON, oldEnv.ID); err != nil { panic(err) } @@ -87,7 +96,15 @@ func AddEnvironment(w http.ResponseWriter, r *http.Request) { project := context.Get(r, "project").(db.Project) var env db.Environment - if err := mulekick.Bind(w, r, &env); err != nil { + if err := c.Bind(&env); err != nil { + return + } + + var js map[string]interface{} + if json.Unmarshal([]byte(env.JSON), &js) != nil { + c.JSON(400, map[string]string{ + "error": "JSON is not valid", + }) return } diff --git a/api/projects/project.go b/api/projects/project.go index e0be2c95..5b33b007 100644 --- a/api/projects/project.go +++ b/api/projects/project.go @@ -61,14 +61,15 @@ func MustBeAdmin(w http.ResponseWriter, r *http.Request) { func UpdateProject(w http.ResponseWriter, r *http.Request) { project := context.Get(r, "project").(db.Project) var body struct { - Name string `json:"name"` + Name string `json:"name"` + Alert bool `json:"alert"` } if err := mulekick.Bind(w, r, &body); err != nil { return } - if _, err := db.Mysql.Exec("update project set name=? where id=?", body.Name, project.ID); err != nil { + if _, err := db.Mysql.Exec("update project set name=?, alert=? where id=?", body.Name, body.Alert, project.ID); err != nil { panic(err) } diff --git a/api/projects/templates.go b/api/projects/templates.go index 4b974e3b..404deb04 100644 --- a/api/projects/templates.go +++ b/api/projects/templates.go @@ -131,6 +131,10 @@ func UpdateTemplate(w http.ResponseWriter, r *http.Request) { return } + if *template.Arguments == "" { + template.Arguments = nil + } + if _, err := db.Mysql.Exec("update project__template set ssh_key_id=?, inventory_id=?, repository_id=?, environment_id=?, alias=?, playbook=?, arguments=?, override_args=? where id=?", template.SshKeyID, template.InventoryID, template.RepositoryID, template.EnvironmentID, template.Alias, template.Playbook, template.Arguments, template.OverrideArguments, oldTemplate.ID); err != nil { panic(err) } diff --git a/api/tasks/alert.go b/api/tasks/alert.go new file mode 100644 index 00000000..85d6773d --- /dev/null +++ b/api/tasks/alert.go @@ -0,0 +1,106 @@ +package tasks + +import ( + "bytes" + "html/template" + "net/http" + "strconv" + + "github.com/ansible-semaphore/semaphore/models" + "github.com/ansible-semaphore/semaphore/util" +) + +const emailTemplate = `Subject: Task '{{ .Alias }}' failed + +Task {{ .TaskId }} with template '{{ .Alias }}' has failed! +Task log: {{ .TaskUrl }}` + +const telegramTemplate = `{"chat_id": "{{ .ChatId }}","text":"Task {{ .TaskId }} with template '{{ .Alias }}' has failed!\nTask log: {{ .TaskUrl }}","parse_mode":"HTML"}` + +type Alert struct { + TaskId string + Alias string + TaskUrl string + ChatId string +} + +func (t *task) sendMailAlert() { + + if util.Config.EmailAlert != true { + return + } + + if t.alert != true { + return + } + + mailHost := util.Config.EmailHost + ":" + util.Config.EmailPort + + var mailBuffer bytes.Buffer + alert := Alert{TaskId: strconv.Itoa(t.task.ID), Alias: t.template.Alias, TaskUrl: util.Config.WebHost + "/project/" + strconv.Itoa(t.template.ProjectID)} + tpl := template.New("mail body template") + tpl, err := tpl.Parse(emailTemplate) + err = tpl.Execute(&mailBuffer, alert) + + if err != nil { + t.log("Can't generate alert template!") + panic(err) + } + + for _, user := range t.users { + + userObj, err := models.FetchUser(user) + + if userObj.Alert != true { + return + } + + if err != nil { + t.log("Can't find user Email!") + panic(err) + } + + t.log("Sending email to " + userObj.Email + " from " + util.Config.EmailSender) + err = util.SendMail(mailHost, util.Config.EmailSender, userObj.Email, mailBuffer) + if err != nil { + t.log("Can't send email!") + t.log("Error: " + err.Error()) + panic(err) + } + + } +} + +func (t *task) sendTelegramAlert() { + + if util.Config.TelegramAlert != true { + return + } + + if t.alert != true { + return + } + + var telegramBuffer bytes.Buffer + alert := Alert{TaskId: strconv.Itoa(t.task.ID), Alias: t.template.Alias, TaskUrl: util.Config.WebHost + "/project/" + strconv.Itoa(t.template.ProjectID), ChatId: util.Config.TelegramChat} + tpl := template.New("telegram body template") + tpl, err := tpl.Parse(telegramTemplate) + err = tpl.Execute(&telegramBuffer, alert) + + if err != nil { + t.log("Can't generate alert template!") + panic(err) + } + + resp, err := http.Post("https://api.telegram.org/bot"+util.Config.TelegramToken+"/sendMessage", "application/json", &telegramBuffer) + + if err != nil { + t.log("Can't send telegram alert!") + panic(err) + } + + if resp.StatusCode != 200 { + t.log("Can't send telegram alert! Response code not 200!") + } + +} diff --git a/api/tasks/runner.go b/api/tasks/runner.go index b858eb9d..582bb3f6 100644 --- a/api/tasks/runner.go +++ b/api/tasks/runner.go @@ -25,11 +25,14 @@ type task struct { environment db.Environment users []int projectID int + alert bool } func (t *task) fail() { t.task.Status = "error" t.updateStatus() + t.sendMailAlert() + t.sendTelegramAlert() } func (t *task) run() { @@ -44,7 +47,7 @@ func (t *task) run() { t.updateStatus() objType := "task" - desc := "Task ID " + strconv.Itoa(t.task.ID) + " finished" + desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Alias + ")" + " finished - " + strings.ToUpper(t.task.Status) if err := (db.Event{ ProjectID: &t.projectID, ObjectType: &objType, @@ -72,7 +75,7 @@ func (t *task) run() { } objType := "task" - desc := "Task ID " + strconv.Itoa(t.task.ID) + " is running" + desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Alias + ")" + " is running" if err := (db.Event{ ProjectID: &t.projectID, ObjectType: &objType, @@ -143,6 +146,11 @@ func (t *task) populateDetails() error { return err } + //get project alert setting + if err := t.fetch("Alert setting not found!", &t.alert, "select alert from project where id=?", t.template.ProjectID); err != nil { + return err + } + // get project users var users []struct { ID int `db:"id"` @@ -302,6 +310,13 @@ func (t *task) runPlaybook() error { } if len(t.environment.JSON) > 0 { + var js map[string]interface{} + err := json.Unmarshal([]byte(*t.template.JSON), &js) + if err != nil { + t.log("JSON is not valid") + return err + } + args = append(args, "--extra-vars", t.environment.JSON) } diff --git a/api/users.go b/api/users.go index 04bc392f..c10e4086 100644 --- a/api/users.go +++ b/api/users.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + log "github.com/Sirupsen/logrus" "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" "github.com/castawaylabs/mulekick" @@ -58,12 +59,19 @@ func getUserMiddleware(w http.ResponseWriter, r *http.Request) { func updateUser(w http.ResponseWriter, r *http.Request) { oldUser := context.Get(r, "_user").(db.User) - var user db.User + var user models.User + if err := c.Bind(&user); err != nil { + return + } + + if oldUser.External == true && oldUser.Username != user.Username { + log.Warn("Username is not editable for external LDAP users") + c.AbortWithStatus(400) if err := mulekick.Bind(w, r, &user); err != nil { return } - if _, err := db.Mysql.Exec("update user set name=?, username=?, email=? where id=?", user.Name, user.Username, user.Email, oldUser.ID); err != nil { + if _, err := db.Mysql.Exec("update user set name=?, username=?, email=?, alert=? where id=?", user.Name, user.Username, user.Email, user.Alert, oldUser.ID); err != nil { panic(err) } @@ -76,6 +84,12 @@ func updateUserPassword(w http.ResponseWriter, r *http.Request) { Pwd string `json:"password"` } + if user.External == true { + log.Warn("Password is not editable for external LDAP users") + c.AbortWithStatus(400) + return + } + if err := mulekick.Bind(w, r, &pwd); err != nil { return } diff --git a/db/Project.go b/db/Project.go index 429d2e1d..c749268e 100644 --- a/db/Project.go +++ b/db/Project.go @@ -8,6 +8,7 @@ type Project struct { ID int `db:"id" json:"id"` Name string `db:"name" json:"name" binding:"required"` Created time.Time `db:"created" json:"created"` + Alert bool `db:"alert" json:"alert"` } func (project *Project) CreateProject() error { diff --git a/db/User.go b/db/User.go index 47760b65..4e9bf033 100644 --- a/db/User.go +++ b/db/User.go @@ -11,6 +11,8 @@ type User struct { Name string `db:"name" json:"name" binding:"required"` Email string `db:"email" json:"email" binding:"required"` Password string `db:"password" json:"-"` + External bool `db:"external" json:"external"` + Alert bool `db:"alert" json:"alert"` } func FetchUser(userID int) (*User, error) { diff --git a/db/migrations/v2.3.0.sql b/db/migrations/v2.3.0.sql new file mode 100644 index 00000000..38db4d3e --- /dev/null +++ b/db/migrations/v2.3.0.sql @@ -0,0 +1,4 @@ +ALTER TABLE user ADD alert BOOLEAN NOT NULL AFTER password; +ALTER TABLE project ADD alert BOOLEAN NOT NULL AFTER name; + +ALTER TABLE user ADD external BOOLEAN NOT NULL AFTER password; \ No newline at end of file diff --git a/db/versionHistory.go b/db/versionHistory.go index 4ccb708f..a91bd383 100644 --- a/db/versionHistory.go +++ b/db/versionHistory.go @@ -67,5 +67,6 @@ func init() { {Major: 1, Minor: 8}, {Major: 1, Minor: 9}, {Major: 2, Minor: 2, Patch: 1}, + {Major: 2, Minor: 3}, } } diff --git a/make.sh b/make.sh index b7f08528..2671d353 100755 --- a/make.sh +++ b/make.sh @@ -22,7 +22,8 @@ if [ "$1" == "ci_test" ]; then "name": "circle_test" }, "session_db": "127.0.0.1:6379", - "port": ":8010" + "port": ":8010", + "email_alert": false } EOF diff --git a/public/html/projects/edit.pug b/public/html/projects/edit.pug index 0341781e..15525b51 100644 --- a/public/html/projects/edit.pug +++ b/public/html/projects/edit.pug @@ -6,9 +6,13 @@ form.form-horizontal .col-sm-6 input.form-control(type="text" ng-model="projectName" placeholder="Project Name") + .form-group + label.control-label.col-sm-4 Allow alerts for this project + .col-sm-8: input.checkbox-inline(type="checkbox" title="Send email alerts about failed tasks" ng-model="alert") + .form-group .col-sm-6.col-sm-offset-4 - button.btn.btn-success(ng-click="save(projectName)") Save + button.btn.btn-success(ng-click="save(projectName, alert)") Save hr diff --git a/public/html/users/user.pug b/public/html/users/user.pug index 9cce7603..ce29d8b7 100644 --- a/public/html/users/user.pug +++ b/public/html/users/user.pug @@ -8,13 +8,18 @@ .col-sm-8: input.form-control(type="text" placeholder="Your name" ng-model="user.name") .form-group label.control-label.col-sm-4 Username - .col-sm-8: input.form-control(type="text" placeholder="Username" ng-model="user.username") + .col-sm-8: input.form-control(type="text" placeholder="Username" ng-model="user.username" ng-if="user.external==false") + .col-sm-8: input.form-control(type="text" placeholder="Username" ng-model="user.username" readonly="readonly" ng-if="user.external==true") .form-group label.control-label.col-sm-4 Email .col-sm-8: input.form-control(type="email" placeholder="Email address" ng-model="user.email") .form-group label.control-label.col-sm-4 Password - .col-sm-8: input.form-control(type="password" placeholder="Enter new password" ng-model="user.password") + .col-sm-8: input.form-control(type="password" placeholder="Not editable for LDAP user" readonly="readonly" ng-model="user.password" ng-if="user.external==true") + .col-sm-8: input.form-control(type="password" placeholder="Enter new password" ng-model="user.password" ng-if="user.external==false") + .form-group + label.control-label.col-sm-4 Send alerts + .col-sm-8: input.checkbox-inline(type="checkbox" title="Send email alerts about failed tasks" ng-model="user.alert") .form-group: .col-sm-8.col-sm-offset-4 button.btn.btn-success(ng-click="updateUser()") Update Profile button.btn.btn-default(ng-if="$state.includes('users.user')" ui-sref="users.list") back diff --git a/public/js/controllers/projects/edit.js b/public/js/controllers/projects/edit.js index 3c944d4b..84267072 100644 --- a/public/js/controllers/projects/edit.js +++ b/public/js/controllers/projects/edit.js @@ -1,9 +1,13 @@ define(function () { app.registerController('ProjectEditCtrl', ['$scope', '$http', 'Project', '$state', function ($scope, $http, Project, $state) { $scope.projectName = Project.name; + $scope.alert = Project.alert; - $scope.save = function (name) { - $http.put(Project.getURL(), { name: name }).success(function () { + console.log(Project.name); + console.log(Project); + + $scope.save = function (name, alert) { + $http.put(Project.getURL(), { name: name, alert: alert }).success(function () { swal('Saved', 'Project settings saved.', 'success'); }).error(function () { swal('Error', 'Project settings were not saved', 'error'); diff --git a/public/js/factories/project.js b/public/js/factories/project.js index 2c848a65..8b684d82 100644 --- a/public/js/factories/project.js +++ b/public/js/factories/project.js @@ -2,6 +2,7 @@ app.factory('ProjectFactory', ['$http', function ($http) { var Project = function (project) { this.id = project.id; this.name = project.name; + this.alert = project.alert; } Project.prototype.getURL = function () { diff --git a/scripts/docker-startup.sh b/scripts/docker-startup.sh old mode 100644 new mode 100755 index ee7aaa7f..93d8cc02 --- a/scripts/docker-startup.sh +++ b/scripts/docker-startup.sh @@ -2,21 +2,41 @@ echoerr() { printf "%s\n" "$*" >&2; } +SEMAPHORE_PLAYBOOK_PATH="${SEMAPHORE_PLAYBOOK_PATH:-/semaphore}" +# Semaphore database env config +SEMAPHORE_DB_HOST="${SEMAPHORE_DB_HOST:-127.0.0.1}" +SEMAPHORE_DB_PORT="${SEMAPHORE_DB_PORT:-3306}" +SEMAPHORE_DB="${SEMAPHORE_DB:-semaphore}" +SEMAPHORE_DB_USER="${SEMAPHORE_DB_USER:-semaphore}" +SEMAPHORE_DB_PASS="${SEMAPHORE_DB_PASS:-semaphore}" +# Semaphore Admin env config +SEMAPHORE_ADMIN="${SEMAPHORE_ADMIN:-admin}" +SEMAPHORE_ADMIN_EMAIL="${SEMAPHORE_ADMIN_EMAIL:-admin@localhost}" +SEMAPHORE_ADMIN_NAME="${SEMAPHORE_ADMIN_NAME:-Semaphore Admin}" +SEMAPHORE_ADMIN_PASSWORD="${SEMAPHORE_ADMIN_PASSWORD:-semaphorepassword}" + +# create semaphore playbook directory +mkdir -p "${SEMAPHORE_PLAYBOOK_PATH}" || { + echo "Can't create Semaphore playbook path '$SEMAPHORE_PLAYBOOK_PATH'." + exit 1 +} + # wait on db to be up -echoerr "Attempting to connect to database ${SEMAPHORE_DB} on ${SEMAPHORE_DB_HOST} with user:pass ${SEMAPHORE_DB_USER}:${SEMAPHORE_DB_PASS}" -until mysql -h ${SEMAPHORE_DB_HOST} -u ${SEMAPHORE_DB_USER} --password=${SEMAPHORE_DB_PASS} ${SEMAPHORE_DB} -e "select version();" &>/dev/null; -do - echoerr "waiting"; - sleep 3; +echoerr "Attempting to connect to database ${SEMAPHORE_DB} on ${SEMAPHORE_DB_HOST}:${SEMAPHORE_DB_PORT} with user ${SEMAPHORE_DB_USER} ..." +TIMEOUT=30 +while ! mysqladmin ping -h"$SEMAPHORE_DB_HOST" -P "$SEMAPHORE_DB_PORT" -u "$SEMAPHORE_DB_USER" --password="$SEMAPHORE_DB_PASS" --silent >/dev/null 2>&1; do + TIMEOUT=$(expr $TIMEOUT - 1) + if [ $TIMEOUT -eq 0 ]; then + echoerr "Could not connect to database server. Exiting." + exit 1 + fi + echo -n "." + sleep 1 done -# generate stdin -if [ -f ${SEMAPHORE_PLAYBOOK_PATH}/config.stdin ] -then - echoerr "already generated stdin" -else - echoerr "generating ${SEMAPHORE_PLAYBOOK_PATH}/config.stdin" - cat << EOF > ${SEMAPHORE_PLAYBOOK_PATH}/config.stdin +if [ ! -f "${SEMAPHORE_PLAYBOOK_PATH}/semaphore_config.json" ]; then + echoerr "Generating ${SEMAPHORE_PLAYBOOK_PATH}/config.stdin ..." + cat << EOF > "${SEMAPHORE_PLAYBOOK_PATH}/config.stdin" ${SEMAPHORE_DB_HOST}:${SEMAPHORE_DB_PORT} ${SEMAPHORE_DB_USER} ${SEMAPHORE_DB_PASS} @@ -28,15 +48,9 @@ ${SEMAPHORE_ADMIN_EMAIL} ${SEMAPHORE_ADMIN_NAME} ${SEMAPHORE_ADMIN_PASSWORD} EOF -fi + /usr/bin/semaphore -setup < "${SEMAPHORE_PLAYBOOK_PATH}/config.stdin" -# test to see if initialzation is needed -if [ -f ${SEMAPHORE_PLAYBOOK_PATH}/semaphore_config.json ] -then - echoerr "already initialized" -else - echoerr "Initializing semaphore" - /usr/bin/semaphore -setup < ${SEMAPHORE_PLAYBOOK_PATH}/config.stdin + ln -s "${SEMAPHORE_PLAYBOOK_PATH}/semaphore_config.json" /etc/semaphore/semaphore_config.json fi # run our command diff --git a/util/config.go b/util/config.go index a4da5e96..889ba1c7 100644 --- a/util/config.go +++ b/util/config.go @@ -26,6 +26,13 @@ type mySQLConfig struct { DbName string `json:"name"` } +type ldapMappings struct { + DN string `json:"dn"` + Mail string `json:"mail"` + Uid string `json:"uid"` + CN string `json:"cn"` +} + type configType struct { MySQL mySQLConfig `json:"mysql"` // Format `:port_num` eg, :3000 @@ -38,6 +45,30 @@ type configType struct { // cookie hashing & encryption CookieHash string `json:"cookie_hash"` CookieEncryption string `json:"cookie_encryption"` + + //email alerting + EmailAlert bool `json:"email_alert"` + EmailSender string `json:"email_sender"` + EmailHost string `json:"email_host"` + EmailPort string `json:"email_port"` + + //web host + WebHost string `json:"web_host"` + + //ldap settings + LdapEnable bool `json:"ldap_enable"` + LdapBindDN string `json:"ldap_binddn"` + LdapBindPassword string `json:"ldap_bindpassword"` + LdapServer string `json:"ldap_server"` + LdapNeedTLS bool `json:"ldap_needtls"` + LdapSearchDN string `json:"ldap_searchdn"` + LdapSearchFilter string `json:"ldap_searchfilter"` + LdapMappings ldapMappings `json:"ldap_mappings"` + + //telegram alerting + TelegramAlert bool `json:"telegram_alert"` + TelegramChat string `json:"telegram_chat"` + TelegramToken string `json:"telegram_token"` } var Config *configType @@ -182,4 +213,152 @@ func (conf *configType) Scan() { conf.TmpPath = "/tmp/semaphore" } conf.TmpPath = path.Clean(conf.TmpPath) + + fmt.Print(" > Web root URL (default http://localhost:8010/): ") + fmt.Scanln(&conf.WebHost) + + if len(conf.WebHost) == 0 { + conf.WebHost = "http://localhost:8010/" + } + + var EmailAlertAnswer string + fmt.Print(" > Enable email alerts (y/n, default n): ") + fmt.Scanln(&EmailAlertAnswer) + if EmailAlertAnswer == "yes" || EmailAlertAnswer == "y" { + + conf.EmailAlert = true + + fmt.Print(" > Mail server host (default localhost): ") + fmt.Scanln(&conf.EmailHost) + + if len(conf.EmailHost) == 0 { + conf.EmailHost = "localhost" + } + + fmt.Print(" > Mail server port (default 25): ") + fmt.Scanln(&conf.EmailPort) + + if len(conf.EmailPort) == 0 { + conf.EmailPort = "25" + } + + fmt.Print(" > Mail sender address (default semaphore@localhost): ") + 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): ") + fmt.Scanln(&TelegramAlertAnswer) + if TelegramAlertAnswer == "yes" || TelegramAlertAnswer == "y" { + + conf.TelegramAlert = true + + fmt.Print(" > Telegram bot token (you can get it from @BotFather) (default ''): ") + fmt.Scanln(&conf.TelegramToken) + + if len(conf.TelegramToken) == 0 { + conf.TelegramToken = "" + } + + fmt.Print(" > Telegram chat ID (default ''): ") + 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): ") + fmt.Scanln(&LdapAnswer) + if LdapAnswer == "yes" || LdapAnswer == "y" { + + conf.LdapEnable = true + + fmt.Print(" > LDAP server host (default localhost:389): ") + 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): ") + fmt.Scanln(&LdapTLSAnswer) + if LdapTLSAnswer == "yes" || LdapTLSAnswer == "y" { + conf.LdapNeedTLS = true + } else { + conf.LdapNeedTLS = false + } + + fmt.Print(" > LDAP DN for bind (default cn=user,ou=users,dc=example): ") + 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): ") + fmt.Scanln(&conf.LdapBindPassword) + + if len(conf.LdapBindPassword) == 0 { + conf.LdapBindPassword = "pa55w0rd" + } + + fmt.Print(" > LDAP DN for user search (default ou=users,dc=example): ") + fmt.Scanln(&conf.LdapSearchDN) + + if len(conf.LdapSearchDN) == 0 { + conf.LdapSearchDN = "ou=users,dc=example" + } + + fmt.Print(" > LDAP search filter (default (uid=" + "%" + "s)): ") + fmt.Scanln(&conf.LdapSearchFilter) + + if len(conf.LdapSearchFilter) == 0 { + conf.LdapSearchFilter = "(uid=%s)" + } + + fmt.Print(" > LDAP mapping for DN field (default dn): ") + fmt.Scanln(&conf.LdapMappings.DN) + + if len(conf.LdapMappings.DN) == 0 { + conf.LdapMappings.DN = "dn" + } + + fmt.Print(" > LDAP mapping for username field (default uid): ") + 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): ") + fmt.Scanln(&conf.LdapMappings.CN) + + if len(conf.LdapMappings.CN) == 0 { + conf.LdapMappings.CN = "cn" + } + + fmt.Print(" > LDAP mapping for email field (default mail): ") + fmt.Scanln(&conf.LdapMappings.Mail) + + if len(conf.LdapMappings.Mail) == 0 { + conf.LdapMappings.Mail = "mail" + } + + } else { + conf.LdapEnable = false + } + } diff --git a/util/mail.go b/util/mail.go new file mode 100644 index 00000000..c1280b12 --- /dev/null +++ b/util/mail.go @@ -0,0 +1,32 @@ +package util + +import ( + "bytes" + "net/smtp" +) + +func SendMail(emailHost, mailSender, mailRecipient string, mail bytes.Buffer) error { + + c, err := smtp.Dial(emailHost) + if err != nil { + return err + } + + defer c.Close() + // Set the sender and recipient. + c.Mail(mailSender) + c.Rcpt(mailRecipient) + + // Send the email body. + wc, err := c.Data() + if err != nil { + return err + } + + defer wc.Close() + if _, err = mail.WriteTo(wc); err != nil { + return err + } + return nil + +}