diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 8b0d705ec..5ddaa6655 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -74,6 +74,7 @@ const ( FieldAuthLoginCert = "auth_login_cert" FieldAuthLoginGCP = "auth_login_gcp" FieldAuthLoginKerberos = "auth_login_kerberos" + FieldAuthLoginRadius = "auth_login_radius" FieldIdentity = "identity" FieldSignature = "signature" FieldPKCS7 = "pkcs7" @@ -125,6 +126,11 @@ const ( // EnvVarKRBKeytab path the keytab file. EnvVarKRBKeytab = "KRB_KEYTAB" + // EnvVarRadiusUsername for the Radius auth login + EnvVarRadiusUsername = "RADIUS_USERNAME" + // EnvVarRadiusPassword for the Radius auth login + EnvVarRadiusPassword = "RADIUS_PASSWORD" + /* common mount types */ @@ -139,6 +145,7 @@ const ( MountTypeCert = "cert" MountTypeGCP = "gcp" MountTypeKerberos = "kerberos" + MountTypeRadius = "radius" /* Vault version constants @@ -155,6 +162,7 @@ const ( AuthMethodCert = "cert" AuthMethodGCP = "gcp" AuthMethodKerberos = "kerberos" + AuthMethodRadius = "radius" /* misc. path related constants diff --git a/internal/provider/auth.go b/internal/provider/auth.go index 0e28b1476..330f64e02 100644 --- a/internal/provider/auth.go +++ b/internal/provider/auth.go @@ -130,6 +130,21 @@ func (l *AuthLoginCommon) init(d *schema.ResourceData) (string, map[string]inter return path, params, nil } +func (l *AuthLoginCommon) checkRequiredFields(d *schema.ResourceData, required ...string) error { + var missing []string + for _, f := range required { + if _, ok := l.getOk(d, f); !ok { + missing = append(missing, f) + } + } + + if len(missing) > 0 { + return fmt.Errorf("required fields are unset: %v", missing) + } + + return nil +} + func (l *AuthLoginCommon) getOk(d *schema.ResourceData, field string) (interface{}, bool) { return d.GetOk(fmt.Sprintf("%s.0.%s", l.authField, field)) } @@ -153,6 +168,8 @@ func GetAuthLogin(r *schema.ResourceData) (AuthLogin, error) { l = &AuthLoginGCP{} case consts.FieldAuthLoginKerberos: l = &AuthLoginKerberos{} + case consts.FieldAuthLoginRadius: + l = &AuthLoginRadius{} default: return nil, nil } diff --git a/internal/provider/auth_radius.go b/internal/provider/auth_radius.go new file mode 100644 index 000000000..8863fb234 --- /dev/null +++ b/internal/provider/auth_radius.go @@ -0,0 +1,82 @@ +package provider + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/vault/api" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" +) + +// GetRadiusLoginSchema for the radius authentication engine. +func GetRadiusLoginSchema(authField string) *schema.Schema { + return getLoginSchema( + authField, + "Login to vault using the radius method", + GetRadiusLoginSchemaResource, + ) +} + +// GetRadiusLoginSchemaResource for the radius authentication engine. +func GetRadiusLoginSchemaResource(_ string) *schema.Resource { + return mustAddLoginSchema(&schema.Resource{ + Schema: map[string]*schema.Schema{ + consts.FieldUsername: { + Type: schema.TypeString, + Description: "The Radius username.", + Required: true, + DefaultFunc: schema.EnvDefaultFunc(consts.EnvVarRadiusUsername, nil), + }, + consts.FieldPassword: { + Type: schema.TypeString, + Required: true, + Description: "The Radius password for username.", + DefaultFunc: schema.EnvDefaultFunc(consts.EnvVarRadiusPassword, nil), + }, + }, + }, consts.MountTypeRadius) +} + +type AuthLoginRadius struct { + AuthLoginCommon +} + +// MountPath for the radius authentication engine. +func (l *AuthLoginRadius) MountPath() string { + if l.mount == "" { + return l.Method() + } + return l.mount +} + +// LoginPath for the radius authentication engine. +func (l *AuthLoginRadius) LoginPath() string { + return fmt.Sprintf("auth/%s/login", l.MountPath()) +} + +func (l *AuthLoginRadius) Init(d *schema.ResourceData, authField string) error { + if err := l.AuthLoginCommon.Init(d, authField); err != nil { + return err + } + + if err := l.checkRequiredFields(d, consts.FieldUsername, consts.FieldPassword); err != nil { + return err + } + + return nil +} + +// Method name for the radius authentication engine. +func (l *AuthLoginRadius) Method() string { + return consts.AuthMethodRadius +} + +// Login using the radius authentication engine. +func (l *AuthLoginRadius) Login(client *api.Client) (*api.Secret, error) { + if !l.initialized { + return nil, fmt.Errorf("auth login not initialized") + } + + return l.login(client, l.LoginPath(), l.copyParams(consts.FieldNamespace, consts.FieldMount)) +} diff --git a/internal/provider/auth_radius_test.go b/internal/provider/auth_radius_test.go new file mode 100644 index 000000000..a90c80440 --- /dev/null +++ b/internal/provider/auth_radius_test.go @@ -0,0 +1,201 @@ +package provider + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/vault/api" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" +) + +func TestAuthLoginRadius_Init(t *testing.T) { + tests := []struct { + name string + authField string + raw map[string]interface{} + wantErr bool + expectParams map[string]interface{} + expectErr error + }{ + { + name: "basic", + authField: consts.FieldAuthLoginRadius, + raw: map[string]interface{}{ + consts.FieldAuthLoginRadius: []interface{}{ + map[string]interface{}{ + consts.FieldNamespace: "ns1", + consts.FieldUsername: "alice", + consts.FieldPassword: "password1", + }, + }, + }, + expectParams: map[string]interface{}{ + consts.FieldNamespace: "ns1", + consts.FieldMount: consts.MountTypeRadius, + consts.FieldUsername: "alice", + consts.FieldPassword: "password1", + }, + wantErr: false, + }, + { + name: "error-missing-resource", + authField: consts.FieldAuthLoginRadius, + expectParams: nil, + wantErr: true, + expectErr: fmt.Errorf("resource data missing field %q", consts.FieldAuthLoginRadius), + }, + { + name: "error-missing-required", + authField: consts.FieldAuthLoginRadius, + raw: map[string]interface{}{ + consts.FieldAuthLoginRadius: []interface{}{ + map[string]interface{}{ + consts.FieldUsername: "alice", + }, + }, + }, + expectParams: nil, + wantErr: true, + expectErr: fmt.Errorf("required fields are unset: %v", []string{ + consts.FieldPassword, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := map[string]*schema.Schema{ + tt.authField: GetRadiusLoginSchema(tt.authField), + } + + d := schema.TestResourceDataRaw(t, s, tt.raw) + l := &AuthLoginRadius{} + err := l.Init(d, tt.authField) + if (err != nil) != tt.wantErr { + t.Fatalf("Init() error = %v, wantErr %v", err, tt.wantErr) + } + + if err != nil { + if tt.expectErr != nil { + if !reflect.DeepEqual(tt.expectErr, err) { + t.Errorf("Init() expected error %#v, actual %#v", tt.expectErr, err) + } + } + } else { + if !reflect.DeepEqual(tt.expectParams, l.params) { + t.Errorf("Init() expected params %#v, actual %#v", tt.expectParams, l.params) + } + } + }) + } +} + +func TestAuthLoginRadius_LoginPath(t *testing.T) { + type fields struct { + AuthLoginCommon AuthLoginCommon + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "default", + fields: fields{ + AuthLoginCommon: AuthLoginCommon{ + params: map[string]interface{}{ + consts.FieldUsername: "alice", + consts.FieldPassword: "password1", + }, + }, + }, + want: "auth/radius/login", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &AuthLoginRadius{ + AuthLoginCommon: tt.fields.AuthLoginCommon, + } + if got := l.LoginPath(); got != tt.want { + t.Errorf("LoginPath() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthLoginRadius_Login(t *testing.T) { + handlerFunc := func(t *testLoginHandler, w http.ResponseWriter, req *http.Request) { + m, err := json.Marshal( + &api.Secret{}, + ) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + if _, err := w.Write(m); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + } + + tests := []authLoginTest{ + { + name: "basic", + authLogin: &AuthLoginRadius{ + AuthLoginCommon: AuthLoginCommon{ + authField: consts.FieldAuthLoginRadius, + params: map[string]interface{}{ + consts.FieldUsername: "alice", + consts.FieldPassword: "password1", + }, + initialized: true, + }, + }, + handler: &testLoginHandler{ + handlerFunc: handlerFunc, + }, + expectReqCount: 1, + expectReqPaths: []string{"/v1/auth/radius/login"}, + expectReqParams: []map[string]interface{}{ + { + consts.FieldUsername: "alice", + consts.FieldPassword: "password1", + }, + }, + want: &api.Secret{}, + wantErr: false, + }, + { + name: "error-uninitialized", + authLogin: &AuthLoginRadius{ + AuthLoginCommon: AuthLoginCommon{ + authField: consts.FieldAuthLoginRadius, + params: map[string]interface{}{ + consts.FieldUsername: "alice", + consts.FieldPassword: "password1", + }, + initialized: false, + }, + }, + handler: &testLoginHandler{ + handlerFunc: handlerFunc, + }, + expectReqCount: 0, + want: nil, + wantErr: true, + expectErr: fmt.Errorf("auth login not initialized"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testAuthLogin(t, tt) + }) + } +} diff --git a/internal/provider/auth_test.go b/internal/provider/auth_test.go index 6f5594782..b6d477f3c 100644 --- a/internal/provider/auth_test.go +++ b/internal/provider/auth_test.go @@ -22,6 +22,7 @@ type authLoginTest struct { expectReqParams []map[string]interface{} expectReqPaths []string wantErr bool + expectErr error skipFunc func(t *testing.T) } @@ -88,6 +89,12 @@ func testAuthLogin(t *testing.T, tt authLoginTest) { return } + if err != nil && tt.expectErr != nil { + if !reflect.DeepEqual(tt.expectErr, err) { + t.Errorf("Login() expected error %#v, actual %#v", tt.expectErr, err) + } + } + if tt.expectReqCount != tt.handler.requestCount { t.Errorf("Login() expected %d requests, actual %d", tt.expectReqCount, tt.handler.requestCount) } diff --git a/vault/provider.go b/vault/provider.go index 91b4c408c..2e0afe928 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -201,6 +201,8 @@ func Provider() *schema.Provider { f = provider.GetGCPLoginSchema case consts.FieldAuthLoginKerberos: f = provider.GetKerberosLoginSchema + case consts.FieldAuthLoginRadius: + f = provider.GetRadiusLoginSchema default: continue } diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index f7ef19f19..1f6d10022 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -135,6 +135,8 @@ variables in order to keep credential information out of the configuration. * `auth_login_kerberos` - (Optional) Utilizes the `kerberos` authentication engine. *[See usage details below.](#kerberos)* +* `auth_login_radius` - (Optional) Utilizes the `radius` authentication engine. *[See usage details below.](#radius)* + * `auth_login` - (Optional) A configuration block, described below, that attempts to authenticate using the `auth//login` path to acquire a token which Terraform will use. Terraform still issues itself @@ -373,6 +375,27 @@ The `auth_login_kerberos` configuration block accepts the following arguments: The following fields are required when token is not specified: `username`, `service`, `realm`, `krb5conf_path`, `keytab_path`* +### Radius + +Provides support for authenticating to Vault using the Radius Auth engine. + +*For more details see: +[Radius Auth Method (API)](https://www.vaultproject.io/api-docs/auth/radius#radius-auth-method-api)* + + +The `auth_login_radius` configuration block accepts the following arguments: + +* `namespace` - (Optional) The path to the namespace that has the mounted auth method. + This defaults to the root namespace. Cannot contain any leading or trailing slashes. + *Available only for Vault Enterprise*. + +* `mount` - (Optional) The name of the authentication engine mount. + Default: `radius` + +* `username` - (Required) The username to Radius username to login into Vault with. + +* `password` - (Required) The password for the Radius `username` to login into Vault with. + ### Generic Provides support for path based authentication to Vault.