Skip to content

Commit

Permalink
CB-17175 Create change password endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Bajzathd authored and keyki committed May 19, 2022
1 parent 0ba8985 commit b8613be
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 41 deletions.
54 changes: 43 additions & 11 deletions saltboot/salt.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,21 +297,12 @@ func SaltServerRunRequestHandler(w http.ResponseWriter, req *http.Request) {
return
}

index, err := strconv.Atoi(req.URL.Query().Get("index"))
saltMaster, err := getSaltMaster(saltActionRequest, req)
if err != nil {
log.Printf("[SaltMinionRunRequestHandler] [ERROR] missing index: %s", err.Error())
model.Response{Status: err.Error()}.WriteBadRequestHttp(w)
return
}

var saltMaster SaltMaster
masters := saltActionRequest.Masters
if masters != nil && len(masters) > 0 {
saltMaster = masters[index]
} else {
saltMaster = saltActionRequest.Master
}

var resp model.Response

if err := ensureHostIsResolvable(saltMaster.Hostname, saltMaster.Domain, saltMaster.Address, saltActionRequest.OS, saltActionRequest.Cloud); err != nil {
Expand All @@ -323,7 +314,7 @@ func SaltServerRunRequestHandler(w http.ResponseWriter, req *http.Request) {

var responses []model.Response

resp, err = CreateUser(saltMaster, saltActionRequest.OS)
resp, err = CreateUser(*saltMaster, saltActionRequest.OS)
if err != nil {
resp.WriteHttp(w)
return
Expand Down Expand Up @@ -363,6 +354,47 @@ func SaltServerStopRequestHandler(w http.ResponseWriter, req *http.Request) {
resp.WriteHttp(w)
}

func SaltServerChangePasswordHandler(w http.ResponseWriter, req *http.Request) {
log.Println("[SaltServerChangePasswordHandler] execute salt master change password request")

decoder := json.NewDecoder(req.Body)
var saltActionRequest SaltActionRequest
if err := decoder.Decode(&saltActionRequest); err != nil {
log.Printf("[SaltServerChangePasswordHandler] [ERROR] couldn't decode json: %s", err.Error())
model.Response{Status: err.Error()}.WriteBadRequestHttp(w)
return
}

saltMaster, err := getSaltMaster(saltActionRequest, req)
if err != nil {
model.Response{Status: err.Error()}.WriteBadRequestHttp(w)
return
}

resp, err := ChangeUserPassword(*saltMaster)
if err != nil {
log.Printf("[SaltServerChangePasswordHandler] [ERROR] Failed to change password: %s", err.Error())
}
resp.WriteHttp(w)
}

func getSaltMaster(saltActionRequest SaltActionRequest, req *http.Request) (*SaltMaster, error) {
index, err := strconv.Atoi(req.URL.Query().Get("index"))
if err != nil {
log.Printf("[getSaltMaster] [ERROR] missing index: %s", err.Error())
return nil, err
}

var saltMaster SaltMaster
masters := saltActionRequest.Masters
if masters != nil && len(masters) > 0 {
saltMaster = masters[index]
} else {
saltMaster = saltActionRequest.Master
}
return &saltMaster, nil
}

func (pillar SaltPillar) WritePillar() (outStr string, err error) {
return writePillarImpl(pillar, "")
}
Expand Down
3 changes: 3 additions & 0 deletions saltboot/testdata/etc/shadow
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
root:!:18267:0:99999:7:::
saltuser:$6$asdfghjklqwertzu$aDauWn56lHV2NozTj4.d9YFfgR4XWMjLvfzpOIQWDFpIvk6ZAbKSqoy7tN8cKHAs3mljtIdStax3Dlg2qRNfw0:19129:1:180:7:30::
bin:*:18113:0:99999:7:::
108 changes: 99 additions & 9 deletions saltboot/user.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
package saltboot

import (
"errors"
"fmt"
"log"
"math/rand"
"net/http"
"time"

"strings"

"github.com/hortonworks/salt-bootstrap/saltboot/model"
"github.com/kless/osutil/user/crypt/sha512_crypt"
"strings"
)

const (
SALT_USER = "saltuser"
SALT_USER = "saltuser"
SHADOW_FILE = "/etc/shadow"
SHADOW_FILE_BACKUP = "/etc/shadow.backup"
SHADOW_FILE_NEW = "/etc/shadow.new"
SHADOW_FILE_PERMISSIONS = 0640
)

func init() {
Expand All @@ -39,12 +46,7 @@ func CreateUser(saltMaster SaltMaster, os *Os) (resp model.Response, err error)
if len(out) == 0 || err != nil {
log.Printf("[CreateUser] user: %s does not exsist and will be created", SALT_USER)

c := sha512_crypt.New()

// Password needs to be "salted" and must start with a magic prefix
salt := "$6$" + randStringRunes(20)

hash, err := c.Generate([]byte(saltMaster.Auth.Password), []byte(salt))
hash, err := generatePasswordHash(saltMaster.Auth.Password)
if err != nil {
return model.Response{ErrorText: err.Error(), StatusCode: http.StatusInternalServerError}, err
}
Expand All @@ -67,13 +69,101 @@ func CreateUser(saltMaster SaltMaster, os *Os) (resp model.Response, err error)
return model.Response{ErrorText: err.Error(), StatusCode: http.StatusInternalServerError}, err
}
} else {
log.Printf("[CreateUser] user: %s exsist", SALT_USER)
log.Printf("[CreateUser] user: %s exists, setting its password", SALT_USER)
_, err = ChangeUserPassword(saltMaster)
if err != nil {
log.Printf("[CreateUser] ChangeUserPassword failed with error: %s", err.Error())
}
}

resp = model.Response{Status: result, StatusCode: http.StatusOK}
return resp, nil
}

func generatePasswordHash(password string) (string, error) {
// Password needs to be "salted" and must start with this prefix for SHA512 encryption
salt := "$6$" + randStringRunes(20)
return generatePasswordHashWithSalt(password, salt)
}

func generatePasswordHashWithSalt(password string, salt string) (string, error) {
c := sha512_crypt.New()
hash, err := c.Generate([]byte(password), []byte(salt))
if err != nil {
return "", err
}
return hash, nil
}

func shouldUseUserAdd(os *Os) bool {
return isOs(os, UBUNTU) || isOs(os, DEBIAN) || isOs(os, SUSE, SLES12)
}

func ChangeUserPassword(saltMaster SaltMaster) (resp model.Response, err error) {
log.Printf("[ChangeUserPassword] execute salt run request")

newPasswordHash, err := generatePasswordHash(saltMaster.Auth.Password)
if err != nil {
return errorResponse("[ChangeUserPassword] Failed to generate password hash", err)
}

shadowFileContents, err := readFile(SHADOW_FILE)
if err != nil {
return errorResponse("[ChangeUserPassword] Failed to read "+SHADOW_FILE, err)
}

shadowFileLines := strings.Split(string(shadowFileContents), "\n")
oldPasswordHash := ""
for i, shadowFileLine := range shadowFileLines {
if strings.HasPrefix(shadowFileLine, SALT_USER+":") {
oldPasswordHash = strings.Split(shadowFileLine, ":")[1]
shadowFileLines[i] = strings.Replace(shadowFileLine, oldPasswordHash, newPasswordHash, 1)
break
}
}

if oldPasswordHash == "" {
return errorResponse("[ChangeUserPassword] Could not find user "+SALT_USER+" in "+SHADOW_FILE, nil)
}
oldPasswordSalt := strings.Join(strings.SplitN(oldPasswordHash, "$", 2), "$")
newPasswordHashWithOldSalt, err := generatePasswordHashWithSalt(saltMaster.Auth.Password, oldPasswordSalt)
if err == nil && oldPasswordHash == newPasswordHashWithOldSalt {
log.Println("[ChangeUserPassword] old and new passwords are the same")
return model.Response{StatusCode: http.StatusOK}, nil
}

_, err = ExecCmd("cp", SHADOW_FILE, SHADOW_FILE_BACKUP)
if err != nil {
return errorResponse("[ChangeUserPassword] Failed to backup "+SHADOW_FILE+" to "+SHADOW_FILE_BACKUP, err)
}

newShadowFileContent := strings.Join(shadowFileLines, "\n")
err = writeFile(SHADOW_FILE_NEW, []byte(newShadowFileContent), SHADOW_FILE_PERMISSIONS)
if err != nil {
return errorResponse("[ChangeUserPassword] Failed to write new shadow file to "+SHADOW_FILE_NEW, err)
}

_, err = ExecCmd("mv", SHADOW_FILE_NEW, SHADOW_FILE)
if err != nil {
return errorResponse("[ChangeUserPassword] Failed to override "+SHADOW_FILE+" with "+SHADOW_FILE_NEW, err)
}

now := time.Now()
today := fmt.Sprintf("%d-%02d-%02d", now.Year(), now.Month(), now.Day())
_, err = ExecCmd("chage", "-d", today, SALT_USER)
if err != nil {
return errorResponse("[ChangeUserPassword] Failed to update last password change date", err)
}

return model.Response{StatusCode: http.StatusOK}, nil
}

func errorResponse(message string, err error) (model.Response, error) {
var detailedErr error
if err != nil {
detailedErr = errors.New(message + ". Error: " + err.Error())
} else {
detailedErr = errors.New(message)
}
return model.Response{ErrorText: message, StatusCode: http.StatusInternalServerError}, detailedErr
}
72 changes: 70 additions & 2 deletions saltboot/user_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package saltboot

import (
"io/ioutil"
"os"
"strings"
"testing"
)

var files map[string]string

func TestCreateUser(t *testing.T) {
watchCommands = true
defer func() { watchCommands = false }()
mockFunctions()
defer endMockFunctions()

master := SaltMaster{
Auth: SaltAuth{Password: "passwd"},
Expand All @@ -23,3 +28,66 @@ func TestCreateUser(t *testing.T) {
"^adduser --no-create-home -G wheel -s /sbin/nologin --password \\$6\\$([a-zA-Z\\$0-9/.]+) saltuser",
}, t)
}

func TestChangePasswordToNewPassword(t *testing.T) {
mockFunctions()
defer endMockFunctions()

master := SaltMaster{
Auth: SaltAuth{Password: "newpassword"},
}

go ChangeUserPassword(master)

checkExecutedCommands([]string{
"cp /etc/shadow /etc/shadow.backup",
"mv /etc/shadow.new /etc/shadow",
"^chage -d (.*) saltuser",
}, t)

shadowFile := files["/etc/shadow.new"]
if !strings.Contains(shadowFile, "root:!:18267:0:99999:7:::") {
t.Error("Shadow file is missing root user")
}
if !strings.Contains(shadowFile, "saltuser") {
t.Error("Shadow file is missing saltuser user")
}
if strings.Contains(shadowFile, "saltuser:$6$asdfghjklqwertzu$aDauWn56lHV2NozTj4.d9YFfgR4XWMjLvfzpOIQWDFpIvk6ZAbKSqoy7tN8cKHAs3mljtIdStax3Dlg2qRNfw0:19129:1:180:7:30::") {
t.Error("Shadow file contains old password for saltuser")
}
if !strings.Contains(shadowFile, "bin:*:18113:0:99999:7:::") {
t.Error("Shadow file is missing bin user")
}
}

func TestChangePasswordToSamePassword(t *testing.T) {
mockFunctions()
defer endMockFunctions()

master := SaltMaster{
Auth: SaltAuth{Password: "oldpassword"},
}

ChangeUserPassword(master)
if _, contains := files["/etc/shadow.new"]; contains {
t.Error("Shadow file was written even though the same password was provided")
}
}

func mockFunctions() {
watchCommands = true
files = map[string]string{}

writeFile = func(filename string, data []byte, perm os.FileMode) error {
files[filename] = string(data)
return nil
}
readFile = func(filename string) ([]byte, error) {
return ioutil.ReadFile("testdata/" + filename)
}
}

func endMockFunctions() {
watchCommands = false
files = map[string]string{}
}
40 changes: 21 additions & 19 deletions saltboot/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,26 @@ import (
)

const (
RootPath = "/saltboot"
HealthEP = RootPath + "/health"
ServerSaveEP = RootPath + "/server/save"
ServerDistributeEP = RootPath + "/server/distribute"
SaltActionDistributeEP = RootPath + "/salt/action/distribute"
SaltMinionEp = RootPath + "/salt/minion"
SaltMinionRunEP = SaltMinionEp + "/run"
SaltMinionStopEP = SaltMinionEp + "/stop"
SaltMinionKeyEP = SaltMinionEp + "/fingerprint"
SaltMinionKeyDistributeEP = SaltMinionEp + "/fingerprint/distribute"
SaltServerEp = RootPath + "/salt/server"
SaltServerRunEP = SaltServerEp + "/run"
SaltServerStopEP = SaltServerEp + "/stop"
SaltPillarEP = RootPath + "/salt/server/pillar"
SaltPillarDistributeEP = RootPath + "/salt/server/pillar/distribute"
HostnameDistributeEP = RootPath + "/hostname/distribute"
HostnameEP = RootPath + "/hostname"
UploadEP = RootPath + "/file"
FileDistributeEP = UploadEP + "/distribute"
RootPath = "/saltboot"
HealthEP = RootPath + "/health"
ServerSaveEP = RootPath + "/server/save"
ServerDistributeEP = RootPath + "/server/distribute"
SaltActionDistributeEP = RootPath + "/salt/action/distribute"
SaltMinionEp = RootPath + "/salt/minion"
SaltMinionRunEP = SaltMinionEp + "/run"
SaltMinionStopEP = SaltMinionEp + "/stop"
SaltMinionKeyEP = SaltMinionEp + "/fingerprint"
SaltMinionKeyDistributeEP = SaltMinionEp + "/fingerprint/distribute"
SaltServerEp = RootPath + "/salt/server"
SaltServerRunEP = SaltServerEp + "/run"
SaltServerStopEP = SaltServerEp + "/stop"
SaltServerChangePasswordEP = SaltServerEp + "/change-password"
SaltPillarEP = RootPath + "/salt/server/pillar"
SaltPillarDistributeEP = RootPath + "/salt/server/pillar/distribute"
HostnameDistributeEP = RootPath + "/hostname/distribute"
HostnameEP = RootPath + "/hostname"
UploadEP = RootPath + "/file"
FileDistributeEP = UploadEP + "/distribute"
)

func NewCloudbreakBootstrapWeb() {
Expand All @@ -50,6 +51,7 @@ func NewCloudbreakBootstrapWeb() {

r.Handle(SaltServerRunEP, authenticator.Wrap(SaltServerRunRequestHandler, SIGNED)).Methods("POST")
r.Handle(SaltServerStopEP, authenticator.Wrap(SaltServerStopRequestHandler, SIGNED)).Methods("POST")
r.Handle(SaltServerChangePasswordEP, authenticator.Wrap(SaltServerChangePasswordHandler, SIGNED)).Methods("POST")

r.Handle(SaltPillarEP, authenticator.Wrap(SaltPillarRequestHandler, SIGNED)).Methods("POST")
r.Handle(SaltPillarDistributeEP, authenticator.Wrap(SaltPillarDistributeRequestHandler, SIGNED)).Methods("POST")
Expand Down

0 comments on commit b8613be

Please sign in to comment.