Skip to content

Commit

Permalink
refactor: moving consul access and role interface
Browse files Browse the repository at this point in the history
Moving configureConsulAccess and createRole to go-mod-secrets to
consolidate consul API.

Closes: #3227

Signed-off-by: Rico Chavez-Lopez <[email protected]>
  • Loading branch information
ItsRico committed Oct 19, 2022
1 parent 7064924 commit 592284b
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 16 deletions.
30 changes: 16 additions & 14 deletions internal/pkg/vault/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
54 changes: 54 additions & 0 deletions internal/pkg/vault/management.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,57 @@ func (c *Client) CheckSecretEngineInstalled(token string, mountPoint string, eng

return false, nil
}

// CreateRole creates a secret store 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
}
79 changes: 78 additions & 1 deletion internal/pkg/vault/management_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package vault

import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
url2 "net/url"
Expand All @@ -35,7 +36,8 @@ import (
)

const (
expectedToken = "fake-token"
expectedToken = "fake-token"
testBootstrapToken = "test-bootstrap-token"
)

func TestHealthCheck(t *testing.T) {
Expand Down Expand Up @@ -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)
Expand Down
69 changes: 69 additions & 0 deletions pkg/types/consulroles.go
Original file line number Diff line number Diff line change
@@ -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",
}
}
6 changes: 6 additions & 0 deletions pkg/types/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
2 changes: 2 additions & 0 deletions secrets/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
45 changes: 44 additions & 1 deletion secrets/mocks/SecretStoreClient.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 592284b

Please sign in to comment.