Globally scoped MFA method Get/List endpoints (#15248)
* Globally scoped MFA method Get/List endpoints * Adding CL * minor changes * removing unwanted information from an error msg
This commit is contained in:
parent
4e9e9b7eda
commit
364f8789cd
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
auth: Globally scoped Login MFA method Get/List endpoints
|
||||
```
|
|
@ -45,6 +45,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
|
|||
|
||||
testCases := []struct {
|
||||
methodName string
|
||||
invalidType string
|
||||
configData map[string]interface{}
|
||||
keyToUpdate string
|
||||
valueToUpdate string
|
||||
|
@ -53,6 +54,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
"totp",
|
||||
"duo",
|
||||
map[string]interface{}{
|
||||
"issuer": "yCorp",
|
||||
"period": 10,
|
||||
|
@ -70,6 +72,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"duo",
|
||||
"totp",
|
||||
map[string]interface{}{
|
||||
"mount_accessor": mountAccessor,
|
||||
"secret_key": "lol-secret",
|
||||
|
@ -83,6 +86,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"okta",
|
||||
"pingid",
|
||||
map[string]interface{}{
|
||||
"mount_accessor": mountAccessor,
|
||||
"base_url": "example.com",
|
||||
|
@ -96,6 +100,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"pingid",
|
||||
"okta",
|
||||
map[string]interface{}{
|
||||
"mount_accessor": mountAccessor,
|
||||
"settings_file_base64": "I0F1dG8tR2VuZXJhdGVkIGZyb20gUGluZ09uZSwgZG93bmxvYWRlZCBieSBpZD1bU1NPXSBlbWFpbD1baGFtaWRAaGFzaGljb3JwLmNvbV0KI1dlZCBEZWMgMTUgMTM6MDg6NDQgTVNUIDIwMjEKdXNlX2Jhc2U2NF9rZXk9YlhrdGMyVmpjbVYwTFd0bGVRPT0KdXNlX3NpZ25hdHVyZT10cnVlCnRva2VuPWxvbC10b2tlbgppZHBfdXJsPWh0dHBzOi8vaWRweG55bDNtLnBpbmdpZGVudGl0eS5jb20vcGluZ2lkCm9yZ19hbGlhcz1sb2wtb3JnLWFsaWFzCmFkbWluX3VybD1odHRwczovL2lkcHhueWwzbS5waW5naWRlbnRpdHkuY29tL3BpbmdpZAphdXRoZW50aWNhdG9yX3VybD1odHRwczovL2F1dGhlbnRpY2F0b3IucGluZ29uZS5jb20vcGluZ2lkL3BwbQ==",
|
||||
|
@ -165,6 +170,23 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// read the id on another MFA type endpoint should fail
|
||||
invalidPath := fmt.Sprintf("identity/mfa/method/%s/%s", tc.invalidType, methodId)
|
||||
resp, err = client.Logical().Read(invalidPath)
|
||||
if err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// read the id globally should succeed
|
||||
globalPath := fmt.Sprintf("identity/mfa/method/%s", methodId)
|
||||
resp, err = client.Logical().Read(globalPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.Data["id"] != methodId {
|
||||
t.Fatal("expected response id to match existing method id but it didn't")
|
||||
}
|
||||
|
||||
// delete it
|
||||
_, err = client.Logical().Delete(myNewPath)
|
||||
if err != nil {
|
||||
|
@ -180,6 +202,109 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestLoginMFA_ListAllMFAConfigs tests listing all configs globally
|
||||
func TestLoginMFA_ListAllMFAConfigsGlobally(t *testing.T) {
|
||||
cluster := vault.NewTestCluster(t, &vault.CoreConfig{
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"userpass": userpass.Factory,
|
||||
},
|
||||
}, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
core := cluster.Cores[0].Core
|
||||
vault.TestWaitActive(t, core)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Enable userpass authentication
|
||||
err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
|
||||
Type: "userpass",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to enable userpass auth: %v", err)
|
||||
}
|
||||
|
||||
auths, err := client.Sys().ListAuth()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mountAccessor := auths["userpass/"].Accessor
|
||||
|
||||
mfaConfigs := []struct {
|
||||
methodType string
|
||||
configData map[string]interface{}
|
||||
}{
|
||||
{
|
||||
"totp",
|
||||
map[string]interface{}{
|
||||
"issuer": "yCorp",
|
||||
"period": 10,
|
||||
"algorithm": "SHA1",
|
||||
"digits": 6,
|
||||
"skew": 1,
|
||||
"key_size": uint(10),
|
||||
"qr_size": 100,
|
||||
"max_validation_attempts": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"duo",
|
||||
map[string]interface{}{
|
||||
"mount_accessor": mountAccessor,
|
||||
"secret_key": "lol-secret",
|
||||
"integration_key": "integration-key",
|
||||
"api_hostname": "some-hostname",
|
||||
},
|
||||
},
|
||||
{
|
||||
"okta",
|
||||
map[string]interface{}{
|
||||
"mount_accessor": mountAccessor,
|
||||
"base_url": "example.com",
|
||||
"org_name": "my-org",
|
||||
"api_token": "lol-token",
|
||||
},
|
||||
},
|
||||
{
|
||||
"pingid",
|
||||
map[string]interface{}{
|
||||
"mount_accessor": mountAccessor,
|
||||
"settings_file_base64": "I0F1dG8tR2VuZXJhdGVkIGZyb20gUGluZ09uZSwgZG93bmxvYWRlZCBieSBpZD1bU1NPXSBlbWFpbD1baGFtaWRAaGFzaGljb3JwLmNvbV0KI1dlZCBEZWMgMTUgMTM6MDg6NDQgTVNUIDIwMjEKdXNlX2Jhc2U2NF9rZXk9YlhrdGMyVmpjbVYwTFd0bGVRPT0KdXNlX3NpZ25hdHVyZT10cnVlCnRva2VuPWxvbC10b2tlbgppZHBfdXJsPWh0dHBzOi8vaWRweG55bDNtLnBpbmdpZGVudGl0eS5jb20vcGluZ2lkCm9yZ19hbGlhcz1sb2wtb3JnLWFsaWFzCmFkbWluX3VybD1odHRwczovL2lkcHhueWwzbS5waW5naWRlbnRpdHkuY29tL3BpbmdpZAphdXRoZW50aWNhdG9yX3VybD1odHRwczovL2F1dGhlbnRpY2F0b3IucGluZ29uZS5jb20vcGluZ2lkL3BwbQ==",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var methodIDs []interface{}
|
||||
for _, method := range mfaConfigs {
|
||||
// create a new method config
|
||||
myPath := fmt.Sprintf("identity/mfa/method/%s", method.methodType)
|
||||
resp, err := client.Logical().Write(myPath, method.configData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
methodId := resp.Data["method_id"]
|
||||
if methodId == "" {
|
||||
t.Fatal("method id is empty")
|
||||
}
|
||||
methodIDs = append(methodIDs, methodId)
|
||||
}
|
||||
// listing should show it
|
||||
resp, err := client.Logical().List("identity/mfa/method")
|
||||
if err != nil || resp == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(resp.Data["keys"].([]interface{})) != len(methodIDs) {
|
||||
t.Fatalf("global list request did not return all MFA method IDs")
|
||||
}
|
||||
if len(resp.Data["key_info"].(map[string]interface{})) != len(methodIDs) {
|
||||
t.Fatal("global list request did not return all MFA method configurations")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoginMFA_LoginEnforcement_CRUD tests creating/reading/updating/deleting a login enforcement config
|
||||
func TestLoginMFA_LoginEnforcement_CRUD(t *testing.T) {
|
||||
cluster := vault.NewTestCluster(t, &vault.CoreConfig{
|
||||
|
|
|
@ -141,6 +141,30 @@ func (i *IdentityStore) paths() []*framework.Path {
|
|||
|
||||
func mfaPaths(i *IdentityStore) []*framework.Path {
|
||||
return []*framework.Path{
|
||||
{
|
||||
Pattern: "mfa/method" + genericOptionalUUIDRegex("method_id"),
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"method_id": {
|
||||
Type: framework.TypeString,
|
||||
Description: `The unique identifier for this MFA method.`,
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: i.handleMFAMethodReadGlobal,
|
||||
Summary: "Read the current configuration for the given ID regardless of the MFA method type",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Pattern: "mfa/method/?$",
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ListOperation: &framework.PathOperation{
|
||||
Callback: i.handleMFAMethodListGlobal,
|
||||
Summary: "List MFA method configurations for all MFA methods",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Pattern: "mfa/method/totp" + genericOptionalUUIDRegex("method_id"),
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
|
@ -189,7 +213,7 @@ func mfaPaths(i *IdentityStore) []*framework.Path {
|
|||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: i.handleMFAMethodRead,
|
||||
Callback: i.handleMFAMethodTOTPRead,
|
||||
Summary: "Read the current configuration for the given MFA method",
|
||||
},
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
|
@ -303,7 +327,7 @@ func mfaPaths(i *IdentityStore) []*framework.Path {
|
|||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: i.handleMFAMethodRead,
|
||||
Callback: i.handleMFAMethodOKTARead,
|
||||
Summary: "Read the current configuration for the given MFA method",
|
||||
},
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
|
@ -359,7 +383,7 @@ func mfaPaths(i *IdentityStore) []*framework.Path {
|
|||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: i.handleMFAMethodRead,
|
||||
Callback: i.handleMFAMethodDuoRead,
|
||||
Summary: "Read the current configuration for the given MFA method",
|
||||
},
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
|
@ -399,7 +423,7 @@ func mfaPaths(i *IdentityStore) []*framework.Path {
|
|||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: i.handleMFAMethodRead,
|
||||
Callback: i.handleMFAMethodPingIDRead,
|
||||
Summary: "Read the current configuration for the given MFA method",
|
||||
},
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
|
|
|
@ -182,6 +182,15 @@ func (i *IdentityStore) handleMFAMethodListPingID(ctx context.Context, req *logi
|
|||
return i.handleMFAMethodList(ctx, req, d, mfaMethodTypePingID)
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleMFAMethodListGlobal(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
keys, configInfo, err := i.mfaBackend.mfaMethodList(ctx, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logical.ListResponseWithInfo(keys, configInfo), nil
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleMFAMethodList(ctx context.Context, req *logical.Request, d *framework.FieldData, methodType string) (*logical.Response, error) {
|
||||
keys, configInfo, err := i.mfaBackend.mfaMethodList(ctx, methodType)
|
||||
if err != nil {
|
||||
|
@ -191,7 +200,27 @@ func (i *IdentityStore) handleMFAMethodList(ctx context.Context, req *logical.Re
|
|||
return logical.ListResponseWithInfo(keys, configInfo), nil
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleMFAMethodRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
func (i *IdentityStore) handleMFAMethodTOTPRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
return i.handleMFAMethodReadCommon(ctx, req, d, mfaMethodTypeTOTP)
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleMFAMethodOKTARead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
return i.handleMFAMethodReadCommon(ctx, req, d, mfaMethodTypeOkta)
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleMFAMethodDuoRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
return i.handleMFAMethodReadCommon(ctx, req, d, mfaMethodTypeDuo)
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleMFAMethodPingIDRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
return i.handleMFAMethodReadCommon(ctx, req, d, mfaMethodTypePingID)
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleMFAMethodReadGlobal(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
return i.handleMFAMethodReadCommon(ctx, req, d, "")
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleMFAMethodReadCommon(ctx context.Context, req *logical.Request, d *framework.FieldData, methodType string) (*logical.Response, error) {
|
||||
methodID := d.Get("method_id").(string)
|
||||
if methodID == "" {
|
||||
return logical.ErrorResponse("missing method ID"), nil
|
||||
|
@ -220,6 +249,11 @@ func (i *IdentityStore) handleMFAMethodRead(ctx context.Context, req *logical.Re
|
|||
if !(ns.ID == mfaNs.ID || mfaNs.HasParent(ns) || ns.HasParent(mfaNs)) {
|
||||
return logical.ErrorResponse("request namespace does not match method namespace"), logical.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if methodType != "" && respData["type"] != methodType {
|
||||
return logical.ErrorResponse("failed to find the method ID under MFA type %s.", methodType), nil
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: respData,
|
||||
}, nil
|
||||
|
@ -460,6 +494,10 @@ func (i *IdentityStore) handleLoginMFAAdminDestroyUpdate(ctx context.Context, re
|
|||
return nil, fmt.Errorf("configuration for method ID %q does not contain an identifier", methodID)
|
||||
}
|
||||
|
||||
if mConfig.Type != mfaMethodTypeTOTP {
|
||||
return nil, fmt.Errorf("method ID does not match TOTP type")
|
||||
}
|
||||
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("failed to retrieve the namespace"), nil
|
||||
|
@ -1188,11 +1226,21 @@ func (b *LoginMFABackend) mfaMethodList(ctx context.Context, methodType string)
|
|||
ws := memdb.NewWatchSet()
|
||||
txn := b.db.Txn(false)
|
||||
|
||||
// get all the configs for the given type
|
||||
iter, err := txn.Get(b.methodTable, "type", methodType)
|
||||
var iter memdb.ResultIterator
|
||||
switch {
|
||||
case methodType == "":
|
||||
// get all the configs
|
||||
iter, err = txn.Get(b.methodTable, "id")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to fetch iterator for login mfa method configs in memdb: %w", err)
|
||||
}
|
||||
default:
|
||||
// get all the configs for the given type
|
||||
iter, err = txn.Get(b.methodTable, "type", methodType)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to fetch iterator for login mfa method configs in memdb: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
ws.Add(iter.WatchCh())
|
||||
|
||||
|
|
Loading…
Reference in New Issue