Log errors to log file
[aaf/sms.git] / sms-service / src / sms / backend / vault.go
index f3e2ac1..a4ebaaa 100644 (file)
 package backend
 
 import (
+       uuid "github.com/hashicorp/go-uuid"
        vaultapi "github.com/hashicorp/vault/api"
+       smslogger "sms/log"
 
-       smsconfig "sms/config"
+       "errors"
+       "fmt"
+       "strings"
+       "sync"
+       "time"
 )
 
 // Vault is the main Struct used in Backend to initialize the struct
 type Vault struct {
-       vaultClient *vaultapi.Client
+       engineType        string
+       initRoleDone      bool
+       policyName        string
+       roleID            string
+       secretID          string
+       tokenLock         sync.Mutex
+       vaultAddress      string
+       vaultClient       *vaultapi.Client
+       vaultMount        string
+       vaultTempTokenTTL time.Time
+       vaultToken        string
 }
 
 // Init will initialize the vault connection
+// It will also create the initial policy if it does not exist
 // TODO: Check to see if we need to wait for vault to be running
 func (v *Vault) Init() error {
        vaultCFG := vaultapi.DefaultConfig()
-       vaultCFG.Address = smsconfig.SMSConfig.VaultAddress
-
+       vaultCFG.Address = v.vaultAddress
        client, err := vaultapi.NewClient(vaultCFG)
        if err != nil {
-               return err
+               smslogger.WriteError(err.Error())
+               return errors.New("Unable to create new vault client")
        }
 
+       v.engineType = "kv"
+       v.initRoleDone = false
+       v.policyName = "smsvaultpolicy"
        v.vaultClient = client
+       v.vaultMount = "sms"
+
+       err = v.initRole()
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               smslogger.WriteInfo("InitRole will try again later")
+       }
+
        return nil
 }
 
@@ -47,49 +75,277 @@ func (v *Vault) GetStatus() (bool, error) {
        sys := v.vaultClient.Sys()
        sealStatus, err := sys.SealStatus()
        if err != nil {
-               return false, err
+               smslogger.WriteError(err.Error())
+               return false, errors.New("Error getting status")
        }
 
        return sealStatus.Sealed, nil
 }
 
-// GetSecretDomain returns any information related to the secretDomain
-// More information can be added in the future with updates to the struct
-func (v *Vault) GetSecretDomain(name string) (SecretDomain, error) {
+// Unseal is a passthrough API that allows any
+// unseal or initialization processes for the backend
+func (v *Vault) Unseal(shard string) error {
+       sys := v.vaultClient.Sys()
+       _, err := sys.Unseal(shard)
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Unable to execute unseal operation with specified shard")
+       }
 
-       return SecretDomain{}, nil
+       return nil
 }
 
 // GetSecret returns a secret mounted on a particular domain name
 // The secret itself is referenced via its name which translates to
 // a mount path in vault
-func (v *Vault) GetSecret(dom string, sec string) (Secret, error) {
+func (v *Vault) GetSecret(dom string, name string) (Secret, error) {
+       err := v.checkToken()
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return Secret{}, errors.New("Token check failed")
+       }
+
+       dom = v.vaultMount + "/" + dom
+
+       sec, err := v.vaultClient.Logical().Read(dom + "/" + name)
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return Secret{}, errors.New("Unable to read Secret at provided path")
+       }
+
+       // sec and err are nil in the case where a path does not exist
+       if sec == nil {
+               smslogger.WriteWarn("Vault read was empty. Invalid Path")
+               return Secret{}, errors.New("Secret not found at the provided path")
+       }
+
+       return Secret{Name: name, Values: sec.Data}, nil
+}
+
+// ListSecret returns a list of secret names on a particular domain
+// The values of the secret are not returned
+func (v *Vault) ListSecret(dom string) ([]string, error) {
+       err := v.checkToken()
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return nil, errors.New("Token check failed")
+       }
+
+       dom = v.vaultMount + "/" + dom
 
-       return Secret{}, nil
+       sec, err := v.vaultClient.Logical().List(dom)
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return nil, errors.New("Unable to read Secret at provided path")
+       }
+
+       // sec and err are nil in the case where a path does not exist
+       if sec == nil {
+               smslogger.WriteWarn("Vaultclient returned empty data")
+               return nil, errors.New("Secret not found at the provided path")
+       }
+
+       val, ok := sec.Data["keys"].([]interface{})
+       if !ok {
+               smslogger.WriteError("Secret not found at the provided path")
+               return nil, errors.New("Secret not found at the provided path")
+       }
+
+       retval := make([]string, len(val))
+       for i, v := range val {
+               retval[i] = fmt.Sprint(v)
+       }
+
+       return retval, nil
 }
 
 // CreateSecretDomain mounts the kv backend on a path with the given name
 func (v *Vault) CreateSecretDomain(name string) (SecretDomain, error) {
+       // Check if token is still valid
+       err := v.checkToken()
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return SecretDomain{}, errors.New("Token Check failed")
+       }
 
-       return SecretDomain{}, nil
+       name = strings.TrimSpace(name)
+       mountPath := v.vaultMount + "/" + name
+       mountInput := &vaultapi.MountInput{
+               Type:        v.engineType,
+               Description: "Mount point for domain: " + name,
+               Local:       false,
+               SealWrap:    false,
+               Config:      vaultapi.MountConfigInput{},
+       }
+
+       err = v.vaultClient.Sys().Mount(mountPath, mountInput)
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return SecretDomain{}, errors.New("Unable to create Secret Domain")
+       }
+
+       uuid, _ := uuid.GenerateUUID()
+       return SecretDomain{uuid, name}, nil
 }
 
 // CreateSecret creates a secret mounted on a particular domain name
 // The secret itself is mounted on a path specified by name
-func (v *Vault) CreateSecret(dom string, sec Secret) (Secret, error) {
+func (v *Vault) CreateSecret(dom string, sec Secret) error {
+       err := v.checkToken()
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Token check failed")
+       }
 
-       return Secret{}, nil
+       dom = v.vaultMount + "/" + dom
+
+       // Vault return is empty on successful write
+       // TODO: Check if values is not empty
+       _, err = v.vaultClient.Logical().Write(dom+"/"+sec.Name, sec.Values)
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Unable to create Secret at provided path")
+       }
+
+       return nil
 }
 
 // DeleteSecretDomain deletes a secret domain which translates to
 // an unmount operation on the given path in Vault
 func (v *Vault) DeleteSecretDomain(name string) error {
+       err := v.checkToken()
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Token Check Failed")
+       }
+
+       name = strings.TrimSpace(name)
+       mountPath := v.vaultMount + "/" + name
+
+       err = v.vaultClient.Sys().Unmount(mountPath)
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Unable to delete domain specified")
+       }
 
        return nil
 }
 
 // DeleteSecret deletes a secret mounted on the path provided
 func (v *Vault) DeleteSecret(dom string, name string) error {
+       err := v.checkToken()
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Token check failed")
+       }
+
+       dom = v.vaultMount + "/" + dom
+
+       // Vault return is empty on successful delete
+       _, err = v.vaultClient.Logical().Delete(dom + "/" + name)
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Unable to delete Secret at provided path")
+       }
+
+       return nil
+}
+
+// initRole is called only once during the service bring up
+func (v *Vault) initRole() error {
+       // Use the root token once here
+       v.vaultClient.SetToken(v.vaultToken)
+       defer v.vaultClient.ClearToken()
+
+       rules := `path "sms/*" { capabilities = ["create", "read", "update", "delete", "list"] }
+                       path "sys/mounts/sms*" { capabilities = ["update","delete","create"] }`
+       err := v.vaultClient.Sys().PutPolicy(v.policyName, rules)
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Unable to create policy for approle creation")
+       }
+
+       rName := v.vaultMount + "-role"
+       data := map[string]interface{}{
+               "token_ttl": "60m",
+               "policies":  [2]string{"default", v.policyName},
+       }
+
+       //Check if applrole is mounted
+       authMounts, err := v.vaultClient.Sys().ListAuth()
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Unable to get mounted auth backends")
+       }
+
+       approleMounted := false
+       for k, v := range authMounts {
+               if v.Type == "approle" && k == "approle/" {
+                       approleMounted = true
+                       break
+               }
+       }
+
+       // Mount approle in case its not already mounted
+       if !approleMounted {
+               v.vaultClient.Sys().EnableAuth("approle", "approle", "")
+       }
+
+       // Create a role-id
+       v.vaultClient.Logical().Write("auth/approle/role/"+rName, data)
+       sec, err := v.vaultClient.Logical().Read("auth/approle/role/" + rName + "/role-id")
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Unable to create role ID for approle")
+       }
+       v.roleID = sec.Data["role_id"].(string)
+
+       // Create a secret-id to go with it
+       sec, err = v.vaultClient.Logical().Write("auth/approle/role/"+rName+"/secret-id",
+               map[string]interface{}{})
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Unable to create secret ID for role")
+       }
+
+       v.secretID = sec.Data["secret_id"].(string)
+       v.initRoleDone = true
+       return nil
+}
+
+// Function checkToken() gets called multiple times to create
+// temporary tokens
+func (v *Vault) checkToken() error {
+       v.tokenLock.Lock()
+       defer v.tokenLock.Unlock()
+
+       // Init Role if it is not yet done
+       // Role needs to be created before token can be created
+       if v.initRoleDone == false {
+               err := v.initRole()
+               if err != nil {
+                       smslogger.WriteError(err.Error())
+                       return errors.New("Unable to initRole in checkToken")
+               }
+       }
+
+       // Return immediately if token still has life
+       if v.vaultClient.Token() != "" &&
+               time.Since(v.vaultTempTokenTTL) < time.Minute*50 {
+               return nil
+       }
+
+       // Create a temporary token using our roleID and secretID
+       out, err := v.vaultClient.Logical().Write("auth/approle/login",
+               map[string]interface{}{"role_id": v.roleID, "secret_id": v.secretID})
+       if err != nil {
+               smslogger.WriteError(err.Error())
+               return errors.New("Unable to create Temporary Token for Role")
+       }
+
+       tok, err := out.TokenID()
 
+       v.vaultTempTokenTTL = time.Now()
+       v.vaultClient.SetToken(tok)
        return nil
 }