diff --git a/internal/pkg/vault/constants.go b/internal/pkg/vault/constants.go index 345bad95..1ee28739 100644 --- a/internal/pkg/vault/constants.go +++ b/internal/pkg/vault/constants.go @@ -20,20 +20,22 @@ const ( NamespaceHeader = "X-Vault-Namespace" AuthTypeHeader = "X-Vault-Token" - HealthAPI = "/v1/sys/health" - InitAPI = "/v1/sys/init" - UnsealAPI = "/v1/sys/unseal" - CreatePolicyPath = "/v1/sys/policies/acl/%s" - CreateTokenAPI = "/v1/auth/token/create" // nolint: gosec - ListAccessorsAPI = "/v1/auth/token/accessors" // nolint: gosec - RevokeAccessorAPI = "/v1/auth/token/revoke-accessor" - LookupAccessorAPI = "/v1/auth/token/lookup-accessor" - LookupSelfAPI = "/v1/auth/token/lookup-self" - RevokeSelfAPI = "/v1/auth/token/revoke-self" - RootTokenControlAPI = "/v1/sys/generate-root/attempt" // nolint: gosec - RootTokenRetrievalAPI = "/v1/sys/generate-root/update" // nolint: gosec - MountsAPI = "/v1/sys/mounts" - GenerateConsulTokenAPI = "/v1/consul/creds/%s" // nolint: gosec + HealthAPI = "/v1/sys/health" + InitAPI = "/v1/sys/init" + UnsealAPI = "/v1/sys/unseal" + CreatePolicyPath = "/v1/sys/policies/acl/%s" + CreateTokenAPI = "/v1/auth/token/create" // nolint: gosec + ListAccessorsAPI = "/v1/auth/token/accessors" // nolint: gosec + RevokeAccessorAPI = "/v1/auth/token/revoke-accessor" + LookupAccessorAPI = "/v1/auth/token/lookup-accessor" + LookupSelfAPI = "/v1/auth/token/lookup-self" + RevokeSelfAPI = "/v1/auth/token/revoke-self" + RootTokenControlAPI = "/v1/sys/generate-root/attempt" // nolint: gosec + RootTokenRetrievalAPI = "/v1/sys/generate-root/update" // nolint: gosec + MountsAPI = "/v1/sys/mounts" + GenerateConsulTokenAPI = "/v1/consul/creds/%s" // nolint: gosec + consulConfigAccessVaultAPI = "/v1/consul/config/access" + createConsulRoleVaultAPI = "/v1/consul/roles/%s" lookupSelfVaultAPI = "/v1/auth/token/lookup-self" renewSelfVaultAPI = "/v1/auth/token/renew-self" diff --git a/internal/pkg/vault/management.go b/internal/pkg/vault/management.go index 6254140c..af2cf7f0 100644 --- a/internal/pkg/vault/management.go +++ b/internal/pkg/vault/management.go @@ -193,3 +193,57 @@ func (c *Client) CheckSecretEngineInstalled(token string, mountPoint string, eng return false, nil } + +// CreateRole creates a Consul role that can be used to generate Consul tokens +// and part of elements for the role ties up with the Consul policies in which it dictates +// the permission of accesses to the Consul kv store or agent etc. +func (c *Client) CreateRole(secretStoreToken string, consulRole types.ConsulRole) error { + if len(secretStoreToken) == 0 { + return fmt.Errorf("required secret store token is empty") + } + + if len(consulRole.RoleName) == 0 { + return fmt.Errorf("required Consul role name is empty") + } + + createRoleURL := fmt.Sprintf(createConsulRoleVaultAPI, consulRole.RoleName) + c.lc.Debugf("configAccessURL: %s", createRoleURL) + _, err := c.doRequest(RequestArgs{ + AuthToken: secretStoreToken, + Method: http.MethodPost, + Path: createRoleURL, + JSONObject: &consulRole, + BodyReader: nil, + OperationDescription: "create Role", + ExpectedStatusCode: http.StatusNoContent, + ResponseObject: nil, + }) + + return err +} + +// ConfigureConsulAccess is to enable the Consul config access to the SecretStore via consul/config/access API +// see the reference: https://www.vaultproject.io/api-docs/secret/consul#configure-access +func (c *Client) ConfigureConsulAccess(secretStoreToken string, bootstrapACLToken string, consulHost string, consulPort int) error { + type ConfigAccess struct { + ConsulAddress string `json:"address"` + BootstrapACLToken string `json:"token"` + } + + payload := &ConfigAccess{ + ConsulAddress: fmt.Sprintf("%s:%d", consulHost, consulPort), + BootstrapACLToken: bootstrapACLToken, + } + + _, err := c.doRequest(RequestArgs{ + AuthToken: secretStoreToken, + Method: http.MethodPost, + Path: consulConfigAccessVaultAPI, + JSONObject: &payload, + BodyReader: nil, + OperationDescription: "Configure Consul Access", + ExpectedStatusCode: http.StatusNoContent, + ResponseObject: nil, + }) + return err +} diff --git a/internal/pkg/vault/management_test.go b/internal/pkg/vault/management_test.go index a896ddd1..9ad96c3f 100644 --- a/internal/pkg/vault/management_test.go +++ b/internal/pkg/vault/management_test.go @@ -18,6 +18,7 @@ package vault import ( "encoding/json" + "errors" "net/http" "net/http/httptest" url2 "net/url" @@ -35,7 +36,8 @@ import ( ) const ( - expectedToken = "fake-token" + expectedToken = "fake-token" + testBootstrapToken = "test-bootstrap-token" ) func TestHealthCheck(t *testing.T) { @@ -429,6 +431,81 @@ func TestEnableConsulSecretEngine(t *testing.T) { require.NoError(t, err) } +func TestConfigureConsulAccess(t *testing.T) { + mockLogger := logger.MockLogger{} + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + defer ts.Close() + client := createClient(t, ts.URL, mockLogger) + err := client.ConfigureConsulAccess(expectedToken, testBootstrapToken, "test-host", 8888) + require.NoError(t, err) +} + +func TestCreateRole(t *testing.T) { + testSinglePolicy := []types.Policy{ + { + ID: "test-ID", + Name: "test-name", + }, + } + testMultiplePolicies := []types.Policy{ + { + ID: "test-ID1", + Name: "test-name1", + }, + { + ID: "test-ID2", + Name: "test-name2", + }, + } + + testRoleWithNilPolicy := types.NewConsulRole("testRoleSingle", "client", nil, true) + testRoleWithEmptyPolicy := types.NewConsulRole("testRoleSingle", "client", []types.Policy{}, true) + testRoleWithSinglePolicy := types.NewConsulRole("testRoleSingle", "client", testSinglePolicy, true) + testRoleWithMultiplePolicies := types.NewConsulRole("testRoleMultiple", "client", testMultiplePolicies, true) + testEmptyRoleName := types.NewConsulRole("", "management", testSinglePolicy, true) + testCreateRoleErr := errors.New("request to create Role failed with status: 403 Forbidden") + testEmptyTokenErr := errors.New("required secret store token is empty") + testEmptyRoleNameErr := errors.New("required Consul role name is empty") + + tests := []struct { + name string + secretstoreToken string + consulRole types.ConsulRole + httpStatusCode int + expectedErr error + }{ + {"Good:create role with single policy ok", "test-secretstore-token", testRoleWithSinglePolicy, http.StatusNoContent, nil}, + {"Good:create role with multiple policies ok", expectedToken, testRoleWithMultiplePolicies, http.StatusNoContent, nil}, + {"Good:create role with empty policy ok", expectedToken, testRoleWithEmptyPolicy, http.StatusNoContent, nil}, + {"Good:create role with nil policy ok", "test-secretstore-token", testRoleWithNilPolicy, http.StatusNoContent, nil}, + {"Bad:create role bad response", expectedToken, testRoleWithSinglePolicy, http.StatusForbidden, testCreateRoleErr}, + {"Bad:empty secretstore token", "", testRoleWithMultiplePolicies, http.StatusForbidden, testEmptyTokenErr}, + {"Bad:empty role name", expectedToken, testEmptyRoleName, http.StatusForbidden, testEmptyRoleNameErr}, + } + + for _, tt := range tests { + test := tt // capture as local copy + t.Run(test.name, func(t *testing.T) { + t.Parallel() + // prepare test + mockLogger := logger.MockLogger{} + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(test.httpStatusCode) + })) + defer ts.Close() + client := createClient(t, ts.URL, mockLogger) + err := client.CreateRole(test.secretstoreToken, test.consulRole) + if test.expectedErr != nil { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + func createClient(t *testing.T, url string, lc logger.LoggingClient) *Client { urlDetails, err := url2.Parse(url) require.NoError(t, err) diff --git a/pkg/types/consulroles.go b/pkg/types/consulroles.go new file mode 100644 index 00000000..0f999b42 --- /dev/null +++ b/pkg/types/consulroles.go @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright 2019 Dell Inc. + * Copyright 2022 Intel Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + *******************************************************************************/ + +package types + +import "strings" + +type ConsulTokenType string + +const ( + /* + * The following are available Consul token types that can be used for specifying in the role-based tokens + * created via /consul/creds secret engine Vault API. + * For the details, see reference https://www.vaultproject.io/api/secret/consul#create-update-role + */ + // ManagementType is the type of Consul role can be used to create tokens when role-based API /consul/creds is called + // the management type of created tokens is automatically granted the built-in global management policy + ManagementType ConsulTokenType = "management" + // ClientType is the type of Consul role that can be used to create tokens when role-based API /consul/creds is called + // the regular client type of created tokens is associated with custom policies + ClientType ConsulTokenType = "client" +) + +type ConsulRole struct { + RoleName string `json:"name"` + TokenType string `json:"token_type"` + PolicyNames []string `json:"policies,omitempty"` + Local bool `json:"local,omitempty"` + TimeToLive string `json:"TTL,omitempty"` +} + +type Policy struct { + ID string `json:"ID"` + Name string `json:"Name"` +} + +func NewConsulRole(name string, tokenType ConsulTokenType, policies []Policy, localUse bool) ConsulRole { + // to conform to the payload of the Consul create role API, + // we convert the slice of policies from type Policy to string and make it unique + // as the policy name needs to be unique per API's requirement + policyNames := make([]string, 0, len(policies)) + tempMap := make(map[string]bool) + for _, policy := range policies { + if _, exists := tempMap[policy.Name]; !exists { + policyNames = append(policyNames, policy.Name) + } + } + + return ConsulRole{ + RoleName: strings.TrimSpace(name), + TokenType: string(tokenType), + PolicyNames: policyNames, + Local: localUse, + // unlimited for now + TimeToLive: "0s", + } +} diff --git a/pkg/types/messages.go b/pkg/types/messages.go index 71962c06..5beb1ec9 100644 --- a/pkg/types/messages.go +++ b/pkg/types/messages.go @@ -37,3 +37,9 @@ type TokenMetadata struct { Renewable bool `json:"renewable"` Ttl int `json:"ttl"` // in seconds } + +// BootStrapACLTokenInfo is the key portion of the response metadata from consulACLBootstrapAPI +type BootStrapACLTokenInfo struct { + SecretID string `json:"SecretID"` + Policies []Policy `json:"Policies"` +} diff --git a/secrets/interfaces.go b/secrets/interfaces.go index c4a8dca3..558fd08e 100644 --- a/secrets/interfaces.go +++ b/secrets/interfaces.go @@ -70,4 +70,6 @@ type SecretStoreClient interface { LookupTokenAccessor(token string, accessor string) (types.TokenMetadata, error) LookupToken(token string) (types.TokenMetadata, error) RevokeToken(token string) error + ConfigureConsulAccess(secretStoreToken string, bootstrapACLToken string, consulHost string, consulPort int) error + CreateRole(secretStoreToken string, consulRole types.ConsulRole) error } diff --git a/secrets/mocks/SecretStoreClient.go b/secrets/mocks/SecretStoreClient.go index 269c3eb1..c06c14b6 100644 --- a/secrets/mocks/SecretStoreClient.go +++ b/secrets/mocks/SecretStoreClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.5.1. DO NOT EDIT. +// Code generated by mockery v2.14.0. DO NOT EDIT. package mocks @@ -34,6 +34,34 @@ func (_m *SecretStoreClient) CheckSecretEngineInstalled(token string, mountPoint return r0, r1 } +// ConfigureConsulAccess provides a mock function with given fields: secretStoreToken, bootstrapACLToken, consulHost, consulPort +func (_m *SecretStoreClient) ConfigureConsulAccess(secretStoreToken string, bootstrapACLToken string, consulHost string, consulPort int) error { + ret := _m.Called(secretStoreToken, bootstrapACLToken, consulHost, consulPort) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, int) error); ok { + r0 = rf(secretStoreToken, bootstrapACLToken, consulHost, consulPort) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateRole provides a mock function with given fields: secretStoreToken, consulRole +func (_m *SecretStoreClient) CreateRole(secretStoreToken string, consulRole types.ConsulRole) error { + ret := _m.Called(secretStoreToken, consulRole) + + var r0 error + if rf, ok := ret.Get(0).(func(string, types.ConsulRole) error); ok { + r0 = rf(secretStoreToken, consulRole) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // CreateToken provides a mock function with given fields: token, parameters func (_m *SecretStoreClient) CreateToken(token string, parameters map[string]interface{}) (map[string]interface{}, error) { ret := _m.Called(token, parameters) @@ -268,3 +296,18 @@ func (_m *SecretStoreClient) Unseal(keysBase64 []string) error { return r0 } + +type mockConstructorTestingTNewSecretStoreClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewSecretStoreClient creates a new instance of SecretStoreClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewSecretStoreClient(t mockConstructorTestingTNewSecretStoreClient) *SecretStoreClient { + mock := &SecretStoreClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}