Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Globally scoped MFA method Get/List endpoints #15248

Merged
merged 5 commits into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/15248.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
auth: Globally scoped Login MFA method Get/List endpoints
```
125 changes: 125 additions & 0 deletions vault/external_tests/mfa/login_mfa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,6 +54,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
}{
{
"totp",
"duo",
map[string]interface{}{
"issuer": "yCorp",
"period": 10,
Expand All @@ -70,6 +72,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
},
{
"duo",
"totp",
map[string]interface{}{
"mount_accessor": mountAccessor,
"secret_key": "lol-secret",
Expand All @@ -83,6 +86,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
},
{
"okta",
"pingid",
map[string]interface{}{
"mount_accessor": mountAccessor,
"base_url": "example.com",
Expand All @@ -96,6 +100,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
},
{
"pingid",
"okta",
map[string]interface{}{
"mount_accessor": mountAccessor,
"settings_file_base64": "I0F1dG8tR2VuZXJhdGVkIGZyb20gUGluZ09uZSwgZG93bmxvYWRlZCBieSBpZD1bU1NPXSBlbWFpbD1baGFtaWRAaGFzaGljb3JwLmNvbV0KI1dlZCBEZWMgMTUgMTM6MDg6NDQgTVNUIDIwMjEKdXNlX2Jhc2U2NF9rZXk9YlhrdGMyVmpjbVYwTFd0bGVRPT0KdXNlX3NpZ25hdHVyZT10cnVlCnRva2VuPWxvbC10b2tlbgppZHBfdXJsPWh0dHBzOi8vaWRweG55bDNtLnBpbmdpZGVudGl0eS5jb20vcGluZ2lkCm9yZ19hbGlhcz1sb2wtb3JnLWFsaWFzCmFkbWluX3VybD1odHRwczovL2lkcHhueWwzbS5waW5naWRlbnRpdHkuY29tL3BpbmdpZAphdXRoZW50aWNhdG9yX3VybD1odHRwczovL2F1dGhlbnRpY2F0b3IucGluZ29uZS5jb20vcGluZ2lkL3BwbQ==",
Expand Down Expand Up @@ -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 {
Expand All @@ -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{
Expand Down
32 changes: 28 additions & 4 deletions vault/identity_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down
58 changes: 53 additions & 5 deletions vault/login_mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,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 {
Expand All @@ -170,7 +179,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
Expand Down Expand Up @@ -199,6 +228,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. Method ID belongs to %s MFA type", methodType, respData["type"]), nil
}
hghaf099 marked this conversation as resolved.
Show resolved Hide resolved

return &logical.Response{
Data: respData,
}, nil
Expand Down Expand Up @@ -439,6 +473,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
Expand Down Expand Up @@ -1054,10 +1092,20 @@ 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)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch iterator for login mfa method configs in memdb: %w", err)
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())
Expand Down