Merge branch 'master' into develop

# Conflicts:
#	Dockerfile
#	api/login.go
#	api/projects/environment.go
#	api/projects/project.go
#	api/projects/templates.go
#	api/tasks/runner.go
#	api/users.go
#	db/versionHistory.go
#	util/bindata.go
This commit is contained in:
Matej Kramny 2017-04-18 14:48:31 +01:00
commit db4948cb89
21 changed files with 581 additions and 54 deletions

View File

@ -1,12 +1,15 @@
FROM alpine FROM alpine:3.5
RUN apk add --no-cache git ansible mysql-client curl openssh-client ENV SEMAPHORE_VERSION="2.2.0" SEMAPHORE_ARCH="linux_amd64"
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
ADD ./scripts/docker-startup.sh /usr/bin/semaphore-startup.sh RUN apk add --no-cache git ansible mysql-client curl openssh-client && \
RUN chmod +x /usr/bin/semaphore-startup.sh 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 EXPOSE 3000
ADD semaphore-startup.sh /usr/bin/semaphore-startup.sh
ENTRYPOINT ["/usr/bin/semaphore-startup.sh"] ENTRYPOINT ["/usr/bin/semaphore-startup.sh"]
CMD ["/usr/bin/semaphore", "-config", "/etc/semaphore/semaphore_config.json"] CMD ["/usr/bin/semaphore", "-config", "/etc/semaphore/semaphore_config.json"]

View File

@ -53,6 +53,8 @@ definitions:
created: created:
type: string type: string
format: date-time format: date-time
alert:
type: boolean
APIToken: APIToken:
type: object type: object
properties: properties:
@ -75,6 +77,8 @@ definitions:
created: created:
type: string type: string
format: date-time format: date-time
alert:
type: boolean
AccessKey: AccessKey:
type: object type: object
properties: properties:

View File

@ -1,19 +1,105 @@
package api package api
import ( import (
"crypto/tls"
"database/sql" "database/sql"
"fmt"
"net/http" "net/http"
"net/mail" "net/mail"
"strings" "strings"
"time" "time"
log "github.com/Sirupsen/logrus"
"github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/db"
"github.com/ansible-semaphore/semaphore/util" "github.com/ansible-semaphore/semaphore/util"
"github.com/castawaylabs/mulekick" "github.com/castawaylabs/mulekick"
sq "github.com/masterminds/squirrel" sq "github.com/masterminds/squirrel"
"golang.org/x/crypto/bcrypt" "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) { func login(w http.ResponseWriter, r *http.Request) {
var login struct { var login struct {
Auth string `json:"auth" binding:"required"` Auth string `json:"auth" binding:"required"`
@ -26,30 +112,59 @@ func login(w http.ResponseWriter, r *http.Request) {
login.Auth = strings.ToLower(login.Auth) login.Auth = strings.ToLower(login.Auth)
q := sq.Select("*").From("user") ldapErr, ldapUser := ldapAuthentication(login.Auth, login.Password)
_, err := mail.ParseAddress(login.Auth) if util.Config.LdapEnable == true && ldapErr != nil {
if err == nil { log.Info(ldapErr.Error())
q = q.Where("email=?", login.Auth)
} else {
q = q.Where("username=?", login.Auth)
} }
query, args, _ := q.ToSql() q := sq.Select("*").
From("user")
var user db.User var user models.User
if err := db.Mysql.SelectOne(&user, query, args...); err != nil { if ldapErr != nil {
if err == sql.ErrNoRows { // 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) w.WriteHeader(http.StatusBadRequest)
return 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 { if err := database.Mysql.SelectOne(&user, query, args...); err != nil {
w.WriteHeader(http.StatusBadRequest) if err == sql.ErrNoRows {
return //Create new user
user = ldapUser
if err := database.Mysql.Insert(&user); err != nil {
panic(err)
}
} else if err != nil {
panic(err)
}
}
} }
session := db.Session{ session := db.Session{

View File

@ -49,7 +49,8 @@ func GetEnvironment(w http.ResponseWriter, r *http.Request) {
} }
q := squirrel.Select("*"). q := squirrel.Select("*").
From("project__environment pe") From("project__environment pe").
Where("project_id=?", project.ID)
switch sort { switch sort {
case "name": case "name":
@ -76,6 +77,14 @@ func UpdateEnvironment(w http.ResponseWriter, r *http.Request) {
return 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 { if _, err := db.Mysql.Exec("update project__environment set name=?, json=? where id=?", env.Name, env.JSON, oldEnv.ID); err != nil {
panic(err) panic(err)
} }
@ -87,7 +96,15 @@ func AddEnvironment(w http.ResponseWriter, r *http.Request) {
project := context.Get(r, "project").(db.Project) project := context.Get(r, "project").(db.Project)
var env db.Environment 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 return
} }

View File

@ -61,14 +61,15 @@ func MustBeAdmin(w http.ResponseWriter, r *http.Request) {
func UpdateProject(w http.ResponseWriter, r *http.Request) { func UpdateProject(w http.ResponseWriter, r *http.Request) {
project := context.Get(r, "project").(db.Project) project := context.Get(r, "project").(db.Project)
var body struct { var body struct {
Name string `json:"name"` Name string `json:"name"`
Alert bool `json:"alert"`
} }
if err := mulekick.Bind(w, r, &body); err != nil { if err := mulekick.Bind(w, r, &body); err != nil {
return 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) panic(err)
} }

View File

@ -131,6 +131,10 @@ func UpdateTemplate(w http.ResponseWriter, r *http.Request) {
return 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 { 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) panic(err)
} }

106
api/tasks/alert.go Normal file
View File

@ -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: <a href='{{ .TaskUrl }}'>{{ .TaskUrl }}</a>`
const telegramTemplate = `{"chat_id": "{{ .ChatId }}","text":"<b>Task {{ .TaskId }} with template '{{ .Alias }}' has failed!</b>\nTask log: <a href='{{ .TaskUrl }}'>{{ .TaskUrl }}</a>","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!")
}
}

View File

@ -25,11 +25,14 @@ type task struct {
environment db.Environment environment db.Environment
users []int users []int
projectID int projectID int
alert bool
} }
func (t *task) fail() { func (t *task) fail() {
t.task.Status = "error" t.task.Status = "error"
t.updateStatus() t.updateStatus()
t.sendMailAlert()
t.sendTelegramAlert()
} }
func (t *task) run() { func (t *task) run() {
@ -44,7 +47,7 @@ func (t *task) run() {
t.updateStatus() t.updateStatus()
objType := "task" 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{ if err := (db.Event{
ProjectID: &t.projectID, ProjectID: &t.projectID,
ObjectType: &objType, ObjectType: &objType,
@ -72,7 +75,7 @@ func (t *task) run() {
} }
objType := "task" 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{ if err := (db.Event{
ProjectID: &t.projectID, ProjectID: &t.projectID,
ObjectType: &objType, ObjectType: &objType,
@ -143,6 +146,11 @@ func (t *task) populateDetails() error {
return err 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 // get project users
var users []struct { var users []struct {
ID int `db:"id"` ID int `db:"id"`
@ -302,6 +310,13 @@ func (t *task) runPlaybook() error {
} }
if len(t.environment.JSON) > 0 { 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) args = append(args, "--extra-vars", t.environment.JSON)
} }

View File

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"time" "time"
log "github.com/Sirupsen/logrus"
"github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/db"
"github.com/ansible-semaphore/semaphore/util" "github.com/ansible-semaphore/semaphore/util"
"github.com/castawaylabs/mulekick" "github.com/castawaylabs/mulekick"
@ -58,12 +59,19 @@ func getUserMiddleware(w http.ResponseWriter, r *http.Request) {
func updateUser(w http.ResponseWriter, r *http.Request) { func updateUser(w http.ResponseWriter, r *http.Request) {
oldUser := context.Get(r, "_user").(db.User) 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 { if err := mulekick.Bind(w, r, &user); err != nil {
return 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) panic(err)
} }
@ -76,6 +84,12 @@ func updateUserPassword(w http.ResponseWriter, r *http.Request) {
Pwd string `json:"password"` 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 { if err := mulekick.Bind(w, r, &pwd); err != nil {
return return
} }

View File

@ -8,6 +8,7 @@ type Project struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Name string `db:"name" json:"name" binding:"required"` Name string `db:"name" json:"name" binding:"required"`
Created time.Time `db:"created" json:"created"` Created time.Time `db:"created" json:"created"`
Alert bool `db:"alert" json:"alert"`
} }
func (project *Project) CreateProject() error { func (project *Project) CreateProject() error {

View File

@ -11,6 +11,8 @@ type User struct {
Name string `db:"name" json:"name" binding:"required"` Name string `db:"name" json:"name" binding:"required"`
Email string `db:"email" json:"email" binding:"required"` Email string `db:"email" json:"email" binding:"required"`
Password string `db:"password" json:"-"` Password string `db:"password" json:"-"`
External bool `db:"external" json:"external"`
Alert bool `db:"alert" json:"alert"`
} }
func FetchUser(userID int) (*User, error) { func FetchUser(userID int) (*User, error) {

4
db/migrations/v2.3.0.sql Normal file
View File

@ -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;

View File

@ -67,5 +67,6 @@ func init() {
{Major: 1, Minor: 8}, {Major: 1, Minor: 8},
{Major: 1, Minor: 9}, {Major: 1, Minor: 9},
{Major: 2, Minor: 2, Patch: 1}, {Major: 2, Minor: 2, Patch: 1},
{Major: 2, Minor: 3},
} }
} }

View File

@ -22,7 +22,8 @@ if [ "$1" == "ci_test" ]; then
"name": "circle_test" "name": "circle_test"
}, },
"session_db": "127.0.0.1:6379", "session_db": "127.0.0.1:6379",
"port": ":8010" "port": ":8010",
"email_alert": false
} }
EOF EOF

View File

@ -6,9 +6,13 @@ form.form-horizontal
.col-sm-6 .col-sm-6
input.form-control(type="text" ng-model="projectName" placeholder="Project Name") 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 .form-group
.col-sm-6.col-sm-offset-4 .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 hr

View File

@ -8,13 +8,18 @@
.col-sm-8: input.form-control(type="text" placeholder="Your name" ng-model="user.name") .col-sm-8: input.form-control(type="text" placeholder="Your name" ng-model="user.name")
.form-group .form-group
label.control-label.col-sm-4 Username 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 .form-group
label.control-label.col-sm-4 Email label.control-label.col-sm-4 Email
.col-sm-8: input.form-control(type="email" placeholder="Email address" ng-model="user.email") .col-sm-8: input.form-control(type="email" placeholder="Email address" ng-model="user.email")
.form-group .form-group
label.control-label.col-sm-4 Password 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 .form-group: .col-sm-8.col-sm-offset-4
button.btn.btn-success(ng-click="updateUser()") Update Profile button.btn.btn-success(ng-click="updateUser()") Update Profile
button.btn.btn-default(ng-if="$state.includes('users.user')" ui-sref="users.list") back button.btn.btn-default(ng-if="$state.includes('users.user')" ui-sref="users.list") back

View File

@ -1,9 +1,13 @@
define(function () { define(function () {
app.registerController('ProjectEditCtrl', ['$scope', '$http', 'Project', '$state', function ($scope, $http, Project, $state) { app.registerController('ProjectEditCtrl', ['$scope', '$http', 'Project', '$state', function ($scope, $http, Project, $state) {
$scope.projectName = Project.name; $scope.projectName = Project.name;
$scope.alert = Project.alert;
$scope.save = function (name) { console.log(Project.name);
$http.put(Project.getURL(), { name: name }).success(function () { console.log(Project);
$scope.save = function (name, alert) {
$http.put(Project.getURL(), { name: name, alert: alert }).success(function () {
swal('Saved', 'Project settings saved.', 'success'); swal('Saved', 'Project settings saved.', 'success');
}).error(function () { }).error(function () {
swal('Error', 'Project settings were not saved', 'error'); swal('Error', 'Project settings were not saved', 'error');

View File

@ -2,6 +2,7 @@ app.factory('ProjectFactory', ['$http', function ($http) {
var Project = function (project) { var Project = function (project) {
this.id = project.id; this.id = project.id;
this.name = project.name; this.name = project.name;
this.alert = project.alert;
} }
Project.prototype.getURL = function () { Project.prototype.getURL = function () {

54
scripts/docker-startup.sh Normal file → Executable file
View File

@ -2,21 +2,41 @@
echoerr() { printf "%s\n" "$*" >&2; } 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 # 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}" echoerr "Attempting to connect to database ${SEMAPHORE_DB} on ${SEMAPHORE_DB_HOST}:${SEMAPHORE_DB_PORT} with user ${SEMAPHORE_DB_USER} ..."
until mysql -h ${SEMAPHORE_DB_HOST} -u ${SEMAPHORE_DB_USER} --password=${SEMAPHORE_DB_PASS} ${SEMAPHORE_DB} -e "select version();" &>/dev/null; TIMEOUT=30
do 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
echoerr "waiting"; TIMEOUT=$(expr $TIMEOUT - 1)
sleep 3; if [ $TIMEOUT -eq 0 ]; then
echoerr "Could not connect to database server. Exiting."
exit 1
fi
echo -n "."
sleep 1
done done
# generate stdin if [ ! -f "${SEMAPHORE_PLAYBOOK_PATH}/semaphore_config.json" ]; then
if [ -f ${SEMAPHORE_PLAYBOOK_PATH}/config.stdin ] echoerr "Generating ${SEMAPHORE_PLAYBOOK_PATH}/config.stdin ..."
then cat << EOF > "${SEMAPHORE_PLAYBOOK_PATH}/config.stdin"
echoerr "already generated stdin"
else
echoerr "generating ${SEMAPHORE_PLAYBOOK_PATH}/config.stdin"
cat << EOF > ${SEMAPHORE_PLAYBOOK_PATH}/config.stdin
${SEMAPHORE_DB_HOST}:${SEMAPHORE_DB_PORT} ${SEMAPHORE_DB_HOST}:${SEMAPHORE_DB_PORT}
${SEMAPHORE_DB_USER} ${SEMAPHORE_DB_USER}
${SEMAPHORE_DB_PASS} ${SEMAPHORE_DB_PASS}
@ -28,15 +48,9 @@ ${SEMAPHORE_ADMIN_EMAIL}
${SEMAPHORE_ADMIN_NAME} ${SEMAPHORE_ADMIN_NAME}
${SEMAPHORE_ADMIN_PASSWORD} ${SEMAPHORE_ADMIN_PASSWORD}
EOF EOF
fi /usr/bin/semaphore -setup < "${SEMAPHORE_PLAYBOOK_PATH}/config.stdin"
# test to see if initialzation is needed ln -s "${SEMAPHORE_PLAYBOOK_PATH}/semaphore_config.json" /etc/semaphore/semaphore_config.json
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
fi fi
# run our command # run our command

View File

@ -26,6 +26,13 @@ type mySQLConfig struct {
DbName string `json:"name"` 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 { type configType struct {
MySQL mySQLConfig `json:"mysql"` MySQL mySQLConfig `json:"mysql"`
// Format `:port_num` eg, :3000 // Format `:port_num` eg, :3000
@ -38,6 +45,30 @@ type configType struct {
// cookie hashing & encryption // cookie hashing & encryption
CookieHash string `json:"cookie_hash"` CookieHash string `json:"cookie_hash"`
CookieEncryption string `json:"cookie_encryption"` 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 var Config *configType
@ -182,4 +213,152 @@ func (conf *configType) Scan() {
conf.TmpPath = "/tmp/semaphore" conf.TmpPath = "/tmp/semaphore"
} }
conf.TmpPath = path.Clean(conf.TmpPath) 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
}
} }

32
util/mail.go Normal file
View File

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