diff --git a/changelog/12787.txt b/changelog/12787.txt new file mode 100644 index 000000000..f7826b21b --- /dev/null +++ b/changelog/12787.txt @@ -0,0 +1,3 @@ +```release-note:improvement +core: Add support to list password policies at `sys/policies/password` +``` diff --git a/vault/logical_system.go b/vault/logical_system.go index 783fce4c1..7f86494b2 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -2345,6 +2345,16 @@ const ( maxPasswordLength = 100 ) +// handlePoliciesPasswordList returns the list of password policies +func (*SystemBackend) handlePoliciesPasswordList(ctx context.Context, req *logical.Request, data *framework.FieldData) (resp *logical.Response, err error) { + keys, err := req.Storage.List(ctx, "password_policy/") + if err != nil { + return nil, err + } + + return logical.ListResponse(keys), nil +} + // handlePoliciesPasswordSet saves/updates password policies func (*SystemBackend) handlePoliciesPasswordSet(ctx context.Context, req *logical.Request, data *framework.FieldData) (resp *logical.Response, err error) { policyName := data.Get("name").(string) diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 4d41b978c..61ffca07b 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -1615,6 +1615,17 @@ func (b *SystemBackend) policyPaths() []*framework.Path { HelpDescription: strings.TrimSpace(sysHelp["policy"][1]), }, + { + Pattern: "policies/password/?$", + + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: b.handlePoliciesPasswordList, + Summary: "List the existing password policies.", + }, + }, + }, + { Pattern: "policies/password/(?P.+)/generate$", diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index f46f39510..edafcb6d9 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -3645,6 +3645,107 @@ func TestHandlePoliciesPasswordDelete(t *testing.T) { } } +func TestHandlePoliciesPasswordList(t *testing.T) { + type testCase struct { + storage logical.Storage + + expectErr bool + expectedResp *logical.Response + } + + tests := map[string]testCase{ + "no policies": { + storage: new(logical.InmemStorage), + + expectedResp: &logical.Response{ + Data: map[string]interface{}{}, + }, + }, + "one policy": { + storage: makeStorage(t, + &logical.StorageEntry{ + Key: getPasswordPolicyKey("testpolicy"), + Value: toJson(t, + passwordPolicyConfig{ + HCLPolicy: "length = 18\n" + + "rule \"charset\" {\n" + + " charset=\"ABCDEFGHIJ\"\n" + + "}", + }), + }, + ), + + expectedResp: &logical.Response{ + Data: map[string]interface{}{ + "keys": []string{"testpolicy"}, + }, + }, + }, + "two policies": { + storage: makeStorage(t, + &logical.StorageEntry{ + Key: getPasswordPolicyKey("testpolicy"), + Value: toJson(t, + passwordPolicyConfig{ + HCLPolicy: "length = 18\n" + + "rule \"charset\" {\n" + + " charset=\"ABCDEFGHIJ\"\n" + + "}", + }), + }, + &logical.StorageEntry{ + Key: getPasswordPolicyKey("unrelated_policy"), + Value: toJson(t, + passwordPolicyConfig{ + HCLPolicy: "length = 20\n" + + "rule \"charset\" {\n" + + " charset=\"abcdefghij\"\n" + + "}", + }), + }, + ), + + expectedResp: &logical.Response{ + Data: map[string]interface{}{ + "keys": []string{ + "testpolicy", + "unrelated_policy", + }, + }, + }, + }, + "storage failure": { + storage: new(logical.InmemStorage).FailList(true), + + expectErr: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + req := &logical.Request{ + Storage: test.storage, + } + + b := &SystemBackend{} + + actualResp, err := b.handlePoliciesPasswordList(ctx, req, nil) + if test.expectErr && err == nil { + t.Fatalf("err expected, got nil") + } + if !test.expectErr && err != nil { + t.Fatalf("no error expected, got: %s", err) + } + if !reflect.DeepEqual(actualResp, test.expectedResp) { + t.Fatalf("Actual response: %#v\nExpected response: %#v", actualResp, test.expectedResp) + } + }) + } +} + func TestHandlePoliciesPasswordGenerate(t *testing.T) { t.Run("errors", func(t *testing.T) { type testCase struct { diff --git a/website/content/api-docs/system/policies-password.mdx b/website/content/api-docs/system/policies-password.mdx index 0b5aabbcf..020a0a7f5 100644 --- a/website/content/api-docs/system/policies-password.mdx +++ b/website/content/api-docs/system/policies-password.mdx @@ -77,6 +77,43 @@ rule "charset" { $ vault write sys/policies/password/my-policy policy=@my-policy.hcl ``` +## List Password Policies + +This endpoints list the password policies. + +| Method | Path | +| :------ | :--------------------------------- | +| `LIST` | `/sys/policies/password` | +| `GET` | `/sys/policies/password?list=true` | + +### Sample Request + +```shell +$ curl \ + --header "X-Vault-Token: ..." \ + --request LIST \ + http://127.0.0.1:8200/v1/sys/policies/password +``` + +### Sample Response + +```json +{ + "request_id": "58e2540f-8c51-6390-46de-38e279e75468", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "keys": [ + "my-policy" + ] + }, + "wrap_info": null, + "warnings": null, + "auth": null +} +``` + ## Read Password Policy This endpoint retrieves information about the named password policy.