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:
Hamid Ghaf 2022-05-17 14:54:16 -04:00 committed by GitHub
parent 4e9e9b7eda
commit 364f8789cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 209 additions and 9 deletions

3
changelog/15248.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
auth: Globally scoped Login MFA method Get/List endpoints
```

View File

@ -45,6 +45,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
testCases := []struct { testCases := []struct {
methodName string methodName string
invalidType string
configData map[string]interface{} configData map[string]interface{}
keyToUpdate string keyToUpdate string
valueToUpdate string valueToUpdate string
@ -53,6 +54,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
}{ }{
{ {
"totp", "totp",
"duo",
map[string]interface{}{ map[string]interface{}{
"issuer": "yCorp", "issuer": "yCorp",
"period": 10, "period": 10,
@ -70,6 +72,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
}, },
{ {
"duo", "duo",
"totp",
map[string]interface{}{ map[string]interface{}{
"mount_accessor": mountAccessor, "mount_accessor": mountAccessor,
"secret_key": "lol-secret", "secret_key": "lol-secret",
@ -83,6 +86,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
}, },
{ {
"okta", "okta",
"pingid",
map[string]interface{}{ map[string]interface{}{
"mount_accessor": mountAccessor, "mount_accessor": mountAccessor,
"base_url": "example.com", "base_url": "example.com",
@ -96,6 +100,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
}, },
{ {
"pingid", "pingid",
"okta",
map[string]interface{}{ map[string]interface{}{
"mount_accessor": mountAccessor, "mount_accessor": mountAccessor,
"settings_file_base64": "I0F1dG8tR2VuZXJhdGVkIGZyb20gUGluZ09uZSwgZG93bmxvYWRlZCBieSBpZD1bU1NPXSBlbWFpbD1baGFtaWRAaGFzaGljb3JwLmNvbV0KI1dlZCBEZWMgMTUgMTM6MDg6NDQgTVNUIDIwMjEKdXNlX2Jhc2U2NF9rZXk9YlhrdGMyVmpjbVYwTFd0bGVRPT0KdXNlX3NpZ25hdHVyZT10cnVlCnRva2VuPWxvbC10b2tlbgppZHBfdXJsPWh0dHBzOi8vaWRweG55bDNtLnBpbmdpZGVudGl0eS5jb20vcGluZ2lkCm9yZ19hbGlhcz1sb2wtb3JnLWFsaWFzCmFkbWluX3VybD1odHRwczovL2lkcHhueWwzbS5waW5naWRlbnRpdHkuY29tL3BpbmdpZAphdXRoZW50aWNhdG9yX3VybD1odHRwczovL2F1dGhlbnRpY2F0b3IucGluZ29uZS5jb20vcGluZ2lkL3BwbQ==", "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 // delete it
_, err = client.Logical().Delete(myNewPath) _, err = client.Logical().Delete(myNewPath)
if err != nil { 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 // TestLoginMFA_LoginEnforcement_CRUD tests creating/reading/updating/deleting a login enforcement config
func TestLoginMFA_LoginEnforcement_CRUD(t *testing.T) { func TestLoginMFA_LoginEnforcement_CRUD(t *testing.T) {
cluster := vault.NewTestCluster(t, &vault.CoreConfig{ cluster := vault.NewTestCluster(t, &vault.CoreConfig{

View File

@ -141,6 +141,30 @@ func (i *IdentityStore) paths() []*framework.Path {
func mfaPaths(i *IdentityStore) []*framework.Path { func mfaPaths(i *IdentityStore) []*framework.Path {
return []*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"), Pattern: "mfa/method/totp" + genericOptionalUUIDRegex("method_id"),
Fields: map[string]*framework.FieldSchema{ Fields: map[string]*framework.FieldSchema{
@ -189,7 +213,7 @@ func mfaPaths(i *IdentityStore) []*framework.Path {
}, },
Operations: map[logical.Operation]framework.OperationHandler{ Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{ logical.ReadOperation: &framework.PathOperation{
Callback: i.handleMFAMethodRead, Callback: i.handleMFAMethodTOTPRead,
Summary: "Read the current configuration for the given MFA method", Summary: "Read the current configuration for the given MFA method",
}, },
logical.UpdateOperation: &framework.PathOperation{ logical.UpdateOperation: &framework.PathOperation{
@ -303,7 +327,7 @@ func mfaPaths(i *IdentityStore) []*framework.Path {
}, },
Operations: map[logical.Operation]framework.OperationHandler{ Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{ logical.ReadOperation: &framework.PathOperation{
Callback: i.handleMFAMethodRead, Callback: i.handleMFAMethodOKTARead,
Summary: "Read the current configuration for the given MFA method", Summary: "Read the current configuration for the given MFA method",
}, },
logical.UpdateOperation: &framework.PathOperation{ logical.UpdateOperation: &framework.PathOperation{
@ -359,7 +383,7 @@ func mfaPaths(i *IdentityStore) []*framework.Path {
}, },
Operations: map[logical.Operation]framework.OperationHandler{ Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{ logical.ReadOperation: &framework.PathOperation{
Callback: i.handleMFAMethodRead, Callback: i.handleMFAMethodDuoRead,
Summary: "Read the current configuration for the given MFA method", Summary: "Read the current configuration for the given MFA method",
}, },
logical.UpdateOperation: &framework.PathOperation{ logical.UpdateOperation: &framework.PathOperation{
@ -399,7 +423,7 @@ func mfaPaths(i *IdentityStore) []*framework.Path {
}, },
Operations: map[logical.Operation]framework.OperationHandler{ Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{ logical.ReadOperation: &framework.PathOperation{
Callback: i.handleMFAMethodRead, Callback: i.handleMFAMethodPingIDRead,
Summary: "Read the current configuration for the given MFA method", Summary: "Read the current configuration for the given MFA method",
}, },
logical.UpdateOperation: &framework.PathOperation{ logical.UpdateOperation: &framework.PathOperation{

View File

@ -182,6 +182,15 @@ func (i *IdentityStore) handleMFAMethodListPingID(ctx context.Context, req *logi
return i.handleMFAMethodList(ctx, req, d, mfaMethodTypePingID) 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) { 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) keys, configInfo, err := i.mfaBackend.mfaMethodList(ctx, methodType)
if err != nil { if err != nil {
@ -191,7 +200,27 @@ func (i *IdentityStore) handleMFAMethodList(ctx context.Context, req *logical.Re
return logical.ListResponseWithInfo(keys, configInfo), nil 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) methodID := d.Get("method_id").(string)
if methodID == "" { if methodID == "" {
return logical.ErrorResponse("missing method ID"), nil 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)) { if !(ns.ID == mfaNs.ID || mfaNs.HasParent(ns) || ns.HasParent(mfaNs)) {
return logical.ErrorResponse("request namespace does not match method namespace"), logical.ErrPermissionDenied 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{ return &logical.Response{
Data: respData, Data: respData,
}, nil }, 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) 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) ns, err := namespace.FromContext(ctx)
if err != nil { if err != nil {
return logical.ErrorResponse("failed to retrieve the namespace"), nil return logical.ErrorResponse("failed to retrieve the namespace"), nil
@ -1188,10 +1226,20 @@ func (b *LoginMFABackend) mfaMethodList(ctx context.Context, methodType string)
ws := memdb.NewWatchSet() ws := memdb.NewWatchSet()
txn := b.db.Txn(false) txn := b.db.Txn(false)
// get all the configs for the given type var iter memdb.ResultIterator
iter, err := txn.Get(b.methodTable, "type", methodType) switch {
if err != nil { case methodType == "":
return nil, nil, fmt.Errorf("failed to fetch iterator for login mfa method configs in memdb: %w", err) // 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()) ws.Add(iter.WatchCh())