diff --git a/CHANGELOG.md b/CHANGELOG.md index 78c9dd1c97..74fd34a7ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,23 +8,36 @@ DEPRECATIONS/CHANGES: FEATURES: + * Azure Key Vault Auto Unseal/Seal Wrap Support (Enterprise): Azure Key Vault + can now be used a support seal for Auto Unseal and Seal Wrapping. * Cert auth CIDR restrictions: When using the `cert` auth method you can now limit authentication to specific CIDRs; these will also be encoded in resultant tokens to limit their use. + * Userpass auth CIDR restrictions: When using the `userpass` auth method you + can now limit authentication to specific CIDRs; these will also be encoded + in resultant tokens to limit their use. + * Vault Browser CLI: The UI now supports usage of read/write/list/delete + commands in a CLI that can be accessed from the nav bar. Complex inputs such + as JSON files are not currently supported. This surfaces features otherwise + unsupported in Vault's UI. IMPROVEMENTS: * api: Close renewer's doneCh when the renewer is stopped, so that programs expecting a final value through doneCh behave correctly [GH-4472] + * auth/cert: Break out `allowed_names` into component parts and add + `allowed_uri_sans` [GH-4231] * cli: `vault login` now supports a `-no-print` flag to suppress printing token information but still allow storing into the token helper [GH-4454] - * core/pkcs11 (enterprise): Add support for CKM_AES_CBS_PAD, CKM_RSA_PKCS, and + * core/pkcs11 (enterprise): Add support for CKM_AES_CBC_PAD, CKM_RSA_PKCS, and CKM_RSA_PKCS_OAEP mechanisms * core/pkcs11 (enterprise): HSM slots can now be selected by token label instead of just slot number - * core/seal (enterprise): Lazily rewrap data when seal keys are rotated * expiration: Allow revoke-prefix and revoke-force to work on single leases as well as prefixes [GH-4450] + * ui: wrapping lookup now distplays the path [GH-4644] + * ui: Identity interface now has more inline actions to make editing and adding + aliases to an entity or group easier [GH-4502] BUG FIXES: @@ -37,12 +50,25 @@ BUG FIXES: require `authorized_addrs` to be set [GH-4065] * core: Fix panic when certain combinations of policy paths and allowed/denied parameters were used [GH-4582] + * secret/gcp: Make `bound_region` able to use short names * secret/kv: Fix response wrapping for KV v2 [GH-4511] + * secret/kv: Fix address flag not being honored correctly [GH-4617] + * secret/pki: Fix `safety_buffer` for tidy being allowed to be negative, + clearing all certs [GH-4641] * secret/pki: Fix `key_type` not being allowed to be set to `any` [GH-4595] * secret/pki: Fix path length parameter being ignored when using `use_csr_values` and signing an intermediate CA cert [GH-4459] * storage/dynamodb: Fix listing when one child is left within a nested path [GH-4570] + * ui: Fix HMAC algorithm in transit [GH-4604] + * ui: Fix unwrap of auth responses via the UI's unwrap tool [GH-4611] + * ui (enterprise): Fix parsing of version string that blocked some users from seeing + enterprise-specific pages in the UI [GH-4547] + * replication: Fix error while running plugins on a newly created replication + secondary + * replication: Fix issue with token store lookups after a secondary's mount table + is invalidated. + * replication: Improve startup time when a large merkle index is in use. ## 0.10.1/0.9.7 (April 25th, 2018) diff --git a/api/client.go b/api/client.go index 8f5a298682..ce10fff141 100644 --- a/api/client.go +++ b/api/client.go @@ -388,11 +388,12 @@ func (c *Client) SetAddress(addr string) error { c.modifyLock.Lock() defer c.modifyLock.Unlock() - var err error - if c.addr, err = url.Parse(addr); err != nil { + parsedAddr, err := url.Parse(addr) + if err != nil { return errwrap.Wrapf("failed to set address: {{err}}", err) } + c.addr = parsedAddr return nil } @@ -411,7 +412,8 @@ func (c *Client) SetLimiter(rateLimit float64, burst int) { c.modifyLock.RLock() c.config.modifyLock.Lock() defer c.config.modifyLock.Unlock() - defer c.modifyLock.RUnlock() + c.modifyLock.RUnlock() + c.config.Limiter = rate.NewLimiter(rate.Limit(rateLimit), burst) } @@ -544,14 +546,20 @@ func (c *Client) SetPolicyOverride(override bool) { // doesn't need to be called externally. func (c *Client) NewRequest(method, requestPath string) *Request { c.modifyLock.RLock() - defer c.modifyLock.RUnlock() + addr := c.addr + token := c.token + mfaCreds := c.mfaCreds + wrappingLookupFunc := c.wrappingLookupFunc + headers := c.headers + policyOverride := c.policyOverride + c.modifyLock.RUnlock() // if SRV records exist (see https://tools.ietf.org/html/draft-andrews-http-srv-02), lookup the SRV // record and take the highest match; this is not designed for high-availability, just discovery - var host string = c.addr.Host - if c.addr.Port() == "" { + var host string = addr.Host + if addr.Port() == "" { // Internet Draft specifies that the SRV record is ignored if a port is given - _, addrs, err := net.LookupSRV("http", "tcp", c.addr.Hostname()) + _, addrs, err := net.LookupSRV("http", "tcp", addr.Hostname()) if err == nil && len(addrs) > 0 { host = fmt.Sprintf("%s:%d", addrs[0].Target, addrs[0].Port) } @@ -560,12 +568,12 @@ func (c *Client) NewRequest(method, requestPath string) *Request { req := &Request{ Method: method, URL: &url.URL{ - User: c.addr.User, - Scheme: c.addr.Scheme, + User: addr.User, + Scheme: addr.Scheme, Host: host, - Path: path.Join(c.addr.Path, requestPath), + Path: path.Join(addr.Path, requestPath), }, - ClientToken: c.token, + ClientToken: token, Params: make(map[string][]string), } @@ -579,21 +587,19 @@ func (c *Client) NewRequest(method, requestPath string) *Request { lookupPath = requestPath } - req.MFAHeaderVals = c.mfaCreds + req.MFAHeaderVals = mfaCreds - if c.wrappingLookupFunc != nil { - req.WrapTTL = c.wrappingLookupFunc(method, lookupPath) + if wrappingLookupFunc != nil { + req.WrapTTL = wrappingLookupFunc(method, lookupPath) } else { req.WrapTTL = DefaultWrappingLookupFunc(method, lookupPath) } - if c.config.Timeout != 0 { - c.config.HttpClient.Timeout = c.config.Timeout - } - if c.headers != nil { - req.Headers = c.headers + + if headers != nil { + req.Headers = headers } - req.PolicyOverride = c.policyOverride + req.PolicyOverride = policyOverride return req } @@ -602,18 +608,23 @@ func (c *Client) NewRequest(method, requestPath string) *Request { // a Vault server not configured with this client. This is an advanced operation // that generally won't need to be called externally. func (c *Client) RawRequest(r *Request) (*Response, error) { - c.modifyLock.RLock() - c.config.modifyLock.RLock() - defer c.config.modifyLock.RUnlock() - - if c.config.Limiter != nil { - c.config.Limiter.Wait(context.Background()) - } - token := c.token + + c.config.modifyLock.RLock() + limiter := c.config.Limiter + maxRetries := c.config.MaxRetries + backoff := c.config.Backoff + httpClient := c.config.HttpClient + timeout := c.config.Timeout + c.config.modifyLock.RUnlock() + c.modifyLock.RUnlock() + if limiter != nil { + limiter.Wait(context.Background()) + } + // Sanity check the token before potentially erroring from the API idx := strings.IndexFunc(token, func(c rune) bool { return !unicode.IsPrint(c) @@ -632,16 +643,23 @@ START: return nil, fmt.Errorf("nil request created") } - backoff := c.config.Backoff + // Set the timeout, if any + var cancelFunc context.CancelFunc + if timeout != 0 { + var ctx context.Context + ctx, cancelFunc = context.WithTimeout(context.Background(), timeout) + req.Request = req.Request.WithContext(ctx) + } + if backoff == nil { backoff = retryablehttp.LinearJitterBackoff } client := &retryablehttp.Client{ - HTTPClient: c.config.HttpClient, + HTTPClient: httpClient, RetryWaitMin: 1000 * time.Millisecond, RetryWaitMax: 1500 * time.Millisecond, - RetryMax: c.config.MaxRetries, + RetryMax: maxRetries, CheckRetry: retryablehttp.DefaultRetryPolicy, Backoff: backoff, ErrorHandler: retryablehttp.PassthroughErrorHandler, @@ -649,6 +667,9 @@ START: var result *Response resp, err := client.Do(req) + if cancelFunc != nil { + cancelFunc() + } if resp != nil { result = &Response{Response: resp} } diff --git a/api/client_test.go b/api/client_test.go index 970354bab1..5678478ea0 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -7,7 +7,6 @@ import ( "os" "strings" "testing" - "time" ) func init() { @@ -244,22 +243,10 @@ func TestClientTimeoutSetting(t *testing.T) { defer os.Setenv(EnvVaultClientTimeout, oldClientTimeout) config := DefaultConfig() config.ReadEnvironment() - client, err := NewClient(config) + _, err := NewClient(config) if err != nil { t.Fatal(err) } - _ = client.NewRequest("PUT", "/") - if client.config.HttpClient.Timeout != time.Second*10 { - t.Fatalf("error setting client timeout using env variable") - } - - // Setting custom client timeout for a new request - client.SetClientTimeout(time.Second * 20) - _ = client.NewRequest("PUT", "/") - if client.config.HttpClient.Timeout != time.Second*20 { - t.Fatalf("error setting client timeout using SetClientTimeout") - } - } type roundTripperFunc func(*http.Request) (*http.Response, error) diff --git a/builtin/credential/cert/backend_test.go b/builtin/credential/cert/backend_test.go index 8942e73669..8b62ec0754 100644 --- a/builtin/credential/cert/backend_test.go +++ b/builtin/credential/cert/backend_test.go @@ -843,9 +843,9 @@ func TestBackend_CertWrites(t *testing.T) { tc := logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "aaa", ca1, "foo", "", "", false), - testAccStepCert(t, "bbb", ca2, "foo", "", "", false), - testAccStepCert(t, "ccc", ca3, "foo", "", "", true), + testAccStepCert(t, "aaa", ca1, "foo", allowed{}, false), + testAccStepCert(t, "bbb", ca2, "foo", allowed{}, false), + testAccStepCert(t, "ccc", ca3, "foo", allowed{}, true), }, } tc.Steps = append(tc.Steps, testAccStepListCerts(t, []string{"aaa", "bbb"})...) @@ -866,7 +866,7 @@ func TestBackend_basic_CA(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", "", "", false), + testAccStepCert(t, "web", ca, "foo", allowed{}, false), testAccStepLogin(t, connState), testAccStepCertLease(t, "web", ca, "foo"), testAccStepCertTTL(t, "web", ca, "foo"), @@ -875,9 +875,9 @@ func TestBackend_basic_CA(t *testing.T) { testAccStepLogin(t, connState), testAccStepCertNoLease(t, "web", ca, "foo"), testAccStepLoginDefaultLease(t, connState), - testAccStepCert(t, "web", ca, "foo", "*.example.com", "", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "*.example.com"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", "*.invalid.com", "", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "*.invalid.com"}, false), testAccStepLoginInvalid(t, connState), }, }) @@ -926,20 +926,45 @@ func TestBackend_basic_singleCert(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", "", "", false), + testAccStepCert(t, "web", ca, "foo", allowed{}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", "example.com", "", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", "invalid", "", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "", "1.2.3.4:invalid", false), + testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.4:invalid"}, false), testAccStepLoginInvalid(t, connState), }, }) } -// Test a self-signed client with custom extensions (root CA) that is trusted -func TestBackend_extensions_singleCert(t *testing.T) { +func TestBackend_common_name_singleCert(t *testing.T) { + connState, err := testConnState("test-fixtures/root/rootcacert.pem", + "test-fixtures/root/rootcakey.pem", "test-fixtures/root/rootcacert.pem") + if err != nil { + t.Fatalf("error testing connection state: %v", err) + } + ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") + if err != nil { + t.Fatalf("err: %v", err) + } + logicaltest.Test(t, logicaltest.TestCase{ + Backend: testFactory(t), + Steps: []logicaltest.TestStep{ + testAccStepCert(t, "web", ca, "foo", allowed{}, false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{common_names: "example.com"}, false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{common_names: "invalid"}, false), + testAccStepLoginInvalid(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.4:invalid"}, false), + testAccStepLoginInvalid(t, connState), + }, + }) +} + +// Test a self-signed client with custom ext (root CA) that is trusted +func TestBackend_ext_singleCert(t *testing.T) { connState, err := testConnState( "test-fixtures/root/rootcawextcert.pem", "test-fixtures/root/rootcawextkey.pem", @@ -955,39 +980,132 @@ func TestBackend_extensions_singleCert(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", "", "2.1.1.1:A UTF8String Extension", false), + testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:A UTF8String Extension"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", "", "2.1.1.1:*,2.1.1.2:A UTF8*", false), + testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", "", "1.2.3.45:*", false), + testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.45:*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "", "2.1.1.1:The Wrong Value", false), + testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "", "2.1.1.1:*,2.1.1.2:The Wrong Value", false), + testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "", "2.1.1.1:", false), + testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "", "2.1.1.1:,2.1.1.2:*", false), + testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:,2.1.1.2:*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "example.com", "2.1.1.1:A UTF8String Extension", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:A UTF8String Extension"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", "example.com", "2.1.1.1:*,2.1.1.2:A UTF8*", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), testAccStepLogin(t, connState), - testAccStepCert(t, "web", ca, "foo", "example.com", "1.2.3.45:*", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "1.2.3.45:*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "example.com", "2.1.1.1:The Wrong Value", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "example.com", "2.1.1.1:*,2.1.1.2:The Wrong Value", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "invalid", "2.1.1.1:A UTF8String Extension", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:A UTF8String Extension"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "invalid", "2.1.1.1:*,2.1.1.2:A UTF8*", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "invalid", "1.2.3.45:*", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "1.2.3.45:*"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "invalid", "2.1.1.1:The Wrong Value", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:The Wrong Value"}, false), testAccStepLoginInvalid(t, connState), - testAccStepCert(t, "web", ca, "foo", "invalid", "2.1.1.1:*,2.1.1.2:The Wrong Value", false), + testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false), + testAccStepLoginInvalid(t, connState), + }, + }) +} + +// Test a self-signed client with URI alt names (root CA) that is trusted +func TestBackend_dns_singleCert(t *testing.T) { + connState, err := testConnState( + "test-fixtures/root/rootcawdnscert.pem", + "test-fixtures/root/rootcawdnskey.pem", + "test-fixtures/root/rootcacert.pem", + ) + if err != nil { + t.Fatalf("error testing connection state: %v", err) + } + ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") + if err != nil { + t.Fatalf("err: %v", err) + } + logicaltest.Test(t, logicaltest.TestCase{ + Backend: testFactory(t), + Steps: []logicaltest.TestStep{ + testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{dns: "*ample.com"}, false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{dns: "notincert.com"}, false), + testAccStepLoginInvalid(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{dns: "abc"}, false), + testAccStepLoginInvalid(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{dns: "*.example.com"}, false), + testAccStepLoginInvalid(t, connState), + }, + }) +} + +// Test a self-signed client with URI alt names (root CA) that is trusted +func TestBackend_email_singleCert(t *testing.T) { + connState, err := testConnState( + "test-fixtures/root/rootcawemailcert.pem", + "test-fixtures/root/rootcawemailkey.pem", + "test-fixtures/root/rootcacert.pem", + ) + if err != nil { + t.Fatalf("error testing connection state: %v", err) + } + ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") + if err != nil { + t.Fatalf("err: %v", err) + } + logicaltest.Test(t, logicaltest.TestCase{ + Backend: testFactory(t), + Steps: []logicaltest.TestStep{ + testAccStepCert(t, "web", ca, "foo", allowed{emails: "valid@example.com"}, false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{emails: "*@example.com"}, false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{emails: "invalid@notincert.com"}, false), + testAccStepLoginInvalid(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{emails: "abc"}, false), + testAccStepLoginInvalid(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{emails: "*.example.com"}, false), + testAccStepLoginInvalid(t, connState), + }, + }) +} + +// Test a self-signed client with URI alt names (root CA) that is trusted +func TestBackend_uri_singleCert(t *testing.T) { + connState, err := testConnState( + "test-fixtures/root/rootcawuricert.pem", + "test-fixtures/root/rootcawurikey.pem", + "test-fixtures/root/rootcacert.pem", + ) + if err != nil { + t.Fatalf("error testing connection state: %v", err) + } + ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") + if err != nil { + t.Fatalf("err: %v", err) + } + logicaltest.Test(t, logicaltest.TestCase{ + Backend: testFactory(t), + Steps: []logicaltest.TestStep{ + testAccStepCert(t, "web", ca, "foo", allowed{uris: "spiffe://example.com/*"}, false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{uris: "spiffe://example.com/host"}, false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{uris: "spiffe://example.com/invalid"}, false), + testAccStepLoginInvalid(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{uris: "abc"}, false), + testAccStepLoginInvalid(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{uris: "http://www.google.com"}, false), testAccStepLoginInvalid(t, connState), }, }) @@ -1007,9 +1125,9 @@ func TestBackend_mixed_constraints(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "1unconstrained", ca, "foo", "", "", false), - testAccStepCert(t, "2matching", ca, "foo", "*.example.com,whatever", "", false), - testAccStepCert(t, "3invalid", ca, "foo", "invalid", "", false), + testAccStepCert(t, "1unconstrained", ca, "foo", allowed{}, false), + testAccStepCert(t, "2matching", ca, "foo", allowed{names: "*.example.com,whatever"}, false), + testAccStepCert(t, "3invalid", ca, "foo", allowed{names: "invalid"}, false), testAccStepLogin(t, connState), // Assumes CertEntries are processed in alphabetical order (due to store.List), so we only match 2matching if 1unconstrained doesn't match testAccStepLoginWithName(t, connState, "2matching"), @@ -1314,19 +1432,32 @@ func testAccStepListCerts( } } +type allowed struct { + names string // allowed names in the certificate, looks at common, name, dns, email [depricated] + common_names string // allowed common names in the certificate + dns string // allowed dns names in the SAN extension of the certificate + emails string // allowed email names in SAN extension of the certificate + uris string // allowed uris in SAN extension of the certificate + ext string // required extensions in the certificate +} + func testAccStepCert( - t *testing.T, name string, cert []byte, policies string, allowedNames string, requiredExtensions string, expectError bool) logicaltest.TestStep { + t *testing.T, name string, cert []byte, policies string, testData allowed, expectError bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "certs/" + name, ErrorOk: expectError, Data: map[string]interface{}{ - "certificate": string(cert), - "policies": policies, - "display_name": name, - "allowed_names": allowedNames, - "required_extensions": requiredExtensions, - "lease": 1000, + "certificate": string(cert), + "policies": policies, + "display_name": name, + "allowed_names": testData.names, + "allowed_common_names": testData.common_names, + "allowed_dns_sans": testData.dns, + "allowed_email_sans": testData.emails, + "allowed_uri_sans": testData.uris, + "required_extensions": testData.ext, + "lease": 1000, }, Check: func(resp *logical.Response) error { if resp == nil && expectError { diff --git a/builtin/credential/cert/path_certs.go b/builtin/credential/cert/path_certs.go index 96c93ac562..384cd47fd6 100644 --- a/builtin/credential/cert/path_certs.go +++ b/builtin/credential/cert/path_certs.go @@ -45,7 +45,33 @@ Must be x509 PEM encoded.`, "allowed_names": &framework.FieldSchema{ Type: framework.TypeCommaStringSlice, Description: `A comma-separated list of names. -At least one must exist in either the Common Name or SANs. Supports globbing.`, +At least one must exist in either the Common Name or SANs. Supports globbing. +This parameter is deprecated, please use allowed_common_names, allowed_dns_sans, +allowed_email_sans, allowed_uri_sans.`, + }, + + "allowed_common_names": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `A comma-separated list of names. +At least one must exist in the Common Name. Supports globbing.`, + }, + + "allowed_dns_sans": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `A comma-separated list of DNS names. +At least one must exist in the SANs. Supports globbing.`, + }, + + "allowed_email_sans": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `A comma-separated list of Email Addresses. +At least one must exist in the SANs. Supports globbing.`, + }, + + "allowed_uri_sans": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `A comma-separated list of URIs. +At least one must exist in the SANs. Supports globbing.`, }, "required_extensions": &framework.FieldSchema{ @@ -77,12 +103,14 @@ seconds. Defaults to system/backend default TTL.`, Description: `TTL for tokens issued by this backend. Defaults to system/backend default TTL time.`, }, + "max_ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Description: `Duration in either an integer number of seconds (3600) or an integer time unit (60m) after which the issued token can no longer be renewed.`, }, + "period": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Description: `If set, indicates that the token generated using this role @@ -151,13 +179,18 @@ func (b *backend) pathCertRead(ctx context.Context, req *logical.Request, d *fra return &logical.Response{ Data: map[string]interface{}{ - "certificate": cert.Certificate, - "display_name": cert.DisplayName, - "policies": cert.Policies, - "ttl": cert.TTL / time.Second, - "max_ttl": cert.MaxTTL / time.Second, - "period": cert.Period / time.Second, - "allowed_names": cert.AllowedNames, + "certificate": cert.Certificate, + "display_name": cert.DisplayName, + "policies": cert.Policies, + "ttl": cert.TTL / time.Second, + "max_ttl": cert.MaxTTL / time.Second, + "period": cert.Period / time.Second, + "allowed_names": cert.AllowedNames, + "allowed_common_names": cert.AllowedCommonNames, + "allowed_dns_sans": cert.AllowedDNSSANs, + "allowed_email_sans": cert.AllowedEmailSANs, + "allowed_uri_sans": cert.AllowedURISANs, + "required_extensions": cert.RequiredExtensions, }, }, nil } @@ -168,6 +201,10 @@ func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *fr displayName := d.Get("display_name").(string) policies := policyutil.ParsePolicies(d.Get("policies")) allowedNames := d.Get("allowed_names").([]string) + allowedCommonNames := d.Get("allowed_common_names").([]string) + allowedDNSSANs := d.Get("allowed_dns_sans").([]string) + allowedEmailSANs := d.Get("allowed_email_sans").([]string) + allowedURISANs := d.Get("allowed_uri_sans").([]string) requiredExtensions := d.Get("required_extensions").([]string) var resp logical.Response @@ -246,6 +283,10 @@ func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *fr DisplayName: displayName, Policies: policies, AllowedNames: allowedNames, + AllowedCommonNames: allowedCommonNames, + AllowedDNSSANs: allowedDNSSANs, + AllowedEmailSANs: allowedEmailSANs, + AllowedURISANs: allowedURISANs, RequiredExtensions: requiredExtensions, TTL: ttl, MaxTTL: maxTTL, @@ -278,6 +319,10 @@ type CertEntry struct { MaxTTL time.Duration Period time.Duration AllowedNames []string + AllowedCommonNames []string + AllowedDNSSANs []string + AllowedEmailSANs []string + AllowedURISANs []string RequiredExtensions []string BoundCIDRs []*sockaddr.SockAddrMarshaler } diff --git a/builtin/credential/cert/path_login.go b/builtin/credential/cert/path_login.go index cf1c6c68f4..767b6fcbec 100644 --- a/builtin/credential/cert/path_login.go +++ b/builtin/credential/cert/path_login.go @@ -253,6 +253,10 @@ func (b *backend) verifyCredentials(ctx context.Context, req *logical.Request, d func (b *backend) matchesConstraints(clientCert *x509.Certificate, trustedChain []*x509.Certificate, config *ParsedCert) bool { return !b.checkForChainInCRLs(trustedChain) && b.matchesNames(clientCert, config) && + b.matchesCommonName(clientCert, config) && + b.matchesDNSSANs(clientCert, config) && + b.matchesEmailSANs(clientCert, config) && + b.matchesURISANs(clientCert, config) && b.matchesCertificateExtensions(clientCert, config) } @@ -280,10 +284,85 @@ func (b *backend) matchesNames(clientCert *x509.Certificate, config *ParsedCert) return true } } + } return false } +// matchesCommonName verifies that the certificate matches at least one configured +// allowed common name +func (b *backend) matchesCommonName(clientCert *x509.Certificate, config *ParsedCert) bool { + // Default behavior (no names) is to allow all names + if len(config.Entry.AllowedCommonNames) == 0 { + return true + } + // At least one pattern must match at least one name if any patterns are specified + for _, allowedCommonName := range config.Entry.AllowedCommonNames { + if glob.Glob(allowedCommonName, clientCert.Subject.CommonName) { + return true + } + } + + return false +} + +// matchesDNSSANs verifies that the certificate matches at least one configured +// allowed dns entry in the subject alternate name extension +func (b *backend) matchesDNSSANs(clientCert *x509.Certificate, config *ParsedCert) bool { + // Default behavior (no names) is to allow all names + if len(config.Entry.AllowedDNSSANs) == 0 { + return true + } + // At least one pattern must match at least one name if any patterns are specified + for _, allowedDNS := range config.Entry.AllowedDNSSANs { + for _, name := range clientCert.DNSNames { + if glob.Glob(allowedDNS, name) { + return true + } + } + } + + return false +} + +// matchesEmailSANs verifies that the certificate matches at least one configured +// allowed email in the subject alternate name extension +func (b *backend) matchesEmailSANs(clientCert *x509.Certificate, config *ParsedCert) bool { + // Default behavior (no names) is to allow all names + if len(config.Entry.AllowedEmailSANs) == 0 { + return true + } + // At least one pattern must match at least one name if any patterns are specified + for _, allowedEmail := range config.Entry.AllowedEmailSANs { + for _, email := range clientCert.EmailAddresses { + if glob.Glob(allowedEmail, email) { + return true + } + } + } + + return false +} + +// matchesURISANs verifies that the certificate matches at least one configured +// allowed uri in the subject alternate name extension +func (b *backend) matchesURISANs(clientCert *x509.Certificate, config *ParsedCert) bool { + // Default behavior (no names) is to allow all names + if len(config.Entry.AllowedURISANs) == 0 { + return true + } + // At least one pattern must match at least one name if any patterns are specified + for _, allowedURI := range config.Entry.AllowedURISANs { + for _, name := range clientCert.URIs { + if glob.Glob(allowedURI, name.String()) { + return true + } + } + } + + return false +} + // matchesCertificateExtensions verifies that the certificate matches configured // required extensions func (b *backend) matchesCertificateExtensions(clientCert *x509.Certificate, config *ParsedCert) bool { diff --git a/builtin/credential/cert/test-fixtures/root/rootcacert.srl b/builtin/credential/cert/test-fixtures/root/rootcacert.srl index 219a6be4b1..1c85d6318e 100644 --- a/builtin/credential/cert/test-fixtures/root/rootcacert.srl +++ b/builtin/credential/cert/test-fixtures/root/rootcacert.srl @@ -1 +1 @@ -92223EAFBBEE17A3 +92223EAFBBEE17AF diff --git a/builtin/credential/cert/test-fixtures/root/rootcawdns.cnf b/builtin/credential/cert/test-fixtures/root/rootcawdns.cnf new file mode 100644 index 0000000000..3c576a95c5 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawdns.cnf @@ -0,0 +1,17 @@ +[ req ] +default_bits = 2048 +encrypt_key = no +prompt = no +default_md = sha256 +req_extensions = req_v3 +distinguished_name = dn + +[ dn ] +CN = example.com + +[ req_v3 ] +subjectAltName = @alt_names + +[ alt_names ] +IP.1 = 127.0.0.1 +DNS.1 = example.com diff --git a/builtin/credential/cert/test-fixtures/root/rootcawdns.csr b/builtin/credential/cert/test-fixtures/root/rootcawdns.csr new file mode 100644 index 0000000000..b56d323140 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawdns.csr @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEijCCAnICAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQDUJ6s97BFxR295bCjpwQ85Vo8DnBFa/awNH107 +QFn/zw0ZDdJMLtEBc/bw7pTYw5ulKbiZDFrmzPEY+QZlo+t1TeWgPRJg0CbYNukS +aNv0vKXjDXYwbrCyOvZucy8hte6IKjZfH+kAsgbbUxfD75BCKsxMxbVHkg0W9Ma2 +pnZj/kpvQE5lkMj5mDvtWdfCRsVg4zL6jhRHkPZ6fOkF3mrfTbQu3oyOcbKLEE/G +t3QRKw3uv0vMDmhg62ZPvD1k70UMjUV2MVqEPZuWY7/bbW8OsfzMyBOGY9LLp7QS +krxWYRj6SPUR4f1bZq7pRbqOfS0okq/XDLf1k6Na5cT6iNdyjEVdSJl7vR7kSreX +8hkwK46Oup8v/vJLu/cRDCpAas0gJJkJDPt5114V0/Xww7EFxs5GijXP8i5RLlgK +/nRscbK+fgjQOnQ5cp0pcP8HAriy2vil7E0fQvMvt5QTyINEYgiYaCIT9WGRC8Xo +WcoUGI2vyrGy6RU6A3/TKeBLtikaSPjFKa1dFTAHfrUkTBpfqc+sbiJ334Bvucg5 +WyS8oAC5Vf++iMnETSdzx1k0/QARVLD38PO8wPaPU1M2XaSA+RHTB9SGFc4VTauT +B167NLlmgJHYuhp+KM1RTy1TEoDlJh2qKj21BLcR1GJ0KgDze6Vpf9xdRTdqMpo2 +h20wdQIDAQABoC8wLQYJKoZIhvcNAQkOMSAwHjAcBgNVHREEFTAThwR/AAABggtl +eGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAgEAGtds+2IUVKzw9bi130mBbb9K +CrXw2NXSp+LJwneIEy0bjAaNr7zCGQsj7q57qFwjc7vLTtGRheP5myrAOq00lp8J +1sGZSETS/y4yeITLZSYWVq2dtF/hY9I+X3uOoibdsQgzYqhBcUr4oTDapf1ZEs0i +wA2J5IcasfaBpWFc9wRN79BBACLGyCbX6VwUISrGe3Hgzkeqtg97cU62ecQsgXiZ +LdQgERvC0wSfAmI4lGulXi1oYYSRxXQ8pEKEAMeJrJVQfvhdbS/o4Bdf3Yj6ibtD +tFSdKLcdRCfMQBEHNpSh665LfBbwU55Fh89tBdGmf6uqsimUY6AxNncnLsc1Kq6F +oINXix3GsBNmCahDeHdGOlNjw0Lpl0m6bnu6LXSDwwuNWAEdDfEmxR+5T/GkGxcG +TTWPwEkpnCe4VmGl9Y10uPSvqneNsdNWjDVK4BeW4VSf9Lp1Zeme1dYFvpyzow+r +4ogpvMPf5vy5I/0HCEf1KlaPyhs8ZGK6YBGaeEDYSaysAWJfYm8eiqwUuKYj/FUe +G3KkaFpOGsQHFNRtG8GukV3r2AK97HFHKNfygZ2xvk5isXz2ZsNX1/J0+GGjalJl +cWBBEiXFM94XJHE9rACsL2UKn8cWCh9lHNLlePOkQuoNY9CUd63xx4Hg97XWP3+U +DhpG7CADsKcPJfbMgrk= +-----END CERTIFICATE REQUEST----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawdnscert.pem b/builtin/credential/cert/test-fixtures/root/rootcawdnscert.pem new file mode 100644 index 0000000000..2ce633e3a4 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawdnscert.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIJAJIiPq+77herMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV +BAMTC2V4YW1wbGUuY29tMB4XDTE4MDQyNjEyMDEzMFoXDTE5MDQyNjEyMDEzMFow +FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDUJ6s97BFxR295bCjpwQ85Vo8DnBFa/awNH107QFn/zw0ZDdJMLtEB +c/bw7pTYw5ulKbiZDFrmzPEY+QZlo+t1TeWgPRJg0CbYNukSaNv0vKXjDXYwbrCy +OvZucy8hte6IKjZfH+kAsgbbUxfD75BCKsxMxbVHkg0W9Ma2pnZj/kpvQE5lkMj5 +mDvtWdfCRsVg4zL6jhRHkPZ6fOkF3mrfTbQu3oyOcbKLEE/Gt3QRKw3uv0vMDmhg +62ZPvD1k70UMjUV2MVqEPZuWY7/bbW8OsfzMyBOGY9LLp7QSkrxWYRj6SPUR4f1b +Zq7pRbqOfS0okq/XDLf1k6Na5cT6iNdyjEVdSJl7vR7kSreX8hkwK46Oup8v/vJL +u/cRDCpAas0gJJkJDPt5114V0/Xww7EFxs5GijXP8i5RLlgK/nRscbK+fgjQOnQ5 +cp0pcP8HAriy2vil7E0fQvMvt5QTyINEYgiYaCIT9WGRC8XoWcoUGI2vyrGy6RU6 +A3/TKeBLtikaSPjFKa1dFTAHfrUkTBpfqc+sbiJ334Bvucg5WyS8oAC5Vf++iMnE +TSdzx1k0/QARVLD38PO8wPaPU1M2XaSA+RHTB9SGFc4VTauTB167NLlmgJHYuhp+ +KM1RTy1TEoDlJh2qKj21BLcR1GJ0KgDze6Vpf9xdRTdqMpo2h20wdQIDAQABoyAw +HjAcBgNVHREEFTAThwR/AAABggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQUFAAOC +AQEA2JswcCYtHvOm2QmSEVeFcCeVNkzr35FXATamJv0oMMjjUFix78MW03EW6vJa +E52e3pBvRdy+k2fuq/RtHIUKzB6jNbv0Vds26Dq+pmGeoaQZOW94/Wht7f9pZgBi +IRPBg9oACtyNAuDsCOdetOyvyoU29sjUOUoQZbEXF+FK4lRJrEmZUJHbp/BOD58V +mQRtjTMjQlZZropqBQmooMRYU0qgWHaIjyoQpu2MgEj3+/1b1IX6SCfRuit0auh/ +YI3/cCtyAG/DpZ6zfyXuyY+iN+l8B6t0nXyV3g8JgBWYPGJv1hgVIgnnqlwuL517 +mEAT5RnHCNJQNuzS1dwfuBrX3w== +-----END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawdnskey.pem b/builtin/credential/cert/test-fixtures/root/rootcawdnskey.pem new file mode 100644 index 0000000000..15db567e03 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawdnskey.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDUJ6s97BFxR295 +bCjpwQ85Vo8DnBFa/awNH107QFn/zw0ZDdJMLtEBc/bw7pTYw5ulKbiZDFrmzPEY ++QZlo+t1TeWgPRJg0CbYNukSaNv0vKXjDXYwbrCyOvZucy8hte6IKjZfH+kAsgbb +UxfD75BCKsxMxbVHkg0W9Ma2pnZj/kpvQE5lkMj5mDvtWdfCRsVg4zL6jhRHkPZ6 +fOkF3mrfTbQu3oyOcbKLEE/Gt3QRKw3uv0vMDmhg62ZPvD1k70UMjUV2MVqEPZuW +Y7/bbW8OsfzMyBOGY9LLp7QSkrxWYRj6SPUR4f1bZq7pRbqOfS0okq/XDLf1k6Na +5cT6iNdyjEVdSJl7vR7kSreX8hkwK46Oup8v/vJLu/cRDCpAas0gJJkJDPt5114V +0/Xww7EFxs5GijXP8i5RLlgK/nRscbK+fgjQOnQ5cp0pcP8HAriy2vil7E0fQvMv +t5QTyINEYgiYaCIT9WGRC8XoWcoUGI2vyrGy6RU6A3/TKeBLtikaSPjFKa1dFTAH +frUkTBpfqc+sbiJ334Bvucg5WyS8oAC5Vf++iMnETSdzx1k0/QARVLD38PO8wPaP +U1M2XaSA+RHTB9SGFc4VTauTB167NLlmgJHYuhp+KM1RTy1TEoDlJh2qKj21BLcR +1GJ0KgDze6Vpf9xdRTdqMpo2h20wdQIDAQABAoICAQDJxszUQQC554I7TsZ+xBJx +q0Sr3zSWgOuxM2Jdpy+x38AKUx3vPRulsSBtN8yzeR9Ab7TVQ231U3f/E2GlK8kW +sTazN0KSd4ZqX5c+3iJM21s+3p/JIo3FhdS5aa2q9zjdoqBByry135wr3xScUu22 +MLRMVEG8x0jRy45vS1UQd1teAiBN8u1ijgp5DNjrOpohMxVaPeVFx7bU+pY58bdd +mK7FYP73v2VbY/EsA3FNntBKgQBbHFzjyR9uuI7/v53BeV9WMUxwt5OR7l8cGDHn +HRtdvPDtAWYMMf1PKOYdlY3HBbqn/nMUCk5TKPFs8dsQWqsI8lzIIVndauj0i0+0 +M/lVMXu4x48o5FfLa4HjkpcDxAU6QDHA9thaDkasZebixVH/p1ZJkLORl5jDLYkU +Av+B3i1efITwNYgosZNjPpw0PyYh9PV9JvB87d5wFpgISfZyRXpBVGeJbt6gg++8 +8/5A/GzSpGy0FhLcP3vuVTcX2VOexjqeaoi4U3cHrbWv/wNj5a4BNk5EJT8fVeSb ++Emqydl9u3n2E315GPC8kwxdE3r3hGrWdZQn9byGvqzwDaLWXQLQWvQN4GOpGTrP +Yxj2Oi8s1MJHkppj4eo52O4J7cBlAJn3RFmlCKGOoWJZMdPktp/gWeT+xIGSaa21 +qB+l/ZFEWLPMxdTBMGFmYQKCAQEA8DgozaZBXr7mb1H25EbW9YmtLc61JMapQrZb +ObygiGR6RZsxCXEvGdhvvmwpO8rA9wAOWqI8NV5GU8EvuRuwvGoX4HqbXkB6ZcyC +6RuZzki2lrKVGUaLc1v6MyhX4IzrqTYWDgQvwd9lMcUGR7r007KPE5ft4v3/TuxQ +qPKxQE7NO2xnTloUchd5g0/d975GZi0g6XDecFOuj43Pi0c/wRcFH6zfVirdcm+M +yP9CsJ/LUgtV1voLqyhfybwHvzpxJ0l25Fw+P85I4czosBp+FaFAwogxZEDnY8Fr +Hqcwdc7vwGDjTbtflDsUdppt2h8nD8bBZGysG8+P8HAt3i5D+QKCAQEA4heKueRQ +Y8nTZlmRSRtB6usRAeymQBJjO+yWwu/06Efg8VW5QRwtP0sx+syrLaQDy8MT07II +XQZmq55xATWbHCxULiceIY2KG5LHCovVotYAll8ov58exJva19C7/41uVrkl3H9j +xFLX0Bn3zMFKBOxKhygP2xqqEJdb1JJt27c2CbXvXOzqIZ4RCaNQdBdrlEiXQihR +JCGMUBfrYIwALQFzYuPGULhg77YcAi5owCPnfK+dDOOvMmW8BwPnRUc14WFIVV+m +dbY22WonLNPP055W5755Xl9RHKW1bcmIH6E4QZpMrlnd1UzPBQq1PJtcO3uRc5T6 +CMQSUmwMGSQ3XQKCAQBiuVHborY+8AnYOkFTc+GoK5rmtosvwA2UA0neoqz/IPw3 +Wx5+GOwYnSDfi6gukJdZa8Z6bS59aG9SwJSSaNTrulZxxTHRPIKRD8nFb7h4VN3l +dSNdreZl1KkxGSV0fbXkZvwNap8N+HeoSqbYF/fCgSHYFZqIrYadsvU7WfKK0Vf7 +UgPq6Y55jTg9RTeeN67LE0Txa5efZmTZTpi7Tt7exk0uxWdMDHXSMBIWEQIhgKqY +31u57C2bfA5R5FrytlwGn2SjWV2j7214jzQaG+kxjoIE8OALqbjvAHC7uk5qPE/A +KpGAQr93Ngik7baz7BWroC2eziK1k0o+sHvJUg5RAoIBABF+ftZ5axr9j+T4gzxj +5orV24AJnqeQhKsrWFMHHC0o+qfR2T7HflzKZbihQ5GJgl2u34be3LTN/P3Eibvt +OO5KI81aa4NvH0OY7NvNDB/IbU01WcLR/iB6asmONi3E9MezFdHk7YRQYLCSgdEP +F7ofyniAyhFLE+OqwolFN0jr+TtxH29SSZ+GSo0zXNNOyJ01rLaKxhSEoAXGhAj5 +bD4PQa1iMIMocR+7OJmWm7ZaUNwd/onzyCefJZhpXejHZMzmqSEqAIhVLBNQmm1m +iks2kkTmQR/jQjR0QgCXunewEtlIpixLedW6Vr5uIK3q240it5N48IvjGAPWpmz/ +l2UCggEBALRlARlBdYcPhWbcyhq9suq2PJqHVvNdP9vVTZpPgmWgxmj9mPQ731Q/ +UpRlBIk6U0qAxBXP9kzPqSivWhY8Jto80Jdr+/80PkdANDkpnXoDlxPk095sD2uN +Jv5FffFgMZH9MGpPTEuZ571/YtVi+1qFt0i3oazpF/g8gU23f2oxaX4xzsltVl8J +rWXYzmYE0i5Qiy81+zZ9dZlnmlKhcYpD6m2t/0hRAoNaoxOUV7WFcIzYIxpKvzYL +QTDL/Se2Ooc0xLQvM1oZ9/1NE2hpGQ/ipASEPlx9KO5ktYW7+LwdcSCMXtx84I/D +VQpWjPdILMpiVrB/9NsENTNv2DUvc+o= +-----END PRIVATE KEY----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawemail.cnf b/builtin/credential/cert/test-fixtures/root/rootcawemail.cnf new file mode 100644 index 0000000000..f679fb9879 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawemail.cnf @@ -0,0 +1,17 @@ +[ req ] +default_bits = 2048 +encrypt_key = no +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = req_v3 + +[ req_v3 ] +subjectAltName = @alt_names + +[ dn ] +CN = example.com + +[ alt_names ] +IP.1 = 127.0.0.1 +email = valid@example.com diff --git a/builtin/credential/cert/test-fixtures/root/rootcawemail.csr b/builtin/credential/cert/test-fixtures/root/rootcawemail.csr new file mode 100644 index 0000000000..44b191495f --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawemail.csr @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEkDCCAngCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQDO7stcdJQwUtVeJriwpAswDAirO827peSlgdxs +fW8X9M2hE9ihvESEILb7TRMRiFlDyQYg1BxKMrJ0DZmixFi8RvUCZbH6TFOUMsk+ +w1FhpzjuqAqxNQ51s7u30sfruJg7XN3YJLEPelom62wvzhvLXJFLQZlQCDrMx+PC +ofWs4IA7jR8JaXZjIGdkEU0GgRPy8zKPUe3dUBHi2UR4eKT4cRCn4IwCrFx4BQjV +AxNKNDGpe+fTVOzII/UX+FppDdGZZ4g0y3E1mQUEKkff4dKCK7vhlGJR9D+5v/V0 +/stwP72aXczijuVtnXXyli+oj24NaijoqQluNCD3MvV/INovLL2Tyk54H3/GvpU1 ++sJbpE2+UPh+Rh8DNkT6RPRguymJO8MSsdLt/qvVD8BlZ7I9V3XZlDKosCRTUyxf +jjFpa+VzB3nt7uFtIXZ9HNGhQIpOULvkFGizWV+tS8PpGdTFVzDjyWg0HUKWn8g8 +IiWR9S40h6mHjVuTuxA9tlO69PuTjGK7MlAvFTDaPC8seau1LUiqtQ+prnSLI0h1 +6GfI9W2G7BKKVPloErODhLcsOcwRcmaJVW+yBda3te8+cMBIvtQYKAYSCtg8qXws +xyfPLo4GChbGGRbRCuM3mB1lG1qHEivJ0vynsgolp0t8jaXSFVBVgYj+C6Vd9/hl +ieUcOwIDAQABoDUwMwYJKoZIhvcNAQkOMSYwJDAiBgNVHREEGzAZhwR/AAABgRF2 +YWxpZEBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAgEAe1u3wYMMKaZ3d5Wz +jKH971CF3sl+KYl+oV0ekD8dbYe+szERgajn6Y5IYVxxi5o9UgeAWqnyHCiqsW8T +MQdUwMa67Ym/pHKSVoBlGePAKHqock0+iSsVBMcPpU9RkxdSW2aVtdb0DGfyB952 +t3dSb0LaITu30fe8p7lxrL0DKESbwd4N2XQE1F5Vf+1OodvpJinn4Wqzn45hqRf0 +imxrCgVjT5VtR+NRzKCK3Msmh+cJGpR3zgXwGKqgHLWzhvSoQwRWYE3apMK5xLk7 +N1sWVxEKK5+L/CDaMNGQFx5lPiCN3bUudCq4uSZcPLO5AuDpSeLKnknBrWA6HcbB +WvnwtKmHeVe2qogPViKGuwE16rnPlp9hysPl2ckmtqEsXRagIAh5fMI3OoRbZmVV +jfJm21U4YkUWuMKet3EU1StT6T8T6O7QEFA4w4s5+m3dsjDZ9iTuK9/dCs1xnIke +4uJYmh3YrNl8IjMffJuWxA+/de3JO1UljC2EAFxa5KAc24+qyeWwky4tMv72gTOp +6q3k2wnsrK5B1errRV37OLgxtoh1I3Rgp+0B77SOK/PpD/JJazJG5O9bBJOvHJc0 +STW9Td2CzgC2lKGfvkX6UYgVy/9HDq7/EKXP/G2f3kRik2NPUhGcnAH9nyL9SvpP ++T4CZ+FumDj5DulARk6arSq+uy4= +-----END CERTIFICATE REQUEST----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawemailcert.pem b/builtin/credential/cert/test-fixtures/root/rootcawemailcert.pem new file mode 100644 index 0000000000..f774a71827 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawemailcert.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID1TCCAr2gAwIBAgIJAJIiPq+77hevMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV +BAMTC2V4YW1wbGUuY29tMB4XDTE4MDQyNjEyMjE1MloXDTE5MDQyNjEyMjE1Mlow +FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDO7stcdJQwUtVeJriwpAswDAirO827peSlgdxsfW8X9M2hE9ihvESE +ILb7TRMRiFlDyQYg1BxKMrJ0DZmixFi8RvUCZbH6TFOUMsk+w1FhpzjuqAqxNQ51 +s7u30sfruJg7XN3YJLEPelom62wvzhvLXJFLQZlQCDrMx+PCofWs4IA7jR8JaXZj +IGdkEU0GgRPy8zKPUe3dUBHi2UR4eKT4cRCn4IwCrFx4BQjVAxNKNDGpe+fTVOzI +I/UX+FppDdGZZ4g0y3E1mQUEKkff4dKCK7vhlGJR9D+5v/V0/stwP72aXczijuVt +nXXyli+oj24NaijoqQluNCD3MvV/INovLL2Tyk54H3/GvpU1+sJbpE2+UPh+Rh8D +NkT6RPRguymJO8MSsdLt/qvVD8BlZ7I9V3XZlDKosCRTUyxfjjFpa+VzB3nt7uFt +IXZ9HNGhQIpOULvkFGizWV+tS8PpGdTFVzDjyWg0HUKWn8g8IiWR9S40h6mHjVuT +uxA9tlO69PuTjGK7MlAvFTDaPC8seau1LUiqtQ+prnSLI0h16GfI9W2G7BKKVPlo +ErODhLcsOcwRcmaJVW+yBda3te8+cMBIvtQYKAYSCtg8qXwsxyfPLo4GChbGGRbR +CuM3mB1lG1qHEivJ0vynsgolp0t8jaXSFVBVgYj+C6Vd9/hlieUcOwIDAQABoyYw +JDAiBgNVHREEGzAZhwR/AAABgRF2YWxpZEBleGFtcGxlLmNvbTANBgkqhkiG9w0B +AQUFAAOCAQEAp2T99t93hxPyCDaqfTF0lsdzIgxZ5GkSzYTYQ2pekLfMDUUy4WFQ +AppdnSJSpm6b+xWO2DkO8UAgOdSEORf/Qpfm+UpHaEYZlQiWQ0zNmIQgBoh6indU +bEZKeL6aAOfIshPNfmqjFt+DpEClrQvCHJggG/rB77Ujj6hPY2+8h4JjbjeX7Pe9 +oUEx9LpZ5Qpo6PK5vB537PP7Q2qp2PIr29DLz1VeLCbqUnV+j7qT0T3hhqurnpTA +QUiRZI0etgeP/B5lw/S4AWijq+R6RasdPAS4UNHsYK+PSGiqdhW/bJvSx5UBXQbk +DuY2A4kdv60H5Aw45/F6enH2Fg1kg7PlQA== +-----END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawemailkey.pem b/builtin/credential/cert/test-fixtures/root/rootcawemailkey.pem new file mode 100644 index 0000000000..13b1657829 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawemailkey.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDO7stcdJQwUtVe +JriwpAswDAirO827peSlgdxsfW8X9M2hE9ihvESEILb7TRMRiFlDyQYg1BxKMrJ0 +DZmixFi8RvUCZbH6TFOUMsk+w1FhpzjuqAqxNQ51s7u30sfruJg7XN3YJLEPelom +62wvzhvLXJFLQZlQCDrMx+PCofWs4IA7jR8JaXZjIGdkEU0GgRPy8zKPUe3dUBHi +2UR4eKT4cRCn4IwCrFx4BQjVAxNKNDGpe+fTVOzII/UX+FppDdGZZ4g0y3E1mQUE +Kkff4dKCK7vhlGJR9D+5v/V0/stwP72aXczijuVtnXXyli+oj24NaijoqQluNCD3 +MvV/INovLL2Tyk54H3/GvpU1+sJbpE2+UPh+Rh8DNkT6RPRguymJO8MSsdLt/qvV +D8BlZ7I9V3XZlDKosCRTUyxfjjFpa+VzB3nt7uFtIXZ9HNGhQIpOULvkFGizWV+t +S8PpGdTFVzDjyWg0HUKWn8g8IiWR9S40h6mHjVuTuxA9tlO69PuTjGK7MlAvFTDa +PC8seau1LUiqtQ+prnSLI0h16GfI9W2G7BKKVPloErODhLcsOcwRcmaJVW+yBda3 +te8+cMBIvtQYKAYSCtg8qXwsxyfPLo4GChbGGRbRCuM3mB1lG1qHEivJ0vynsgol +p0t8jaXSFVBVgYj+C6Vd9/hlieUcOwIDAQABAoICAQDFitqh6TxqITlFBwv6vK9d +b696371XrFdo1F57RwcdxHnkklCUnWh/BcgIgJx6eUJV3nq2LibPgjQva6hF5NCc +89QDNNfBjMmgyRaqjsSKx5sm4U5Lus2R+UFzi4mEcpUI3m99XhGVKAUV8Fo4DLcl +3LlrMTVNXH3dbdj0va4NGcfwkZiWYJI+sPliYs24LtK/dADJJro/MqfQef7OTsWV +0kHHMSoXhzlC7fNvfd8VUFw0Ym99pC3iJclc155feWyk2FwDok7xjqFmR4KTrD1M +PLm/7+ooOFX5WdHVnULSZlb3HSJxCV7l1JJ7QXo/nKS/s59X875n8OWjdoc7lD4T +Xw/K9CzJCyhJ/HDhTAea1+MNTig4a6wjdSim6vasig/Gkot6jjS2lhnZae8ZhYxP +GUx4JcPthHppgHt8s6Jb2PHuqNVRmVB0x41c5mmXOnJcSqOX0XhbSbeS1TUV8BiC +HMaa+agt7RpQOb5uxpb+Hath/88tsjDXI0ZHNAG43ndkHxSQQ9P/q/m5uaLwuJyo +Yb06yUy/g7ceXpJFjGsjO+33DmamvligqOswgg+oazMFo8S9ZUJw6sSXhM/XiHla +JOj+Vatfj0ViVcaGlO2kWughuCT5thn92bgC9V2VnJhbaSzSaQlRphlbuSYJEYj0 +S1uIbwPzTrcBQuekwY50YQKCAQEA/vve5K/nAnw4KLSrKwwCp9trYSm8C5czv2jV +tn6vQtckQMrw/hubX7TcTTgTuGboGdHMwZMFJBKpx6AlRHCR5IBw8fR1z+58c+2V +VJgllc23eKwCcBMKoe6LmsiUXOWmc7MuHc+qQS+9OemO93nNafsSwFCkucBFQs/3 +Yx7J3zNvMOuy+dq3jrxO0xl2jBF0pcmJF/czrvbMCD7tvDntgqvpAnybgrwm2cu3 +Q5F6i+E5w6VDhCprQL/aK95iT7cPmfdGxsUCdfNzDGIJFHZp2Hrar1TsOP6ESsDl +Q/Oz9oO1vMy7MymJjWFoVELBlCBxDEgubyM1f8cE1tQ6UAqFSwKCAQEAz8HnKWPe +NWZtqdAzSmY+3ZxSe1BbukOo4XtCV8LfRHGazKpXMTqsO9l7ynK7ifXv3b3GHTr+ +ck2Af/vyiVx6f7Ty2dmBotFQDzg0HfKD2skAPyH8cHpA8TUeL3yMOR3XQU5/pOnG +tn84n7KWpAyZXh8gzMnmzWjMlb9pUlkKcATUj0gb8iSa9PV0zBwMKYKY0ngznJT2 +CgE1vhy59rpuUVMrQ8i5iW9jbqYVrqID+ta2DWgcLsEXft7jKfupnRHF0Dvc650p ++Lkxv0YgKjUg5sYc2QJbIiBxXaW0cTRrw/KfOe4kvdG5RMF60Six+W1DIW2l+qi3 +irnDRvRm1N6e0QKCAQEA86d5MaxJIl3TSEqEeikK7J3GuV0pHSZKQ7EI70+VaFiv +gt6qdReqXEU2cu+QIJjtV6bcc2lq8zKGXITSt9ieAO0fgIWqgpyQ/jJcjS6qU8D1 +fnFYDwKTGXQaoTjkVPT6HvtsqP4E4i+dMZbWj/MrcAeEvpMRJZLuXE7gRi5ol0nO +CcBhEVKILvQQmrZtSqFvhvDTeTw2fg3FoGeJw2DTbheaHE84RzBGK774C7Abm0kI +asUkhEoInSH3eA4UgbobRXQ+hLhDhrSxDncr2ArjUALtr7eF11yWy9wR+OIK6Rio +9JXqmJQrphcbm9ECq+poPGVJQdgySjzCigrZAh1biwKCAQBiBnFVXCOaOov/lZa9 +weRjl8BrIo1FI2tpiEjTM8U4fAm4C588QRzG2GTKLrxB6eKVU1dIr28i62J4AJ59 +JT8/RldXZoL+GZiWtcQRZT3FWxVctGJxh51gsdleOnvG70eDLtCXNR5nOTu0TgU5 +viAXAsTtG05lGM9+0GOXUR/VntHUEQfuhkr+zVmgfJNYeqA0njZr6PT134BGBTPR +MEGg6Yb+YpT4PbBCouaUESmjju8zAC5b+Qtm9y9jvbRXwez9xWEFYpBNJMROJX5D +q/GsMUmnMq9hOMGEmAy9ZSh7udxa7vwy++NYh5m1Wmgu8di8ywmHbVe8gs2aivKB ++dAhAoIBAQC7nSuRSmRGeKJCAqikMFPVdFdLaTnEw4i/fcyYsPa+o2hTWXyLll8K +lwSnBxe+BCvdeQ8cWzg3rPaIBVzUjDecZjdwjcHnhlHvgHjEvFX339rvpD7J2HIb +DaVqvtPniCFdNK4Jyvd3JMtNq34SIHFAcmB9358JuKsOwCmk8CpMAqKPVsKj7m6H +ETISh/K8aI2vZxVZ4WN4FsQTCqmtQDXFSGpZF5EZSpMJIB3ZZLt2jyyDW2DaZ+1T +yuVl9jU56fTtacQROQY7cvrwznX0lFpmniwl0Aj0wln/svFAqKo1+RujqApw5iYn +ssH1dH2tESx6RpMMyLYihjHVDC/ULUVu +-----END PRIVATE KEY----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawext.cnf b/builtin/credential/cert/test-fixtures/root/rootcawext.cnf index 524efd2e40..77e8258e10 100644 --- a/builtin/credential/cert/test-fixtures/root/rootcawext.cnf +++ b/builtin/credential/cert/test-fixtures/root/rootcawext.cnf @@ -10,12 +10,7 @@ distinguished_name = dn CN = example.com [ req_v3 ] -subjectAltName = @alt_names 2.1.1.1=ASN1:UTF8String:A UTF8String Extension 2.1.1.2=ASN1:UTF8:A UTF8 Extension 2.1.1.3=ASN1:IA5:An IA5 Extension 2.1.1.4=ASN1:VISIBLE:A Visible Extension - -[ alt_names ] -DNS.1 = example.com -IP.1 = 127.0.0.1 diff --git a/builtin/credential/cert/test-fixtures/root/rootcawuri.cnf b/builtin/credential/cert/test-fixtures/root/rootcawuri.cnf new file mode 100644 index 0000000000..bb15540ca7 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawuri.cnf @@ -0,0 +1,18 @@ +[ req ] +default_bits = 2048 +encrypt_key = no +prompt = no +default_md = sha256 +req_extensions = req_v3 +distinguished_name = dn + +[ dn ] +CN = example.com + +[ req_v3 ] +subjectAltName = @alt_names + +[ alt_names ] +DNS.1 = example.com +IP.1 = 127.0.0.1 +URI.1 = spiffe://example.com/host diff --git a/builtin/credential/cert/test-fixtures/root/rootcawuri.csr b/builtin/credential/cert/test-fixtures/root/rootcawuri.csr new file mode 100644 index 0000000000..0ababe1ce0 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawuri.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICpTCCAY0CAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDEtoz6THzA8RFNJ+wu40Pa30Inyprv3xRGYA71 +0T3yLrWUA0xaS8i7HHXDaEVmtHi7I+dFRqGwCgtDLY3sXN1C1t/U6V6xhhQ1hRW7 +PJhbGfsfi8uBx83amWiSMlmEBYPryQzPS+8mmRErBi6EdmgbdGWV5IcovMddDxE1 +Npc1vwmTxDUOe6mRSa8UkaR9nwFl8LTz9clIkGlOJLHWD2oX15PVr7SKYco+MrIh +HLKkYMgATFJ05EKLyRxO/lQWD6ibUYJuGhFeNyjk34swl3uoWQBGndxcs2BQP4OL +EfnsoXVDrHwjZ1FWSu/Bf6TfKvwo5It1IZLnm+cCTqxCnaLRAgMBAAGgSjBIBgkq +hkiG9w0BCQ4xOzA5MDcGA1UdEQQwMC6CC2V4YW1wbGUuY29thwR/AAABhhlzcGlm +ZmU6Ly9leGFtcGxlLmNvbS9ob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQBw2y7bPrLk +B7DrZRvO/s8yj/Mi2iS/q3KEACEUxHTXH9GrqnQJ1n00WjaEu5JgXW8F08738nj/ +QhO5IM9ZMBtFyt9/GguZzGWnGUGUvtfM/ps/qzF6lAnjxYnFfqJeDWhg4SQsW6ZW +eFZ3S1kx0iQjy+Y7oWZNObbgDhszdJa6swN1WJBB8BZuiDJYXMBzfWdR6aZStJ0Z +lUHyaQbILXRc+meuDY7KeILJhldlE8oU/NENO1w1WXcsseXg8790pPYg+uR/uXg0 +0iWPtqgjO+55eAvkZ5nY0N/kABV1oaCB8bVs6/2HPqquPX6c+xkcUI/HY8SJgWzk +AHCG7VIB4W94 +-----END CERTIFICATE REQUEST----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawuricert.pem b/builtin/credential/cert/test-fixtures/root/rootcawuricert.pem new file mode 100644 index 0000000000..171f4de004 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawuricert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6jCCAdKgAwIBAgIJAJIiPq+77hekMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV +BAMTC2V4YW1wbGUuY29tMB4XDTE4MDMzMTE2MTE0NVoXDTE5MDMzMTE2MTE0NVow +FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDEtoz6THzA8RFNJ+wu40Pa30Inyprv3xRGYA710T3yLrWUA0xaS8i7 +HHXDaEVmtHi7I+dFRqGwCgtDLY3sXN1C1t/U6V6xhhQ1hRW7PJhbGfsfi8uBx83a +mWiSMlmEBYPryQzPS+8mmRErBi6EdmgbdGWV5IcovMddDxE1Npc1vwmTxDUOe6mR +Sa8UkaR9nwFl8LTz9clIkGlOJLHWD2oX15PVr7SKYco+MrIhHLKkYMgATFJ05EKL +yRxO/lQWD6ibUYJuGhFeNyjk34swl3uoWQBGndxcs2BQP4OLEfnsoXVDrHwjZ1FW +Su/Bf6TfKvwo5It1IZLnm+cCTqxCnaLRAgMBAAGjOzA5MDcGA1UdEQQwMC6CC2V4 +YW1wbGUuY29thwR/AAABhhlzcGlmZmU6Ly9leGFtcGxlLmNvbS9ob3N0MA0GCSqG +SIb3DQEBBQUAA4IBAQDhR59hSpL4k4wbK3bA17YoNwFBsDpDcoU2iB9NDUTj+j+T +Rgumt+VHtgxuGRDFPQ+0D2hmJJHNCHKulgeDKVLtY/c5dCEsk8epLQwoCd/pQsNR +Lj102g83rCrU0pfTFjAUoecmHBFt7GDxVyWDsJgGItMatPQuWyZXTzO8JdhCfpMP +m7z65VYZjIPgevpSR5NVJDU8u2jRCkRQBFqOXotJS6EObu4P8aly4YhwiMf1B0L8 +60XHbBksOQSZOky37uFhaab78bAu5nd2kN1K4qSObTJshCZAwRYk0XdCjDrMcZRJ +Fp+yygib+K8e7o71Co0zUdSU0yxOKGsWvjz1BUVl +-----END CERTIFICATE----- diff --git a/builtin/credential/cert/test-fixtures/root/rootcawurikey.pem b/builtin/credential/cert/test-fixtures/root/rootcawurikey.pem new file mode 100644 index 0000000000..81ac978291 --- /dev/null +++ b/builtin/credential/cert/test-fixtures/root/rootcawurikey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEtoz6THzA8RFN +J+wu40Pa30Inyprv3xRGYA710T3yLrWUA0xaS8i7HHXDaEVmtHi7I+dFRqGwCgtD +LY3sXN1C1t/U6V6xhhQ1hRW7PJhbGfsfi8uBx83amWiSMlmEBYPryQzPS+8mmREr +Bi6EdmgbdGWV5IcovMddDxE1Npc1vwmTxDUOe6mRSa8UkaR9nwFl8LTz9clIkGlO +JLHWD2oX15PVr7SKYco+MrIhHLKkYMgATFJ05EKLyRxO/lQWD6ibUYJuGhFeNyjk +34swl3uoWQBGndxcs2BQP4OLEfnsoXVDrHwjZ1FWSu/Bf6TfKvwo5It1IZLnm+cC +TqxCnaLRAgMBAAECggEAYLdYbR/6HmroFMVSLGN000H9ps7IirNlpoxIDrhH+rDY +eeN9QNAN62E8zUyRAsQsr+YhKUBm8sSdcPQO2W13JAu9lVMAScwgV4gNfTd3uSL3 +AzWaYz63iYjvjyHOPUjw6Za6A5nUBWgwtrSdXmdRHF6IK8Bma7MVWj20OjOS+MsM +ScXk+yMTzpQYZ+AhP6rgcccn6djtk+Mqrpa7yW5cTDkQ0+/MF0KR7tYUbakRSimI +Ph6e+zFt4infOWP5fDr0oSpMXA2chh0INTtxbltnJzvaaPF8LSzyihWTZszABc84 +Ckgrvmt5DViYbmfKHk0csS/xF/wdygfkkJHML8l/IQKBgQD9CMaDgfpM78uH8Kgm +Ja/ANu4Te5zO/n5E96PHdvCN+m7pCMgYuXuKgXDADgD1O6MItzDnEpkubluffARf +1eJyw9ner0tTAs8bZgtKdLQvaghq5Afk1+m8XDTskJsVLVGrozvJLuabPqnZrkRH +AxLdZjiAh6z2csFVYTQnMQSfhQKBgQDHBMjapcDx9y/jUq/yoFvwkdT3THQO9JgK +XC5NOHGVhyT3695wpqi/ANA4b8P9MmAzcUkT8a3jcqV87OIQmK3Y1oGvjHQCKS60 +OYE9TadpxwW2uzxS5T7YegXf5L3uHinoWHlLklN+Q9pvJStw4QrDzhd8rtcZA+FN +KBmjzYdJ3QKBgQDYutl97qi7mXEVgPYlpoYA94u4OFq5mZYB8LLhuGiW03iINbNe +KhE9M12lwtjjNC+S2YYThgSaln/3/LuqcoLBlitY54B3G6LVbvQg1BE5w3JuS97P +Dnjvk3LpZXrQCr83altdGMUBGA1XnEJzKJjR9ipTPOLTPLuIK/gF0aCKGQKBgQCm +ZFitfZGge4M9Mt/KIcpciwCcNf5+ln8bglBv3XYRhykgYsLaOmyxLLPpy3/4DAsk +V1263//7PtofZUnoiE4pEcbhh7NiLx5OLhngsDD9Hhmn2kkoIWR2xyZsN6mYEP4G +tRnMVi2aTo6tCE2WlYBTjtZSNze9QWI4CQPO0MKAvQKBgQCzpJAJXl04zQv9S5uW +pH3xShmd0Zjv9tNyOVNqWUeg47IFzNC2w/6FqYkhd9C4DCAibzPx7WkVjYAR+ivY +NQv1usVhV3maJX5rw+C4Zck8kAmiqMbLacUVdy/5E2Mbk7xqjAvu+qrMFdSk/2GR +raR1xOEvE0cKWIwr8c8wIva4wA== +-----END PRIVATE KEY----- diff --git a/builtin/credential/ldap/path_config.go b/builtin/credential/ldap/path_config.go index 7632d05268..81237930b6 100644 --- a/builtin/credential/ldap/path_config.go +++ b/builtin/credential/ldap/path_config.go @@ -2,16 +2,9 @@ package ldap import ( "context" - "crypto/x509" - "encoding/pem" - "fmt" - "strings" - "text/template" - "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/ldaputil" - "github.com/hashicorp/vault/helper/tlsutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -19,105 +12,7 @@ import ( func pathConfig(b *backend) *framework.Path { return &framework.Path{ Pattern: `config`, - Fields: map[string]*framework.FieldSchema{ - "url": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "ldap://127.0.0.1", - Description: "LDAP URL to connect to (default: ldap://127.0.0.1). Multiple URLs can be specified by concatenating them with commas; they will be tried in-order.", - }, - - "userdn": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "LDAP domain to use for users (eg: ou=People,dc=example,dc=org)", - }, - - "binddn": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "LDAP DN for searching for the user DN (optional)", - }, - - "bindpass": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "LDAP password for searching for the user DN (optional)", - }, - - "groupdn": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "LDAP search base to use for group membership search (eg: ou=Groups,dc=example,dc=org)", - }, - - "groupfilter": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))", - Description: `Go template for querying group membership of user (optional) -The template can access the following context variables: UserDN, Username -Example: (&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}})) -Default: (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))`, - }, - - "groupattr": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "cn", - Description: `LDAP attribute to follow on objects returned by -in order to enumerate user group membership. -Examples: "cn" or "memberOf", etc. -Default: cn`, - }, - - "upndomain": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Enables userPrincipalDomain login with [username]@UPNDomain (optional)", - }, - - "userattr": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "cn", - Description: "Attribute used for users (default: cn)", - }, - - "certificate": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "CA certificate to use when verifying LDAP server certificate, must be x509 PEM encoded (optional)", - }, - - "discoverdn": &framework.FieldSchema{ - Type: framework.TypeBool, - Description: "Use anonymous bind to discover the bind DN of a user (optional)", - }, - - "insecure_tls": &framework.FieldSchema{ - Type: framework.TypeBool, - Description: "Skip LDAP server SSL Certificate verification - VERY insecure (optional)", - }, - - "starttls": &framework.FieldSchema{ - Type: framework.TypeBool, - Description: "Issue a StartTLS command after establishing unencrypted connection (optional)", - }, - - "tls_min_version": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "tls12", - Description: "Minimum TLS version to use. Accepted values are 'tls10', 'tls11' or 'tls12'. Defaults to 'tls12'", - }, - - "tls_max_version": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "tls12", - Description: "Maximum TLS version to use. Accepted values are 'tls10', 'tls11' or 'tls12'. Defaults to 'tls12'", - }, - - "deny_null_bind": &framework.FieldSchema{ - Type: framework.TypeBool, - Default: true, - Description: "Denies an unauthenticated LDAP bind request if the user's password is empty; defaults to true", - }, - - "case_sensitive_names": &framework.FieldSchema{ - Type: framework.TypeBool, - Description: "If true, case sensitivity will be used when comparing usernames and groups for matching policies.", - }, - }, + Fields: ldaputil.ConfigFields(), Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.pathConfigRead, @@ -140,7 +35,7 @@ func (b *backend) Config(ctx context.Context, req *logical.Request) (*ldaputil.C } // Create a new ConfigEntry, filling in defaults where appropriate - result, err := b.newConfigEntry(fd) + result, err := ldaputil.NewConfigEntry(fd) if err != nil { return nil, err } @@ -195,147 +90,14 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *f } resp := &logical.Response{ - Data: map[string]interface{}{ - "url": cfg.Url, - "userdn": cfg.UserDN, - "groupdn": cfg.GroupDN, - "groupfilter": cfg.GroupFilter, - "groupattr": cfg.GroupAttr, - "upndomain": cfg.UPNDomain, - "userattr": cfg.UserAttr, - "certificate": cfg.Certificate, - "insecure_tls": cfg.InsecureTLS, - "starttls": cfg.StartTLS, - "binddn": cfg.BindDN, - "deny_null_bind": cfg.DenyNullBind, - "discoverdn": cfg.DiscoverDN, - "tls_min_version": cfg.TLSMinVersion, - "tls_max_version": cfg.TLSMaxVersion, - "case_sensitive_names": *cfg.CaseSensitiveNames, - }, + Data: cfg.PasswordlessMap(), } return resp, nil } -/* - * Creates and initializes a ConfigEntry object with its default values, - * as specified by the passed schema. - */ -func (b *backend) newConfigEntry(d *framework.FieldData) (*ldaputil.ConfigEntry, error) { - cfg := new(ldaputil.ConfigEntry) - - url := d.Get("url").(string) - if url != "" { - cfg.Url = strings.ToLower(url) - } - userattr := d.Get("userattr").(string) - if userattr != "" { - cfg.UserAttr = strings.ToLower(userattr) - } - userdn := d.Get("userdn").(string) - if userdn != "" { - cfg.UserDN = userdn - } - groupdn := d.Get("groupdn").(string) - if groupdn != "" { - cfg.GroupDN = groupdn - } - groupfilter := d.Get("groupfilter").(string) - if groupfilter != "" { - // Validate the template before proceeding - _, err := template.New("queryTemplate").Parse(groupfilter) - if err != nil { - return nil, errwrap.Wrapf("invalid groupfilter: {{err}}", err) - } - - cfg.GroupFilter = groupfilter - } - groupattr := d.Get("groupattr").(string) - if groupattr != "" { - cfg.GroupAttr = groupattr - } - upndomain := d.Get("upndomain").(string) - if upndomain != "" { - cfg.UPNDomain = upndomain - } - certificate := d.Get("certificate").(string) - if certificate != "" { - block, _ := pem.Decode([]byte(certificate)) - - if block == nil || block.Type != "CERTIFICATE" { - return nil, fmt.Errorf("failed to decode PEM block in the certificate") - } - _, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, errwrap.Wrapf("failed to parse certificate: {{err}}", err) - } - cfg.Certificate = certificate - } - insecureTLS := d.Get("insecure_tls").(bool) - if insecureTLS { - cfg.InsecureTLS = insecureTLS - } - cfg.TLSMinVersion = d.Get("tls_min_version").(string) - if cfg.TLSMinVersion == "" { - return nil, fmt.Errorf("failed to get 'tls_min_version' value") - } - - var ok bool - _, ok = tlsutil.TLSLookup[cfg.TLSMinVersion] - if !ok { - return nil, fmt.Errorf("invalid 'tls_min_version'") - } - - cfg.TLSMaxVersion = d.Get("tls_max_version").(string) - if cfg.TLSMaxVersion == "" { - return nil, fmt.Errorf("failed to get 'tls_max_version' value") - } - - _, ok = tlsutil.TLSLookup[cfg.TLSMaxVersion] - if !ok { - return nil, fmt.Errorf("invalid 'tls_max_version'") - } - if cfg.TLSMaxVersion < cfg.TLSMinVersion { - return nil, fmt.Errorf("'tls_max_version' must be greater than or equal to 'tls_min_version'") - } - - startTLS := d.Get("starttls").(bool) - if startTLS { - cfg.StartTLS = startTLS - } - - bindDN := d.Get("binddn").(string) - if bindDN != "" { - cfg.BindDN = bindDN - } - - bindPass := d.Get("bindpass").(string) - if bindPass != "" { - cfg.BindPassword = bindPass - } - - denyNullBind := d.Get("deny_null_bind").(bool) - if denyNullBind { - cfg.DenyNullBind = denyNullBind - } - - discoverDN := d.Get("discoverdn").(bool) - if discoverDN { - cfg.DiscoverDN = discoverDN - } - - caseSensitiveNames, ok := d.GetOk("case_sensitive_names") - if ok { - cfg.CaseSensitiveNames = new(bool) - *cfg.CaseSensitiveNames = caseSensitiveNames.(bool) - } - - return cfg, nil -} - func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { // Build a ConfigEntry struct out of the supplied FieldData - cfg, err := b.newConfigEntry(d) + cfg, err := ldaputil.NewConfigEntry(d) if err != nil { return logical.ErrorResponse(err.Error()), nil } diff --git a/builtin/credential/userpass/backend_test.go b/builtin/credential/userpass/backend_test.go index 807c8879f0..355f94e5a5 100644 --- a/builtin/credential/userpass/backend_test.go +++ b/builtin/credential/userpass/backend_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "crypto/tls" + "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/logical" logicaltest "github.com/hashicorp/vault/logical/testing" @@ -225,36 +227,6 @@ func testUpdatePolicies(t *testing.T, user, policies string) logicaltest.TestSte } } -func testUsersWrite(t *testing.T, user string, data map[string]interface{}, expectError bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "users/" + user, - Data: data, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp == nil && expectError { - return fmt.Errorf("expected error but received nil") - } - return nil - }, - } -} - -func testLoginWrite(t *testing.T, user string, data map[string]interface{}, expectError bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "login/" + user, - Data: data, - ErrorOk: true, - Check: func(resp *logical.Response) error { - if resp == nil && expectError { - return fmt.Errorf("expected error but received nil") - } - return nil - }, - } -} - func testAccStepList(t *testing.T, users []string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ListOperation, @@ -282,7 +254,8 @@ func testAccStepLogin(t *testing.T, user string, pass string, policies []string) }, Unauthenticated: true, - Check: logicaltest.TestCheckAuth(policies), + Check: logicaltest.TestCheckAuth(policies), + ConnState: &tls.ConnectionState{}, } } diff --git a/builtin/credential/userpass/path_login.go b/builtin/credential/userpass/path_login.go index 4164196b29..c495875c43 100644 --- a/builtin/credential/userpass/path_login.go +++ b/builtin/credential/userpass/path_login.go @@ -3,9 +3,11 @@ package userpass import ( "context" "crypto/subtle" + "errors" "fmt" "strings" + "github.com/hashicorp/vault/helper/cidrutil" "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -69,6 +71,11 @@ func (b *backend) pathLogin(ctx context.Context, req *logical.Request, d *framew return logical.ErrorResponse("invalid username or password"), nil } + // Check for a CIDR match. + if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, user.BoundCIDRs) { + return logical.ErrorResponse("login request originated from invalid CIDR"), nil + } + // Check for a password match. Check for a hash collision for Vault 0.2+, // but handle the older legacy passwords with a constant time comparison. passwordBytes := []byte(password) @@ -97,6 +104,7 @@ func (b *backend) pathLogin(ctx context.Context, req *logical.Request, d *framew Alias: &logical.Alias{ Name: username, }, + BoundCIDRs: user.BoundCIDRs, }, }, nil } @@ -116,9 +124,15 @@ func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, d *f return nil, fmt.Errorf("policies have changed, not renewing") } + // Check for a CIDR match. + if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, user.BoundCIDRs) { + return nil, errors.New("renewal request originated from invalid CIDR") + } + resp := &logical.Response{Auth: req.Auth} resp.Auth.TTL = user.TTL resp.Auth.MaxTTL = user.MaxTTL + resp.Auth.BoundCIDRs = user.BoundCIDRs return resp, nil } diff --git a/builtin/credential/userpass/path_users.go b/builtin/credential/userpass/path_users.go index 9a748c5c7d..0e8eee886f 100644 --- a/builtin/credential/userpass/path_users.go +++ b/builtin/credential/userpass/path_users.go @@ -6,6 +6,8 @@ import ( "strings" "time" + "github.com/hashicorp/go-sockaddr" + "github.com/hashicorp/vault/helper/parseutil" "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -52,6 +54,12 @@ func pathUsers(b *backend) *framework.Path { Type: framework.TypeDurationSecond, Description: "Maximum duration after which authentication will be expired", }, + + "bound_cidrs": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `Comma separated string or list of CIDR blocks. If set, specifies the blocks of +IP addresses which can perform the login operation.`, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -135,9 +143,10 @@ func (b *backend) pathUserRead(ctx context.Context, req *logical.Request, d *fra return &logical.Response{ Data: map[string]interface{}{ - "policies": user.Policies, - "ttl": user.TTL.Seconds(), - "max_ttl": user.MaxTTL.Seconds(), + "policies": user.Policies, + "ttl": user.TTL.Seconds(), + "max_ttl": user.MaxTTL.Seconds(), + "bound_cidrs": user.BoundCIDRs, }, }, nil } @@ -177,6 +186,12 @@ func (b *backend) userCreateUpdate(ctx context.Context, req *logical.Request, d userEntry.MaxTTL = time.Duration(maxTTL.(int)) * time.Second } + boundCIDRs, err := parseutil.ParseAddrs(d.Get("bound_cidrs")) + if err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } + userEntry.BoundCIDRs = boundCIDRs + return nil, b.setUser(ctx, req.Storage, username, userEntry) } @@ -204,6 +219,8 @@ type UserEntry struct { // Maximum duration for which user can be valid MaxTTL time.Duration + + BoundCIDRs []*sockaddr.SockAddrMarshaler } const pathUserHelpSyn = ` diff --git a/builtin/logical/pki/path_tidy.go b/builtin/logical/pki/path_tidy.go index b254321a36..9b0a86df9c 100644 --- a/builtin/logical/pki/path_tidy.go +++ b/builtin/logical/pki/path_tidy.go @@ -53,6 +53,10 @@ func (b *backend) pathTidyWrite(ctx context.Context, req *logical.Request, d *fr tidyCertStore := d.Get("tidy_cert_store").(bool) tidyRevocationList := d.Get("tidy_revocation_list").(bool) + if safetyBuffer < 1 { + return logical.ErrorResponse("safety_buffer must be greater than zero"), nil + } + bufferDuration := time.Duration(safetyBuffer) * time.Second var resp *logical.Response diff --git a/command/auth.go b/command/auth.go index ec1cd065c9..265f2a2b7b 100644 --- a/command/auth.go +++ b/command/auth.go @@ -66,10 +66,12 @@ func (c *AuthCommand) Run(args []string) int { for _, arg := range args { switch { case strings.HasPrefix(arg, "-methods"): - c.UI.Warn(wrapAtLength( - "WARNING! The -methods flag is deprecated. Please use "+ - "\"vault auth list\" instead. This flag will be removed in "+ - "Vault 0.11 (or later).") + "\n") + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! The -methods flag is deprecated. Please use "+ + "\"vault auth list\" instead. This flag will be removed in "+ + "Vault 0.11 (or later).") + "\n") + } return (&AuthListCommand{ BaseCommand: &BaseCommand{ UI: c.UI, @@ -77,10 +79,12 @@ func (c *AuthCommand) Run(args []string) int { }, }).Run(nil) case strings.HasPrefix(arg, "-method-help"): - c.UI.Warn(wrapAtLength( - "WARNING! The -method-help flag is deprecated. Please use "+ - "\"vault auth help\" instead. This flag will be removed in "+ - "Vault 0.11 (or later).") + "\n") + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! The -method-help flag is deprecated. Please use "+ + "\"vault auth help\" instead. This flag will be removed in "+ + "Vault 0.11 (or later).") + "\n") + } // Parse the args to pull out the method, suppressing any errors because // there could be other flags that we don't care about. f := flag.NewFlagSet("", flag.ContinueOnError) @@ -101,11 +105,13 @@ func (c *AuthCommand) Run(args []string) int { // If we got this far, we have an arg or a series of args that should be // passed directly to the new "vault login" command. - c.UI.Warn(wrapAtLength( - "WARNING! The \"vault auth ARG\" command is deprecated and is now a "+ - "subcommand for interacting with auth methods. To authenticate "+ - "locally to Vault, use \"vault login\" instead. This backwards "+ - "compatibility will be removed in Vault 0.11 (or later).") + "\n") + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! The \"vault auth ARG\" command is deprecated and is now a "+ + "subcommand for interacting with auth methods. To authenticate "+ + "locally to Vault, use \"vault login\" instead. This backwards "+ + "compatibility will be removed in Vault 0.11 (or later).") + "\n") + } return (&LoginCommand{ BaseCommand: &BaseCommand{ UI: c.UI, diff --git a/command/base.go b/command/base.go index f86f8b622d..79f8cf40a9 100644 --- a/command/base.go +++ b/command/base.go @@ -284,7 +284,7 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { Usage: "Print only the field with the given name. Specifying " + "this option will take precedence over other formatting " + "directives. The result will not have a trailing newline " + - "making it idea for piping to other processes.", + "making it ideal for piping to other processes.", }) } diff --git a/command/commands.go b/command/commands.go index 7b9e4a60e5..9e092ed479 100644 --- a/command/commands.go +++ b/command/commands.go @@ -6,6 +6,7 @@ import ( "os/signal" "syscall" + ad "github.com/hashicorp/vault-plugin-secrets-ad/plugin" gcp "github.com/hashicorp/vault-plugin-secrets-gcp/plugin" kv "github.com/hashicorp/vault-plugin-secrets-kv" "github.com/hashicorp/vault/audit" @@ -110,6 +111,7 @@ var ( } logicalBackends = map[string]logical.Factory{ + "ad": ad.Factory, "aws": aws.Factory, "cassandra": cassandra.Factory, "consul": consul.Factory, @@ -211,359 +213,212 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { }, } + getBaseCommand := func() *BaseCommand { + return &BaseCommand{ + UI: ui, + tokenHelper: runOpts.TokenHelper, + flagAddress: runOpts.Address, + client: runOpts.Client, + } + } + Commands = map[string]cli.CommandFactory{ "audit": func() (cli.Command, error) { return &AuditCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "audit disable": func() (cli.Command, error) { return &AuditDisableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "audit enable": func() (cli.Command, error) { return &AuditEnableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "audit list": func() (cli.Command, error) { return &AuditListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "auth tune": func() (cli.Command, error) { return &AuthTuneCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "auth": func() (cli.Command, error) { return &AuthCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, - Handlers: loginHandlers, + BaseCommand: getBaseCommand(), + Handlers: loginHandlers, }, nil }, "auth disable": func() (cli.Command, error) { return &AuthDisableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "auth enable": func() (cli.Command, error) { return &AuthEnableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "auth help": func() (cli.Command, error) { return &AuthHelpCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, - Handlers: loginHandlers, + BaseCommand: getBaseCommand(), + Handlers: loginHandlers, }, nil }, "auth list": func() (cli.Command, error) { return &AuthListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "delete": func() (cli.Command, error) { return &DeleteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "lease": func() (cli.Command, error) { return &LeaseCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "lease renew": func() (cli.Command, error) { return &LeaseRenewCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "lease revoke": func() (cli.Command, error) { return &LeaseRevokeCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "list": func() (cli.Command, error) { return &ListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "login": func() (cli.Command, error) { return &LoginCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, - Handlers: loginHandlers, + BaseCommand: getBaseCommand(), + Handlers: loginHandlers, }, nil }, "operator": func() (cli.Command, error) { return &OperatorCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "operator generate-root": func() (cli.Command, error) { return &OperatorGenerateRootCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "operator init": func() (cli.Command, error) { return &OperatorInitCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "operator key-status": func() (cli.Command, error) { return &OperatorKeyStatusCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "operator rekey": func() (cli.Command, error) { return &OperatorRekeyCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "operator rotate": func() (cli.Command, error) { return &OperatorRotateCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "operator seal": func() (cli.Command, error) { return &OperatorSealCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "operator step-down": func() (cli.Command, error) { return &OperatorStepDownCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "operator unseal": func() (cli.Command, error) { return &OperatorUnsealCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "path-help": func() (cli.Command, error) { return &PathHelpCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "policy": func() (cli.Command, error) { return &PolicyCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "policy delete": func() (cli.Command, error) { return &PolicyDeleteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "policy fmt": func() (cli.Command, error) { return &PolicyFmtCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "policy list": func() (cli.Command, error) { return &PolicyListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "policy read": func() (cli.Command, error) { return &PolicyReadCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "policy write": func() (cli.Command, error) { return &PolicyWriteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "read": func() (cli.Command, error) { return &ReadCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "secrets": func() (cli.Command, error) { return &SecretsCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "secrets disable": func() (cli.Command, error) { return &SecretsDisableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "secrets enable": func() (cli.Command, error) { return &SecretsEnableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "secrets list": func() (cli.Command, error) { return &SecretsListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "secrets move": func() (cli.Command, error) { return &SecretsMoveCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "secrets tune": func() (cli.Command, error) { return &SecretsTuneCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "server": func() (cli.Command, error) { @@ -583,193 +438,123 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { }, "ssh": func() (cli.Command, error) { return &SSHCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "status": func() (cli.Command, error) { return &StatusCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "token": func() (cli.Command, error) { return &TokenCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "token create": func() (cli.Command, error) { return &TokenCreateCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "token capabilities": func() (cli.Command, error) { return &TokenCapabilitiesCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "token lookup": func() (cli.Command, error) { return &TokenLookupCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "token renew": func() (cli.Command, error) { return &TokenRenewCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "token revoke": func() (cli.Command, error) { return &TokenRevokeCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "unwrap": func() (cli.Command, error) { return &UnwrapCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "version": func() (cli.Command, error) { return &VersionCommand{ VersionInfo: version.GetVersion(), - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "write": func() (cli.Command, error) { return &WriteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv": func() (cli.Command, error) { return &KVCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv put": func() (cli.Command, error) { return &KVPutCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv patch": func() (cli.Command, error) { return &KVPatchCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv get": func() (cli.Command, error) { return &KVGetCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv delete": func() (cli.Command, error) { return &KVDeleteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv list": func() (cli.Command, error) { return &KVListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv destroy": func() (cli.Command, error) { return &KVDestroyCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv undelete": func() (cli.Command, error) { return &KVUndeleteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv enable-versioning": func() (cli.Command, error) { return &KVEnableVersioningCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv metadata": func() (cli.Command, error) { return &KVMetadataCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv metadata put": func() (cli.Command, error) { return &KVMetadataPutCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv metadata get": func() (cli.Command, error) { return &KVMetadataGetCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, "kv metadata delete": func() (cli.Command, error) { return &KVMetadataDeleteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, + BaseCommand: getBaseCommand(), }, nil }, } @@ -782,12 +567,9 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { return &DeprecatedCommand{ Old: "audit-disable", New: "audit disable", + UI: ui, Command: &AuditDisableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -798,11 +580,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "audit enable", UI: ui, Command: &AuditEnableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -813,11 +591,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "audit list", UI: ui, Command: &AuditListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -828,11 +602,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "auth disable", UI: ui, Command: &AuthDisableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -843,11 +613,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "auth enable", UI: ui, Command: &AuthEnableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -858,11 +624,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "token capabilities", UI: ui, Command: &TokenCapabilitiesCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -873,11 +635,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "operator generate-root", UI: ui, Command: &OperatorGenerateRootCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -888,11 +646,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "operator init", UI: ui, Command: &OperatorInitCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -903,11 +657,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "operator key-status", UI: ui, Command: &OperatorKeyStatusCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -918,11 +668,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "lease renew", UI: ui, Command: &LeaseRenewCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -933,11 +679,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "lease revoke", UI: ui, Command: &LeaseRevokeCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -948,11 +690,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "secrets enable", UI: ui, Command: &SecretsEnableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -963,11 +701,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "secrets tune", UI: ui, Command: &SecretsTuneCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -978,11 +712,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "secrets list", UI: ui, Command: &SecretsListCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -993,11 +723,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "policy read\" or \"vault policy list", // lol UI: ui, Command: &PoliciesDeprecatedCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1008,11 +734,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "policy delete", UI: ui, Command: &PolicyDeleteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1023,11 +745,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "policy write", UI: ui, Command: &PolicyWriteCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1038,11 +756,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "operator rekey", UI: ui, Command: &OperatorRekeyCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1053,11 +767,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "secrets move", UI: ui, Command: &SecretsMoveCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1068,11 +778,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "operator rotate", UI: ui, Command: &OperatorRotateCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1083,11 +789,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "operator seal", UI: ui, Command: &OperatorSealCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1098,11 +800,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "operator step-down", UI: ui, Command: &OperatorStepDownCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1113,11 +811,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "token create", UI: ui, Command: &TokenCreateCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1128,11 +822,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "token lookup", UI: ui, Command: &TokenLookupCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1143,11 +833,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "token renew", UI: ui, Command: &TokenRenewCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1158,11 +844,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "token revoke", UI: ui, Command: &TokenRevokeCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1173,11 +855,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "secrets disable", UI: ui, Command: &SecretsDisableCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, @@ -1188,11 +866,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { New: "operator unseal", UI: ui, Command: &OperatorUnsealCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - tokenHelper: runOpts.TokenHelper, - flagAddress: runOpts.Address, - }, + BaseCommand: getBaseCommand(), }, }, nil }, diff --git a/command/format.go b/command/format.go index 4e378c89cd..c4c55e00e9 100644 --- a/command/format.go +++ b/command/format.go @@ -69,20 +69,18 @@ var Formatters = map[string]Formatter{ "yml": YamlFormatter{}, } -func format() string { - format := os.Getenv(EnvVaultFormat) - if format == "" { - format = "table" - } - return format -} - func Format(ui cli.Ui) string { switch ui.(type) { case *VaultUI: return ui.(*VaultUI).format } - return format() + + format := os.Getenv(EnvVaultFormat) + if format == "" { + format = "table" + } + + return format } // An output formatter for json output of an object diff --git a/command/format_test.go b/command/format_test.go index b97ca27315..c9478301fa 100644 --- a/command/format_test.go +++ b/command/format_test.go @@ -1,6 +1,7 @@ package command import ( + "bytes" "os" "strings" "testing" @@ -91,13 +92,13 @@ func Test_Format_Parsing(t *testing.T) { }{ { "format", - []string{"-format", "json"}, + []string{"token", "renew", "-format", "json"}, "{", 0, }, { "format_bad", - []string{"-format", "nope-not-real"}, + []string{"token", "renew", "-format", "nope-not-real"}, "Invalid output format", 1, }, @@ -110,21 +111,24 @@ func Test_Format_Parsing(t *testing.T) { client, closer := testVaultServer(t) defer closer() + stdout := bytes.NewBuffer(nil) + stderr := bytes.NewBuffer(nil) + runOpts := &RunOptions{ + Stdout: stdout, + Stderr: stderr, + Client: client, + } + // Login with the token so we can renew-self. token, _ := testTokenAndAccessor(t, client) client.SetToken(token) - ui, cmd := testTokenRenewCommand(t) - cmd.client = client - - tc.args = setupEnv(tc.args) - - code := cmd.Run(tc.args) + code := RunCustom(tc.args, runOpts) if code != tc.code { t.Errorf("expected %d to be %d", code, tc.code) } - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + combined := stdout.String() + stderr.String() if !strings.Contains(combined, tc.out) { t.Errorf("expected %q to contain %q", combined, tc.out) } diff --git a/command/lease_renew.go b/command/lease_renew.go index c12f53d2e8..07b177046a 100644 --- a/command/lease_renew.go +++ b/command/lease_renew.go @@ -94,11 +94,12 @@ func (c *LeaseRenewCommand) Run(args []string) int { case 2: // Deprecation // TODO: remove in 0.9.0 - c.UI.Warn(wrapAtLength( - "WARNING! Specifying INCREMENT as a second argument is deprecated. " + - "Please use -increment instead. This will be removed in the next " + - "major release of Vault.")) - + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! Specifying INCREMENT as a second argument is deprecated. " + + "Please use -increment instead. This will be removed in the next " + + "major release of Vault.")) + } leaseID = strings.TrimSpace(args[0]) parsed, err := time.ParseDuration(appendDurationSuffix(args[1])) if err != nil { diff --git a/command/login.go b/command/login.go index b6603e855a..d9c2a3f523 100644 --- a/command/login.go +++ b/command/login.go @@ -167,26 +167,28 @@ func (c *LoginCommand) Run(args []string) int { // TODO: remove in 0.10.0 switch { case c.flagNoVerify: - c.UI.Warn(wrapAtLength( - "WARNING! The -no-verify flag is deprecated. In the past, Vault " + - "performed a lookup on a token after authentication. This is no " + - "longer the case for all auth methods except \"token\". Vault will " + - "still attempt to perform a lookup when given a token directly " + - "because that is how it gets the list of policies, ttl, and other " + - "metadata. To disable this lookup, specify \"lookup=false\" as a " + - "configuration option to the token auth method, like this:")) - c.UI.Warn("") - c.UI.Warn(" $ vault auth token=ABCD lookup=false") - c.UI.Warn("") - c.UI.Warn("Or omit the token and Vault will prompt for it:") - c.UI.Warn("") - c.UI.Warn(" $ vault auth lookup=false") - c.UI.Warn(" Token (will be hidden): ...") - c.UI.Warn("") - c.UI.Warn(wrapAtLength( - "If you are not using token authentication, you can safely omit this " + - "flag. Vault will not perform a lookup after authentication.")) - c.UI.Warn("") + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! The -no-verify flag is deprecated. In the past, Vault " + + "performed a lookup on a token after authentication. This is no " + + "longer the case for all auth methods except \"token\". Vault will " + + "still attempt to perform a lookup when given a token directly " + + "because that is how it gets the list of policies, ttl, and other " + + "metadata. To disable this lookup, specify \"lookup=false\" as a " + + "configuration option to the token auth method, like this:")) + c.UI.Warn("") + c.UI.Warn(" $ vault auth token=ABCD lookup=false") + c.UI.Warn("") + c.UI.Warn("Or omit the token and Vault will prompt for it:") + c.UI.Warn("") + c.UI.Warn(" $ vault auth lookup=false") + c.UI.Warn(" Token (will be hidden): ...") + c.UI.Warn("") + c.UI.Warn(wrapAtLength( + "If you are not using token authentication, you can safely omit this " + + "flag. Vault will not perform a lookup after authentication.")) + c.UI.Warn("") + } // There's no point in passing this to other auth handlers... if c.flagMethod == "token" { diff --git a/command/main.go b/command/main.go index 3fd4134be4..9b52e622ae 100644 --- a/command/main.go +++ b/command/main.go @@ -10,6 +10,7 @@ import ( "text/tabwriter" "github.com/fatih/color" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/command/token" colorable "github.com/mattn/go-colorable" "github.com/mitchellh/cli" @@ -22,8 +23,7 @@ type VaultUI struct { // setupEnv parses args and may replace them and sets some env vars to known // values based on format options -func setupEnv(args []string) []string { - var format string +func setupEnv(args []string) (retArgs []string, format string) { var nextArgFormat bool for _, arg := range args { @@ -65,10 +65,8 @@ func setupEnv(args []string) []string { if format == "" { format = "table" } - // Put back into the env for later - os.Setenv(EnvVaultFormat, format) - return args + return args, format } type RunOptions struct { @@ -76,6 +74,7 @@ type RunOptions struct { Stdout io.Writer Stderr io.Writer Address string + Client *api.Client } func Run(args []string) int { @@ -89,7 +88,8 @@ func RunCustom(args []string, runOpts *RunOptions) int { runOpts = &RunOptions{} } - args = setupEnv(args) + var format string + args, format = setupEnv(args) // Don't use color if disabled useColor := true @@ -104,8 +104,6 @@ func RunCustom(args []string, runOpts *RunOptions) int { runOpts.Stderr = os.Stderr } - format := format() - // Only use colored UI if stdout is a tty, and not disabled if useColor && format == "table" { if f, ok := runOpts.Stdout.(*os.File); ok { diff --git a/command/operator_generate_root.go b/command/operator_generate_root.go index 78fd0b9d1b..b647a6af04 100644 --- a/command/operator_generate_root.go +++ b/command/operator_generate_root.go @@ -217,9 +217,11 @@ func (c *OperatorGenerateRootCommand) Run(args []string) int { // TODO: remove in 0.9.0 switch { case c.flagGenOTP: - c.UI.Warn(wrapAtLength( - "The -gen-otp flag is deprecated. Please use the -generate-otp flag " + - "instead.")) + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! The -gen-otp flag is deprecated. Please use the -generate-otp flag " + + "instead.")) + } c.flagGenerateOTP = c.flagGenOTP } diff --git a/command/operator_init.go b/command/operator_init.go index 4503d2eae9..d5e4103ea5 100644 --- a/command/operator_init.go +++ b/command/operator_init.go @@ -244,14 +244,18 @@ func (c *OperatorInitCommand) Run(args []string) int { // Deprecations // TODO: remove in 0.9.0 if c.flagAuto { - c.UI.Warn(wrapAtLength("WARNING! -auto is deprecated. Please use " + - "-consul-auto instead. This will be removed in Vault 0.11 " + - "(or later).")) + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength("WARNING! -auto is deprecated. Please use " + + "-consul-auto instead. This will be removed in Vault 0.11 " + + "(or later).")) + } c.flagConsulAuto = true } if c.flagCheck { - c.UI.Warn(wrapAtLength("WARNING! -check is deprecated. Please use " + - "-status instead. This will be removed in Vault 0.11 (or later).")) + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength("WARNING! -check is deprecated. Please use " + + "-status instead. This will be removed in Vault 0.11 (or later).")) + } c.flagStatus = true } diff --git a/command/operator_rekey.go b/command/operator_rekey.go index 0784a0492e..b587333d20 100644 --- a/command/operator_rekey.go +++ b/command/operator_rekey.go @@ -279,21 +279,27 @@ func (c *OperatorRekeyCommand) Run(args []string) int { // Deprecations // TODO: remove in 0.9.0 if c.flagDelete { - c.UI.Warn(wrapAtLength( - "WARNING! The -delete flag is deprecated. Please use -backup-delete " + - "instead. This flag will be removed in Vault 0.11 (or later).")) + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! The -delete flag is deprecated. Please use -backup-delete " + + "instead. This flag will be removed in Vault 0.11 (or later).")) + } c.flagBackupDelete = true } if c.flagRetrieve { - c.UI.Warn(wrapAtLength( - "WARNING! The -retrieve flag is deprecated. Please use -backup-retrieve " + - "instead. This flag will be removed in Vault 0.11 (or later).")) + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! The -retrieve flag is deprecated. Please use -backup-retrieve " + + "instead. This flag will be removed in Vault 0.11 (or later).")) + } c.flagBackupRetrieve = true } if c.flagRecoveryKey { - c.UI.Warn(wrapAtLength( - "WARNING! The -recovery-key flag is deprecated. Please use -target=recovery " + - "instead. This flag will be removed in Vault 0.11 (or later).")) + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! The -recovery-key flag is deprecated. Please use -target=recovery " + + "instead. This flag will be removed in Vault 0.11 (or later).")) + } c.flagTarget = "recovery" } @@ -355,25 +361,29 @@ func (c *OperatorRekeyCommand) init(client *api.Client) int { // Print warnings about recovery, etc. if len(c.flagPGPKeys) == 0 { - c.UI.Warn(wrapAtLength( - "WARNING! If you lose the keys after they are returned, there is no " + - "recovery. Consider canceling this operation and re-initializing " + - "with the -pgp-keys flag to protect the returned unseal keys along " + - "with -backup to allow recovery of the encrypted keys in case of " + - "emergency. You can delete the stored keys later using the -delete " + - "flag.")) - c.UI.Output("") + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! If you lose the keys after they are returned, there is no " + + "recovery. Consider canceling this operation and re-initializing " + + "with the -pgp-keys flag to protect the returned unseal keys along " + + "with -backup to allow recovery of the encrypted keys in case of " + + "emergency. You can delete the stored keys later using the -delete " + + "flag.")) + c.UI.Output("") + } } if len(c.flagPGPKeys) > 0 && !c.flagBackup { - c.UI.Warn(wrapAtLength( - "WARNING! You are using PGP keys for encrypted the resulting unseal " + - "keys, but you did not enable the option to backup the keys to " + - "Vault's core. If you lose the encrypted keys after they are " + - "returned, you will not be able to recover them. Consider canceling " + - "this operation and re-running with -backup to allow recovery of the " + - "encrypted unseal keys in case of emergency. You can delete the " + - "stored keys later using the -delete flag.")) - c.UI.Output("") + if Format(c.UI) == "table" { + c.UI.Warn(wrapAtLength( + "WARNING! You are using PGP keys for encrypted the resulting unseal " + + "keys, but you did not enable the option to backup the keys to " + + "Vault's core. If you lose the encrypted keys after they are " + + "returned, you will not be able to recover them. Consider canceling " + + "this operation and re-running with -backup to allow recovery of the " + + "encrypted unseal keys in case of emergency. You can delete the " + + "stored keys later using the -delete flag.")) + c.UI.Output("") + } } // Provide the current status diff --git a/command/operator_unseal_test.go b/command/operator_unseal_test.go index 7078b17cfc..b9b5c658af 100644 --- a/command/operator_unseal_test.go +++ b/command/operator_unseal_test.go @@ -1,6 +1,7 @@ package command import ( + "bytes" "encoding/json" "io/ioutil" "os" @@ -147,7 +148,6 @@ func TestOperatorUnsealCommand_Run(t *testing.T) { func TestOperatorUnsealCommand_Format(t *testing.T) { defer func() { - os.Setenv(EnvVaultFormat, "") os.Setenv(EnvVaultCLINoColor, "") }() @@ -159,21 +159,28 @@ func TestOperatorUnsealCommand_Format(t *testing.T) { t.Fatal(err) } - ui, cmd := testOperatorUnsealCommand(t) - cmd.client = client - cmd.testOutput = ioutil.Discard - - args := setupEnv([]string{"-format", "json"}) - - // Unseal with one key - code := cmd.Run(append(args, []string{ - keys[0], - }...)) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + stdout := bytes.NewBuffer(nil) + stderr := bytes.NewBuffer(nil) + runOpts := &RunOptions{ + Stdout: stdout, + Stderr: stderr, + Client: client, } - if !json.Valid(ui.OutputWriter.Bytes()) { + args, format := setupEnv([]string{"unseal", "-format", "json"}) + if format != "json" { + t.Fatalf("expected %q, got %q", "json", format) + } + + // Unseal with one key + code := RunCustom(append(args, []string{ + keys[0], + }...), runOpts) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, stderr.String()) + } + + if !json.Valid(stdout.Bytes()) { t.Error("expected output to be valid JSON") } } diff --git a/command/token_create.go b/command/token_create.go index 738030406e..fd269d947a 100644 --- a/command/token_create.go +++ b/command/token_create.go @@ -207,8 +207,10 @@ func (c *TokenCreateCommand) Run(args []string) int { // TODO: remove in 0.9.0 if c.flagLease != 0 { - c.UI.Warn("The -lease flag is deprecated. Please use -ttl instead.") - c.flagTTL = c.flagLease + if Format(c.UI) == "table" { + c.UI.Warn("The -lease flag is deprecated. Please use -ttl instead.") + c.flagTTL = c.flagLease + } } client, err := c.Client() diff --git a/command/token_renew.go b/command/token_renew.go index 7e7f00775b..ee6a626028 100644 --- a/command/token_renew.go +++ b/command/token_renew.go @@ -99,9 +99,10 @@ func (c *TokenRenewCommand) Run(args []string) int { token = strings.TrimSpace(args[0]) case 2: // TODO: remove in 0.9.0 - backwards compat - c.UI.Warn("Specifying increment as a second argument is deprecated. " + - "Please use -increment instead.") - + if Format(c.UI) == "table" { + c.UI.Warn("Specifying increment as a second argument is deprecated. " + + "Please use -increment instead.") + } token = strings.TrimSpace(args[0]) parsed, err := time.ParseDuration(appendDurationSuffix(args[1])) if err != nil { diff --git a/helper/ldaputil/config.go b/helper/ldaputil/config.go index f62da7575e..42d1b0a31d 100644 --- a/helper/ldaputil/config.go +++ b/helper/ldaputil/config.go @@ -5,10 +5,235 @@ import ( "encoding/pem" "errors" "fmt" + "strings" + "text/template" "github.com/hashicorp/vault/helper/tlsutil" + "github.com/hashicorp/vault/logical/framework" + + "github.com/hashicorp/errwrap" ) +// ConfigFields returns all the config fields that can potentially be used by the LDAP client. +// Not all fields will be used by every integration. +func ConfigFields() map[string]*framework.FieldSchema { + return map[string]*framework.FieldSchema{ + "url": { + Type: framework.TypeString, + Default: "ldap://127.0.0.1", + Description: "LDAP URL to connect to (default: ldap://127.0.0.1). Multiple URLs can be specified by concatenating them with commas; they will be tried in-order.", + }, + + "userdn": { + Type: framework.TypeString, + Description: "LDAP domain to use for users (eg: ou=People,dc=example,dc=org)", + }, + + "binddn": { + Type: framework.TypeString, + Description: "LDAP DN for searching for the user DN (optional)", + }, + + "bindpass": { + Type: framework.TypeString, + Description: "LDAP password for searching for the user DN (optional)", + }, + + "groupdn": { + Type: framework.TypeString, + Description: "LDAP search base to use for group membership search (eg: ou=Groups,dc=example,dc=org)", + }, + + "groupfilter": { + Type: framework.TypeString, + Default: "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))", + Description: `Go template for querying group membership of user (optional) +The template can access the following context variables: UserDN, Username +Example: (&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}})) +Default: (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))`, + }, + + "groupattr": { + Type: framework.TypeString, + Default: "cn", + Description: `LDAP attribute to follow on objects returned by +in order to enumerate user group membership. +Examples: "cn" or "memberOf", etc. +Default: cn`, + }, + + "upndomain": { + Type: framework.TypeString, + Description: "Enables userPrincipalDomain login with [username]@UPNDomain (optional)", + }, + + "userattr": { + Type: framework.TypeString, + Default: "cn", + Description: "Attribute used for users (default: cn)", + }, + + "certificate": { + Type: framework.TypeString, + Description: "CA certificate to use when verifying LDAP server certificate, must be x509 PEM encoded (optional)", + }, + + "discoverdn": { + Type: framework.TypeBool, + Description: "Use anonymous bind to discover the bind DN of a user (optional)", + }, + + "insecure_tls": { + Type: framework.TypeBool, + Description: "Skip LDAP server SSL Certificate verification - VERY insecure (optional)", + }, + + "starttls": { + Type: framework.TypeBool, + Description: "Issue a StartTLS command after establishing unencrypted connection (optional)", + }, + + "tls_min_version": { + Type: framework.TypeString, + Default: "tls12", + Description: "Minimum TLS version to use. Accepted values are 'tls10', 'tls11' or 'tls12'. Defaults to 'tls12'", + }, + + "tls_max_version": { + Type: framework.TypeString, + Default: "tls12", + Description: "Maximum TLS version to use. Accepted values are 'tls10', 'tls11' or 'tls12'. Defaults to 'tls12'", + }, + + "deny_null_bind": { + Type: framework.TypeBool, + Default: true, + Description: "Denies an unauthenticated LDAP bind request if the user's password is empty; defaults to true", + }, + + "case_sensitive_names": { + Type: framework.TypeBool, + Description: "If true, case sensitivity will be used when comparing usernames and groups for matching policies.", + }, + } +} + +/* + * Creates and initializes a ConfigEntry object with its default values, + * as specified by the passed schema. + */ +func NewConfigEntry(d *framework.FieldData) (*ConfigEntry, error) { + cfg := new(ConfigEntry) + + url := d.Get("url").(string) + if url != "" { + cfg.Url = strings.ToLower(url) + } + userattr := d.Get("userattr").(string) + if userattr != "" { + cfg.UserAttr = strings.ToLower(userattr) + } + userdn := d.Get("userdn").(string) + if userdn != "" { + cfg.UserDN = userdn + } + groupdn := d.Get("groupdn").(string) + if groupdn != "" { + cfg.GroupDN = groupdn + } + groupfilter := d.Get("groupfilter").(string) + if groupfilter != "" { + // Validate the template before proceeding + _, err := template.New("queryTemplate").Parse(groupfilter) + if err != nil { + return nil, errwrap.Wrapf("invalid groupfilter: {{err}}", err) + } + + cfg.GroupFilter = groupfilter + } + groupattr := d.Get("groupattr").(string) + if groupattr != "" { + cfg.GroupAttr = groupattr + } + upndomain := d.Get("upndomain").(string) + if upndomain != "" { + cfg.UPNDomain = upndomain + } + certificate := d.Get("certificate").(string) + if certificate != "" { + block, _ := pem.Decode([]byte(certificate)) + + if block == nil || block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("failed to decode PEM block in the certificate") + } + _, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errwrap.Wrapf("failed to parse certificate: {{err}}", err) + } + cfg.Certificate = certificate + } + insecureTLS := d.Get("insecure_tls").(bool) + if insecureTLS { + cfg.InsecureTLS = insecureTLS + } + cfg.TLSMinVersion = d.Get("tls_min_version").(string) + if cfg.TLSMinVersion == "" { + return nil, fmt.Errorf("failed to get 'tls_min_version' value") + } + + var ok bool + _, ok = tlsutil.TLSLookup[cfg.TLSMinVersion] + if !ok { + return nil, fmt.Errorf("invalid 'tls_min_version'") + } + + cfg.TLSMaxVersion = d.Get("tls_max_version").(string) + if cfg.TLSMaxVersion == "" { + return nil, fmt.Errorf("failed to get 'tls_max_version' value") + } + + _, ok = tlsutil.TLSLookup[cfg.TLSMaxVersion] + if !ok { + return nil, fmt.Errorf("invalid 'tls_max_version'") + } + if cfg.TLSMaxVersion < cfg.TLSMinVersion { + return nil, fmt.Errorf("'tls_max_version' must be greater than or equal to 'tls_min_version'") + } + + startTLS := d.Get("starttls").(bool) + if startTLS { + cfg.StartTLS = startTLS + } + + bindDN := d.Get("binddn").(string) + if bindDN != "" { + cfg.BindDN = bindDN + } + + bindPass := d.Get("bindpass").(string) + if bindPass != "" { + cfg.BindPassword = bindPass + } + + denyNullBind := d.Get("deny_null_bind").(bool) + if denyNullBind { + cfg.DenyNullBind = denyNullBind + } + + discoverDN := d.Get("discoverdn").(bool) + if discoverDN { + cfg.DiscoverDN = discoverDN + } + + caseSensitiveNames, ok := d.GetOk("case_sensitive_names") + if ok { + cfg.CaseSensitiveNames = new(bool) + *cfg.CaseSensitiveNames = caseSensitiveNames.(bool) + } + + return cfg, nil +} + type ConfigEntry struct { Url string `json:"url"` UserDN string `json:"userdn"` @@ -34,6 +259,36 @@ type ConfigEntry struct { CaseSensitiveNames *bool `json:"CaseSensitiveNames,omitempty"` } +func (c *ConfigEntry) Map() map[string]interface{} { + m := c.PasswordlessMap() + m["bindpass"] = c.BindPassword + return m +} + +func (c *ConfigEntry) PasswordlessMap() map[string]interface{} { + m := map[string]interface{}{ + "url": c.Url, + "userdn": c.UserDN, + "groupdn": c.GroupDN, + "groupfilter": c.GroupFilter, + "groupattr": c.GroupAttr, + "upndomain": c.UPNDomain, + "userattr": c.UserAttr, + "certificate": c.Certificate, + "insecure_tls": c.InsecureTLS, + "starttls": c.StartTLS, + "binddn": c.BindDN, + "deny_null_bind": c.DenyNullBind, + "discoverdn": c.DiscoverDN, + "tls_min_version": c.TLSMinVersion, + "tls_max_version": c.TLSMaxVersion, + } + if c.CaseSensitiveNames != nil { + m["case_sensitive_names"] = *c.CaseSensitiveNames + } + return m +} + func (c *ConfigEntry) Validate() error { if len(c.Url) == 0 { return errors.New("at least one url must be provided") diff --git a/physical/etcd/etcd2.go b/physical/etcd/etcd2.go index a2a479f2e7..b8e84a4179 100644 --- a/physical/etcd/etcd2.go +++ b/physical/etcd/etcd2.go @@ -138,9 +138,9 @@ func newEtcdV2Client(conf map[string]string) (client.Client, error) { if (hasCert && hasKey) || hasCa { var transportErr error tls := transport.TLSInfo{ - CAFile: ca, - CertFile: cert, - KeyFile: key, + TrustedCAFile: ca, + CertFile: cert, + KeyFile: key, } cTransport, transportErr = transport.NewTransport(tls, 30*time.Second) diff --git a/physical/etcd/etcd3.go b/physical/etcd/etcd3.go index 5c99527882..4c9a008f4b 100644 --- a/physical/etcd/etcd3.go +++ b/physical/etcd/etcd3.go @@ -86,9 +86,9 @@ func newEtcd3Backend(conf map[string]string, logger log.Logger) (physical.Backen ca, hasCa := conf["tls_ca_file"] if (hasCert && hasKey) || hasCa { tls := transport.TLSInfo{ - CAFile: ca, - CertFile: cert, - KeyFile: key, + TrustedCAFile: ca, + CertFile: cert, + KeyFile: key, } tlscfg, err := tls.ClientConfig() diff --git a/scripts/update_deps.sh b/scripts/update_deps.sh index 82b85b04ae..e839df3ac2 100755 --- a/scripts/update_deps.sh +++ b/scripts/update_deps.sh @@ -32,8 +32,12 @@ govendor init echo "Fetching deps, will take some time..." govendor fetch +missing +# Clean up after the logrus mess govendor remove github.com/Sirupsen/logrus cd vendor find -type f | grep '.go' | xargs sed -i -e 's/Sirupsen/sirupsen/' +# Need the v2 branch for Azure +govendor fetch github.com/coreos/go-oidc@v2 + echo "Done; to commit run \n\ncd ${GOPATH}/src/github.com/hashicorp/${TOOL}\n" diff --git a/ui/app/adapters/console.js b/ui/app/adapters/console.js new file mode 100644 index 0000000000..473f2ce424 --- /dev/null +++ b/ui/app/adapters/console.js @@ -0,0 +1,8 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + namespace: 'v1', + pathForType(modelName) { + return modelName; + }, +}); diff --git a/ui/app/components/confirm-action.js b/ui/app/components/confirm-action.js index ca6fdf5190..d3532584d1 100644 --- a/ui/app/components/confirm-action.js +++ b/ui/app/components/confirm-action.js @@ -16,6 +16,7 @@ export default Ember.Component.extend({ class={{buttonClasses}} type="button" disabled={{disabled}} + data-test-confirm-action-trigger=true {{action 'toggleConfirm'}} > {{yield}} diff --git a/ui/app/components/console/command-input.js b/ui/app/components/console/command-input.js new file mode 100644 index 0000000000..43854b0b7d --- /dev/null +++ b/ui/app/components/console/command-input.js @@ -0,0 +1,36 @@ +import Ember from 'ember'; +import keys from 'vault/lib/keycodes'; + +export default Ember.Component.extend({ + 'data-test-component': 'console/command-input', + classNames: 'console-ui-input', + onExecuteCommand() {}, + onFullscreen() {}, + onValueUpdate() {}, + onShiftCommand() {}, + value: null, + isFullscreen: null, + + didRender() { + this.element.scrollIntoView(); + }, + actions: { + handleKeyUp(event) { + const keyCode = event.keyCode; + switch (keyCode) { + case keys.ENTER: + this.get('onExecuteCommand')(event.target.value); + break; + case keys.UP: + case keys.DOWN: + this.get('onShiftCommand')(keyCode); + break; + default: + this.get('onValueUpdate')(event.target.value); + } + }, + fullscreen() { + this.get('onFullscreen')(); + }, + }, +}); diff --git a/ui/app/components/console/log-command.js b/ui/app/components/console/log-command.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-command.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/log-error.js b/ui/app/components/console/log-error.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-error.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/log-help.js b/ui/app/components/console/log-help.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-help.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/log-json.js b/ui/app/components/console/log-json.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-json.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/log-list.js b/ui/app/components/console/log-list.js new file mode 100644 index 0000000000..fcca15f272 --- /dev/null +++ b/ui/app/components/console/log-list.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; +const { computed } = Ember; + +export default Ember.Component.extend({ + content: null, + list: computed('content', function() { + return this.get('content').keys; + }), +}); diff --git a/ui/app/components/console/log-object.js b/ui/app/components/console/log-object.js new file mode 100644 index 0000000000..275f1edb72 --- /dev/null +++ b/ui/app/components/console/log-object.js @@ -0,0 +1,28 @@ +import Ember from 'ember'; +import columnify from 'columnify'; +const { computed } = Ember; + +export function stringifyObjectValues(data) { + Object.keys(data).forEach(item => { + let val = data[item]; + if (typeof val !== 'string') { + val = JSON.stringify(val); + } + data[item] = val; + }); +} + +export default Ember.Component.extend({ + content: null, + columns: computed('content', function() { + let data = this.get('content'); + stringifyObjectValues(data); + + return columnify(data, { + preserveNewLines: true, + headingTransform: function(heading) { + return Ember.String.capitalize(heading); + }, + }); + }), +}); diff --git a/ui/app/components/console/log-success.js b/ui/app/components/console/log-success.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-success.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/log-text.js b/ui/app/components/console/log-text.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-text.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/output-log.js b/ui/app/components/console/output-log.js new file mode 100644 index 0000000000..a4c209e243 --- /dev/null +++ b/ui/app/components/console/output-log.js @@ -0,0 +1,6 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + 'data-test-component': 'console/output-log', + log: null, +}); diff --git a/ui/app/components/console/ui-panel.js b/ui/app/components/console/ui-panel.js new file mode 100644 index 0000000000..61c782a817 --- /dev/null +++ b/ui/app/components/console/ui-panel.js @@ -0,0 +1,92 @@ +import Ember from 'ember'; +import { + parseCommand, + extractDataAndFlags, + logFromResponse, + logFromError, + logErrorFromInput, + executeUICommand, +} from 'vault/lib/console-helpers'; + +const { inject, computed } = Ember; + +export default Ember.Component.extend({ + classNames: 'console-ui-panel-scroller', + classNameBindings: ['isFullscreen:fullscreen'], + isFullscreen: false, + console: inject.service(), + inputValue: null, + log: computed.alias('console.log'), + + logAndOutput(command, logContent) { + this.set('inputValue', ''); + this.get('console').logAndOutput(command, logContent); + }, + + executeCommand(command, shouldThrow = false) { + let service = this.get('console'); + let serviceArgs; + + if ( + executeUICommand( + command, + args => this.logAndOutput(args), + args => service.clearLog(args), + () => this.toggleProperty('isFullscreen') + ) + ) { + return; + } + + // parse to verify it's valid + try { + serviceArgs = parseCommand(command, shouldThrow); + } catch (e) { + this.logAndOutput(command, { type: 'help' }); + return; + } + // we have a invalid command but don't want to throw + if (serviceArgs === false) { + return; + } + + let [method, flagArray, path, dataArray] = serviceArgs; + + if (dataArray || flagArray) { + var { data, flags } = extractDataAndFlags(dataArray, flagArray); + } + + let inputError = logErrorFromInput(path, method, flags, dataArray); + if (inputError) { + this.logAndOutput(command, inputError); + return; + } + let serviceFn = service[method]; + serviceFn + .call(service, path, data, flags.wrapTTL) + .then(resp => { + this.logAndOutput(command, logFromResponse(resp, path, method, flags)); + }) + .catch(error => { + this.logAndOutput(command, logFromError(error, path, method)); + }); + }, + + shiftCommandIndex(keyCode) { + this.get('console').shiftCommandIndex(keyCode, val => { + this.set('inputValue', val); + }); + }, + + actions: { + toggleFullscreen() { + this.toggleProperty('isFullscreen'); + }, + executeCommand(val) { + this.executeCommand(val, true); + }, + shiftCommandIndex(direction) { + this.shiftCommandIndex(direction); + }, + }, +}); diff --git a/ui/app/components/identity/_popup-base.js b/ui/app/components/identity/_popup-base.js new file mode 100644 index 0000000000..c74803ade2 --- /dev/null +++ b/ui/app/components/identity/_popup-base.js @@ -0,0 +1,40 @@ +import Ember from 'ember'; +const { assert, inject, Component } = Ember; + +export default Component.extend({ + tagName: '', + flashMessages: inject.service(), + params: null, + successMessage() { + return 'Save was successful'; + }, + errorMessage() { + return 'There was an error saving'; + }, + onError(model) { + if (model && model.rollbackAttributes) { + model.rollbackAttributes(); + } + }, + onSuccess() {}, + // override and return a promise + transaction() { + assert('override transaction call in an extension of popup-base', false); + }, + + actions: { + performTransaction() { + let args = [...arguments]; + let messageArgs = this.messageArgs(...args); + return this.transaction(...args) + .then(() => { + this.get('onSuccess')(); + this.get('flashMessages').success(this.successMessage(...messageArgs)); + }) + .catch(e => { + this.onError(...messageArgs); + this.get('flashMessages').success(this.errorMessage(e, ...messageArgs)); + }); + }, + }, +}); diff --git a/ui/app/components/identity/edit-form.js b/ui/app/components/identity/edit-form.js index 64d222848f..59e6173b55 100644 --- a/ui/app/components/identity/edit-form.js +++ b/ui/app/components/identity/edit-form.js @@ -2,20 +2,23 @@ import Ember from 'ember'; import { task } from 'ember-concurrency'; import { humanize } from 'vault/helpers/humanize'; -const { computed } = Ember; +const { computed, inject } = Ember; export default Ember.Component.extend({ + flashMessages: inject.service(), + 'data-test-component': 'identity-edit-form', model: null, + + // 'create', 'edit', 'merge' mode: 'create', /* * @param Function * @public * - * Optional param to call a function upon successfully mounting a backend - * + * Optional param to call a function upon successfully saving an entity */ onSave: () => {}, - cancelLink: computed('mode', 'model', function() { + cancelLink: computed('mode', 'model.identityType', function() { let { model, mode } = this.getProperties('model', 'mode'); let key = `${mode}-${model.get('identityType')}`; let routes = { @@ -33,16 +36,17 @@ export default Ember.Component.extend({ return routes[key]; }), - getMessage(model) { + getMessage(model, isDelete = false) { let mode = this.get('mode'); let typeDisplay = humanize([model.get('identityType')]); + let action = isDelete ? 'deleted' : 'saved'; if (mode === 'merge') { return 'Successfully merged entities'; } if (model.get('id')) { - return `Successfully saved ${typeDisplay} ${model.id}.`; + return `Successfully ${action} ${typeDisplay} ${model.id}.`; } - return `Successfully saved ${typeDisplay}.`; + return `Successfully ${action} ${typeDisplay}.`; }, save: task(function*() { @@ -56,13 +60,24 @@ export default Ember.Component.extend({ return; } this.get('flashMessages').success(message); - yield this.get('onSave')(model); + yield this.get('onSave')({ saveType: 'save', model }); }).drop(), willDestroy() { let model = this.get('model'); - if (!model.isDestroyed || !model.isDestroying) { + if ((model.get('isDirty') && !model.isDestroyed) || !model.isDestroying) { model.rollbackAttributes(); } }, + + actions: { + deleteItem(model) { + let message = this.getMessage(model, true); + let flash = this.get('flashMessages'); + model.destroyRecord().then(() => { + flash.success(message); + return this.get('onSave')({ saveType: 'delete', model }); + }); + }, + }, }); diff --git a/ui/app/components/identity/item-details.js b/ui/app/components/identity/item-details.js new file mode 100644 index 0000000000..eafc9fb5d8 --- /dev/null +++ b/ui/app/components/identity/item-details.js @@ -0,0 +1,24 @@ +import Ember from 'ember'; + +const { inject } = Ember; + +export default Ember.Component.extend({ + flashMessages: inject.service(), + + actions: { + enable(model) { + model.set('disabled', false); + + model + .save() + .then(() => { + this.get('flashMessages').success(`Successfully enabled entity: ${model.id}`); + }) + .catch(e => { + this.get('flashMessages').success( + `There was a problem enabling the entity: ${model.id} - ${e.error.join(' ') || e.message}` + ); + }); + }, + }, +}); diff --git a/ui/app/components/identity/popup-alias.js b/ui/app/components/identity/popup-alias.js new file mode 100644 index 0000000000..20852421c4 --- /dev/null +++ b/ui/app/components/identity/popup-alias.js @@ -0,0 +1,22 @@ +import Base from './_popup-base'; + +export default Base.extend({ + messageArgs(model) { + let type = model.get('identityType'); + let id = model.id; + return [type, id]; + }, + + successMessage(type, id) { + return `Successfully deleted ${type}: ${id}`; + }, + + errorMessage(e, type, id) { + let error = e.errors ? e.errors.join(' ') : e.message; + return `There was a problem deleting ${type}: ${id} - ${error}`; + }, + + transaction(model) { + return model.destroyRecord(); + }, +}); diff --git a/ui/app/components/identity/popup-members.js b/ui/app/components/identity/popup-members.js new file mode 100644 index 0000000000..6c096916f7 --- /dev/null +++ b/ui/app/components/identity/popup-members.js @@ -0,0 +1,34 @@ +import Base from './_popup-base'; +import Ember from 'ember'; +const { computed } = Ember; + +export default Base.extend({ + model: computed.alias('params.firstObject'), + + groupArray: computed('params', function() { + return this.get('params').objectAt(1); + }), + + memberId: computed('params', function() { + return this.get('params').objectAt(2); + }), + + messageArgs(/*model, groupArray, memberId*/) { + return [...arguments]; + }, + + successMessage(model, groupArray, memberId) { + return `Successfully removed '${memberId}' from the group`; + }, + + errorMessage(e, model, groupArray, memberId) { + let error = e.errors ? e.errors.join(' ') : e.message; + return `There was a problem removing '${memberId}' from the group - ${error}`; + }, + + transaction(model, groupArray, memberId) { + let members = model.get(groupArray); + model.set(groupArray, members.without(memberId)); + return model.save(); + }, +}); diff --git a/ui/app/components/identity/popup-metadata.js b/ui/app/components/identity/popup-metadata.js new file mode 100644 index 0000000000..c6d99fc43f --- /dev/null +++ b/ui/app/components/identity/popup-metadata.js @@ -0,0 +1,29 @@ +import Base from './_popup-base'; +import Ember from 'ember'; +const { computed } = Ember; + +export default Base.extend({ + model: computed.alias('params.firstObject'), + key: computed('params', function() { + return this.get('params').objectAt(1); + }), + + messageArgs(model, key) { + return [model, key]; + }, + + successMessage(model, key) { + return `Successfully removed '${key}' from metadata`; + }, + errorMessage(e, model, key) { + let error = e.errors ? e.errors.join(' ') : e.message; + return `There was a problem removing '${key}' from the metadata - ${error}`; + }, + + transaction(model, key) { + let metadata = model.get('metadata'); + delete metadata[key]; + model.set('metadata', { ...metadata }); + return model.save(); + }, +}); diff --git a/ui/app/components/identity/popup-policy.js b/ui/app/components/identity/popup-policy.js new file mode 100644 index 0000000000..b626b23c2b --- /dev/null +++ b/ui/app/components/identity/popup-policy.js @@ -0,0 +1,29 @@ +import Base from './_popup-base'; +import Ember from 'ember'; +const { computed } = Ember; + +export default Base.extend({ + model: computed.alias('params.firstObject'), + policyName: computed('params', function() { + return this.get('params').objectAt(1); + }), + + messageArgs(model, policyName) { + return [model, policyName]; + }, + + successMessage(model, policyName) { + return `Successfully removed '${policyName}' policy from ${model.id} `; + }, + + errorMessage(e, model, policyName) { + let error = e.errors ? e.errors.join(' ') : e.message; + return `There was a problem removing '${policyName}' policy - ${error}`; + }, + + transaction(model, policyName) { + let policies = model.get('policies'); + model.set('policies', policies.without(policyName)); + return model.save(); + }, +}); diff --git a/ui/app/components/info-table-row.js b/ui/app/components/info-table-row.js index 914573614f..0c12595f1d 100644 --- a/ui/app/components/info-table-row.js +++ b/ui/app/components/info-table-row.js @@ -1,6 +1,7 @@ import Ember from 'ember'; export default Ember.Component.extend({ + 'data-test-component': 'info-table-row', classNames: ['info-table-row'], isVisible: Ember.computed.or('alwaysRender', 'value'), diff --git a/ui/app/components/json-editor.js b/ui/app/components/json-editor.js index 7b1a1c6692..8377e75e31 100644 --- a/ui/app/components/json-editor.js +++ b/ui/app/components/json-editor.js @@ -18,6 +18,10 @@ export default IvyCodemirrorComponent.extend({ 'data-test-component': 'json-editor', updateCodeMirrorOptions() { const options = assign({}, JSON_EDITOR_DEFAULTS, this.get('options')); + if (options.autoHeight) { + options.viewportMargin = Infinity; + delete options.autoHeight; + } if (options) { Object.keys(options).forEach(function(option) { diff --git a/ui/app/components/message-in-page.js b/ui/app/components/message-in-page.js index f25076d90a..736739ab35 100644 --- a/ui/app/components/message-in-page.js +++ b/ui/app/components/message-in-page.js @@ -6,6 +6,8 @@ const { computed } = Ember; export default Ember.Component.extend({ type: null, + yieldWithoutColumn: false, + classNameBindings: ['containerClass'], containerClass: computed('type', function() { diff --git a/ui/app/components/page-header-level-left.js b/ui/app/components/page-header-level-left.js new file mode 100644 index 0000000000..e3ac4fb5c0 --- /dev/null +++ b/ui/app/components/page-header-level-left.js @@ -0,0 +1,5 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: '', +}); diff --git a/ui/app/components/page-header-level-right.js b/ui/app/components/page-header-level-right.js new file mode 100644 index 0000000000..e3ac4fb5c0 --- /dev/null +++ b/ui/app/components/page-header-level-right.js @@ -0,0 +1,5 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: '', +}); diff --git a/ui/app/components/page-header-top.js b/ui/app/components/page-header-top.js new file mode 100644 index 0000000000..e3ac4fb5c0 --- /dev/null +++ b/ui/app/components/page-header-top.js @@ -0,0 +1,5 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: '', +}); diff --git a/ui/app/components/page-header.js b/ui/app/components/page-header.js new file mode 100644 index 0000000000..0c8a391eed --- /dev/null +++ b/ui/app/components/page-header.js @@ -0,0 +1,6 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: '', + hasLevel: true, +}); diff --git a/ui/app/components/secret-list-header.js b/ui/app/components/secret-list-header.js new file mode 100644 index 0000000000..3835ff8ad4 --- /dev/null +++ b/ui/app/components/secret-list-header.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: '', + + // api + isCertTab: false, + isConfigure: false, + baseKey: null, + backendCrumb: null, + model: null, +}); diff --git a/ui/app/components/tool-actions-form.js b/ui/app/components/tool-actions-form.js index c89cc41917..61c9bea335 100644 --- a/ui/app/components/tool-actions-form.js +++ b/ui/app/components/tool-actions-form.js @@ -71,10 +71,11 @@ export default Ember.Component.extend(DEFAULTS, { handleSuccess(resp, action) { let props = {}; - if (resp && resp.data && action === 'unwrap') { - props = Ember.assign({}, props, { unwrap_data: resp.data }); + let secret = (resp && resp.data) || resp.auth; + if (secret && action === 'unwrap') { + props = Ember.assign({}, props, { unwrap_data: secret }); } - props = Ember.assign({}, props, resp.data); + props = Ember.assign({}, props, secret); if (resp && resp.wrap_info) { const keyName = action === 'rewrap' ? 'rewrap_token' : 'token'; diff --git a/ui/app/components/transit-key-actions.js b/ui/app/components/transit-key-actions.js index e1d73d9099..9719605926 100644 --- a/ui/app/components/transit-key-actions.js +++ b/ui/app/components/transit-key-actions.js @@ -3,6 +3,7 @@ const { get, set } = Ember; const TRANSIT_PARAMS = { hash_algorithm: 'sha2-256', + algorithm: 'sha2-256', signature_algorithm: 'pss', bits: 256, bytes: 32, @@ -31,7 +32,7 @@ const TRANSIT_PARAMS = { const PARAMS_FOR_ACTION = { sign: ['input', 'hash_algorithm', 'key_version', 'prehashed', 'signature_algorithm'], verify: ['input', 'hmac', 'signature', 'hash_algorithm', 'prehashed'], - hmac: ['input', 'hash_algorithm', 'key_version'], + hmac: ['input', 'algorithm', 'key_version'], encrypt: ['plaintext', 'context', 'nonce', 'key_version'], decrypt: ['ciphertext', 'context', 'nonce'], rewrap: ['ciphertext', 'context', 'nonce', 'key_version'], diff --git a/ui/app/components/upgrade-link.js b/ui/app/components/upgrade-link.js index 979838c682..01bc9ea11d 100644 --- a/ui/app/components/upgrade-link.js +++ b/ui/app/components/upgrade-link.js @@ -1,9 +1,19 @@ import Ember from 'ember'; +const { computed } = Ember; + export default Ember.Component.extend({ isAnimated: false, isActive: false, tagName: 'span', + trackingSource: computed('pageName', function() { + let trackingSource = 'vaultui'; + let pageName = this.get('pageName'); + if (pageName) { + trackingSource = trackingSource + '_' + encodeURIComponent(pageName); + } + return trackingSource; + }), actions: { openOverlay() { this.set('isActive', true); diff --git a/ui/app/controllers/application.js b/ui/app/controllers/application.js index 5f6f6c9a9e..353649234f 100644 --- a/ui/app/controllers/application.js +++ b/ui/app/controllers/application.js @@ -1,18 +1,21 @@ import Ember from 'ember'; import config from '../config/environment'; +const { computed, inject } = Ember; export default Ember.Controller.extend({ env: config.environment, - auth: Ember.inject.service(), - vaultVersion: Ember.inject.service('version'), - activeCluster: Ember.computed('auth.activeCluster', function() { + auth: inject.service(), + vaultVersion: inject.service('version'), + console: inject.service(), + consoleOpen: computed.alias('console.isOpen'), + activeCluster: computed('auth.activeCluster', function() { return this.store.peekRecord('cluster', this.get('auth.activeCluster')); }), - activeClusterName: Ember.computed('auth.activeCluster', function() { + activeClusterName: computed('auth.activeCluster', function() { const activeCluster = this.store.peekRecord('cluster', this.get('auth.activeCluster')); return activeCluster ? activeCluster.get('name') : null; }), - showNav: Ember.computed( + showNav: computed( 'activeClusterName', 'auth.currentToken', 'activeCluster.dr.isSecondary', @@ -30,4 +33,9 @@ export default Ember.Controller.extend({ } } ), + actions: { + toggleConsole() { + this.toggleProperty('consoleOpen'); + }, + }, }); diff --git a/ui/app/controllers/vault/cluster/access/identity/aliases/index.js b/ui/app/controllers/vault/cluster/access/identity/aliases/index.js index fd2e726634..10d0973925 100644 --- a/ui/app/controllers/vault/cluster/access/identity/aliases/index.js +++ b/ui/app/controllers/vault/cluster/access/identity/aliases/index.js @@ -1,4 +1,10 @@ import Ember from 'ember'; import ListController from 'vault/mixins/list-controller'; -export default Ember.Controller.extend(ListController); +export default Ember.Controller.extend(ListController, { + actions: { + onDelete() { + this.send('reload'); + }, + }, +}); diff --git a/ui/app/controllers/vault/cluster/access/identity/create.js b/ui/app/controllers/vault/cluster/access/identity/create.js index 46d9d64377..a94bf3b4b0 100644 --- a/ui/app/controllers/vault/cluster/access/identity/create.js +++ b/ui/app/controllers/vault/cluster/access/identity/create.js @@ -4,7 +4,20 @@ import { task } from 'ember-concurrency'; export default Ember.Controller.extend({ showRoute: 'vault.cluster.access.identity.show', showTab: 'details', - navToShow: task(function*(model) { - yield this.transitionToRoute(this.get('showRoute'), model.id, this.get('showTab')); + navAfterSave: task(function*({ saveType, model }) { + let isDelete = saveType === 'delete'; + let type = model.get('identityType'); + let listRoutes = { + 'entity-alias': 'vault.cluster.access.identity.aliases.index', + 'group-alias': 'vault.cluster.access.identity.aliases.index', + group: 'vault.cluster.access.identity.index', + entity: 'vault.cluster.access.identity.index', + }; + let routeName = listRoutes[type]; + if (!isDelete) { + yield this.transitionToRoute(this.get('showRoute'), model.id, this.get('showTab')); + return; + } + yield this.transitionToRoute(routeName); }), }); diff --git a/ui/app/controllers/vault/cluster/access/identity/index.js b/ui/app/controllers/vault/cluster/access/identity/index.js index fd2e726634..0bfe797cff 100644 --- a/ui/app/controllers/vault/cluster/access/identity/index.js +++ b/ui/app/controllers/vault/cluster/access/identity/index.js @@ -1,4 +1,47 @@ import Ember from 'ember'; import ListController from 'vault/mixins/list-controller'; -export default Ember.Controller.extend(ListController); +const { inject } = Ember; + +export default Ember.Controller.extend(ListController, { + flashMessages: inject.service(), + + actions: { + delete(model) { + let type = model.get('identityType'); + let id = model.id; + return model + .destroyRecord() + .then(() => { + this.send('reload'); + this.get('flashMessages').success(`Successfully deleted ${type}: ${id}`); + }) + .catch(e => { + this.get('flashMessages').success( + `There was a problem deleting ${type}: ${id} - ${e.error.join(' ') || e.message}` + ); + }); + }, + + toggleDisabled(model) { + let action = model.get('disabled') ? ['enabled', 'enabling'] : ['disabled', 'disabling']; + let type = model.get('identityType'); + let id = model.id; + model.toggleProperty('disabled'); + + model + .save() + .then(() => { + this.get('flashMessages').success(`Successfully ${action[0]} ${type}: ${id}`); + }) + .catch(e => { + this.get('flashMessages').success( + `There was a problem ${action[1]} ${type}: ${id} - ${e.error.join(' ') || e.message}` + ); + }); + }, + reloadRecord(model) { + model.reload(); + }, + }, +}); diff --git a/ui/app/controllers/vault/cluster/access/leases/list.js b/ui/app/controllers/vault/cluster/access/leases/list.js index 955192bbc2..feaa06b7f2 100644 --- a/ui/app/controllers/vault/cluster/access/leases/list.js +++ b/ui/app/controllers/vault/cluster/access/leases/list.js @@ -1,9 +1,11 @@ import Ember from 'ember'; import utils from 'vault/lib/key-utils'; -export default Ember.Controller.extend({ - flashMessages: Ember.inject.service(), - clusterController: Ember.inject.controller('vault.cluster'), +const { inject, computed, Controller } = Ember; +export default Controller.extend({ + flashMessages: inject.service(), + store: inject.service(), + clusterController: inject.controller('vault.cluster'), queryParams: { page: 'page', pageFilter: 'pageFilter', @@ -13,7 +15,7 @@ export default Ember.Controller.extend({ pageFilter: null, filter: null, - backendCrumb: Ember.computed(function() { + backendCrumb: computed(function() { return { label: 'leases', text: 'leases', @@ -24,13 +26,13 @@ export default Ember.Controller.extend({ isLoading: false, - filterMatchesKey: Ember.computed('filter', 'model', 'model.[]', function() { + filterMatchesKey: computed('filter', 'model', 'model.[]', function() { var filter = this.get('filter'); var content = this.get('model'); return !!(content.length && content.findBy('id', filter)); }), - firstPartialMatch: Ember.computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() { + firstPartialMatch: computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() { var filter = this.get('filter'); var content = this.get('model'); var filterMatchesKey = this.get('filterMatchesKey'); @@ -42,7 +44,7 @@ export default Ember.Controller.extend({ }); }), - filterIsFolder: Ember.computed('filter', function() { + filterIsFolder: computed('filter', function() { return !!utils.keyIsFolder(this.get('filter')); }), @@ -56,7 +58,7 @@ export default Ember.Controller.extend({ }, revokePrefix(prefix, isForce) { - const adapter = this.model.store.adapterFor('lease'); + const adapter = this.get('store').adapterFor('lease'); const method = isForce ? 'forceRevokePrefix' : 'revokePrefix'; const fn = adapter[method]; fn diff --git a/ui/app/controllers/vault/cluster/secrets/backend/list.js b/ui/app/controllers/vault/cluster/secrets/backend/list.js index a113f5cb1a..3f540baf8a 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/list.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/list.js @@ -66,6 +66,7 @@ export default Ember.Controller.extend(BackendCrumbMixin, { delete(item) { const name = item.id; item.destroyRecord().then(() => { + this.send('reload'); this.get('flashMessages').success(`${name} was successfully deleted.`); }); }, diff --git a/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js b/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js index 2c51a88cb1..973d0544e9 100644 --- a/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js +++ b/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js @@ -7,22 +7,23 @@ const { computed } = Ember; export default Ember.Controller.extend({ mountTypes: [ + { label: 'Active Directory', value: 'ad' }, { label: 'AWS', value: 'aws' }, - { label: 'Cassandra', value: 'cassandra', deprecated: true }, { label: 'Consul', value: 'consul' }, { label: 'Databases', value: 'database' }, { label: 'Google Cloud', value: 'gcp' }, { label: 'KV', value: 'kv' }, - { label: 'MongoDB', value: 'mongodb', deprecated: true }, - { label: 'MSSQL', value: 'mssql', deprecated: true }, - { label: 'MySQL', value: 'mysql', deprecated: true }, { label: 'Nomad', value: 'nomad' }, { label: 'PKI', value: 'pki' }, - { label: 'PostgreSQL', value: 'postgresql', deprecated: true }, { label: 'RabbitMQ', value: 'rabbitmq' }, { label: 'SSH', value: 'ssh' }, { label: 'Transit', value: 'transit' }, { label: 'TOTP', value: 'totp' }, + { label: 'Cassandra', value: 'cassandra', deprecated: true }, + { label: 'MongoDB', value: 'mongodb', deprecated: true }, + { label: 'MSSQL', value: 'mssql', deprecated: true }, + { label: 'MySQL', value: 'mysql', deprecated: true }, + { label: 'PostgreSQL', value: 'postgresql', deprecated: true }, ], selectedType: null, @@ -30,7 +31,6 @@ export default Ember.Controller.extend({ description: null, default_lease_ttl: null, max_lease_ttl: null, - force_no_cache: null, showConfig: false, local: false, sealWrap: false, @@ -50,7 +50,6 @@ export default Ember.Controller.extend({ description: null, default_lease_ttl: null, max_lease_ttl: null, - force_no_cache: null, local: false, showConfig: false, sealWrap: false, @@ -82,7 +81,6 @@ export default Ember.Controller.extend({ selectedType: type, description, default_lease_ttl, - force_no_cache, local, max_lease_ttl, sealWrap, @@ -92,7 +90,6 @@ export default Ember.Controller.extend({ 'selectedType', 'description', 'default_lease_ttl', - 'force_no_cache', 'local', 'max_lease_ttl', 'sealWrap', @@ -112,9 +109,8 @@ export default Ember.Controller.extend({ if (this.get('showConfig')) { attrs.config = { - default_lease_ttl, - max_lease_ttl, - force_no_cache, + defaultLeaseTtl: default_lease_ttl, + maxLeaseTtl: max_lease_ttl, }; } diff --git a/ui/app/helpers/multi-line-join.js b/ui/app/helpers/multi-line-join.js new file mode 100644 index 0000000000..cd22380f11 --- /dev/null +++ b/ui/app/helpers/multi-line-join.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +export function multiLineJoin([arr]) { + return arr.join('\n'); +} + +export default Ember.Helper.helper(multiLineJoin); diff --git a/ui/app/lib/console-helpers.js b/ui/app/lib/console-helpers.js new file mode 100644 index 0000000000..1637063352 --- /dev/null +++ b/ui/app/lib/console-helpers.js @@ -0,0 +1,185 @@ +import keys from 'vault/lib/keycodes'; +import argTokenizer from 'yargs-parser-tokenizer'; + +const supportedCommands = ['read', 'write', 'list', 'delete']; +const uiCommands = ['clearall', 'clear', 'fullscreen']; + +export function extractDataAndFlags(data, flags) { + return data.concat(flags).reduce((accumulator, val) => { + // will be "key=value" or "-flag=value" or "foo=bar=baz" + // split on the first = + let [item, value] = val.split(/=(.+)/); + if (item.startsWith('-')) { + let flagName = item.replace(/^-/, ''); + if (flagName === 'wrap-ttl') { + flagName = 'wrapTTL'; + } + accumulator.flags[flagName] = value || true; + return accumulator; + } + // if it exists in data already, then we have multiple + // foo=bar in the list and need to make it an array + if (accumulator.data[item]) { + accumulator.data[item] = [].concat(accumulator.data[item], value); + return accumulator; + } + accumulator.data[item] = value; + + return accumulator; + }, { data: {}, flags: {} }); +} + +export function executeUICommand(command, logAndOutput, clearLog, toggleFullscreen) { + const isUICommand = uiCommands.includes(command); + if (isUICommand) { + logAndOutput(command); + } + switch (command) { + case 'clearall': + clearLog(true); + break; + case 'clear': + clearLog(); + break; + case 'fullscreen': + toggleFullscreen(); + break; + } + + return isUICommand; +} + +export function parseCommand(command, shouldThrow) { + let args = argTokenizer(command); + if (args[0] === 'vault') { + args.shift(); + } + + let [method, ...rest] = args; + let path; + let flags = []; + let data = []; + + rest.forEach(arg => { + if (arg.startsWith('-')) { + flags.push(arg); + } else { + if (path) { + data.push(arg); + } else { + path = arg; + } + } + }); + + if (!supportedCommands.includes(method)) { + if (shouldThrow) { + throw new Error('invalid command'); + } + return false; + } + return [method, flags, path, data]; +} + +export function logFromResponse(response, path, method, flags) { + if (!response) { + let message = + method === 'write' + ? `Success! Data written to: ${path}` + : `Success! Data deleted (if it existed) at: ${path}`; + + return { type: 'success', content: message }; + } + let { format, field } = flags; + let secret = response.auth || response.data || response.wrap_info; + + if (field) { + let fieldValue = secret[field]; + let response; + if (fieldValue) { + if (format && format === 'json') { + return { type: 'json', content: fieldValue }; + } + switch (typeof fieldValue) { + case 'string': + response = { type: 'text', content: fieldValue }; + break; + default: + response = { type: 'object', content: fieldValue }; + break; + } + } else { + response = { type: 'error', content: `Field "${field}" not present in secret` }; + } + return response; + } + + if (format && format === 'json') { + // just print whole response + return { type: 'json', content: response }; + } + + if (method === 'list') { + return { type: 'list', content: secret }; + } + + return { type: 'object', content: secret }; +} + +export function logFromError(error, vaultPath, method) { + let content; + let { httpStatus, path } = error; + let verbClause = { + read: 'reading from', + write: 'writing to', + list: 'listing', + delete: 'deleting at', + }[method]; + + content = `Error ${verbClause}: ${vaultPath}.\nURL: ${path}\nCode: ${httpStatus}`; + + if (typeof error.errors[0] === 'string') { + content = `${content}\nErrors:\n ${error.errors.join('\n ')}`; + } + + return { type: 'error', content }; +} + +export function shiftCommandIndex(keyCode, history, index) { + let newInputValue; + let commandHistoryLength = history.length; + + if (!commandHistoryLength) { + return []; + } + + if (keyCode === keys.UP) { + index -= 1; + if (index < 0) { + index = commandHistoryLength - 1; + } + } else { + index += 1; + if (index === commandHistoryLength) { + newInputValue = ''; + } + if (index > commandHistoryLength) { + index -= 1; + } + } + + if (newInputValue !== '') { + newInputValue = history.objectAt(index).content; + } + + return [index, newInputValue]; +} + +export function logErrorFromInput(path, method, flags, dataArray) { + if (path === undefined) { + return { type: 'error', content: 'A path is required to make a request.' }; + } + if (method === 'write' && !flags.force && dataArray.length === 0) { + return { type: 'error', content: 'Must supply data or use -force' }; + } +} diff --git a/ui/app/macros/identity-capabilities.js b/ui/app/macros/identity-capabilities.js new file mode 100644 index 0000000000..f36eb29052 --- /dev/null +++ b/ui/app/macros/identity-capabilities.js @@ -0,0 +1,5 @@ +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; + +export default function() { + return lazyCapabilities(apiPath`identity/${'identityType'}/id/${'id'}`, 'id', 'identityType'); +} diff --git a/ui/app/macros/lazy-capabilities.js b/ui/app/macros/lazy-capabilities.js new file mode 100644 index 0000000000..108d66a45a --- /dev/null +++ b/ui/app/macros/lazy-capabilities.js @@ -0,0 +1,25 @@ +import { queryRecord } from 'ember-computed-query'; + +export function apiPath(strings, ...keys) { + return function(data) { + let dict = data || {}; + let result = [strings[0]]; + keys.forEach((key, i) => { + result.push(dict[key], strings[i + 1]); + }); + return result.join(''); + }; +} + +export default function() { + let [templateFn, ...keys] = arguments; + return queryRecord( + 'capabilities', + context => { + return { + id: templateFn(context.getProperties(...keys)), + }; + }, + ...keys + ); +} diff --git a/ui/app/models/cluster.js b/ui/app/models/cluster.js index ca4f20742d..9eac03c4ee 100644 --- a/ui/app/models/cluster.js +++ b/ui/app/models/cluster.js @@ -72,7 +72,7 @@ export default DS.Model.extend({ }), stateGlyph(state) { - const glyph = 'checkmark'; + const glyph = 'checkmark-circled-outline'; const glyphs = { 'stream-wals': 'android-sync', diff --git a/ui/app/models/identity/entity-alias.js b/ui/app/models/identity/entity-alias.js index 6e5dbaeaf8..b38b823f23 100644 --- a/ui/app/models/identity/entity-alias.js +++ b/ui/app/models/identity/entity-alias.js @@ -1,8 +1,12 @@ import IdentityModel from './_base'; import DS from 'ember-data'; +import Ember from 'ember'; +import identityCapabilities from 'vault/macros/identity-capabilities'; const { attr, belongsTo } = DS; +const { computed } = Ember; export default IdentityModel.extend({ + parentType: 'entity', formFields: ['name', 'mountAccessor', 'metadata'], entity: belongsTo('identity/entity', { readOnly: true, async: false }), @@ -12,7 +16,7 @@ export default IdentityModel.extend({ label: 'Auth Backend', editType: 'mountAccessor', }), - metadata: attr('object', { + metadata: attr({ editType: 'kv', }), mountPath: attr('string', { @@ -28,4 +32,8 @@ export default IdentityModel.extend({ readOnly: true, }), mergedFromCanonicalIds: attr(), + + updatePath: identityCapabilities(), + canDelete: computed.alias('updatePath.canDelete'), + canEdit: computed.alias('updatePath.canUpdate'), }); diff --git a/ui/app/models/identity/entity.js b/ui/app/models/identity/entity.js index 77b6853189..2f63eaca0c 100644 --- a/ui/app/models/identity/entity.js +++ b/ui/app/models/identity/entity.js @@ -1,12 +1,23 @@ +import Ember from 'ember'; import IdentityModel from './_base'; import DS from 'ember-data'; +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; +import identityCapabilities from 'vault/macros/identity-capabilities'; + +const { computed } = Ember; + const { attr, hasMany } = DS; export default IdentityModel.extend({ - formFields: ['name', 'policies', 'metadata'], + formFields: ['name', 'disabled', 'policies', 'metadata'], name: attr('string'), + disabled: attr('boolean', { + defaultValue: false, + label: 'Disable entity', + helpText: 'All associated tokens cannot be used, but are not revoked.', + }), mergedEntityIds: attr(), - metadata: attr('object', { + metadata: attr({ editType: 'kv', }), policies: attr({ @@ -28,4 +39,11 @@ export default IdentityModel.extend({ inheritedGroupIds: attr({ readOnly: true, }), + + updatePath: identityCapabilities(), + canDelete: computed.alias('updatePath.canDelete'), + canEdit: computed.alias('updatePath.canUpdate'), + + aliasPath: lazyCapabilities(apiPath`identity/entity-alias`), + canAddAlias: computed.alias('aliasPath.canCreate'), }); diff --git a/ui/app/models/identity/group-alias.js b/ui/app/models/identity/group-alias.js index 14af8c1105..47a74ebb28 100644 --- a/ui/app/models/identity/group-alias.js +++ b/ui/app/models/identity/group-alias.js @@ -1,8 +1,13 @@ import IdentityModel from './_base'; import DS from 'ember-data'; +import Ember from 'ember'; +import identityCapabilities from 'vault/macros/identity-capabilities'; + const { attr, belongsTo } = DS; +const { computed } = Ember; export default IdentityModel.extend({ + parentType: 'group', formFields: ['name', 'mountAccessor'], group: belongsTo('identity/group', { readOnly: true, async: false }), @@ -26,4 +31,8 @@ export default IdentityModel.extend({ lastUpdateTime: attr('string', { readOnly: true, }), + + updatePath: identityCapabilities(), + canDelete: computed.alias('updatePath.canDelete'), + canEdit: computed.alias('updatePath.canUpdate'), }); diff --git a/ui/app/models/identity/group.js b/ui/app/models/identity/group.js index 37212c0c90..47483dd529 100644 --- a/ui/app/models/identity/group.js +++ b/ui/app/models/identity/group.js @@ -1,6 +1,8 @@ import Ember from 'ember'; import IdentityModel from './_base'; import DS from 'ember-data'; +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; +import identityCapabilities from 'vault/macros/identity-capabilities'; const { computed } = Ember; const { attr, belongsTo } = DS; @@ -52,4 +54,18 @@ export default IdentityModel.extend({ ), alias: belongsTo('identity/group-alias', { async: false, readOnly: true }), + updatePath: identityCapabilities(), + canDelete: computed.alias('updatePath.canDelete'), + canEdit: computed.alias('updatePath.canUpdate'), + + aliasPath: lazyCapabilities(apiPath`identity/group-alias`), + canAddAlias: computed('aliasPath.canCreate', 'type', 'alias', function() { + let type = this.get('type'); + let alias = this.get('alias'); + // internal groups can't have aliases, and external groups can only have one + if (type === 'internal' || alias) { + return false; + } + return this.get('aliasPath.canCreate'); + }), }); diff --git a/ui/app/models/mount-options.js b/ui/app/models/mount-options.js index d40c18f9b0..9870456f68 100644 --- a/ui/app/models/mount-options.js +++ b/ui/app/models/mount-options.js @@ -2,5 +2,7 @@ import attr from 'ember-data/attr'; import Fragment from 'ember-data-model-fragments/fragment'; export default Fragment.extend({ - version: attr('number'), + version: attr('number', { + label: 'Version', + }), }); diff --git a/ui/app/models/secret-engine.js b/ui/app/models/secret-engine.js index 3e0a57858d..a015f1b33f 100644 --- a/ui/app/models/secret-engine.js +++ b/ui/app/models/secret-engine.js @@ -3,6 +3,8 @@ import DS from 'ember-data'; import { queryRecord } from 'ember-computed-query'; import { fragment } from 'ember-data-model-fragments/attributes'; +import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; + const { attr } = DS; const { computed } = Ember; @@ -16,11 +18,26 @@ export default DS.Model.extend({ name: attr('string'), type: attr('string'), description: attr('string'), - config: attr('object'), - options: fragment('mount-options'), + config: fragment('mount-config', { defaultValue: {} }), + options: fragment('mount-options', { defaultValue: {} }), local: attr('boolean'), sealWrap: attr('boolean'), + formFields: [ + 'type', + 'path', + 'description', + 'accessor', + 'local', + 'sealWrap', + 'config.{defaultLeaseTtl,maxLeaseTtl}', + 'options.{version}', + ], + + attrs: computed('formFields', function() { + return expandAttributeMeta(this, this.get('formFields')); + }), + shouldIncludeInList: computed('type', function() { return !LIST_EXCLUDED_BACKENDS.includes(this.get('type')); }), diff --git a/ui/app/router.js b/ui/app/router.js index ecb289b575..c4ddb49d71 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -68,6 +68,7 @@ Router.map(function() { this.route('backends', { path: '/' }); this.route('backend', { path: '/:backend' }, function() { this.route('index', { path: '/' }); + this.route('configuration'); // because globs / params can't be empty, // we have to special-case ids of '' with thier own routes this.route('list-root', { path: '/list/' }); diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js index 433c7f701d..a099784c91 100644 --- a/ui/app/routes/vault/cluster.js +++ b/ui/app/routes/vault/cluster.js @@ -32,7 +32,7 @@ export default Ember.Route.extend(ModelBoundaryRoute, ClusterRoute, { model(params) { const id = this.getClusterId(params); - return this.get('store').findRecord('cluster', id); + return this.get('store').findRecord('cluster', id); }, stopPoll: Ember.on('deactivate', function() { diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/add.js b/ui/app/routes/vault/cluster/access/identity/aliases/add.js index 5d9af4ba3e..bfd9ce6dcf 100644 --- a/ui/app/routes/vault/cluster/access/identity/aliases/add.js +++ b/ui/app/routes/vault/cluster/access/identity/aliases/add.js @@ -1,6 +1,8 @@ import Ember from 'ember'; +import UnloadModelRoute from 'vault/mixins/unload-model-route'; +import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; -export default Ember.Route.extend({ +export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { model(params) { let itemType = this.modelFor('vault.cluster.access.identity'); let modelType = `identity/${itemType}-alias`; diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/edit.js b/ui/app/routes/vault/cluster/access/identity/aliases/edit.js index 15e9b36dc3..9c2c87eb2a 100644 --- a/ui/app/routes/vault/cluster/access/identity/aliases/edit.js +++ b/ui/app/routes/vault/cluster/access/identity/aliases/edit.js @@ -1,6 +1,8 @@ import Ember from 'ember'; +import UnloadModelRoute from 'vault/mixins/unload-model-route'; +import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; -export default Ember.Route.extend({ +export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { model(params) { let itemType = this.modelFor('vault.cluster.access.identity'); let modelType = `identity/${itemType}-alias`; diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/index.js b/ui/app/routes/vault/cluster/access/identity/aliases/index.js index 027cc1f7d5..b0b27d0e32 100644 --- a/ui/app/routes/vault/cluster/access/identity/aliases/index.js +++ b/ui/app/routes/vault/cluster/access/identity/aliases/index.js @@ -27,10 +27,14 @@ export default Ember.Route.extend(ListRoute, { actions: { willTransition(transition) { window.scrollTo(0, 0); - if (transition.targetName !== this.routeName) { + if (!transition || transition.targetName !== this.routeName) { this.store.clearAllDatasets(); } return true; }, + reload() { + this.store.clearAllDatasets(); + this.refresh(); + }, }, }); diff --git a/ui/app/routes/vault/cluster/access/identity/create.js b/ui/app/routes/vault/cluster/access/identity/create.js index a147b9deb8..c0231a0099 100644 --- a/ui/app/routes/vault/cluster/access/identity/create.js +++ b/ui/app/routes/vault/cluster/access/identity/create.js @@ -1,6 +1,8 @@ import Ember from 'ember'; +import UnloadModelRoute from 'vault/mixins/unload-model-route'; +import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; -export default Ember.Route.extend({ +export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { model() { let itemType = this.modelFor('vault.cluster.access.identity'); let modelType = `identity/${itemType}`; diff --git a/ui/app/routes/vault/cluster/access/identity/edit.js b/ui/app/routes/vault/cluster/access/identity/edit.js index df1e5327e4..495e5e9aab 100644 --- a/ui/app/routes/vault/cluster/access/identity/edit.js +++ b/ui/app/routes/vault/cluster/access/identity/edit.js @@ -1,6 +1,8 @@ import Ember from 'ember'; +import UnloadModelRoute from 'vault/mixins/unload-model-route'; +import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; -export default Ember.Route.extend({ +export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { model(params) { let itemType = this.modelFor('vault.cluster.access.identity'); let modelType = `identity/${itemType}`; diff --git a/ui/app/routes/vault/cluster/access/identity/index.js b/ui/app/routes/vault/cluster/access/identity/index.js index 200061ed72..4b19fcb80a 100644 --- a/ui/app/routes/vault/cluster/access/identity/index.js +++ b/ui/app/routes/vault/cluster/access/identity/index.js @@ -34,5 +34,9 @@ export default Ember.Route.extend(ListRoute, { } return true; }, + reload() { + this.store.clearAllDatasets(); + this.refresh(); + }, }, }); diff --git a/ui/app/routes/vault/cluster/access/identity/show.js b/ui/app/routes/vault/cluster/access/identity/show.js index bb4d43277e..11361085e4 100644 --- a/ui/app/routes/vault/cluster/access/identity/show.js +++ b/ui/app/routes/vault/cluster/access/identity/show.js @@ -13,13 +13,36 @@ export default Ember.Route.extend({ Ember.set(error, 'httpStatus', 404); throw error; } - // TODO peekRecord here to see if we have the record already + + // if the record is in the store use that + let model = this.store.peekRecord(modelType, params.item_id); + + // if we don't have creationTime, we only have a partial model so reload + if (model && !model.get('creationTime')) { + model = model.reload(); + } + + // if there's no model, we need to fetch it + if (!model) { + model = this.store.findRecord(modelType, params.item_id); + } + return Ember.RSVP.hash({ - model: this.store.findRecord(modelType, params.item_id), + model, section, }); }, + activate() { + // if we're just entering the route, and it's not a hard reload + // reload to make sure we have the newest info + if (this.currentModel) { + Ember.run.next(() => { + this.controller.get('model').reload(); + }); + } + }, + afterModel(resolvedModel) { let { section, model } = resolvedModel; if (model.get('identityType') === 'group' && model.get('type') === 'internal' && section === 'aliases') { diff --git a/ui/app/routes/vault/cluster/logout.js b/ui/app/routes/vault/cluster/logout.js index 0ddc7c110f..824d931164 100644 --- a/ui/app/routes/vault/cluster/logout.js +++ b/ui/app/routes/vault/cluster/logout.js @@ -1,14 +1,18 @@ import Ember from 'ember'; import ModelBoundaryRoute from 'vault/mixins/model-boundary-route'; +const { inject } = Ember; export default Ember.Route.extend(ModelBoundaryRoute, { - auth: Ember.inject.service(), - flashMessages: Ember.inject.service(), + auth: inject.service(), + flashMessages: inject.service(), + console: inject.service(), modelTypes: ['secret', 'secret-engine'], beforeModel() { this.get('auth').deleteCurrentToken(); + this.get('console').set('isOpen', false); + this.get('console').clearLog(true); this.clearModelCache(); this.replaceWith('vault.cluster'); this.get('flashMessages').clearMessages(); diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration.js b/ui/app/routes/vault/cluster/secrets/backend/configuration.js new file mode 100644 index 0000000000..e18705600d --- /dev/null +++ b/ui/app/routes/vault/cluster/secrets/backend/configuration.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + model() { + return this.modelFor('vault.cluster.secrets.backend'); + }, +}); diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index d3ebd7851b..0c8bfb5702 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -49,25 +49,25 @@ export default Ember.Route.extend({ return Ember.RSVP.hash({ secret, secrets: this.store - .lazyPaginatedQuery(this.getModelType(backend, params.tab), { - id: secret, - backend, - responsePath: 'data.keys', - page: params.page, - pageFilter: params.pageFilter, - size: 100, - }) - .then(model => { - this.set('has404', false); - return model; - }) - .catch(err => { - if (backendModel && err.httpStatus === 404 && secret === '') { - return []; - } else { - throw err; - } - }) + .lazyPaginatedQuery(this.getModelType(backend, params.tab), { + id: secret, + backend, + responsePath: 'data.keys', + page: params.page, + pageFilter: params.pageFilter, + size: 100, + }) + .then(model => { + this.set('has404', false); + return model; + }) + .catch(err => { + if (backendModel && err.httpStatus === 404 && secret === '') { + return []; + } else { + throw err; + } + }), }); }, @@ -159,5 +159,9 @@ export default Ember.Route.extend({ } return true; }, + reload() { + this.refresh(); + this.store.clearAllDatasets(); + }, }, }); diff --git a/ui/app/services/console.js b/ui/app/services/console.js new file mode 100644 index 0000000000..3e17842b8e --- /dev/null +++ b/ui/app/services/console.js @@ -0,0 +1,103 @@ +// Low level service that allows users to input paths to make requests to vault +// this service provides the UI synecdote to the cli commands read, write, delete, and list +import Ember from 'ember'; +import { shiftCommandIndex } from 'vault/lib/console-helpers'; + +const { Service, getOwner, computed } = Ember; + +export function sanitizePath(path) { + //remove whitespace + remove trailing and leading slashes + return path.trim().replace(/^\/+|\/+$/g, ''); +} +export function ensureTrailingSlash(path) { + return path.replace(/(\w+[^/]$)/g, '$1/'); +} + +const VERBS = { + read: 'GET', + list: 'GET', + write: 'POST', + delete: 'DELETE', +}; + +export default Service.extend({ + isOpen: false, + + adapter() { + return getOwner(this).lookup('adapter:console'); + }, + commandHistory: computed('log.[]', function() { + return this.get('log').filterBy('type', 'command'); + }), + log: computed(function() { + return []; + }), + commandIndex: null, + + shiftCommandIndex(keyCode, setCommandFn = () => {}) { + let [newIndex, newCommand] = shiftCommandIndex( + keyCode, + this.get('commandHistory'), + this.get('commandIndex') + ); + if (newCommand !== undefined && newIndex !== undefined) { + this.set('commandIndex', newIndex); + setCommandFn(newCommand); + } + }, + + clearLog(clearAll = false) { + let log = this.get('log'); + let history; + if (!clearAll) { + history = this.get('commandHistory').slice(); + history.setEach('hidden', true); + } + log.clear(); + if (history) { + log.addObjects(history); + } + }, + + logAndOutput(command, logContent) { + let log = this.get('log'); + log.pushObject({ type: 'command', content: command }); + this.set('commandIndex', null); + if (logContent) { + log.pushObject(logContent); + } + }, + + ajax(operation, path, options = {}) { + let verb = VERBS[operation]; + let adapter = this.adapter(); + let url = adapter.buildURL(path); + let { data, wrapTTL } = options; + return adapter.ajax(url, verb, { + data, + wrapTTL, + }); + }, + + read(path, data, wrapTTL) { + return this.ajax('read', sanitizePath(path), { wrapTTL }); + }, + + write(path, data, wrapTTL) { + return this.ajax('write', sanitizePath(path), { data, wrapTTL }); + }, + + delete(path) { + return this.ajax('delete', sanitizePath(path)); + }, + + list(path, data, wrapTTL) { + let listPath = ensureTrailingSlash(sanitizePath(path)); + return this.ajax('list', listPath, { + data: { + list: true, + }, + wrapTTL, + }); + }, +}); diff --git a/ui/app/services/store.js b/ui/app/services/store.js index bc092f9237..711cbdb850 100644 --- a/ui/app/services/store.js +++ b/ui/app/services/store.js @@ -131,13 +131,25 @@ export default DS.Store.extend({ // pushes records into the store and returns the result fetchPage(modelName, query) { const response = this.constructResponse(modelName, query); - this.unloadAll(modelName); - this.push( - this.serializerFor(modelName).normalizeResponse(this, this.modelFor(modelName), response, null, 'query') - ); - const model = this.peekAll(modelName); - model.set('meta', response.meta); - return model; + this.peekAll(modelName).forEach(record => { + record.unloadRecord(); + }); + return new Ember.RSVP.Promise(resolve => { + Ember.run.schedule('destroy', () => { + this.push( + this.serializerFor(modelName).normalizeResponse( + this, + this.modelFor(modelName), + response, + null, + 'query' + ) + ); + let model = this.peekAll(modelName).toArray(); + model.set('meta', response.meta); + resolve(model); + }); + }); }, // get cached data diff --git a/ui/app/styles/components/codemirror.scss b/ui/app/styles/components/codemirror.scss index 37f773a040..68cb9971c6 100644 --- a/ui/app/styles/components/codemirror.scss +++ b/ui/app/styles/components/codemirror.scss @@ -171,3 +171,7 @@ $gutter-grey: #2a2f36; } } } + +.cm-s-auto-height.CodeMirror { + height: auto; +} diff --git a/ui/app/styles/components/console-ui-panel.scss b/ui/app/styles/components/console-ui-panel.scss new file mode 100644 index 0000000000..b9e75b261d --- /dev/null +++ b/ui/app/styles/components/console-ui-panel.scss @@ -0,0 +1,154 @@ +.console-ui-panel-scroller { + background: linear-gradient(to right, #191A1C, #1B212D); + height: 0; + left: 0; + overflow: auto; + position: absolute; + min-height: 0; + right: 0; + transform: translate3d(0, -400px, 0); + transition: min-height $speed ease-out, transform $speed ease-in; + will-change: transform, min-height; + -webkit-overflow-scrolling: touch; + z-index: 199; +} + +.console-ui-panel { + display: flex; + flex-direction: column; + justify-content: flex-end; + padding: $size-8 $size-8 $size-4; + min-height: 100%; + color: $white; + font-size: $body-size; + font-weight: $font-weight-semibold; + transition: justify-content $speed ease-in; + + + pre, p { + background: none; + color: inherit; + font-size: $body-size; + + &:not(.console-ui-command):not(.CodeMirror-line) { + padding-left: $console-spacing; + } + } + + .cm-s-hashi.CodeMirror { + background-color: rgba($black, 0.5) !important; + font-weight: $font-weight-normal; + margin-left: $console-spacing; + padding: $size-8 $size-4; + } + + .button, + { + background: transparent; + border: none; + color: $grey-dark; + min-width: 0; + padding: 0 $size-8; + + &.active, + &:hover { + background: $blue; + color: $white; + } + } +} + +.console-ui-input { + align-items: center; + display: flex; + + + input { + background-color: rgba($black, 0.5); + border: 0; + caret-color: $white; + color: $white; + flex: 1; + font-family: $family-monospace; + font-size: 16px; + font-weight: $font-weight-bold; + margin-left: -$size-10; + outline: none; + padding: $size-10; + transition: background-color $speed; + } +} + +.console-ui-command { + line-height: 2; +} + +.console-ui-output { + transition: background-color $speed; + padding-right: $size-2; + position: relative; + + .console-ui-output-actions { + opacity: 0; + position: absolute; + right: 0; + top: 0; + transition: opacity $speed; + will-change: opacity; + } + + &:hover { + background: rgba($black, 0.25); + + .console-ui-output-actions { + opacity: 1; + } + } +} + +.console-ui-alert { + margin-left: calc(#{$console-spacing} - 0.33rem); + position: relative; + + .icon { + position: absolute; + left: 0; + top: 0; + } +} + +.panel-open .console-ui-panel-scroller { + box-shadow: $box-shadow-highest; + transform: translate3d(0, 0, 0); + min-height: 400px; +} + +.panel-open .console-ui-panel-scroller.fullscreen { + bottom: 0; + top: 0; + position: fixed; + min-height: 100%; +} + +.panel-open { + .navbar, .navbar-sections{ + transition: transform $speed ease-in; + } +} + +.panel-open.panel-fullscreen { + .navbar, .navbar-sections{ + transform: translate3d(0, -100px, 0); + } +} + +.page-container > header { + background: linear-gradient(to right, #191A1C, #1B212D); +} + +header .navbar, +header .navbar-sections { + z-index: 200; + transform: translate3d(0, 0, 0); + will-change: transform; +} diff --git a/ui/app/styles/components/env-banner.scss b/ui/app/styles/components/env-banner.scss new file mode 100644 index 0000000000..d2851dcf69 --- /dev/null +++ b/ui/app/styles/components/env-banner.scss @@ -0,0 +1,10 @@ +.env-banner { + &, + &:not(:last-child):not(:last-child) { + margin: 0; + } + + .level-item { + padding: $size-10 $size-8; + } +} diff --git a/ui/app/styles/components/global-flash.scss b/ui/app/styles/components/global-flash.scss index d1c0597786..c4d2fd29bb 100644 --- a/ui/app/styles/components/global-flash.scss +++ b/ui/app/styles/components/global-flash.scss @@ -1,21 +1,14 @@ .global-flash { position: fixed; - @include until($desktop) { - position: -webkit-sticky; - position: sticky; - top: 0; - bottom: auto; - margin: 0 auto; - width: 95%; - } - width: 450px; + max-width: 450px; + width: 95%; bottom: 0; left: 0; margin: 10px; z-index: 1; .notification { - box-shadow: 0 0 25px rgba($black, 0.2); + box-shadow: $box-shadow-high; margin: 20px; @include until($desktop) { margin: 1rem 0; diff --git a/ui/app/styles/components/linked-block.scss b/ui/app/styles/components/linked-block.scss index dfa6d7c8d0..8dc3ec8726 100644 --- a/ui/app/styles/components/linked-block.scss +++ b/ui/app/styles/components/linked-block.scss @@ -1,13 +1,13 @@ .linked-block { cursor: pointer; + transition: box-shadow $speed; + will-change: box-shadow; + &:hover, - &:focus { - position: relative; - box-shadow: $box-link-hover-shadow; - } + &:focus, &:active { position: relative; - box-shadow: $box-link-active-shadow; + box-shadow: $box-link-hover-shadow, $box-shadow-middle; } } diff --git a/ui/app/styles/components/login-form.scss b/ui/app/styles/components/login-form.scss new file mode 100644 index 0000000000..d458abd8f8 --- /dev/null +++ b/ui/app/styles/components/login-form.scss @@ -0,0 +1,3 @@ +.login-form { + box-shadow: $box-shadow, $box-shadow-high; +} \ No newline at end of file diff --git a/ui/app/styles/components/popup-menu.scss b/ui/app/styles/components/popup-menu.scss index 8e2406737c..3594d830f9 100644 --- a/ui/app/styles/components/popup-menu.scss +++ b/ui/app/styles/components/popup-menu.scss @@ -1,16 +1,17 @@ .popup-menu-content { border-radius: 2px; margin: -2px 0 0 0; + & > .box { border-radius: 2px; + box-shadow: $box-shadow, $box-shadow-middle; padding: 0; position: relative; width: 175px; - @include css-top-arrow(8px, $white, 1px, $blue, 155px); } + &.is-wide > .box { - width: 200px; - @include css-top-arrow(8px, $white, 1px, $blue, 178px); + width: 300px; } .confirm-action span .button { @@ -18,58 +19,98 @@ margin: .25rem auto; width: 95%; } -} -.popup-menu-trigger { - width: 3rem; - height: 2rem; -} -.popup-menu-trigger.is-active { - &, - &:active, - &:focus { - box-shadow: 0 0 0 1px $blue; + + .menu { + padding: $size-11 0; + + button.link, + a { + background: transparent; + box-shadow: none; + border: none; + color: $menu-item-color; + display: block; + height: auto; + font-size: $size-7; + font-weight: $font-weight-semibold; + padding: $size-9 $size-8; + text-align: left; + text-decoration: none; + width: 100%; + + &:hover { + background-color: $menu-item-hover-background-color; + color: $menu-item-hover-color; + } + + &.is-active { + background-color: $menu-item-active-background-color; + color: $menu-item-active-color; + } + + &.is-destroy { + color: $red; + + &:hover { + background-color: $red; + color: $white; + } + } + } } -} -.ember-basic-dropdown-content--left.popup-menu { - margin: 0px 0 0 -8px; -} - -.popup-menu-content .menu { - button.link, - a { - border-radius: $menu-item-radius; - color: $menu-item-color; - font-size: $size-7; + .menu-label { + color: $grey-dark; + font-size: $size-8; font-weight: $font-weight-semibold; - display: block; - padding: $size-9 $size-6; - box-shadow: none; - border: none; - background: transparent; - height: auto; - width: 100%; - text-align: left; + letter-spacing: 0; + margin: 0; + padding: $size-10 $size-8 $size-11; + text-transform: none; + } - &:hover { - background-color: $menu-item-hover-background-color; - color: $menu-item-hover-color; + .menu-content { + padding: $size-10 $size-8; + } + + hr { + background-color: $grey-light; + margin: $size-11 0; + } +} + +.popup-menu-trigger { + height: 2rem; + min-width: 0; + padding: 0 $size-10; +} + +.status-menu-content { + margin-top: 8px; +} + +.ember-basic-dropdown-content { + &--left.popup-menu { + margin: 0px 0 0 -8px; + } + + &--below { + &.ember-basic-dropdown--transitioning-in { + animation: drop-fade-above .15s; } - &.is-active { - background-color: $menu-item-active-background-color; - color: $menu-item-active-color; + &.ember-basic-dropdown--transitioning-out { + animation: drop-fade-above .15s reverse; + } + } + + &--above { + &.ember-basic-dropdown--transitioning-in { + animation: drop-fade-below .15s; + } + + &.ember-basic-dropdown--transitioning-out { + animation: drop-fade-below .15s reverse; } } } -.popup-menu-content .menu-list { - margin: 0.1rem; -} -.popup-menu-content .menu-label { - background: $grey-lighter; - font-size: $size-8; - letter-spacing: 0; - margin: 0; - padding: $size-10 calc(#{$size-6} + .1rem); - text-transform: none; -} diff --git a/ui/app/styles/components/sidebar.scss b/ui/app/styles/components/sidebar.scss index 5e6e213538..521486199e 100644 --- a/ui/app/styles/components/sidebar.scss +++ b/ui/app/styles/components/sidebar.scss @@ -55,7 +55,8 @@ } .menu-label { - color: $grey-light; + color: $grey; + font-weight: $font-weight-semibold; font-size: $size-small; line-height: 1; margin-bottom: $size-8; @@ -72,14 +73,8 @@ transition: 250ms border-width; &.is-active { - color: $blue; - background-color: $dark-white; border-right: 4px solid $blue; } - - &:hover { - background-color: $dark-white; - } } } } diff --git a/ui/app/styles/components/status-menu.scss b/ui/app/styles/components/status-menu.scss index 1c6937dae0..00d27c7b44 100644 --- a/ui/app/styles/components/status-menu.scss +++ b/ui/app/styles/components/status-menu.scss @@ -1,65 +1,6 @@ -.status-menu-content { - max-width: 360px; - min-width: 280px; - border-radius: 3px; - margin: 0 -17px 0 0; - will-change: transform, opacity; - - .card { - position: relative; - border-radius: 2px; - border: $base-border; - box-shadow: 0 0 4px rgba($black, 0.21); - color: $black; - @include css-top-arrow(8px, $grey-lighter, 1px, $grey, 100%, -25px); - .card-content { - padding: $size-6 $size-3; - } - } - - .card-header-title, - .menu-label { - background: $grey-lighter; - color: $grey-dark; - font-size: $size-7; - font-weight: normal; - letter-spacing: 0; - margin: 0; - padding: 8px $size-3; - text-transform: uppercase; - } - - .replication-card { - @include css-top-arrow(8px, $grey-lighter, 1px, $grey, 100%, -98px); - .box-label { - box-shadow: none; - } - a.is-active, - a:hover { - box-shadow: 0 0 0 1px $blue; - } - } - - &.ember-basic-dropdown-content--below.ember-basic-dropdown--transitioning-in { - animation: drop-fade-above .15s; - } - &.ember-basic-dropdown-content--below.ember-basic-dropdown--transitioning-out { - animation: drop-fade-above .15s reverse; - } - &.ember-basic-dropdown-content--above.ember-basic-dropdown--transitioning-in { - animation: drop-fade-below .15s; - } - &.ember-basic-dropdown-content--above.ember-basic-dropdown--transitioning-out { - animation: drop-fade-below .15s reverse; - } -} -.status-menu-content-replication { - margin-top: 5px; -} - .is-status-chevron { line-height: 0; - padding: 0.25em 0 0.25em 0.25em; + padding: 0.3em 0 0 $size-11; } .status-menu-user-trigger { @@ -73,39 +14,3 @@ min-width: 0; } } - -.status-menu-content .menu { - button.link, - a { - font-size: $size-6; - font-weight: 500; - padding: $size-9 $size-3; - border-radius: $menu-item-radius; - color: $menu-item-color; - display: block; - padding: $size-9 $size-3; - box-shadow: none; - border: none; - background: transparent; - height: auto; - width: 100%; - text-align: left; - - .icon { - color: $menu-item-hover-background-color; - } - &:hover { - background-color: $menu-item-hover-background-color; - color: $menu-item-hover-color; - .icon { - color: $menu-item-hover-color; - } - - } - - &.is-active { - background-color: $menu-item-active-background-color; - color: $menu-item-active-color; - } - } -} diff --git a/ui/app/styles/components/sub-nav.scss b/ui/app/styles/components/sub-nav.scss index 5c2f4447e4..9c336fa77d 100644 --- a/ui/app/styles/components/sub-nav.scss +++ b/ui/app/styles/components/sub-nav.scss @@ -1,29 +1,35 @@ .sub-nav { &.tabs { - background: $grey-lighter; - padding: 0 1.25rem; + box-shadow: inset 0 -1px 0 $grey-light; + ul { border-color: transparent; } + + li { + &:focus { + box-shadow: none; + } + &.is-active a { + border-color: $blue; + color: $blue; + } + } + a { color: $grey-dark; font-weight: $font-weight-semibold; text-decoration: none; - padding: 1.5rem 1rem; + padding: $size-6 $size-8 $size-8; border-bottom: 2px solid transparent; transition: border-color $speed; + + &:hover, + &:active { + border-color: $grey-light; + } } - a:hover, - a:active { - border-color: $grey-light; - } - li:focus { - box-shadow: none; - } - li.is-active a { - border-color: $blue; - color: $blue; - } + .ember-basic-dropdown-trigger { outline: none; } diff --git a/ui/app/styles/components/tool-tip.scss b/ui/app/styles/components/tool-tip.scss index a971e6e9b2..5d8c02b3cf 100644 --- a/ui/app/styles/components/tool-tip.scss +++ b/ui/app/styles/components/tool-tip.scss @@ -5,7 +5,7 @@ .box { position: relative; color: $white; - width: 200px; + max-width: 200px; background: $grey; padding: 0.5rem; line-height: 1.4; @@ -28,6 +28,16 @@ .ember-basic-dropdown-content--left.tool-tip { margin: 8px 0 0 -11px; } + +.ember-basic-dropdown-content--below.ember-basic-dropdown-content--right.tool-tip { + @include css-top-arrow(8px, $grey, 1px, $grey-dark, calc(100% - 20px)); +} +.ember-basic-dropdown-content--above.ember-basic-dropdown-content--right.tool-tip { + @include css-bottom-arrow(8px, $grey, 1px, $grey-dark, calc(100% - 20px)); +} +.ember-basic-dropdown-content--above.tool-tip { + margin-top: -2px; +} .tool-tip-trigger { border: none; border-radius: 20px; diff --git a/ui/app/styles/components/upgrade-overlay.scss b/ui/app/styles/components/upgrade-overlay.scss index 5ec71408eb..4ad81dc4ca 100644 --- a/ui/app/styles/components/upgrade-overlay.scss +++ b/ui/app/styles/components/upgrade-overlay.scss @@ -10,7 +10,8 @@ } .modal-background { - background-image: url("/ui/vault-hex.svg"), linear-gradient(90deg, #191A1C, #1B212D); + background-image: url("/ui/vault-hex.svg"), + linear-gradient(90deg, #191a1c, #1b212d); opacity: 0.97; } diff --git a/ui/app/styles/components/vault-loading.scss b/ui/app/styles/components/vault-loading.scss index 1634dfa7ee..610375007d 100644 --- a/ui/app/styles/components/vault-loading.scss +++ b/ui/app/styles/components/vault-loading.scss @@ -1,52 +1,52 @@ - @keyframes vault-loading-animation { - 0%, - 70%, - 100% { - transform: scale3D(1, 1, 1); - } - - 35% { - transform: scale3D(0, 0, 1); - } +@keyframes vault-loading-animation { + 0%, + 70%, + 100% { + transform: scale3D(1, 1, 1); } - #vault-loading { - polygon { - animation: vault-loading-animation 1.3s infinite ease-in-out; - transform-origin: 50% 50%; - fill: #DCE2E9; - } - - .vault-loading-order-1 { - animation-delay: .1s; - } - - .vault-loading-order-2 { - animation-delay: .2s; - } - - .vault-loading-order-3 { - animation-delay: .3s; - } - - .vault-loading-order-4 { - animation-delay: .4s; - } + 35% { + transform: scale3D(0, 0, 1); + } +} + +#vault-loading { + polygon { + animation: vault-loading-animation 1.3s infinite ease-in-out; + transform-origin: 50% 50%; + fill: #dce2e9; } - #vault-loading-animated { - @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { - // For IE11 - display: none; - } + .vault-loading-order-1 { + animation-delay: .1s; } - #vault-loading-static { + .vault-loading-order-2 { + animation-delay: .2s; + } + + .vault-loading-order-3 { + animation-delay: .3s; + } + + .vault-loading-order-4 { + animation-delay: .4s; + } +} + +#vault-loading-animated { + @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + // For IE11 display: none; - font-size: 9px; + } +} - @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { - // For IE11 - display: block; - } - } \ No newline at end of file +#vault-loading-static { + display: none; + font-size: 9px; + + @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + // For IE11 + display: block; + } +} diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index 3c8acc6437..0b19ace128 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -46,6 +46,8 @@ @import "./components/box-label"; @import "./components/codemirror"; @import "./components/confirm"; +@import "./components/console-ui-panel"; +@import "./components/env-banner"; @import "./components/form-section"; @import "./components/global-flash"; @import "./components/init-illustration"; @@ -54,6 +56,7 @@ @import "./components/linked-block"; @import "./components/list-pagination"; @import "./components/loader"; +@import "./components/login-form"; @import "./components/message-in-page"; @import "./components/page-header"; @import "./components/popup-menu"; diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss index ef23caf786..b588ae78c0 100644 --- a/ui/app/styles/core/buttons.scss +++ b/ui/app/styles/core/buttons.scss @@ -1,8 +1,8 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); .button { - box-shadow: $button-box-shadow-standard; border: 1px solid $grey-light; + box-shadow: $box-shadow-low; color: $grey-dark; display: inline-block; font-size: $size-small; @@ -12,7 +12,8 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); min-width: 6rem; padding: $size-10 $size-8; text-decoration: none; - transition: background-color $speed, border-color $speed, box-shadow $speed, color $speed; + transition: background-color $speed, border-color $speed, box-shadow $speed, + color $speed; vertical-align: middle; &.is-icon { @@ -57,13 +58,14 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); &.is-hovered { background-color: darken($color, 5%); border-color: darken($color, 5%); + box-shadow: $box-shadow-middle; } &:active, &.is-active { background-color: darken($color, 10%); border-color: darken($color, 10%); - box-shadow: none; + box-shadow: $box-shadow-middle; } &:focus, @@ -133,6 +135,17 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); } } + &.is-orange { + background-color: $orange; + border-color: $orange; + color: $white; + + &:hover, + &.is-hovered { + background-color: darken($orange, 5%); + border-color: darken($orange, 5%); + } + } &.is-compact { height: 2rem; padding: $size-11 $size-8; @@ -146,7 +159,6 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); } } - &.is-more-icon, &.tool-tip-trigger { color: $black; min-width: auto; diff --git a/ui/app/styles/core/generic.scss b/ui/app/styles/core/generic.scss index 358b6dd323..664c2a56b9 100644 --- a/ui/app/styles/core/generic.scss +++ b/ui/app/styles/core/generic.scss @@ -33,13 +33,16 @@ input::-webkit-inner-spin-button { .link { background: transparent; border: 0; - color: $blue; - cursor: pointer; - display: inline; - font: inherit; - line-height: normal; - margin: 0; - padding: 0; - text-decoration: underline; - -moz-user-select: text; + color: $blue; + cursor: pointer; + display: inline; + font: inherit; + line-height: normal; + margin: 0; + padding: 0; + text-decoration: underline; + -webkit-user-select: text; /* Chrome all / Safari all */ + -moz-user-select: text; /* Firefox all */ + -ms-user-select: text; /* IE 10+ */ + user-select: text; } diff --git a/ui/app/styles/core/menu.scss b/ui/app/styles/core/menu.scss index 81fb5de04d..f970ade367 100644 --- a/ui/app/styles/core/menu.scss +++ b/ui/app/styles/core/menu.scss @@ -1,11 +1,11 @@ -.column .menu-list a { +.column .menu-list a { border-radius: 0; border-right: 0 solid transparent; font-weight: $font-weight-semibold; &:hover, &.is-active { - color: $menu-item-hover-background-color; - background-color: $menu-item-active-color; + color: $menu-item-active-color; + background-color: $menu-item-active-background-color; } } diff --git a/ui/app/styles/core/tabs.scss b/ui/app/styles/core/tabs.scss index 27b5d1c1f9..23c8b45407 100644 --- a/ui/app/styles/core/tabs.scss +++ b/ui/app/styles/core/tabs.scss @@ -5,6 +5,7 @@ padding: $size-10 $size-10; @include until($tablet) { + position:relative; background-color: $grey; flex: 0 0 100%; height: 3rem; diff --git a/ui/app/styles/core/tags.scss b/ui/app/styles/core/tags.scss index 99eb6be525..8ad30752e5 100644 --- a/ui/app/styles/core/tags.scss +++ b/ui/app/styles/core/tags.scss @@ -1,17 +1,21 @@ .tag:not(body) { background-color: lighten($grey-light, 17%); border-radius: 2px; + color: $grey-dark; height: auto; - padding: 0 0.75em; + padding: 0 $size-10; margin-right: 0.5rem; font-weight: normal; + code { - color: $grey; + color: $grey-dark; } } + .tag.is-outlined { border: 1px solid currentColor; } + .tag.is-inverted { border-color: $grey; background: none; @@ -19,6 +23,7 @@ color: $grey-dark; } } + .tag.is-bold { font-weight: bold; } diff --git a/ui/app/styles/utils/_bulma_variables.scss b/ui/app/styles/utils/_bulma_variables.scss index 7d68128498..33f9e5b3ff 100644 --- a/ui/app/styles/utils/_bulma_variables.scss +++ b/ui/app/styles/utils/_bulma_variables.scss @@ -37,7 +37,9 @@ $border: $grey-light; $hr-margin: 1rem 0; //typography -$family-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; +$family-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; $family-primary: $family-sans; $body-size: 14px; $size-3: (24/14) + 0rem; @@ -46,6 +48,7 @@ $size-8: (12/14) + 0rem; $size-9: 0.75rem; $size-10: 0.5rem; $size-11: 0.25rem; +$console-spacing: 1.5rem; $size-small: $size-8; $font-weight-normal: 400; $font-weight-semibold: 600; @@ -61,6 +64,10 @@ $radius: 2px; //box $box-radius: 0; $box-shadow: 0 0 0 1px rgba($black, 0.1); +$box-shadow-low: 0 5px 1px -2px rgba($black, 0.12), 0 3px 2px -1px rgba($black, 0); +$box-shadow-middle: 0 8px 4px -4px rgba($black, 0.10), 0 6px 8px -2px rgba($black, 0.05); +$box-shadow-high: 0 12px 5px -7px rgba($black, 0.08), 0 11px 10px -3px rgba($black, 0.10); +$box-shadow-highest: 0 16px 6px -10px rgba($black, 0.06), 0 16px 16px -4px rgba($black, 0.20); $link: $blue; $text: $black; @@ -78,6 +85,12 @@ $progress-bar-background-color: lighten($grey-light, 15%); $base-border: 1px solid $grey-light; +//menu +$menu-item-hover-color: $text; +$menu-item-hover-background-color: $grey-lighter; +$menu-item-active-color: $link; +$menu-item-active-background-color: transparent; + // animations $speed: 150ms; $speed-slow: $speed * 2; diff --git a/ui/app/styles/utils/animations.scss b/ui/app/styles/utils/animations.scss index 8c7c3b2d7b..0020823b45 100644 --- a/ui/app/styles/utils/animations.scss +++ b/ui/app/styles/utils/animations.scss @@ -14,7 +14,7 @@ } @include keyframes(drop-fade-below) { - 0% { + 0% { opacity: 0; transform: translateY(-1rem); } @@ -25,7 +25,7 @@ } @include keyframes(drop-fade-above) { - 0% { + 0% { opacity: 0; transform: translateY(1rem); } diff --git a/ui/app/styles/utils/mixins.scss b/ui/app/styles/utils/mixins.scss index 97f3cc0126..9832a8e86c 100644 --- a/ui/app/styles/utils/mixins.scss +++ b/ui/app/styles/utils/mixins.scss @@ -1,11 +1,15 @@ -@mixin css-top-arrow($size, $color, $border-width, $border-color, $left: 50%, $left-offset: 0px) { +@mixin css-arrow($vertical-direction, $size, $color, $border-width, $border-color, $left: 50%, $left-offset: 0px) { & { border: 1px solid $border-color; } &:after, &:before { - bottom: 100%; + @if ($vertical-direction == 'top') { + bottom: 100%; + } @else { + top: 100%; + } border: solid transparent; content: " "; height: 0; @@ -28,6 +32,12 @@ left: calc(#{$left} + #{$left-offset}); margin-left: -($size + round(1.41421356 * $border-width)); } + &:before, + &:after { + @if ($vertical-direction == 'bottom') { + transform: rotate(180deg); + } + } @at-root .ember-basic-dropdown-content--left#{&} { &:after, @@ -38,6 +48,13 @@ } } +@mixin css-top-arrow($size, $color, $border-width, $border-color, $left: 50%, $left-offset: 0px) { + @include css-arrow('top', $size, $color, $border-width, $border-color, $left, $left-offset); +} +@mixin css-bottom-arrow($size, $color, $border-width, $border-color, $left: 50%, $left-offset: 0px) { + @include css-arrow('bottom', $size, $color, $border-width, $border-color, $left, $left-offset); +} + @mixin vault-block { &:not(:last-child) { margin-bottom: (5/14) + 0rem; diff --git a/ui/app/templates/application.hbs b/ui/app/templates/application.hbs index fb6884ce52..e4480ce1be 100644 --- a/ui/app/templates/application.hbs +++ b/ui/app/templates/application.hbs @@ -1,6 +1,6 @@
{{#if showNav}} -
+
{{/if}}
@@ -129,7 +141,7 @@
{{#if (eq env "development") }} -
+
{{i-con glyph="wand" class="type-icon"}}Local Development
diff --git a/ui/app/templates/components/auth-info.hbs b/ui/app/templates/components/auth-info.hbs index 80e0d4f9eb..e2f83610e4 100644 --- a/ui/app/templates/components/auth-info.hbs +++ b/ui/app/templates/components/auth-info.hbs @@ -1,56 +1,57 @@ -
-
-

+

-
+ + +
  • + + Sign out + +
  • + + +
    diff --git a/ui/app/templates/components/console/command-input.hbs b/ui/app/templates/components/console/command-input.hbs new file mode 100644 index 0000000000..83fd9981cf --- /dev/null +++ b/ui/app/templates/components/console/command-input.hbs @@ -0,0 +1,16 @@ +{{i-con glyph="chevron-right" size=12}} + +{{#tool-tip horizontalPosition="auto-right" verticalPosition=(if isFullscreen "above" "below") as |d|}} + {{#d.trigger tagName="button" type="button" class=(concat "button is-compact" (if isFullscreen " active")) click=(action "fullscreen") data-test-tool-tip-trigger=true}} + {{i-con glyph=(if isFullscreen "fullscreen-close" "fullscreen-open") aria-hidden="true" size=16}} + {{/d.trigger}} + {{#d.content class="tool-tip"}} +
    + {{#if isFullscreen}} + Minimize + {{else}} + Maximize + {{/if}} +
    + {{/d.content}} +{{/tool-tip}} diff --git a/ui/app/templates/components/console/log-command.hbs b/ui/app/templates/components/console/log-command.hbs new file mode 100644 index 0000000000..39b9e7d1f2 --- /dev/null +++ b/ui/app/templates/components/console/log-command.hbs @@ -0,0 +1 @@ +
    {{i-con glyph="chevron-right" size=12}}{{content}}
    diff --git a/ui/app/templates/components/console/log-error.hbs b/ui/app/templates/components/console/log-error.hbs new file mode 100644 index 0000000000..add86ec492 --- /dev/null +++ b/ui/app/templates/components/console/log-error.hbs @@ -0,0 +1,4 @@ +
    + {{i-con glyph="close-circled" aria-hidden="true" size=12}} +
    {{content}}
    +
    diff --git a/ui/app/templates/components/console/log-help.hbs b/ui/app/templates/components/console/log-help.hbs new file mode 100644 index 0000000000..0022c1f399 --- /dev/null +++ b/ui/app/templates/components/console/log-help.hbs @@ -0,0 +1,16 @@ +
    +{{i-con glyph="information-circled" aria-hidden="true" size=12}} +
    Usage: vault <command> [args]
    +
    +Commands:
    +  read        Read data and retrieves secrets
    +  write       Write data, configuration, and secrets
    +  delete      Delete secrets and configuration
    +  list        List data or secrets
    +
    +Web CLI Commands:
    +  fullscreen  Toggle fullscreen display
    +  clear       Clear output from the log
    +  clearall    Clear output and command history
    +
    +
    diff --git a/ui/app/templates/components/console/log-json.hbs b/ui/app/templates/components/console/log-json.hbs new file mode 100644 index 0000000000..293ee6f238 --- /dev/null +++ b/ui/app/templates/components/console/log-json.hbs @@ -0,0 +1,10 @@ +{{json-editor + value=(stringify content) + options=(hash + readOnly=true + lineNumbers=false + autoHeight=true + gutters=false + theme='hashi auto-height' + ) + }} diff --git a/ui/app/templates/components/console/log-list.hbs b/ui/app/templates/components/console/log-list.hbs new file mode 100644 index 0000000000..83fc544bdc --- /dev/null +++ b/ui/app/templates/components/console/log-list.hbs @@ -0,0 +1,21 @@ +
    +
    Keys
    +{{#each list as |item|}}
    +{{item}}
    +{{/each}}
    +
    +
    + {{#tool-tip renderInPlace=true as |d|}} + {{#d.trigger data-test-tool-tip-trigger=true}} + {{#copy-button clipboardText=(multi-line-join list) class="button is-compact"}} + {{i-con glyph="copy" aria-hidden="true" size=16}} + {{/copy-button}} + {{/d.trigger}} + {{#d.content class="tool-tip"}} +
    + Copy +
    + {{/d.content}} + {{/tool-tip}} +
    +
    diff --git a/ui/app/templates/components/console/log-object.hbs b/ui/app/templates/components/console/log-object.hbs new file mode 100644 index 0000000000..3c8d77cf4a --- /dev/null +++ b/ui/app/templates/components/console/log-object.hbs @@ -0,0 +1,18 @@ +
    +
    {{columns}}
    + +
    + {{#tool-tip renderInPlace=true as |d|}} + {{#d.trigger data-test-tool-tip-trigger=true}} + {{#copy-button clipboardText=columns class="button is-compact"}} + {{i-con glyph="copy" aria-hidden="true" size=16}} + {{/copy-button}} + {{/d.trigger}} + {{#d.content class="tool-tip"}} +
    + Copy +
    + {{/d.content}} + {{/tool-tip}} +
    +
    diff --git a/ui/app/templates/components/console/log-success.hbs b/ui/app/templates/components/console/log-success.hbs new file mode 100644 index 0000000000..e16ae924c5 --- /dev/null +++ b/ui/app/templates/components/console/log-success.hbs @@ -0,0 +1,4 @@ +
    + {{i-con glyph="checkmark-circled" aria-hidden="true" size=12}} +
    {{content}}
    +
    diff --git a/ui/app/templates/components/console/log-text.hbs b/ui/app/templates/components/console/log-text.hbs new file mode 100644 index 0000000000..3da4104754 --- /dev/null +++ b/ui/app/templates/components/console/log-text.hbs @@ -0,0 +1 @@ +
    {{content}}
    \ No newline at end of file diff --git a/ui/app/templates/components/console/output-log.hbs b/ui/app/templates/components/console/output-log.hbs new file mode 100644 index 0000000000..e464c7fa0b --- /dev/null +++ b/ui/app/templates/components/console/output-log.hbs @@ -0,0 +1,5 @@ +{{#each log as |message|}} + {{#unless message.hidden}} + {{component (concat 'console/log-' message.type) content=message.content}} + {{/unless}} +{{/each}} diff --git a/ui/app/templates/components/console/ui-panel.hbs b/ui/app/templates/components/console/ui-panel.hbs new file mode 100644 index 0000000000..f374e20d29 --- /dev/null +++ b/ui/app/templates/components/console/ui-panel.hbs @@ -0,0 +1,16 @@ +
    +
    +

    + The Vault Browser CLI provides an easy way to execute the most common CLI commands, such as write, read, delete, and list. +

    +
    + {{console/output-log log=log}} + {{console/command-input + isFullscreen=isFullscreen + value=inputValue + onValueUpdate=(action (mut inputValue)) + onFullscreen=(action 'toggleFullscreen') + onExecuteCommand=(action 'executeCommand') + onShiftCommand=(action 'shiftCommandIndex') + }} +
    diff --git a/ui/app/templates/components/identity/edit-form.hbs b/ui/app/templates/components/identity/edit-form.hbs index 8ed98941f7..fb8cedb161 100644 --- a/ui/app/templates/components/identity/edit-form.hbs +++ b/ui/app/templates/components/identity/edit-form.hbs @@ -10,24 +10,39 @@ {{form-field data-test-field attr=attr model=model}} {{/each}}
    -
    -
    - + {{#if (or (eq mode "merge") (eq mode "create" ))}} + + Cancel + {{else}} - Save + + Cancel + {{/if}} - - {{#if (or (eq mode "merge") (eq mode "create" ))}} - - Cancel - - {{else}} - - Cancel - - {{/if}} +
    + + {{#if (and (eq mode "edit") model.canDelete)}} + {{#confirm-action + buttonClasses="button is-ghost" + onConfirmAction=(action "deleteItem" model) + confirmMessage=(concat "Are you sure you want to delete " model.id "?") + data-test-entity-item-delete=true + }} + Delete + {{/confirm-action}} + {{/if}} + diff --git a/ui/app/templates/components/identity/entity-nav.hbs b/ui/app/templates/components/identity/entity-nav.hbs index a7e34fa120..1c38812a86 100644 --- a/ui/app/templates/components/identity/entity-nav.hbs +++ b/ui/app/templates/components/identity/entity-nav.hbs @@ -7,12 +7,12 @@
    {{#if (eq identityType "entity")}} - + Merge {{pluralize identityType}} {{i-con glyph="chevron-right" size=11}} {{/if}} - + Create {{identityType}} {{i-con glyph="chevron-right" size=11}} diff --git a/ui/app/templates/components/identity/item-alias/alias-details.hbs b/ui/app/templates/components/identity/item-alias/alias-details.hbs index d7ecb17c56..d83cb0b20f 100644 --- a/ui/app/templates/components/identity/item-alias/alias-details.hbs +++ b/ui/app/templates/components/identity/item-alias/alias-details.hbs @@ -1,4 +1,4 @@ -{{info-table-row label="Name" value=model.name }} +{{info-table-row label="Name" value=model.name data-test-alias-name=true}} {{info-table-row label="ID" value=model.id }} {{#info-table-row label=(if (eq model.identityType "entity-alias") "Entity ID" "Group ID") value=model.canonicalId}} {{model.mountPath}}
    - {{model.mountType}} + {{model.mountType}} {{model.mountAccessor}}
    diff --git a/ui/app/templates/components/identity/item-alias/alias-metadata.hbs b/ui/app/templates/components/identity/item-alias/alias-metadata.hbs index efa6658b85..f87dcb24bd 100644 --- a/ui/app/templates/components/identity/item-alias/alias-metadata.hbs +++ b/ui/app/templates/components/identity/item-alias/alias-metadata.hbs @@ -8,6 +8,9 @@ {{value}}
    + {{#if model.canEdit}} + {{identity/popup-metadata params=(array model key)}} + {{/if}}
    diff --git a/ui/app/templates/components/identity/item-aliases.hbs b/ui/app/templates/components/identity/item-aliases.hbs index f972272ebd..afe3f7cddf 100644 --- a/ui/app/templates/components/identity/item-aliases.hbs +++ b/ui/app/templates/components/identity/item-aliases.hbs @@ -14,10 +14,11 @@ size=14 class="has-text-grey-light" }}{{item.name}}
    - {{item.mountType}} + {{item.mountType}} {{item.mountAccessor}}
    + {{identity/popup-alias params=(array item)}}
    {{/linked-block}} diff --git a/ui/app/templates/components/identity/item-details.hbs b/ui/app/templates/components/identity/item-details.hbs index 75ba63cd16..55ecb3d59c 100644 --- a/ui/app/templates/components/identity/item-details.hbs +++ b/ui/app/templates/components/identity/item-details.hbs @@ -1,4 +1,20 @@ -{{info-table-row label="Name" value=model.name }} +{{#if model.disabled}} +
    + {{#message-in-page type="warning" yieldWithoutColumn=true messageClass="message-body is-marginless" data-test-disabled-warning=true}} +
    + Attention This {{model.identityType}} is disabled. All associated tokens cannot be used, but are not revoked. +
    + {{#if model.canEdit}} +
    + +
    + {{/if}} + {{/message-in-page}} +
    +{{/if}} +{{info-table-row label="Name" value=model.name data-test-identity-item-name=true}} {{info-table-row label="Type" value=model.type }} {{info-table-row label="ID" value=model.id }} {{#info-table-row label="Merged Ids" value=model.mergedEntityIds }} diff --git a/ui/app/templates/components/identity/item-members.hbs b/ui/app/templates/components/identity/item-members.hbs index f31f16f624..17aba2c3a0 100644 --- a/ui/app/templates/components/identity/item-members.hbs +++ b/ui/app/templates/components/identity/item-members.hbs @@ -1,21 +1,56 @@ {{#if model.hasMembers}} {{#each model.memberGroupIds as |gid|}} - {{i-con - glyph='folder' - size=14 - class="has-text-grey-light" - }}{{gid}} + + {{#linked-block + "vault.cluster.access.identity.show" + "groups" + gid + details + class="box is-sideless is-marginless" + }} +
    + +
    + {{#if model.canEdit}} + {{identity/popup-members params=(array model "memberGroupIds" gid)}} + {{/if}} +
    +
    + {{/linked-block}} {{/each}} {{#each model.memberEntityIds as |gid|}} - {{i-con - glyph='role' - size=14 - class="has-text-grey-light" - }}{{gid}} + }} +
    + +
    + {{#if model.canEdit}} + {{identity/popup-members params=(array model "memberEntityIds" gid)}} + {{/if}} +
    +
    + {{/linked-block}} {{/each}} {{else}}
    diff --git a/ui/app/templates/components/identity/item-metadata.hbs b/ui/app/templates/components/identity/item-metadata.hbs index efa6658b85..f87dcb24bd 100644 --- a/ui/app/templates/components/identity/item-metadata.hbs +++ b/ui/app/templates/components/identity/item-metadata.hbs @@ -8,6 +8,9 @@ {{value}}
    + {{#if model.canEdit}} + {{identity/popup-metadata params=(array model key)}} + {{/if}}
    diff --git a/ui/app/templates/components/identity/item-policies.hbs b/ui/app/templates/components/identity/item-policies.hbs index 2211c16f7b..649d1609b4 100644 --- a/ui/app/templates/components/identity/item-policies.hbs +++ b/ui/app/templates/components/identity/item-policies.hbs @@ -1,4 +1,4 @@ -{{#each model.policies as |item|}} +{{#each model.policies as |policyName|}} {{#linked-block "vault.cluster.policy.show" "acl" @@ -7,12 +7,15 @@ }}
    + {{#if model.canEdit}} + {{identity/popup-policy params=(array model policyName)}} + {{/if}}
    {{/linked-block}} diff --git a/ui/app/templates/components/identity/popup-alias.hbs b/ui/app/templates/components/identity/popup-alias.hbs new file mode 100644 index 0000000000..c2a6105f72 --- /dev/null +++ b/ui/app/templates/components/identity/popup-alias.hbs @@ -0,0 +1,45 @@ +{{#popup-menu name="alias-menu"}} + {{#with params.firstObject as |item|}} + +{{/with}} +{{/popup-menu}} diff --git a/ui/app/templates/components/identity/popup-members.hbs b/ui/app/templates/components/identity/popup-members.hbs new file mode 100644 index 0000000000..eca4a68872 --- /dev/null +++ b/ui/app/templates/components/identity/popup-members.hbs @@ -0,0 +1,21 @@ +{{#popup-menu name="member-edit-menu"}} + +{{/popup-menu}} diff --git a/ui/app/templates/components/identity/popup-metadata.hbs b/ui/app/templates/components/identity/popup-metadata.hbs new file mode 100644 index 0000000000..8f251aae80 --- /dev/null +++ b/ui/app/templates/components/identity/popup-metadata.hbs @@ -0,0 +1,21 @@ +{{#popup-menu name="metadata-edit-menu"}} + +{{/popup-menu}} diff --git a/ui/app/templates/components/identity/popup-policy.hbs b/ui/app/templates/components/identity/popup-policy.hbs new file mode 100644 index 0000000000..4371a88def --- /dev/null +++ b/ui/app/templates/components/identity/popup-policy.hbs @@ -0,0 +1,31 @@ +{{#popup-menu name="policy-menu"}} + +{{/popup-menu}} diff --git a/ui/app/templates/components/key-value-header.hbs b/ui/app/templates/components/key-value-header.hbs index 272f4296de..ed57f90661 100644 --- a/ui/app/templates/components/key-value-header.hbs +++ b/ui/app/templates/components/key-value-header.hbs @@ -1,4 +1,5 @@