2017-02-23 06:12:16 +01:00
|
|
|
package db
|
2016-04-04 01:10:12 +02:00
|
|
|
|
2016-04-08 21:41:20 +02:00
|
|
|
import (
|
2021-08-31 01:02:41 +02:00
|
|
|
"crypto/aes"
|
|
|
|
"crypto/cipher"
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
2021-09-01 16:38:28 +02:00
|
|
|
"encoding/json"
|
2021-08-31 01:02:41 +02:00
|
|
|
"fmt"
|
2023-09-23 17:47:27 +02:00
|
|
|
"github.com/ansible-semaphore/semaphore/lib"
|
2021-08-31 01:02:41 +02:00
|
|
|
"io"
|
2021-11-03 13:51:36 +01:00
|
|
|
"math/big"
|
|
|
|
"os"
|
2023-09-23 17:47:27 +02:00
|
|
|
"path"
|
2016-04-08 21:41:20 +02:00
|
|
|
"strconv"
|
2023-09-23 17:47:27 +02:00
|
|
|
"time"
|
2016-04-08 21:41:20 +02:00
|
|
|
|
|
|
|
"github.com/ansible-semaphore/semaphore/util"
|
|
|
|
)
|
2016-04-07 14:49:34 +02:00
|
|
|
|
2022-01-27 15:16:58 +01:00
|
|
|
type AccessKeyType string
|
|
|
|
|
2021-08-30 16:24:20 +02:00
|
|
|
const (
|
2022-01-27 15:16:58 +01:00
|
|
|
AccessKeySSH AccessKeyType = "ssh"
|
|
|
|
AccessKeyNone AccessKeyType = "none"
|
|
|
|
AccessKeyLoginPassword AccessKeyType = "login_password"
|
2021-08-30 16:24:20 +02:00
|
|
|
)
|
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
// AccessKey represents a key used to access a machine with ansible from semaphore
|
2016-04-04 01:10:12 +02:00
|
|
|
type AccessKey struct {
|
|
|
|
ID int `db:"id" json:"id"`
|
|
|
|
Name string `db:"name" json:"name" binding:"required"`
|
2021-09-01 16:38:28 +02:00
|
|
|
// 'ssh/login_password/none'
|
2022-01-27 15:16:58 +01:00
|
|
|
Type AccessKeyType `db:"type" json:"type" binding:"required"`
|
2016-04-04 01:10:12 +02:00
|
|
|
|
2021-09-01 16:38:28 +02:00
|
|
|
ProjectID *int `db:"project_id" json:"project_id"`
|
|
|
|
|
|
|
|
// Secret used internally, do not assign this field.
|
|
|
|
// You should use methods SerializeSecret to fill this field.
|
|
|
|
Secret *string `db:"secret" json:"-"`
|
2016-06-17 22:16:46 +02:00
|
|
|
|
2021-09-01 16:38:28 +02:00
|
|
|
LoginPassword LoginPassword `db:"-" json:"login_password"`
|
|
|
|
SshKey SshKey `db:"-" json:"ssh"`
|
2021-09-12 00:18:26 +02:00
|
|
|
OverrideSecret bool `db:"-" json:"override_secret"`
|
2021-09-01 16:38:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type LoginPassword struct {
|
|
|
|
Login string `json:"login"`
|
|
|
|
Password string `json:"password"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type SshKey struct {
|
2021-09-12 00:18:26 +02:00
|
|
|
Login string `json:"login"`
|
2021-09-01 16:38:28 +02:00
|
|
|
Passphrase string `json:"passphrase"`
|
|
|
|
PrivateKey string `json:"private_key"`
|
2016-04-04 01:10:12 +02:00
|
|
|
}
|
2016-04-07 14:49:34 +02:00
|
|
|
|
2022-02-05 19:42:09 +01:00
|
|
|
type AccessKeyRole int
|
2021-09-12 00:18:26 +02:00
|
|
|
|
|
|
|
const (
|
2022-02-05 19:42:09 +01:00
|
|
|
AccessKeyRoleAnsibleUser = iota
|
|
|
|
AccessKeyRoleAnsibleBecomeUser
|
|
|
|
AccessKeyRoleAnsiblePasswordVault
|
|
|
|
AccessKeyRoleGit
|
2021-09-12 00:18:26 +02:00
|
|
|
)
|
|
|
|
|
2023-09-23 17:12:35 +02:00
|
|
|
type AccessKeyInstallation struct {
|
|
|
|
InstallationKey int64
|
2023-09-23 17:47:27 +02:00
|
|
|
SshAgent *lib.SshAgent
|
2023-09-23 17:12:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (key AccessKeyInstallation) Destroy() error {
|
2023-09-23 17:47:27 +02:00
|
|
|
if key.SshAgent != nil {
|
|
|
|
return key.SshAgent.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
installPath := key.GetPath()
|
|
|
|
_, err := os.Stat(installPath)
|
2023-09-23 17:12:35 +02:00
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil
|
|
|
|
}
|
2023-09-23 17:47:27 +02:00
|
|
|
return os.Remove(installPath)
|
2023-09-23 17:12:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetPath returns the location of the access key once written to disk
|
|
|
|
func (key AccessKeyInstallation) GetPath() string {
|
|
|
|
return util.Config.TmpPath + "/access_key_" + strconv.FormatInt(key.InstallationKey, 10)
|
|
|
|
}
|
|
|
|
|
2023-09-23 17:47:27 +02:00
|
|
|
func (key *AccessKey) startSshAgent(logger lib.Logger) (lib.SshAgent, error) {
|
|
|
|
|
|
|
|
sshAgent := lib.SshAgent{
|
|
|
|
Logger: logger,
|
|
|
|
Keys: []lib.SshAgentKey{
|
|
|
|
{
|
|
|
|
Key: []byte(key.SshKey.PrivateKey),
|
|
|
|
Passphrase: []byte(key.SshKey.Passphrase),
|
|
|
|
},
|
|
|
|
},
|
2023-10-01 23:02:32 +02:00
|
|
|
SocketFile: path.Join(util.Config.TmpPath, fmt.Sprintf("ssh-agent-%d-%d.sock", key.ID, time.Now().Unix())),
|
2023-09-23 17:47:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return sshAgent, sshAgent.Listen()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (key *AccessKey) Install(usage AccessKeyRole, logger lib.Logger) (installation AccessKeyInstallation, err error) {
|
2021-11-03 13:51:36 +01:00
|
|
|
rnd, err := rand.Int(rand.Reader, big.NewInt(1000000000))
|
|
|
|
if err != nil {
|
2023-09-23 17:12:35 +02:00
|
|
|
return
|
2021-11-03 13:51:36 +01:00
|
|
|
}
|
|
|
|
|
2023-09-23 17:12:35 +02:00
|
|
|
installation.InstallationKey = rnd.Int64()
|
2021-11-03 13:51:36 +01:00
|
|
|
|
2021-09-12 00:18:26 +02:00
|
|
|
if key.Type == AccessKeyNone {
|
2023-09-23 17:12:35 +02:00
|
|
|
return
|
2021-09-12 00:18:26 +02:00
|
|
|
}
|
|
|
|
|
2023-09-23 17:47:27 +02:00
|
|
|
installationPath := installation.GetPath()
|
2021-09-12 00:18:26 +02:00
|
|
|
|
2021-11-03 13:51:36 +01:00
|
|
|
err = key.DeserializeSecret()
|
2021-09-12 00:18:26 +02:00
|
|
|
|
|
|
|
if err != nil {
|
2023-09-23 17:12:35 +02:00
|
|
|
return
|
2021-09-12 00:18:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
switch usage {
|
2022-02-05 19:42:09 +01:00
|
|
|
case AccessKeyRoleGit:
|
|
|
|
switch key.Type {
|
|
|
|
case AccessKeySSH:
|
2023-09-23 17:47:27 +02:00
|
|
|
var agent lib.SshAgent
|
|
|
|
agent, err = key.startSshAgent(logger)
|
|
|
|
installation.SshAgent = &agent
|
2021-09-12 00:18:26 +02:00
|
|
|
}
|
2022-02-05 19:42:09 +01:00
|
|
|
case AccessKeyRoleAnsiblePasswordVault:
|
2021-09-12 00:18:26 +02:00
|
|
|
switch key.Type {
|
|
|
|
case AccessKeyLoginPassword:
|
2024-03-10 20:07:19 +01:00
|
|
|
err = os.WriteFile(installationPath, []byte(key.LoginPassword.Password), 0600)
|
2021-09-12 00:18:26 +02:00
|
|
|
}
|
2022-02-05 19:42:09 +01:00
|
|
|
case AccessKeyRoleAnsibleBecomeUser:
|
2021-09-12 00:18:26 +02:00
|
|
|
switch key.Type {
|
|
|
|
case AccessKeyLoginPassword:
|
2021-09-17 01:17:19 +02:00
|
|
|
content := make(map[string]string)
|
2023-12-07 23:40:51 +01:00
|
|
|
if len(key.LoginPassword.Login) > 0 {
|
|
|
|
content["ansible_become_user"] = key.LoginPassword.Login
|
|
|
|
}
|
2021-09-17 01:17:19 +02:00
|
|
|
content["ansible_become_password"] = key.LoginPassword.Password
|
|
|
|
var bytes []byte
|
|
|
|
bytes, err = json.Marshal(content)
|
|
|
|
if err != nil {
|
2023-09-23 17:12:35 +02:00
|
|
|
return
|
2021-09-17 01:17:19 +02:00
|
|
|
}
|
2024-03-10 20:07:19 +01:00
|
|
|
err = os.WriteFile(installationPath, bytes, 0600)
|
2021-09-12 00:18:26 +02:00
|
|
|
default:
|
2023-09-23 17:12:35 +02:00
|
|
|
err = fmt.Errorf("access key type not supported for ansible user")
|
2021-09-12 00:18:26 +02:00
|
|
|
}
|
2022-02-05 19:42:09 +01:00
|
|
|
case AccessKeyRoleAnsibleUser:
|
2021-09-12 00:18:26 +02:00
|
|
|
switch key.Type {
|
|
|
|
case AccessKeySSH:
|
2023-09-23 17:47:27 +02:00
|
|
|
var agent lib.SshAgent
|
|
|
|
agent, err = key.startSshAgent(logger)
|
|
|
|
installation.SshAgent = &agent
|
2021-09-12 00:18:26 +02:00
|
|
|
case AccessKeyLoginPassword:
|
2021-09-17 01:17:19 +02:00
|
|
|
content := make(map[string]string)
|
|
|
|
content["ansible_user"] = key.LoginPassword.Login
|
|
|
|
content["ansible_password"] = key.LoginPassword.Password
|
|
|
|
var bytes []byte
|
|
|
|
bytes, err = json.Marshal(content)
|
|
|
|
if err != nil {
|
2023-09-23 17:12:35 +02:00
|
|
|
return
|
2021-09-17 01:17:19 +02:00
|
|
|
}
|
2024-03-10 20:07:19 +01:00
|
|
|
err = os.WriteFile(installationPath, bytes, 0600)
|
2021-09-17 01:17:19 +02:00
|
|
|
|
2021-09-12 00:18:26 +02:00
|
|
|
default:
|
2023-09-23 17:12:35 +02:00
|
|
|
err = fmt.Errorf("access key type not supported for ansible user")
|
2021-09-12 00:18:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-23 17:12:35 +02:00
|
|
|
return
|
2016-04-08 21:41:20 +02:00
|
|
|
}
|
2021-08-31 01:02:41 +02:00
|
|
|
|
2023-09-23 17:12:35 +02:00
|
|
|
func (key *AccessKey) Validate(validateSecretFields bool) error {
|
2021-09-01 19:56:44 +02:00
|
|
|
if key.Name == "" {
|
|
|
|
return fmt.Errorf("name can not be empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
if !validateSecretFields {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
switch key.Type {
|
|
|
|
case AccessKeySSH:
|
|
|
|
if key.SshKey.PrivateKey == "" {
|
|
|
|
return fmt.Errorf("private key can not be empty")
|
|
|
|
}
|
|
|
|
case AccessKeyLoginPassword:
|
2021-09-01 23:14:32 +02:00
|
|
|
if key.LoginPassword.Password == "" {
|
2021-09-01 19:56:44 +02:00
|
|
|
return fmt.Errorf("password can not be empty")
|
|
|
|
}
|
|
|
|
}
|
2021-09-01 16:38:28 +02:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (key *AccessKey) SerializeSecret() error {
|
|
|
|
var plaintext []byte
|
|
|
|
var err error
|
|
|
|
|
|
|
|
switch key.Type {
|
|
|
|
case AccessKeySSH:
|
|
|
|
plaintext, err = json.Marshal(key.SshKey)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
case AccessKeyLoginPassword:
|
|
|
|
plaintext, err = json.Marshal(key.LoginPassword)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-01-27 15:16:58 +01:00
|
|
|
case AccessKeyNone:
|
2021-09-01 16:38:28 +02:00
|
|
|
key.Secret = nil
|
2021-08-31 01:02:41 +02:00
|
|
|
return nil
|
2022-01-27 15:16:58 +01:00
|
|
|
default:
|
|
|
|
return fmt.Errorf("invalid access token type")
|
2021-08-31 01:02:41 +02:00
|
|
|
}
|
|
|
|
|
2023-08-05 15:56:39 +02:00
|
|
|
encryptionString := util.Config.AccessKeyEncryption
|
2022-01-25 15:34:52 +01:00
|
|
|
|
|
|
|
if encryptionString == "" {
|
2021-09-01 16:38:28 +02:00
|
|
|
secret := base64.StdEncoding.EncodeToString(plaintext)
|
|
|
|
key.Secret = &secret
|
|
|
|
return nil
|
|
|
|
}
|
2021-08-31 01:02:41 +02:00
|
|
|
|
2022-01-25 15:34:52 +01:00
|
|
|
encryption, err := base64.StdEncoding.DecodeString(encryptionString)
|
2021-08-31 01:02:41 +02:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
c, err := aes.NewCipher(encryption)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
gcm, err := cipher.NewGCM(c)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
|
|
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-08-31 01:27:15 +02:00
|
|
|
secret := base64.StdEncoding.EncodeToString(gcm.Seal(nonce, nonce, plaintext, nil))
|
2021-08-31 01:02:41 +02:00
|
|
|
key.Secret = &secret
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-01 16:38:28 +02:00
|
|
|
func (key *AccessKey) unmarshalAppropriateField(secret []byte) (err error) {
|
|
|
|
switch key.Type {
|
|
|
|
case AccessKeySSH:
|
|
|
|
sshKey := SshKey{}
|
|
|
|
err = json.Unmarshal(secret, &sshKey)
|
|
|
|
if err == nil {
|
|
|
|
key.SshKey = sshKey
|
|
|
|
}
|
|
|
|
case AccessKeyLoginPassword:
|
|
|
|
loginPass := LoginPassword{}
|
|
|
|
err = json.Unmarshal(secret, &loginPass)
|
|
|
|
if err == nil {
|
|
|
|
key.LoginPassword = loginPass
|
|
|
|
}
|
2021-08-31 01:02:41 +02:00
|
|
|
}
|
2021-09-01 16:38:28 +02:00
|
|
|
return
|
|
|
|
}
|
2021-08-31 01:02:41 +02:00
|
|
|
|
2021-09-01 16:38:28 +02:00
|
|
|
func (key *AccessKey) DeserializeSecret() error {
|
2023-09-09 17:10:29 +02:00
|
|
|
return key.DeserializeSecret2(util.Config.AccessKeyEncryption)
|
2023-09-09 14:41:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (key *AccessKey) DeserializeSecret2(encryptionString string) error {
|
2021-09-01 16:38:28 +02:00
|
|
|
if key.Secret == nil || *key.Secret == "" {
|
|
|
|
return nil
|
2021-08-31 01:02:41 +02:00
|
|
|
}
|
|
|
|
|
2021-09-01 16:38:28 +02:00
|
|
|
ciphertext := []byte(*key.Secret)
|
|
|
|
|
|
|
|
if ciphertext[len(*key.Secret)-1] == '\n' { // not encrypted private key, used for back compatibility
|
|
|
|
if key.Type != AccessKeySSH {
|
|
|
|
return fmt.Errorf("invalid access key type")
|
|
|
|
}
|
|
|
|
key.SshKey = SshKey{
|
|
|
|
PrivateKey: *key.Secret,
|
|
|
|
}
|
|
|
|
return nil
|
2021-08-31 01:02:41 +02:00
|
|
|
}
|
|
|
|
|
2021-08-31 01:27:15 +02:00
|
|
|
ciphertext, err := base64.StdEncoding.DecodeString(*key.Secret)
|
|
|
|
if err != nil {
|
2021-09-01 16:38:28 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-01-25 09:33:32 +01:00
|
|
|
if encryptionString == "" {
|
2021-09-10 00:41:36 +02:00
|
|
|
err = key.unmarshalAppropriateField(ciphertext)
|
|
|
|
if _, ok := err.(*json.SyntaxError); ok {
|
2023-09-09 14:41:41 +02:00
|
|
|
err = fmt.Errorf("secret must be valid json in key '%s'", key.Name)
|
2021-09-10 00:41:36 +02:00
|
|
|
}
|
|
|
|
return err
|
2021-08-31 01:27:15 +02:00
|
|
|
}
|
|
|
|
|
2022-01-25 09:33:32 +01:00
|
|
|
encryption, err := base64.StdEncoding.DecodeString(encryptionString)
|
2021-08-31 01:02:41 +02:00
|
|
|
if err != nil {
|
2021-09-01 16:38:28 +02:00
|
|
|
return err
|
2021-08-31 01:02:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
c, err := aes.NewCipher(encryption)
|
|
|
|
if err != nil {
|
2021-09-01 16:38:28 +02:00
|
|
|
return err
|
2021-08-31 01:02:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
gcm, err := cipher.NewGCM(c)
|
|
|
|
if err != nil {
|
2021-09-01 16:38:28 +02:00
|
|
|
return err
|
2021-08-31 01:02:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
nonceSize := gcm.NonceSize()
|
|
|
|
if len(ciphertext) < nonceSize {
|
2021-09-01 16:38:28 +02:00
|
|
|
return fmt.Errorf("ciphertext too short")
|
2021-08-31 01:02:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
|
|
|
|
2021-09-01 16:38:28 +02:00
|
|
|
ciphertext, err = gcm.Open(nil, nonce, ciphertext, nil)
|
2021-08-31 01:02:41 +02:00
|
|
|
|
|
|
|
if err != nil {
|
2021-09-10 00:41:36 +02:00
|
|
|
if err.Error() == "cipher: message authentication failed" {
|
2021-11-03 13:51:36 +01:00
|
|
|
err = fmt.Errorf("cannot decrypt access key, perhaps encryption key was changed")
|
2021-09-10 00:41:36 +02:00
|
|
|
}
|
2021-09-01 16:38:28 +02:00
|
|
|
return err
|
2021-08-31 01:02:41 +02:00
|
|
|
}
|
|
|
|
|
2021-09-01 16:38:28 +02:00
|
|
|
return key.unmarshalAppropriateField(ciphertext)
|
2021-08-31 01:02:41 +02:00
|
|
|
}
|