Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

refactor: moving consul access and role interface #163

Merged
merged 1 commit into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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
}
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.