Semaphore/db/AccessKey.go

341 lines
7.9 KiB
Go
Raw Normal View History

2017-02-23 06:12:16 +01:00
package db
2016-04-04 01:10:12 +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"
"github.com/ansible-semaphore/semaphore/pkg/random"
"github.com/ansible-semaphore/semaphore/pkg/ssh"
"github.com/ansible-semaphore/semaphore/pkg/task_logger"
"github.com/ansible-semaphore/semaphore/util"
"io"
"path"
)
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"
2024-07-02 11:42:12 +02:00
AccessKeyString AccessKeyType = "string"
2021-08-30 16:24:20 +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" backup:"-"`
2016-04-04 01:10:12 +02:00
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
ProjectID *int `db:"project_id" json:"project_id" backup:"-"`
2021-09-01 16:38:28 +02:00
// Secret used internally, do not assign this field.
// You should use methods SerializeSecret to fill this field.
Secret *string `db:"secret" json:"-" backup:"-"`
2024-07-02 11:42:12 +02:00
String string `db:"-" json:"string"`
2021-09-01 16:38:28 +02:00
LoginPassword LoginPassword `db:"-" json:"login_password"`
SshKey SshKey `db:"-" json:"ssh"`
OverrideSecret bool `db:"-" json:"override_secret"`
2024-07-02 11:42:12 +02:00
// EnvironmentID is an ID of environment which owns the access key.
EnvironmentID *int `db:"environment_id" json:"-" backup:"-"`
// UserID is an ID of user which owns the access key.
UserID *int `db:"user_id" json:"-" backup:"-"`
2024-10-08 11:42:51 +02:00
Empty bool `db:"-" json:"empty,omitempty"`
2021-09-01 16:38:28 +02:00
}
type LoginPassword struct {
Login string `json:"login"`
Password string `json:"password"`
}
type SshKey struct {
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
}
type AccessKeyRole int
const (
AccessKeyRoleAnsibleUser = iota
AccessKeyRoleAnsibleBecomeUser
AccessKeyRoleAnsiblePasswordVault
AccessKeyRoleGit
)
2023-09-23 17:12:35 +02:00
type AccessKeyInstallation struct {
2024-05-30 14:17:43 +02:00
SSHAgent *ssh.Agent
Login string
Password string
2023-09-23 17:12:35 +02:00
}
func (key AccessKeyInstallation) Destroy() error {
if key.SSHAgent != nil {
return key.SSHAgent.Close()
2023-09-23 17:47:27 +02:00
}
return nil
2023-09-23 17:12:35 +02:00
}
func (key *AccessKey) startSSHAgent(logger task_logger.Logger) (ssh.Agent, error) {
sshAgent := ssh.Agent{
2023-09-23 17:47:27 +02:00
Logger: logger,
Keys: []ssh.AgentKey{
2023-09-23 17:47:27 +02:00
{
Key: []byte(key.SshKey.PrivateKey),
Passphrase: []byte(key.SshKey.Passphrase),
},
},
SocketFile: path.Join(util.Config.TmpPath, fmt.Sprintf("ssh-agent-%d-%s.sock", key.ID, random.String(10))),
2023-09-23 17:47:27 +02:00
}
return sshAgent, sshAgent.Listen()
}
func (key *AccessKey) Install(usage AccessKeyRole, logger task_logger.Logger) (installation AccessKeyInstallation, err error) {
if key.Type == AccessKeyNone {
2023-09-23 17:12:35 +02:00
return
}
err = key.DeserializeSecret()
if err != nil {
2023-09-23 17:12:35 +02:00
return
}
switch usage {
case AccessKeyRoleGit:
switch key.Type {
case AccessKeySSH:
var agent ssh.Agent
agent, err = key.startSSHAgent(logger)
installation.SSHAgent = &agent
installation.Login = key.SshKey.Login
}
case AccessKeyRoleAnsiblePasswordVault:
if key.Type != AccessKeyLoginPassword {
err = fmt.Errorf("access key type not supported for ansible user")
}
installation.Password = key.LoginPassword.Password
case AccessKeyRoleAnsibleBecomeUser:
if key.Type != AccessKeyLoginPassword {
2023-09-23 17:12:35 +02:00
err = fmt.Errorf("access key type not supported for ansible user")
}
installation.Login = key.LoginPassword.Login
installation.Password = key.LoginPassword.Password
case AccessKeyRoleAnsibleUser:
switch key.Type {
case AccessKeySSH:
var agent ssh.Agent
agent, err = key.startSSHAgent(logger)
installation.SSHAgent = &agent
installation.Login = key.SshKey.Login
case AccessKeyLoginPassword:
installation.Login = key.LoginPassword.Login
installation.Password = key.LoginPassword.Password
default:
2023-09-23 17:12:35 +02:00
err = fmt.Errorf("access key type not supported for ansible user")
}
}
2023-09-23 17:12:35 +02:00
return
}
2021-08-31 01:02:41 +02:00
2023-09-23 17:12:35 +02:00
func (key *AccessKey) Validate(validateSecretFields bool) error {
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:
if key.LoginPassword.Password == "" {
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 AccessKeyString:
2024-10-08 11:42:51 +02:00
if key.String == "" {
key.Secret = nil
return nil
}
plaintext = []byte(key.String)
2021-09-01 16:38:28 +02:00
case AccessKeySSH:
2024-10-08 11:42:51 +02:00
if key.SshKey.PrivateKey == "" {
if key.SshKey.Login != "" || key.SshKey.Passphrase != "" {
return fmt.Errorf("invalid ssh key")
}
key.Secret = nil
return nil
}
2021-09-01 16:38:28 +02:00
plaintext, err = json.Marshal(key.SshKey)
if err != nil {
return err
}
case AccessKeyLoginPassword:
2024-10-08 11:42:51 +02:00
if key.LoginPassword.Password == "" {
if key.LoginPassword.Login != "" {
return fmt.Errorf("invalid password key")
}
key.Secret = nil
return nil
}
2021-09-01 16:38:28 +02:00
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
}
encryptionString := util.Config.AccessKeyEncryption
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
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
}
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 AccessKeyString:
key.String = string(secret)
2021-09-01 16:38:28 +02:00
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)
}
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
}
ciphertext, err := base64.StdEncoding.DecodeString(*key.Secret)
if err != nil {
2021-09-01 16:38:28 +02:00
return err
}
if encryptionString == "" {
err = key.unmarshalAppropriateField(ciphertext)
if _, ok := err.(*json.SyntaxError); ok {
err = fmt.Errorf("secret must be valid json in key '%s'", key.Name)
}
return err
}
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 {
if err.Error() == "cipher: message authentication failed" {
err = fmt.Errorf("cannot decrypt access key, perhaps encryption key was changed")
}
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
}