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
+
+}