package vault
import (
"context"
"fmt"
"github.com/grafana/agent/pkg/river/rivertypes"
vault "github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/api/auth/approle"
"github.com/hashicorp/vault/api/auth/aws"
"github.com/hashicorp/vault/api/auth/azure"
"github.com/hashicorp/vault/api/auth/gcp"
"github.com/hashicorp/vault/api/auth/kubernetes"
"github.com/hashicorp/vault/api/auth/ldap"
"github.com/hashicorp/vault/api/auth/userpass"
)
type authMethod interface {
vaultAuthenticate(context.Context, *vault.Client) (*vault.Secret, error)
}
type AuthArguments struct {
AuthToken *AuthToken `river:"token,block,optional"`
AuthAppRole *AuthAppRole `river:"approle,block,optional"`
AuthAWS *AuthAWS `river:"aws,block,optional"`
AuthAzure *AuthAzure `river:"azure,block,optional"`
AuthGCP *AuthGCP `river:"gcp,block,optional"`
AuthKubernetes *AuthKubernetes `river:"kubernetes,block,optional"`
AuthLDAP *AuthLDAP `river:"ldap,block,optional"`
AuthUserPass *AuthUserPass `river:"userpass,block,optional"`
AuthCustom *AuthCustom `river:"custom,block,optional"`
}
func (a *AuthArguments) authMethod() authMethod {
switch {
case a.AuthToken != nil:
return a.AuthToken
case a.AuthAppRole != nil:
return a.AuthAppRole
case a.AuthAWS != nil:
return a.AuthAWS
case a.AuthAzure != nil:
return a.AuthAzure
case a.AuthGCP != nil:
return a.AuthGCP
case a.AuthKubernetes != nil:
return a.AuthKubernetes
case a.AuthLDAP != nil:
return a.AuthLDAP
case a.AuthUserPass != nil:
return a.AuthUserPass
case a.AuthCustom != nil:
return a.AuthCustom
}
return invalidAuth{}
}
type AuthToken struct {
Token rivertypes.Secret `river:"token,attr"`
}
func (a *AuthToken) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
cli.SetToken(string(a.Token))
return nil, nil
}
type AuthAppRole struct {
RoleID string `river:"role_id,attr"`
Secret rivertypes.Secret `river:"secret,attr"`
WrappingToken bool `river:"wrapping_token,attr,optional"`
MountPath string `river:"mount_path,attr,optional"`
}
var DefaultAuthAppRole = AuthAppRole{
MountPath: "approle",
}
func (a *AuthAppRole) UnmarshalRiver(f func(interface{}) error) error {
*a = DefaultAuthAppRole
type authAppRole AuthAppRole
return f((*authAppRole)(a))
}
func (a *AuthAppRole) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
secret := &approle.SecretID{FromString: string(a.Secret)}
var opts []approle.LoginOption
if a.WrappingToken {
opts = append(opts, approle.WithWrappingToken())
}
if a.MountPath != "" {
opts = append(opts, approle.WithMountPath(a.MountPath))
}
auth, err := approle.NewAppRoleAuth(a.RoleID, secret, opts...)
if err != nil {
return nil, fmt.Errorf("auth.approle: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.approle: %w", err)
}
return s, nil
}
type AuthAWS struct {
Type string `river:"type,attr"`
Region string `river:"region,attr,optional"`
Role string `river:"role,attr,optional"`
IAMServerIDHeader string `river:"iam_server_id_header,attr,optional"`
EC2SignatureType string `river:"ec2_signature_type,attr,optional"`
MountPath string `river:"mount_path,attr,optional"`
}
const (
authAWSTypeEC2 = "ec2"
authAWSTypeIAM = "iam"
)
var DefaultAuthAWS = AuthAWS{
MountPath: "aws",
Type: authAWSTypeIAM,
Region: "us-east-1",
EC2SignatureType: "pkcs7",
}
func (a *AuthAWS) UnmarshalRiver(f func(interface{}) error) error {
*a = DefaultAuthAWS
type authAWS AuthAWS
if err := f((*authAWS)(a)); err != nil {
return err
}
return a.Validate()
}
func (a *AuthAWS) Validate() error {
switch a.Type {
case "":
return fmt.Errorf("type must not be empty")
case authAWSTypeEC2, authAWSTypeIAM:
default:
return fmt.Errorf("unrecognized type %q, expected one of ec2,iam", a.Type)
}
switch a.EC2SignatureType {
case "":
return fmt.Errorf("ec2_signature_type must not be empty")
case "pkcs7", "identity":
default:
return fmt.Errorf(": unrecognized ec2_signature_type %q, expected one of pkcs7,identity", a.Type)
}
return nil
}
func (a *AuthAWS) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
if err := a.Validate(); err != nil {
return nil, err
}
var opts []aws.LoginOption
switch a.Type {
case authAWSTypeEC2:
opts = append(opts, aws.WithEC2Auth())
case authAWSTypeIAM:
opts = append(opts, aws.WithIAMAuth())
}
if a.Region != "" {
opts = append(opts, aws.WithRegion(a.Region))
}
if a.Role != "" {
opts = append(opts, aws.WithRole(a.Role))
}
if a.IAMServerIDHeader != "" {
opts = append(opts, aws.WithIAMServerIDHeader(a.IAMServerIDHeader))
}
switch a.EC2SignatureType {
case "", "pkcs7":
opts = append(opts, aws.WithPKCS7Signature())
case "identity":
opts = append(opts, aws.WithIdentitySignature())
}
if a.MountPath != "" {
opts = append(opts, aws.WithMountPath(a.MountPath))
}
auth, err := aws.NewAWSAuth(opts...)
if err != nil {
return nil, fmt.Errorf("auth.aws: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.aws: %w", err)
}
return s, nil
}
type AuthAzure struct {
Role string `river:"role,attr"`
ResourceURL string `river:"resource_url,attr,optional"`
MountPath string `river:"mount_path,attr,optional"`
}
var DefaultAuthAzure = AuthAzure{
MountPath: "azure",
ResourceURL: "https://management.azure.com/",
}
func (a *AuthAzure) UnmarshalRiver(f func(interface{}) error) error {
*a = DefaultAuthAzure
type authAzure AuthAzure
return f((*authAzure)(a))
}
func (a *AuthAzure) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
var opts []azure.LoginOption
if a.ResourceURL != "" {
opts = append(opts, azure.WithResource(a.ResourceURL))
}
if a.MountPath != "" {
opts = append(opts, azure.WithMountPath(a.MountPath))
}
auth, err := azure.NewAzureAuth(a.Role, opts...)
if err != nil {
return nil, fmt.Errorf("auth.azure: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.azure: %w", err)
}
return s, nil
}
type AuthGCP struct {
Role string `river:"role,attr"`
Type string `river:"type,attr"`
IAMServiceAccount string `river:"iam_service_account,attr,optional"`
MountPath string `river:"mount_path,attr,optional"`
}
const (
authGCPTypeGCE = "gce"
authGCPTypeIAM = "iam"
)
var DefaultAuthGCP = AuthGCP{
MountPath: "gcp",
Type: authGCPTypeGCE,
}
func (a *AuthGCP) UnmarshalRiver(f func(interface{}) error) error {
*a = DefaultAuthGCP
type authGCP AuthGCP
if err := f((*authGCP)(a)); err != nil {
return err
}
return a.Validate()
}
func (a *AuthGCP) Validate() error {
switch a.Type {
case authGCPTypeGCE:
case authGCPTypeIAM:
if a.IAMServiceAccount == "" {
return fmt.Errorf("iam_service_account must be provided when type is iam")
}
default:
return fmt.Errorf("unrecognized type %q, expected one of gce,iam", a.Type)
}
return nil
}
func (a *AuthGCP) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
if err := a.Validate(); err != nil {
return nil, err
}
var opts []gcp.LoginOption
switch a.Type {
case authGCPTypeGCE:
opts = append(opts, gcp.WithGCEAuth())
case authGCPTypeIAM:
opts = append(opts, gcp.WithIAMAuth(a.IAMServiceAccount))
}
if a.MountPath != "" {
opts = append(opts, gcp.WithMountPath(a.MountPath))
}
auth, err := gcp.NewGCPAuth(a.Role, opts...)
if err != nil {
return nil, fmt.Errorf("auth.gcp: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.gcp: %w", err)
}
return s, nil
}
type AuthKubernetes struct {
Role string `river:"role,attr"`
ServiceAccountTokenFile string `river:"service_account_file,attr,optional"`
MountPath string `river:"mount_path,attr,optional"`
}
var DefaultAuthKubernetes = AuthKubernetes{
MountPath: "kubernetes",
ServiceAccountTokenFile: "/var/run/secrets/kubernetes.io/serviceaccount/token",
}
func (a *AuthKubernetes) UnmarshalRiver(f func(interface{}) error) error {
*a = DefaultAuthKubernetes
type authKubernetes AuthKubernetes
return f((*authKubernetes)(a))
}
func (a *AuthKubernetes) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
var opts []kubernetes.LoginOption
if a.ServiceAccountTokenFile != "" {
opts = append(opts, kubernetes.WithServiceAccountTokenPath(a.ServiceAccountTokenFile))
}
if a.MountPath != "" {
opts = append(opts, kubernetes.WithMountPath(a.MountPath))
}
auth, err := kubernetes.NewKubernetesAuth(a.Role, opts...)
if err != nil {
return nil, fmt.Errorf("auth.kubernetes: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.kubernetes: %w", err)
}
return s, nil
}
type AuthLDAP struct {
Username string `river:"username,attr"`
Password rivertypes.Secret `river:"password,attr"`
MountPath string `river:"mount_path,attr,optional"`
}
var DefaultAuthLDAP = AuthLDAP{
MountPath: "ldap",
}
func (a *AuthLDAP) UnmarshalRiver(f func(interface{}) error) error {
*a = DefaultAuthLDAP
type authLDAP AuthLDAP
return f((*authLDAP)(a))
}
func (a *AuthLDAP) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
secret := &ldap.Password{FromString: string(a.Password)}
var opts []ldap.LoginOption
if a.MountPath != "" {
opts = append(opts, ldap.WithMountPath(a.MountPath))
}
auth, err := ldap.NewLDAPAuth(a.Username, secret, opts...)
if err != nil {
return nil, fmt.Errorf("auth.ldap: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.ldap: %w", err)
}
return s, nil
}
type AuthUserPass struct {
Username string `river:"username,attr"`
Password rivertypes.Secret `river:"password,attr"`
MountPath string `river:"mount_path,attr,optional"`
}
var DefaultAuthUserPass = AuthUserPass{
MountPath: "userpass",
}
func (a *AuthUserPass) UnmarshalRiver(f func(interface{}) error) error {
*a = DefaultAuthUserPass
type authUserPass AuthUserPass
return f((*authUserPass)(a))
}
func (a *AuthUserPass) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
secret := &userpass.Password{FromString: string(a.Password)}
var opts []userpass.LoginOption
if a.MountPath != "" {
opts = append(opts, userpass.WithMountPath(a.MountPath))
}
auth, err := userpass.NewUserpassAuth(a.Username, secret, opts...)
if err != nil {
return nil, fmt.Errorf("auth.userpass: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.userpass: %w", err)
}
return s, nil
}
type AuthCustom struct {
Path string `river:"path,attr"`
Data map[string]rivertypes.Secret `river:"data,attr"`
}
func (a *AuthCustom) Login(ctx context.Context, client *vault.Client) (*vault.Secret, error) {
data := make(map[string]interface{}, len(a.Data))
for k, v := range a.Data {
data[k] = string(v)
}
return client.Logical().WriteWithContext(ctx, a.Path, data)
}
func (a *AuthCustom) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
s, err := cli.Auth().Login(ctx, a)
if err != nil {
return nil, fmt.Errorf("auth.custom: %w", err)
}
return s, nil
}
type invalidAuth struct {
Name string
}
func (a invalidAuth) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
return nil, fmt.Errorf("unsupported auth method configured")
}