diff --git a/cmd/security-bootstrapper/res/configuration.toml b/cmd/security-bootstrapper/res/configuration.toml index f1eef43f69..b9b3324ed0 100644 --- a/cmd/security-bootstrapper/res/configuration.toml +++ b/cmd/security-bootstrapper/res/configuration.toml @@ -23,11 +23,28 @@ LogLevel = 'INFO' [StageGate.Registry.ACL] Protocol = "http" # this is the filepath for the generated Consul management token from ACL bootstrap - BootstrapTokenPath = "/tmp/edgex/secrets/edgex-consul/admin/bootstrap_token.json" + BootstrapTokenPath = "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" # this is the filepath for the Vault token created from secretstore-setup SecretsAdminTokenPath = "/tmp/edgex/secrets/edgex-consul/admin/token.json" # this is the filepath for the sentinel file to indicate the registry ACL is set up successfully SentinelFilePath = "/edgex-init/consul-bootstrapper/consul_acl_done" + # this is the filepath for the created Consul management token + ManagementTokenPath = "/tmp/edgex/secrets/consul-acl-token/mgmt_token.json" + + # this section contains the list of registry roles for EdgeX services + # the service keys are the role names + [StageGate.Registry.ACL.Roles] + [StageGate.Registry.ACL.Roles.edgex-core-data] + Description = "role for coredata" + [StageGate.Registry.ACL.Roles.edgex-core-metadata] + Description = "role for metadata" + [StageGate.Registry.ACL.Roles.edgex-core-command] + Description = "role for command" + [StageGate.Registry.ACL.Roles.edgex-support-notifications] + Description = "role for notifications" + [StageGate.Registry.ACL.Roles.edgex-support-scheduler] + Description = "role for scheduler" + [StageGate.KongDb] Host = "kong-db" Port = 5432 diff --git a/internal/security/bootstrapper/command/setupacl/aclpolicies.go b/internal/security/bootstrapper/command/setupacl/aclpolicies.go new file mode 100644 index 0000000000..ef08b62960 --- /dev/null +++ b/internal/security/bootstrapper/command/setupacl/aclpolicies.go @@ -0,0 +1,197 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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 setupacl + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" +) + +const ( + // edgeXPolicyRules are rules for edgex services + // in Phase 2, we will use the same policy for all EdgeX services + // TODO: phase 3 will have more finer grained policies for each service + edgeXPolicyRules = ` + # HCL definition of server agent policy for EdgeX + agent "" { + policy = "read" + } + agent_prefix "edgex" { + policy = "write" + } + node "" { + policy = "read" + } + node_prefix "edgex" { + policy = "write" + } + service "" { + policy = "read" + } + service_prefix "edgex" { + policy = "write" + } + # allow key value store put + # once the default_policy is switched to "deny", + # this is needed if wants to allow updating Key/Value configuration + key "" { + policy = "read" + } + key_prefix "edgex" { + policy = "write" + } + ` + + // edgeXServicePolicyName is the name of the agent policy for edgex + edgeXServicePolicyName = "edgex-service-policy" + + consulCreatePolicyAPI = "/v1/acl/policy" + consulReadPolicyByNameAPI = "/v1/acl/policy/name/%s" + + aclNotFoundMessage = "ACL not found" +) + +// getOrCreateRegistryPolicy retrieves or creates a new policy +// it inserts a new policy if the policy name does not exist and returns a policy +// it returns the same policy if the policy name already exists +func (c *cmd) getOrCreateRegistryPolicy(tokenID, policyName, policyRules string) (*Policy, error) { + // try to get the policy to see if it exists or not + policy, err := c.getPolicyByName(tokenID, policyName) + if err != nil { + return nil, fmt.Errorf("failed to get policy ID by name %s: %v", policyName, err) + } + + if policy != nil { + // policy exists, return this one + return policy, nil + } + + createPolicyURL, err := c.getRegistryApiUrl(consulCreatePolicyAPI) + if err != nil { + return nil, err + } + + // payload struct for creating a new policy + type CreatePolicy struct { + Name string `json:"Name"` + Description string `json:"Description,omitempty"` + Rules string `json:"Rules,omitempty"` + } + + createPolicy := &CreatePolicy{ + Name: policyName, + Description: "agent policy for EdgeX microservices", + Rules: policyRules, + } + + jsonPayload, err := json.Marshal(createPolicy) + c.loggingClient.Tracef("payload: %v", createPolicy) + if err != nil { + return nil, fmt.Errorf("failed to marshal CreatePolicy JSON string payload: %v", err) + } + + req, err := http.NewRequest(http.MethodPut, createPolicyURL, bytes.NewBuffer(jsonPayload)) + if err != nil { + return nil, fmt.Errorf("failed to prepare create a new policy request for http URL: %w", err) + } + + req.Header.Add(consulTokenHeader, tokenID) + req.Header.Add(clients.ContentType, clients.ContentTypeJSON) + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("Failed to send create a new policy request for http URL: %w", err) + } + + defer func() { + _ = resp.Body.Close() + }() + + createPolicyResp, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("Failed to read create a new policy response body: %w", err) + } + + var created Policy + + switch resp.StatusCode { + case http.StatusOK: + if err := json.NewDecoder(bytes.NewReader(createPolicyResp)).Decode(&created); err != nil { + return nil, fmt.Errorf("failed to decode create a new policy response body: %v", err) + } + + c.loggingClient.Infof("successfully created a new agent policy with name %s", policyName) + + return &created, nil + default: + return nil, fmt.Errorf("failed to create a new policy with name %s via URL [%s] and status code= %d: %s", + policyName, consulCreatePolicyAPI, resp.StatusCode, string(createPolicyResp)) + } +} + +// getPolicyByName gets policy by policy name, returns nil if not found +func (c *cmd) getPolicyByName(tokenID, policyName string) (*Policy, error) { + readPolicyByNameURL, err := c.getRegistryApiUrl(fmt.Sprintf(consulReadPolicyByNameAPI, policyName)) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, readPolicyByNameURL, http.NoBody) + if err != nil { + return nil, fmt.Errorf("Failed to prepare readPolicyByName request for http URL: %w", err) + } + + req.Header.Add(consulTokenHeader, tokenID) + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("Failed to send readPolicyByName request for http URL: %w", err) + } + + defer func() { + _ = resp.Body.Close() + }() + + readPolicyResp, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("Failed to read readPolicyByName response body: %w", err) + } + + switch resp.StatusCode { + case http.StatusOK: + var existing Policy + if err := json.NewDecoder(bytes.NewReader(readPolicyResp)).Decode(&existing); err != nil { + return nil, fmt.Errorf("failed to decode Policy json data: %v", err) + } + + return &existing, nil + case http.StatusForbidden: + // when the policy cannot be found by the name, the body returns "ACL not found" + // so we treat it as non-error case + if strings.EqualFold(aclNotFoundMessage, string(readPolicyResp)) { + return nil, nil + } + + return nil, fmt.Errorf("failed to read policy by name with error %s", string(readPolicyResp)) + default: + return nil, fmt.Errorf("failed to read policy by name with status code= %d: %s", + resp.StatusCode, string(readPolicyResp)) + } +} diff --git a/internal/security/bootstrapper/command/setupacl/aclpolicies_test.go b/internal/security/bootstrapper/command/setupacl/aclpolicies_test.go new file mode 100644 index 0000000000..9141310de3 --- /dev/null +++ b/internal/security/bootstrapper/command/setupacl/aclpolicies_test.go @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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 setupacl + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +func TestGetOrCreatePolicy(t *testing.T) { + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + testBootstrapToken := "test-bootstrap-token" + testPolicyName := "test-policy-name" + + tests := []struct { + name string + bootstrapToken string + policyName string + getPolicyOkResponse bool + policyNameAlreadyExists bool + createPolicyOkResponse bool + expectedErr bool + }{ + {"Good:create policy ok with non-existing name yet", testBootstrapToken, testPolicyName, true, false, true, false}, + {"Good:get or create policy ok with pre-existing name", testBootstrapToken, testPolicyName, true, true, true, false}, + {"Bad:get policy bad response", testBootstrapToken, testPolicyName, false, false, false, true}, + {"Bad:create policy bad response", testBootstrapToken, testPolicyName, true, false, false, true}, + {"Bad:empty bootstrap token", "", testPolicyName, false, false, false, true}, + {"Bad:empty policy name", testBootstrapToken, "", false, false, false, true}, + } + + for _, tt := range tests { + test := tt // capture as local copy + t.Run(test.name, func(t *testing.T) { + t.Parallel() + // prepare test + responseOpts := serverOptions{ + readPolicyByNameOk: test.getPolicyOkResponse, + policyAlreadyExists: test.policyNameAlreadyExists, + createNewPolicyOk: test.createPolicyOkResponse, + } + testSrv := newRegistryTestServer(responseOpts) + conf := testSrv.getRegistryServerConf(t) + defer testSrv.close() + + command, err := NewCommand(ctx, wg, lc, conf, []string{}) + require.NoError(t, err) + require.NotNil(t, command) + require.Equal(t, "setupRegistryACL", command.GetCommandName()) + setupRegistryACL := command.(*cmd) + setupRegistryACL.retryTimeout = 2 * time.Second + + policyActual, err := setupRegistryACL.getOrCreateRegistryPolicy(test.bootstrapToken, test.policyName, edgeXPolicyRules) + + if test.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, policyActual) + require.Equal(t, test.policyName, policyActual.Name) + } + }) + } +} diff --git a/internal/security/bootstrapper/command/setupacl/aclroles.go b/internal/security/bootstrapper/command/setupacl/aclroles.go new file mode 100644 index 0000000000..dfe887da14 --- /dev/null +++ b/internal/security/bootstrapper/command/setupacl/aclroles.go @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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 setupacl + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" +) + +// RegistryTokenType is the type of registry tokens that will be created when the role is using to call token creds API +type RegistryTokenType string + +const ( + /* + * The following are available registry 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 registry 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 RegistryTokenType = "management" + // ClientType is the type of registry 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 RegistryTokenType = "client" + + createConsulRoleVaultAPI = "/v1/consul/roles/%s" +) + +// RegistryRole is the meta definition for creating registry's role +type RegistryRole 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"` +} + +// NewRegistryRole instantiates a new RegistryRole with the given inputs +func NewRegistryRole(name string, tokenType RegistryTokenType, policies []Policy, localUse bool) RegistryRole { + // to conform to the payload of the registry 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 RegistryRole{ + RoleName: strings.TrimSpace(name), + TokenType: string(tokenType), + PolicyNames: policyNames, + Local: localUse, + // unlimited for now + TimeToLive: "0s", + } +} + +// createRole creates a secret store role that can be used to generate registry tokens +// and part of elements for the role ties up with the registry policies in which it dictates +// the permission of accesses to the registry kv store or agent etc. +func (c *cmd) createRole(secretStoreToken string, registryRole RegistryRole) error { + if len(secretStoreToken) == 0 { + return errors.New("required secret store token is empty") + } + + if len(registryRole.RoleName) == 0 { + return errors.New("required role name cannot be empty") + } + + createRoleURL := fmt.Sprintf("%s://%s:%d%s", c.configuration.SecretStore.Protocol, + c.configuration.SecretStore.Host, c.configuration.SecretStore.Port, + fmt.Sprintf(createConsulRoleVaultAPI, registryRole.RoleName)) + _, err := url.Parse(createRoleURL) + if err != nil { + return fmt.Errorf("failed to parse create role URL: %v", err) + } + + c.loggingClient.Debugf("createRoleURL: %s", createRoleURL) + + jsonPayload, err := json.Marshal(®istryRole) + + if err != nil { + return fmt.Errorf("failed to marshal JSON string payload: %v", err) + } + + req, err := http.NewRequest(http.MethodPost, createRoleURL, bytes.NewBuffer(jsonPayload)) + if err != nil { + return fmt.Errorf("failed to prepare POST request for http URL: %w", err) + } + + req.Header.Add("X-Vault-Token", secretStoreToken) + req.Header.Add(clients.ContentType, clients.ContentTypeJSON) + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request for http URL: %w", err) + } + + defer func() { + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusNoContent: + // no response body returned in this case + c.loggingClient.Infof("successfully created a role [%s] for secretstore", registryRole.RoleName) + return nil + default: + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + c.loggingClient.Errorf("cannot read resp.Body: %v", err) + } + return fmt.Errorf("failed to create a role %s for secretstore via URL [%s] and status code= %d: %s", + registryRole.RoleName, createRoleURL, resp.StatusCode, string(body)) + } +} diff --git a/internal/security/bootstrapper/command/setupacl/aclroles_test.go b/internal/security/bootstrapper/command/setupacl/aclroles_test.go new file mode 100644 index 0000000000..49696e4854 --- /dev/null +++ b/internal/security/bootstrapper/command/setupacl/aclroles_test.go @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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 setupacl + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +func TestCreateRole(t *testing.T) { + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + testSecretStoreToken := "test-secretstore-token" + testSinglePolicy := []Policy{ + { + ID: "test-ID", + Name: "test-name", + }, + } + testMultiplePolicies := []Policy{ + { + ID: "test-ID1", + Name: "test-name1", + }, + { + ID: "test-ID2", + Name: "test-name2", + }, + } + + testRoleWithNilPolicy := NewRegistryRole("testRoleSingle", ClientType, nil, true) + testRoleWithEmptyPolicy := NewRegistryRole("testRoleSingle", ClientType, []Policy{}, true) + testRoleWithSinglePolicy := NewRegistryRole("testRoleSingle", ClientType, testSinglePolicy, true) + testRoleWithMultiplePolicies := NewRegistryRole("testRoleMultiple", ClientType, testMultiplePolicies, true) + testEmptyRoleName := NewRegistryRole("", ManagementType, testSinglePolicy, true) + + tests := []struct { + name string + secretstoreToken string + registryRole RegistryRole + creatRoleOkResponse bool + expectedErr bool + }{ + {"Good:create role with single policy ok", testSecretStoreToken, testRoleWithSinglePolicy, true, false}, + {"Good:create role with multiple policies ok", testSecretStoreToken, testRoleWithMultiplePolicies, true, false}, + {"Good:create role with empty policy ok", testSecretStoreToken, testRoleWithEmptyPolicy, true, false}, + {"Good:create role with nil policy ok", testSecretStoreToken, testRoleWithNilPolicy, true, false}, + {"Bad:create role bad response", testSecretStoreToken, testRoleWithSinglePolicy, false, true}, + {"Bad:empty secretstore token", "", testRoleWithMultiplePolicies, false, true}, + {"Bad:empty role name", testSecretStoreToken, testEmptyRoleName, false, true}, + } + + for _, tt := range tests { + test := tt // capture as local copy + t.Run(test.name, func(t *testing.T) { + t.Parallel() + // prepare test + responseOpts := serverOptions{ + createRoleOk: test.creatRoleOkResponse, + } + testSrv := newRegistryTestServer(responseOpts) + conf := testSrv.getRegistryServerConf(t) + defer testSrv.close() + + command, err := NewCommand(ctx, wg, lc, conf, []string{}) + require.NoError(t, err) + require.NotNil(t, command) + require.Equal(t, "setupRegistryACL", command.GetCommandName()) + setupRegistryACL := command.(*cmd) + setupRegistryACL.retryTimeout = 2 * time.Second + + err = setupRegistryACL.createRole(test.secretstoreToken, test.registryRole) + + if test.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/security/bootstrapper/command/setupacl/acltokens.go b/internal/security/bootstrapper/command/setupacl/acltokens.go index 158751b413..db81c1b544 100644 --- a/internal/security/bootstrapper/command/setupacl/acltokens.go +++ b/internal/security/bootstrapper/command/setupacl/acltokens.go @@ -56,6 +56,24 @@ const ( consulTokenRUDAPI = "/v1/acl/token/%s" ) +// CreateRegistryToken is the structure to create a new registry token +type CreateRegistryToken struct { + Description string `json:"Description"` + Policies []Policy `json:"Policies"` + Local bool `json:"Local"` + TTL *string `json:"ExpirationTTL,omitempty"` +} + +// NewCreateRegistryToken instantiates a new CreateRegistryToken with a given inputs +func NewCreateRegistryToken(description string, policies []Policy, local bool, timeToLive *string) CreateRegistryToken { + return CreateRegistryToken{ + Description: description, + Policies: policies, + Local: local, + TTL: timeToLive, + } +} + // isACLTokenPersistent checks Consul agent's configuration property for EnablePersistence of ACLTokens // it returns true if the token persistence is enabled; false otherwise // once ACL rules are enforced, this call requires at least agent read permission and hence we use @@ -283,72 +301,21 @@ func (c *cmd) readTokenIDBy(bootstrapACLToken BootStrapACLTokenInfo, accessorID // insertNewAgentToken creates a new Consul token // it returns the token's ID and error if any error occurs func (c *cmd) insertNewAgentToken(bootstrapACLToken BootStrapACLTokenInfo) (string, error) { - createTokenURL, err := c.getRegistryApiUrl(consulCreateTokenAPI) - if err != nil { - return emptyToken, err - } - - // payload struct for creating a new token - type CreateToken struct { - Description string `json:"Description"` - Policies []Policy `json:"Policies"` - Local bool `json:"Local"` - TTL *string `json:"ExpirationTTL,omitempty"` - } - unlimitedDuration := "0s" - createToken := &CreateToken{ - Description: "edgex-core-consul agent token", - // uses global mgmt policy in Phase 1 - Policies: bootstrapACLToken.Policies, - // only applies to the local agent as of today's use cases (no need to be replicated to other agents) - Local: true, - // never expired, based on Phase 1 or 2 from ADR - TTL: &unlimitedDuration, - } - - jsonPayload, err := json.Marshal(createToken) - c.loggingClient.Tracef("payload: %v", createToken) - if err != nil { - return emptyToken, fmt.Errorf("Failed to marshal CreatToken JSON string payload: %v", err) - } - - req, err := http.NewRequest(http.MethodPut, createTokenURL, bytes.NewBuffer(jsonPayload)) - if err != nil { - return emptyToken, fmt.Errorf("Failed to prepare creat a new token request for http URL: %w", err) - } - - req.Header.Add(consulTokenHeader, bootstrapACLToken.SecretID) - req.Header.Add(clients.ContentType, clients.ContentTypeJSON) - resp, err := c.client.Do(req) + createToken := NewCreateRegistryToken("edgex-core-consul agent token", bootstrapACLToken.Policies, true, &unlimitedDuration) + newTokenInfo, err := c.createNewToken(bootstrapACLToken.SecretID, createToken) if err != nil { - return emptyToken, fmt.Errorf("Failed to send create a new token request for http URL: %w", err) - } - - defer func() { - _ = resp.Body.Close() - }() - - createTokenResp, err := ioutil.ReadAll(resp.Body) - if err != nil { - return emptyToken, fmt.Errorf("Failed to read create a new token response body: %w", err) + return emptyToken, fmt.Errorf("failed to insert new edgex agent token: %v", err) } var parsedTokenResponse map[string]interface{} + if err := json.NewDecoder(strings.NewReader(newTokenInfo)).Decode(&parsedTokenResponse); err != nil { + return emptyToken, fmt.Errorf("Failed to decode create token info: %v", err) + } - switch resp.StatusCode { - case http.StatusOK: - if err := json.NewDecoder(bytes.NewReader(createTokenResp)).Decode(&parsedTokenResponse); err != nil { - return emptyToken, fmt.Errorf("Failed to decode create token response body: %v", err) - } - - c.loggingClient.Info("successfully created a new agent token") + c.loggingClient.Info("successfully created a new agent token") - return fmt.Sprintf("%s", parsedTokenResponse["SecretID"]), nil - default: - return emptyToken, fmt.Errorf("failed to create a new token via URL [%s] and status code= %d: %s", - consulCreateTokenAPI, resp.StatusCode, string(createTokenResp)) - } + return fmt.Sprintf("%s", parsedTokenResponse["SecretID"]), nil } // setAgentToken sets the ACL token currently in use by the agent @@ -411,3 +378,52 @@ func (c *cmd) setAgentToken(bootstrapACLToken BootStrapACLTokenInfo, agentTokenI return nil } + +// createNewToken creates a new token based on the provided inputs +// it returns the whole json string containing the token and thus can be written to the file later +func (c *cmd) createNewToken(bootstrapACLTokenID string, createToken CreateRegistryToken) (string, error) { + if len(bootstrapACLTokenID) == 0 { + return emptyToken, fmt.Errorf("bootstrap token ID cannot be empty") + } + + createTokenURL, err := c.getRegistryApiUrl(consulCreateTokenAPI) + if err != nil { + return emptyToken, err + } + + jsonPayload, err := json.Marshal(&createToken) + c.loggingClient.Tracef("payload: %v", createToken) + if err != nil { + return emptyToken, fmt.Errorf("Failed to marshal CreatRegistryToken JSON string payload: %v", err) + } + + req, err := http.NewRequest(http.MethodPut, createTokenURL, bytes.NewBuffer(jsonPayload)) + if err != nil { + return emptyToken, fmt.Errorf("Failed to prepare creat a new token request for http URL: %w", err) + } + + req.Header.Add(consulTokenHeader, bootstrapACLTokenID) + req.Header.Add(clients.ContentType, clients.ContentTypeJSON) + resp, err := c.client.Do(req) + if err != nil { + return emptyToken, fmt.Errorf("Failed to send create a new token request for http URL: %w", err) + } + + defer func() { + _ = resp.Body.Close() + }() + + createTokenResp, err := ioutil.ReadAll(resp.Body) + if err != nil { + return emptyToken, fmt.Errorf("Failed to read create a new token response body: %w", err) + } + + switch resp.StatusCode { + case http.StatusOK: + c.loggingClient.Info("successfully created a new registry token") + return string(createTokenResp), nil + default: + return emptyToken, fmt.Errorf("failed to create a new token via URL [%s] and status code= %d: %s", + consulCreateTokenAPI, resp.StatusCode, string(createTokenResp)) + } +} diff --git a/internal/security/bootstrapper/command/setupacl/acltokens_test.go b/internal/security/bootstrapper/command/setupacl/acltokens_test.go index ae0d727ba8..b3d6e5922e 100644 --- a/internal/security/bootstrapper/command/setupacl/acltokens_test.go +++ b/internal/security/bootstrapper/command/setupacl/acltokens_test.go @@ -17,20 +17,12 @@ package setupacl import ( "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strconv" "sync" "testing" "time" "github.com/stretchr/testify/require" - "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" ) @@ -58,12 +50,13 @@ func TestIsACLTokenPersistent(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() // prepare test - responseOpts := responseOptions{ + responseOpts := serverOptions{ enablePersistence: test.enablePersist, consulCheckAgentOk: test.checkAgentOkResponse, } testSrv := newRegistryTestServer(responseOpts) conf := testSrv.getRegistryServerConf(t) + defer testSrv.close() command, err := NewCommand(ctx, wg, lc, conf, []string{}) require.NoError(t, err) @@ -78,7 +71,7 @@ func TestIsACLTokenPersistent(t *testing.T) { require.Error(t, err) } else { require.NoError(t, err) - require.Equal(t, testSrv.responseOptions.enablePersistence, persistent) + require.Equal(t, testSrv.serverOptions.enablePersistence, persistent) } }) } @@ -119,13 +112,14 @@ func TestCreateAgentToken(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() // prepare test - responseOpts := responseOptions{ + responseOpts := serverOptions{ listTokensOk: test.listTokensOkResponse, createTokenOk: test.createTokenOkResponse, readTokenOk: test.readTokenOkResponse, } testSrv := newRegistryTestServer(responseOpts) conf := testSrv.getRegistryServerConf(t) + defer testSrv.close() command, err := NewCommand(ctx, wg, lc, conf, []string{}) require.NoError(t, err) @@ -191,11 +185,12 @@ func TestSetAgentTokenToAgent(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() // prepare test - responseOpts := responseOptions{ + responseOpts := serverOptions{ setAgentTokenOk: test.setAgentTokenOkResponse, } testSrv := newRegistryTestServer(responseOpts) conf := testSrv.getRegistryServerConf(t) + defer testSrv.close() command, err := NewCommand(ctx, wg, lc, conf, []string{}) require.NoError(t, err) @@ -214,180 +209,3 @@ func TestSetAgentTokenToAgent(t *testing.T) { }) } } - -type registryTestServer struct { - config *config.ConfigurationStruct - responseOptions responseOptions -} - -type responseOptions struct { - enablePersistence bool - consulCheckAgentOk bool - listTokensOk bool - listTokensWithRetriesOk bool - createTokenOk bool - readTokenOk bool - setAgentTokenOk bool -} - -func newRegistryTestServer(respOpts responseOptions) *registryTestServer { - return ®istryTestServer{ - config: &config.ConfigurationStruct{}, - responseOptions: respOpts, - } -} - -func (registry *registryTestServer) getRegistryServerConf(t *testing.T) *config.ConfigurationStruct { - registryTestConf := registry.config - testAgentTokenAccessorID := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - tokens := []map[string]interface{}{ - { - "AccessorID": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", - "Description": "some other type of agent token", - }, - { - "AccessorID": "00000000-0000-0000-0000-000000000002", - "Description": "Anonymous Token", - }, - { - "AccessorID": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", - "Description": "Bootstrap Token (Global Management)", - }, - } - respCnt := 0 - testSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.EscapedPath() { - case consulCheckAgentAPI: - require.Equal(t, http.MethodGet, r.Method) - if registry.responseOptions.consulCheckAgentOk { - w.WriteHeader(http.StatusOK) - jsonResponse := map[string]interface{}{ - "DebugConfig": map[string]interface{}{ - "ACLDatacenter": "dc1", - "ACLDefaultPolicy": "allow", - "ACLDisabledTTL": "2m0s", - "ACLTokens": map[string]interface{}{ - "EnablePersistence": registry.responseOptions.enablePersistence, - }, - "ACLsEnabled": true, - }, - } - err := json.NewEncoder(w).Encode(jsonResponse) - require.NoError(t, err) - } else { - w.WriteHeader(http.StatusForbidden) - _, _ = w.Write([]byte("permission denied")) - } - case consulListTokensAPI: - require.Equal(t, http.MethodGet, r.Method) - if registry.responseOptions.listTokensOk { - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(tokens) - require.NoError(t, err) - } else if registry.responseOptions.listTokensWithRetriesOk { - if respCnt >= 2 { - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(tokens) - require.NoError(t, err) - } else { - respCnt++ - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(consulLegacyACLModeError)) - } - } else if !registry.responseOptions.listTokensWithRetriesOk { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(consulLegacyACLModeError)) - } else { - w.WriteHeader(http.StatusForbidden) - _, _ = w.Write([]byte("permission denied")) - } - case consulCreateTokenAPI: - require.Equal(t, http.MethodPut, r.Method) - if registry.responseOptions.createTokenOk { - w.WriteHeader(http.StatusOK) - jsonResponse := map[string]interface{}{ - "AccessorID": testAgentTokenAccessorID, - "Description": "edgex-core-consul agent token", - "Policies": []map[string]interface{}{ - { - "ID": "00000000-0000-0000-0000-000000000001", - "Name": "global-management", - }, - { - "ID": "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", - "Name": "node-read policy", - }, - }, - "Local": true, - "CreateTime": "2021-03-10T12:25:06.123456-07:00", - "Hash": "UuiRkOQPRCvoRZHRtUxxbrmwZ5crYrOdZ0Z1FTFbTbA=", - "CreateIndex": 59, - "ModifyIndex": 59, - } - - err := json.NewEncoder(w).Encode(jsonResponse) - require.NoError(t, err) - tokens = append(tokens, jsonResponse) - } else { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("cannot create token")) - } - case fmt.Sprintf(consulTokenRUDAPI, testAgentTokenAccessorID): - if r.Method == http.MethodGet && registry.responseOptions.readTokenOk { - w.WriteHeader(http.StatusOK) - jsonResponse := map[string]interface{}{ - "AccessorID": testAgentTokenAccessorID, - "Description": "edgex-core-consul agent token", - "Policies": []map[string]interface{}{ - { - "ID": "00000000-0000-0000-0000-000000000001", - "Name": "global-management", - }, - { - "ID": "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", - "Name": "node-read policy", - }, - }, - "Local": false, - "CreateTime": "2021-03-10T12:25:06.123456-07:00", - "Hash": "UuiRkOQPRCvoRZHRtUxxbrmwZ5crYrOdZ0Z1FTFbTbA=", - "CreateIndex": 59, - "ModifyIndex": 59, - } - - err := json.NewEncoder(w).Encode(jsonResponse) - require.NoError(t, err) - } else if r.Method == http.MethodGet { - w.WriteHeader(http.StatusForbidden) - _, _ = w.Write([]byte("permission denied")) - } else { - w.WriteHeader(http.StatusInternalServerError) - t.Fatal(fmt.Sprintf("Unexpected method %s to URL %s", r.Method, r.URL.EscapedPath())) - } - case fmt.Sprintf(consulSetAgentTokenAPI, AgentType): - require.Equal(t, http.MethodPut, r.Method) - if registry.responseOptions.setAgentTokenOk { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("agent token set successfully")) - } else { - w.WriteHeader(http.StatusForbidden) - _, _ = w.Write([]byte("permission denied")) - } - default: - t.Fatal(fmt.Sprintf("Unexpected call to URL %s", r.URL.EscapedPath())) - } - })) - tsURL, err := url.Parse(testSrv.URL) - require.NoError(t, err) - portNum, _ := strconv.Atoi(tsURL.Port()) - registryTestConf.StageGate.Registry.ACL.Protocol = tsURL.Scheme - registryTestConf.StageGate.Registry.Host = tsURL.Hostname() - registryTestConf.StageGate.Registry.Port = portNum - registryTestConf.StageGate.WaitFor.Timeout = "1m" - registryTestConf.StageGate.WaitFor.RetryInterval = "1s" - // for the sake of simplicity, we use the same test server as the secret store server - registryTestConf.SecretStore.Protocol = tsURL.Scheme - registryTestConf.SecretStore.Host = tsURL.Hostname() - registryTestConf.SecretStore.Port = portNum - return registryTestConf -} diff --git a/internal/security/bootstrapper/command/setupacl/command.go b/internal/security/bootstrapper/command/setupacl/command.go index a86518f26c..c93fdade94 100644 --- a/internal/security/bootstrapper/command/setupacl/command.go +++ b/internal/security/bootstrapper/command/setupacl/command.go @@ -126,8 +126,56 @@ func (c *cmd) Execute() (statusCode int, err error) { return } - var bootstrapACLToken *BootStrapACLTokenInfo - bootstrapACLToken, err = c.generateBootStrapACLToken() + bootstrapACLToken, err := c.createBootstrapACLToken() + if err != nil { + return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to create bootstrap ACL token: %v", err) + } + + // retrieve the secretstore (Vault) token from the file produced by secretstore-setup + secretstoreToken, err := c.getSecretStoreTokenFromFile() + if err != nil { + return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to retrieve secretstore token: %v", err) + } + + // configure Consul access with both Secret Store token and consul's bootstrap acl token + if err := c.configureConsulAccess(secretstoreToken, bootstrapACLToken.SecretID); err != nil { + return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to configure Consul access: %v", err) + } + + c.loggingClient.Info("successfully get secretstore token and configuring the registry access for secretestore") + + // create all ACL token roles for EdgeX services defined in configuration (static) + if err := c.createEdgeXACLTokenRoles(bootstrapACLToken.SecretID, secretstoreToken); err != nil { + return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to createEdgeXACLTokenRoles: %v", err) + } + + // set up agent token to agent for the first time + if err := c.setupAgentToken(bootstrapACLToken); err != nil { + return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to set up agent token: %v", err) + } + + if err := c.saveACLTokens(bootstrapACLToken); err != nil { + return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to save ACL tokens: %v", err) + } + + // write a sentinel file to indicate Consul ACL bootstrap is done so that we don't bootstrap ACL again, + // this is to avoid re-bootstrapping error and that error can cause the snap crash if restart this process + if err := c.writeSentinelFile(); err != nil { + return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to write sentinel file: %v", err) + } + + c.loggingClient.Info("setupRegistryACL successfully done") + + return +} + +// GetCommandName returns the name of this command +func (c *cmd) GetCommandName() string { + return CommandName +} + +func (c *cmd) createBootstrapACLToken() (*BootStrapACLTokenInfo, error) { + bootstrapACLToken, err := c.generateBootStrapACLToken() if err != nil { // although we have a leader, but it is a very very rare chance that we could hit an error on legacy mode // here we will sleep a bit of time and then retry once if there is error on Legacy ACL type of message @@ -136,56 +184,60 @@ func (c *cmd) Execute() (statusCode int, err error) { // https://github.com/hashicorp/consul/issues/5218#issuecomment-457212336 if !strings.Contains(err.Error(), consulLegacyACLModeError) { // other type of ACL bootstrapping error, cannot continue - return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to bootstrap registry's ACL: %v", err) + return nil, fmt.Errorf("failed to bootstrap registry's ACL: %v", err) } c.loggingClient.Warnf("found Consul still in ACL legacy mode, will retry once again: %v", err) time.Sleep(5 * time.Second) bootstrapACLToken, err = c.generateBootStrapACLToken() if err != nil { - return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to bootstrap registry's ACL: %v", err) + return nil, fmt.Errorf("failed to bootstrap registry's ACL: %v", err) } } c.loggingClient.Info("successfully bootstrap registry ACL") - // Save the bootstrap token into the file so that it can be used later on + return bootstrapACLToken, nil +} + +func (c *cmd) saveACLTokens(bootstrapACLToken *BootStrapACLTokenInfo) error { + // Save the bootstrap ACL token into json file so that it can be used later on if err := c.saveBootstrapACLToken(bootstrapACLToken); err != nil { - return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to save registry's bootstrap ACL token: %v", err) + return fmt.Errorf("failed to save registry's bootstrap ACL token: %v", err) } - // retrieve the secretstore (Vault) token from the file produced by secretstore-setup - secretstoreToken, err := c.getSecretStoreTokenFromFile() + return nil +} + +// createEdgeXACLTokenRoles creates secret store roles that can be used for genearting registry tokens +// via Consul secret engine API /consul/creds/[role_name] later on for all EdgeX microservices +func (c *cmd) createEdgeXACLTokenRoles(bootstrapACLTokenID, secretstoreToken string) error { + edgexServicePolicy, err := c.getOrCreateRegistryPolicy(bootstrapACLTokenID, edgeXServicePolicyName, edgeXPolicyRules) if err != nil { - return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to retrieve secretstore token: %v", err) + return fmt.Errorf("failed to create edgex service policy: %v", err) } - c.loggingClient.Info("successfully get secretstore token and configuring the registry access for secretestore") - - // configure Consul access with both Secret Store token and consul's bootstrap acl token - if err := c.configureConsulAccess(secretstoreToken, bootstrapACLToken.SecretID); err != nil { - return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to configure Consul access: %v", err) + roleNames := c.configuration.StageGate.Registry.ACL.GetACLRoleNames() + if len(roleNames) == 0 { + c.loggingClient.Warn("found no ACL role names defined in configuration, skip create ACL roles") + return nil } - // write a sentinel file to indicate Consul ACL bootstrap is done so that we don't bootstrap ACL again, - // this is to avoid re-bootstrapping error and that error can cause the snap crash if restart this process - if err := c.writeSentinelFile(); err != nil { - return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to write sentinel file: %v", err) - } + for _, roleName := range roleNames { + // create roles based on the service keys as the role names + // in phase 2, we are using the same policy rule for all services + edgexACLTokenRole := NewRegistryRole(roleName, ClientType, []Policy{ + *edgexServicePolicy, + // localUse set to false as some EdgeX services may be running in a different node + }, false) - // set up agent token to agent for the first time - if err := c.setupAgentToken(bootstrapACLToken); err != nil { - return interfaces.StatusCodeExitWithError, fmt.Errorf("failed to set up agent token: %v", err) + // fail all if any one of the role creation failed + if err := c.createRole(secretstoreToken, edgexACLTokenRole); err != nil { + return fmt.Errorf("failed to create edgex role: %v", err) + } } - c.loggingClient.Info("setupRegistryACL successfully done") - - return -} - -// GetCommandName returns the name of this command -func (c *cmd) GetCommandName() string { - return CommandName + return nil } // setupAgentToken is to set up the agent token using the inputToken to the running agent if haven't set up yet diff --git a/internal/security/bootstrapper/command/setupacl/command_test.go b/internal/security/bootstrapper/command/setupacl/command_test.go index 6d42ff7816..9b10069d51 100644 --- a/internal/security/bootstrapper/command/setupacl/command_test.go +++ b/internal/security/bootstrapper/command/setupacl/command_test.go @@ -17,15 +17,11 @@ package setupacl import ( "context" - "encoding/json" - "fmt" "io/ioutil" "net/http" "net/http/httptest" - "net/url" "os" "path/filepath" - "strconv" "sync" "testing" "time" @@ -68,7 +64,7 @@ func TestNewCommand(t *testing.T) { } } -type prepareTestFunc func(aclOkResponse bool, configAccessOkResponse bool, t *testing.T) (*config.ConfigurationStruct, +type prepareTestFunc func(serverOptions serverOptions, t *testing.T) (*config.ConfigurationStruct, *httptest.Server) func TestExecute(t *testing.T) { @@ -89,7 +85,7 @@ func TestExecute(t *testing.T) { {"Bad:setupRegistryACL with bootstrap ACL API failed response from server", "test2", prepareTestRegistryServer, false, false, true}, {"Bad:setupRegistryACL with non-existing server", "test3", - func(_ bool, _ bool, _ *testing.T) (*config.ConfigurationStruct, *httptest.Server) { + func(_ serverOptions, _ *testing.T) (*config.ConfigurationStruct, *httptest.Server) { return &config.ConfigurationStruct{ StageGate: config.StageGateInfo{ Registry: config.RegistryInfo{ @@ -100,7 +96,7 @@ func TestExecute(t *testing.T) { }}, httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) }, false, false, true}, {"Bad:setupRegistryACL with empty api protocol", "test4", - func(_ bool, _ bool, _ *testing.T) (*config.ConfigurationStruct, *httptest.Server) { + func(_ serverOptions, _ *testing.T) (*config.ConfigurationStruct, *httptest.Server) { return &config.ConfigurationStruct{ StageGate: config.StageGateInfo{ Registry: config.RegistryInfo{ @@ -121,12 +117,28 @@ func TestExecute(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() // prepare test - conf, testServer := test.prepare(test.aclOkResponse, test.configAccessOkResponse, t) + testSrvOptions := serverOptions{ + aclBootstrapOkResponse: test.aclOkResponse, + configAccessOkResponse: test.configAccessOkResponse, + enablePersistence: true, + consulCheckAgentOk: true, + listTokensOk: true, + listTokensWithRetriesOk: true, + createTokenOk: true, + readTokenOk: true, + setAgentTokenOk: true, + readPolicyByNameOk: true, + policyAlreadyExists: true, + createNewPolicyOk: true, + createRoleOk: true, + } + conf, testServer := test.prepare(testSrvOptions, t) defer testServer.Close() // setup token related configs conf.StageGate.Registry.ACL.SecretsAdminTokenPath = filepath.Join(test.adminDir, "secret_token.json") conf.StageGate.Registry.ACL.BootstrapTokenPath = filepath.Join(test.adminDir, "bootstrap_token.json") conf.StageGate.Registry.ACL.SentinelFilePath = filepath.Join(test.adminDir, "sentinel_test_file") + conf.StageGate.Registry.ACL.ManagementTokenPath = filepath.Join(test.adminDir, "mgmt_token.json") setupRegistryACL, err := NewCommand(ctx, wg, lc, conf, []string{}) require.NoError(t, err) @@ -192,12 +204,28 @@ func TestMultipleExecuteCalls(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() // prepare test - conf, testServer := test.prepare(true, true, t) + testSrvOptions := serverOptions{ + aclBootstrapOkResponse: true, + configAccessOkResponse: true, + enablePersistence: true, + consulCheckAgentOk: true, + listTokensOk: true, + listTokensWithRetriesOk: true, + createTokenOk: true, + readTokenOk: true, + setAgentTokenOk: true, + readPolicyByNameOk: true, + policyAlreadyExists: true, + createNewPolicyOk: true, + createRoleOk: true, + } + conf, testServer := test.prepare(testSrvOptions, t) defer testServer.Close() // setup token related configs conf.StageGate.Registry.ACL.SecretsAdminTokenPath = filepath.Join(test.adminDir, "secret_token.json") conf.StageGate.Registry.ACL.BootstrapTokenPath = filepath.Join(test.adminDir, "bootstrap_token.json") conf.StageGate.Registry.ACL.SentinelFilePath = filepath.Join(test.adminDir, "sentinel_test_file") + conf.StageGate.Registry.ACL.ManagementTokenPath = filepath.Join(test.adminDir, "mgmt_token.json") setupRegistryACL, err := NewCommand(ctx, wg, lc, conf, []string{}) require.NoError(t, err) @@ -250,165 +278,17 @@ func TestMultipleExecuteCalls(t *testing.T) { } } -func prepareTestRegistryServer(aclOkResponse bool, configAccessOkResponse bool, t *testing.T) (*config.ConfigurationStruct, +func prepareTestRegistryServer(testSrvOptions serverOptions, t *testing.T) (*config.ConfigurationStruct, *httptest.Server) { - registryTestConf := &config.ConfigurationStruct{} - - testAgentTokenAccessorID := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - respCnt := 0 - testSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.EscapedPath() { - case consulGetLeaderAPI: - require.Equal(t, http.MethodGet, r.Method) - respCnt++ - w.WriteHeader(http.StatusOK) - var err error - if respCnt >= 2 { - _, err = w.Write([]byte("127.0.0.1:12345")) - } else { - _, err = w.Write([]byte("")) - } - require.NoError(t, err) - case consulACLBootstrapAPI: - require.Equal(t, http.MethodPut, r.Method) - if aclOkResponse { - w.WriteHeader(http.StatusOK) - jsonResponse := map[string]interface{}{ - "AccessorID": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", - "SecretID": "22222222-bbbb-3333-cccc-444444444444", - "Description": "Bootstrap Token (Global Management)", - "Policies": []map[string]interface{}{ - { - "ID": "00000000-0000-0000-0000-000000000001", - "Name": "global-management", - }, - }, - "Local": false, - "CreateTime": "2021-03-01T10:34:20.843397-07:00", - } - err := json.NewEncoder(w).Encode(jsonResponse) - require.NoError(t, err) - } else { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte("The ACL system is currently in legacy mode.")) - } - case consulConfigAccessVaultAPI: - require.Equal(t, http.MethodPost, r.Method) - if configAccessOkResponse { - w.WriteHeader(http.StatusNoContent) - } else { - w.WriteHeader(http.StatusForbidden) - } - case consulCheckAgentAPI: - require.Equal(t, http.MethodGet, r.Method) - w.WriteHeader(http.StatusOK) - jsonResponse := map[string]interface{}{ - "DebugConfig": map[string]interface{}{ - "ACLDatacenter": "dc1", - "ACLDefaultPolicy": "allow", - "ACLDisabledTTL": "2m0s", - "ACLTokens": map[string]interface{}{ - "EnablePersistence": true, - }, - "ACLsEnabled": true, - }, - } - err := json.NewEncoder(w).Encode(jsonResponse) - require.NoError(t, err) - case consulListTokensAPI: - require.Equal(t, http.MethodGet, r.Method) - w.WriteHeader(http.StatusOK) - tokens := []map[string]interface{}{ - { - "AccessorID": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", - "Description": "some other type of agent token", - }, - { - "AccessorID": "00000000-0000-0000-0000-000000000002", - "Description": "Anonymous Token", - }, - { - "AccessorID": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", - "Description": "Bootstrap Token (Global Management)", - }, - } - err := json.NewEncoder(w).Encode(tokens) - require.NoError(t, err) - case consulCreateTokenAPI: - require.Equal(t, http.MethodPut, r.Method) - w.WriteHeader(http.StatusOK) - jsonResponse := map[string]interface{}{ - "AccessorID": testAgentTokenAccessorID, - "Description": "edgex-core-consul agent token", - "Policies": []map[string]interface{}{ - { - "ID": "00000000-0000-0000-0000-000000000001", - "Name": "global-management", - }, - { - "ID": "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", - "Name": "node-read policy", - }, - }, - "Local": true, - "CreateTime": "2021-03-10T12:25:06.123456-07:00", - "Hash": "UuiRkOQPRCvoRZHRtUxxbrmwZ5crYrOdZ0Z1FTFbTbA=", - "CreateIndex": 59, - "ModifyIndex": 59, - } - - err := json.NewEncoder(w).Encode(jsonResponse) - require.NoError(t, err) - case fmt.Sprintf(consulTokenRUDAPI, testAgentTokenAccessorID): - if r.Method == http.MethodGet { - w.WriteHeader(http.StatusOK) - jsonResponse := map[string]interface{}{ - "AccessorID": testAgentTokenAccessorID, - "Description": "edgex-core-consul agent token", - "Policies": []map[string]interface{}{ - { - "ID": "00000000-0000-0000-0000-000000000001", - "Name": "global-management", - }, - { - "ID": "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", - "Name": "node-read policy", - }, - }, - "Local": false, - "CreateTime": "2021-03-10T12:25:06.123456-07:00", - "Hash": "UuiRkOQPRCvoRZHRtUxxbrmwZ5crYrOdZ0Z1FTFbTbA=", - "CreateIndex": 59, - "ModifyIndex": 59, - } - - err := json.NewEncoder(w).Encode(jsonResponse) - require.NoError(t, err) - } else { - w.WriteHeader(http.StatusInternalServerError) - t.Fatal(fmt.Sprintf("Unexpected method %s to URL %s", r.Method, r.URL.EscapedPath())) - } - case fmt.Sprintf(consulSetAgentTokenAPI, AgentType): - require.Equal(t, http.MethodPut, r.Method) - - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("agent token set successfully")) - default: - t.Fatal(fmt.Sprintf("Unexpected call to URL %s", r.URL.EscapedPath())) - } - })) - tsURL, err := url.Parse(testSrv.URL) - require.NoError(t, err) - portNum, _ := strconv.Atoi(tsURL.Port()) - registryTestConf.StageGate.Registry.ACL.Protocol = tsURL.Scheme - registryTestConf.StageGate.Registry.Host = tsURL.Hostname() - registryTestConf.StageGate.Registry.Port = portNum - registryTestConf.StageGate.WaitFor.Timeout = "1m" - registryTestConf.StageGate.WaitFor.RetryInterval = "1s" - // for the sake of simplicity, we use the same test server as the secret store server - registryTestConf.SecretStore.Protocol = tsURL.Scheme - registryTestConf.SecretStore.Host = tsURL.Hostname() - registryTestConf.SecretStore.Port = portNum - - return registryTestConf, testSrv + testSrv := newRegistryTestServer(testSrvOptions) + conf := testSrv.getRegistryServerConf(t) + testRoles := make(map[string]config.ACLRoleInfo) + testRoles["Role1"] = config.ACLRoleInfo{ + Description: "test for role 1", + } + testRoles["Role2"] = config.ACLRoleInfo{ + Description: "test for role 2", + } + conf.StageGate.Registry.ACL.Roles = testRoles + return conf, testSrv.server } diff --git a/internal/security/bootstrapper/command/setupacl/stubregistryserver_test.go b/internal/security/bootstrapper/command/setupacl/stubregistryserver_test.go new file mode 100644 index 0000000000..42cd8e743f --- /dev/null +++ b/internal/security/bootstrapper/command/setupacl/stubregistryserver_test.go @@ -0,0 +1,317 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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 setupacl + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "path" + "strconv" + "testing" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/stretchr/testify/require" +) + +type registryTestServer struct { + serverOptions serverOptions + server *httptest.Server +} + +type serverOptions struct { + aclBootstrapOkResponse bool + configAccessOkResponse bool + enablePersistence bool + consulCheckAgentOk bool + listTokensOk bool + listTokensWithRetriesOk bool + createTokenOk bool + readTokenOk bool + setAgentTokenOk bool + readPolicyByNameOk bool + policyAlreadyExists bool + createNewPolicyOk bool + createRoleOk bool +} + +func newRegistryTestServer(respOpts serverOptions) *registryTestServer { + return ®istryTestServer{ + serverOptions: respOpts, + } +} + +func (registry *registryTestServer) close() { + if registry.server != nil { + registry.server.Close() + } +} + +func (registry *registryTestServer) getRegistryServerConf(t *testing.T) *config.ConfigurationStruct { + registryTestConf := &config.ConfigurationStruct{} + testAgentTokenAccessorID := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + testEdgeXPolicyID := "eeeeeeeee-eeee-eeee-eeee-eeeeeeeee" + tokens := []map[string]interface{}{ + { + "AccessorID": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", + "Description": "some other type of agent token", + }, + { + "AccessorID": "00000000-0000-0000-0000-000000000002", + "Description": "Anonymous Token", + }, + { + "AccessorID": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "Description": "Bootstrap Token (Global Management)", + }, + } + respCnt := 0 + testSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + pathBase := path.Base(r.URL.Path) + switch r.URL.EscapedPath() { + case consulGetLeaderAPI: + require.Equal(t, http.MethodGet, r.Method) + respCnt++ + w.WriteHeader(http.StatusOK) + var err error + if respCnt >= 2 { + _, err = w.Write([]byte("127.0.0.1:12345")) + } else { + _, err = w.Write([]byte("")) + } + require.NoError(t, err) + case consulACLBootstrapAPI: + require.Equal(t, http.MethodPut, r.Method) + if registry.serverOptions.aclBootstrapOkResponse { + w.WriteHeader(http.StatusOK) + jsonResponse := map[string]interface{}{ + "AccessorID": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "SecretID": "22222222-bbbb-3333-cccc-444444444444", + "Description": "Bootstrap Token (Global Management)", + "Policies": []map[string]interface{}{ + { + "ID": "00000000-0000-0000-0000-000000000001", + "Name": "global-management", + }, + }, + "Local": false, + "CreateTime": "2021-03-01T10:34:20.843397-07:00", + } + err := json.NewEncoder(w).Encode(jsonResponse) + require.NoError(t, err) + } else { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("The ACL system is currently in legacy mode.")) + } + case consulConfigAccessVaultAPI: + require.Equal(t, http.MethodPost, r.Method) + if registry.serverOptions.configAccessOkResponse { + w.WriteHeader(http.StatusNoContent) + } else { + w.WriteHeader(http.StatusForbidden) + } + case fmt.Sprintf(createConsulRoleVaultAPI, pathBase): + require.Equal(t, http.MethodPost, r.Method) + if registry.serverOptions.createRoleOk { + w.WriteHeader(http.StatusNoContent) + } else { + w.WriteHeader(http.StatusForbidden) + } + case consulCheckAgentAPI: + require.Equal(t, http.MethodGet, r.Method) + if registry.serverOptions.consulCheckAgentOk { + w.WriteHeader(http.StatusOK) + jsonResponse := map[string]interface{}{ + "DebugConfig": map[string]interface{}{ + "ACLDatacenter": "dc1", + "ACLDefaultPolicy": "allow", + "ACLDisabledTTL": "2m0s", + "ACLTokens": map[string]interface{}{ + "EnablePersistence": registry.serverOptions.enablePersistence, + }, + "ACLsEnabled": true, + }, + } + err := json.NewEncoder(w).Encode(jsonResponse) + require.NoError(t, err) + } else { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte("permission denied")) + } + case consulListTokensAPI: + require.Equal(t, http.MethodGet, r.Method) + if registry.serverOptions.listTokensOk { + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(tokens) + require.NoError(t, err) + } else if registry.serverOptions.listTokensWithRetriesOk { + if respCnt >= 2 { + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(tokens) + require.NoError(t, err) + } else { + respCnt++ + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(consulLegacyACLModeError)) + } + } else if !registry.serverOptions.listTokensWithRetriesOk { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(consulLegacyACLModeError)) + } else { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte("permission denied")) + } + case consulCreateTokenAPI: + require.Equal(t, http.MethodPut, r.Method) + if registry.serverOptions.createTokenOk { + w.WriteHeader(http.StatusOK) + jsonResponse := map[string]interface{}{ + "AccessorID": testAgentTokenAccessorID, + "Description": "edgex-core-consul agent token", + "Policies": []map[string]interface{}{ + { + "ID": "00000000-0000-0000-0000-000000000001", + "Name": "global-management", + }, + { + "ID": "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", + "Name": "node-read policy", + }, + }, + "Local": true, + "CreateTime": "2021-03-10T12:25:06.123456-07:00", + "Hash": "UuiRkOQPRCvoRZHRtUxxbrmwZ5crYrOdZ0Z1FTFbTbA=", + "CreateIndex": 59, + "ModifyIndex": 59, + } + + err := json.NewEncoder(w).Encode(jsonResponse) + require.NoError(t, err) + tokens = append(tokens, jsonResponse) + } else { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("cannot create token")) + } + case fmt.Sprintf(consulTokenRUDAPI, testAgentTokenAccessorID): + if r.Method == http.MethodGet && registry.serverOptions.readTokenOk { + w.WriteHeader(http.StatusOK) + jsonResponse := map[string]interface{}{ + "AccessorID": testAgentTokenAccessorID, + "Description": "edgex-core-consul agent token", + "Policies": []map[string]interface{}{ + { + "ID": "00000000-0000-0000-0000-000000000001", + "Name": "global-management", + }, + { + "ID": "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", + "Name": "node-read policy", + }, + }, + "Local": false, + "CreateTime": "2021-03-10T12:25:06.123456-07:00", + "Hash": "UuiRkOQPRCvoRZHRtUxxbrmwZ5crYrOdZ0Z1FTFbTbA=", + "CreateIndex": 59, + "ModifyIndex": 59, + } + + err := json.NewEncoder(w).Encode(jsonResponse) + require.NoError(t, err) + } else if r.Method == http.MethodGet { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte("permission denied")) + } else { + w.WriteHeader(http.StatusInternalServerError) + t.Fatal(fmt.Sprintf("Unexpected method %s to URL %s", r.Method, r.URL.EscapedPath())) + } + case fmt.Sprintf(consulSetAgentTokenAPI, AgentType): + require.Equal(t, http.MethodPut, r.Method) + if registry.serverOptions.setAgentTokenOk { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("agent token set successfully")) + } else { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte("permission denied")) + } + case fmt.Sprintf(consulReadPolicyByNameAPI, ""): + require.Equal(t, http.MethodGet, r.Method) + // the policy name is empty + w.WriteHeader(http.StatusBadRequest) + case fmt.Sprintf(consulReadPolicyByNameAPI, pathBase): + require.Equal(t, http.MethodGet, r.Method) + if registry.serverOptions.readPolicyByNameOk && registry.serverOptions.policyAlreadyExists { + w.WriteHeader(http.StatusOK) + jsonResponse := map[string]interface{}{ + "ID": testEdgeXPolicyID, + "Name": pathBase, + "Description": "test edgex policy", + "Rules": edgeXPolicyRules, + } + + err := json.NewEncoder(w).Encode(jsonResponse) + require.NoError(t, err) + } else if registry.serverOptions.readPolicyByNameOk { + // no existing policy + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(aclNotFoundMessage)) + } else { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte("permission denied")) + } + case consulCreatePolicyAPI: + require.Equal(t, http.MethodPut, r.Method) + if registry.serverOptions.createNewPolicyOk { + w.WriteHeader(http.StatusOK) + reqBody, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + var policyMap map[string]interface{} + err = json.Unmarshal(reqBody, &policyMap) + require.NoError(t, err) + jsonResponse := map[string]interface{}{ + "ID": testEdgeXPolicyID, + "Name": policyMap["Name"], + "Description": "test edgex policy", + "Rules": policyMap["Rules"], + } + + err = json.NewEncoder(w).Encode(jsonResponse) + require.NoError(t, err) + } else { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Invalid Policy: A Policy with Name " + edgeXServicePolicyName + " already exists")) + } + default: + t.Fatal(fmt.Sprintf("Unexpected call to URL %s", r.URL.EscapedPath())) + } + })) + tsURL, err := url.Parse(testSrv.URL) + require.NoError(t, err) + portNum, _ := strconv.Atoi(tsURL.Port()) + registryTestConf.StageGate.Registry.ACL.Protocol = tsURL.Scheme + registryTestConf.StageGate.Registry.Host = tsURL.Hostname() + registryTestConf.StageGate.Registry.Port = portNum + registryTestConf.StageGate.WaitFor.Timeout = "1m" + registryTestConf.StageGate.WaitFor.RetryInterval = "1s" + // for the sake of simplicity, we use the same test server as the secret store server + registryTestConf.SecretStore.Protocol = tsURL.Scheme + registryTestConf.SecretStore.Host = tsURL.Hostname() + registryTestConf.SecretStore.Port = portNum + registry.server = testSrv + return registryTestConf +} diff --git a/internal/security/bootstrapper/config/config.go b/internal/security/bootstrapper/config/config.go index 3f6b657e35..1be6ac952f 100644 --- a/internal/security/bootstrapper/config/config.go +++ b/internal/security/bootstrapper/config/config.go @@ -16,6 +16,8 @@ package config import ( + "strings" + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/v2/config" ) @@ -71,3 +73,13 @@ func (c *ConfigurationStruct) GetDatabaseInfo() map[string]bootstrapConfig.Datab func (c *ConfigurationStruct) GetInsecureSecrets() bootstrapConfig.InsecureSecrets { return nil } + +// GetRoleNames gets the slice of the keys (i.e. the service keys) from map Roles as ACL role names +func (acl ACLInfo) GetACLRoleNames() []string { + roleNames := make([]string, 0, len(acl.Roles)) + for serviceKey := range acl.Roles { + // always converts to lower cases by design + roleNames = append(roleNames, strings.ToLower(serviceKey)) + } + return roleNames +} diff --git a/internal/security/bootstrapper/config/types.go b/internal/security/bootstrapper/config/types.go index 694b7170af..42a563727c 100644 --- a/internal/security/bootstrapper/config/types.go +++ b/internal/security/bootstrapper/config/types.go @@ -69,6 +69,16 @@ type ACLInfo struct { SecretsAdminTokenPath string // filepath for the sentinel file to indicate the registry ACL is set up successfully SentinelFilePath string + // filepath to save the registry's token created for management purposes + ManagementTokenPath string + // the roles for registry role-based access control list + Roles map[string]ACLRoleInfo +} + +// ACLRoleInfo defines the fields related to Registry's ACL roles +type ACLRoleInfo struct { + // the details about the role + Description string } // KongDBInfo defines the fields related to diff --git a/internal/security/fileprovider/defaults.go b/internal/security/fileprovider/defaults.go index 0d2d045bc8..b1a1e7180c 100644 --- a/internal/security/fileprovider/defaults.go +++ b/internal/security/fileprovider/defaults.go @@ -17,10 +17,18 @@ package fileprovider func makeDefaultTokenPolicy(serviceName string) map[string]interface{} { + // protected path for secret/ protectedPath := "secret/edgex/" + serviceName + "/*" capabilities := []string{"create", "update", "delete", "list", "read"} acl := map[string]interface{}{"capabilities": capabilities} - pathObject := map[string]interface{}{protectedPath: acl} + // path for consul tokens + registryCredsPath := "consul/creds/" + serviceName + registryCredsCapabilities := []string{"read"} + registryCredsACL := map[string]interface{}{"capabilities": registryCredsCapabilities} + pathObject := map[string]interface{}{ + protectedPath: acl, + registryCredsPath: registryCredsACL, + } retval := map[string]interface{}{"path": pathObject} return retval @@ -29,9 +37,12 @@ func makeDefaultTokenPolicy(serviceName string) map[string]interface{} { "path": { "secret/edgex/service-name/*": { "capabilities": [ "create", "update", "delete", "list", "read" ] + }, + "consul/creds/service-name": { + "capabilities": [ "read" ] } } - } + } */ } diff --git a/internal/security/fileprovider/defaults_test.go b/internal/security/fileprovider/defaults_test.go index 6ed5c579df..277d3987f1 100644 --- a/internal/security/fileprovider/defaults_test.go +++ b/internal/security/fileprovider/defaults_test.go @@ -19,7 +19,7 @@ import ( "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDefaultTokenPolicy(t *testing.T) { @@ -28,11 +28,21 @@ func TestDefaultTokenPolicy(t *testing.T) { // Assert bytes, err := json.Marshal(policies) - assert.NoError(t, err) - - expected := `{"path":{"secret/edgex/service-name/*":{"capabilities":["create","update","delete","list","read"]}}}` - actual := string(bytes) - assert.Equal(t, expected, actual) + require.NoError(t, err) + require.NotEmpty(t, bytes) + + expected := map[string]interface{}{ + "path": map[string]interface{}{ + "secret/edgex/service-name/*": map[string]interface{}{ + "capabilities": []string{"create", "update", "delete", "list", "read"}, + }, + "consul/creds/service-name": map[string]interface{}{ + "capabilities": []string{"read"}, + }, + }, + } + + require.Equal(t, expected, policies) } func TestDefaultTokenParameters(t *testing.T) { @@ -41,9 +51,9 @@ func TestDefaultTokenParameters(t *testing.T) { // Assert bytes, err := json.Marshal(parameters) - assert.NoError(t, err) + require.NoError(t, err) expected := `{"display_name":"service-name","no_parent":true,"period":"1h","policies":["edgex-service-service-name"],"ttl":"1h"}` actual := string(bytes) - assert.Equal(t, expected, actual) + require.Equal(t, expected, actual) } diff --git a/internal/security/fileprovider/provider.go b/internal/security/fileprovider/provider.go index 8fb54a5e46..d2ca551d6c 100644 --- a/internal/security/fileprovider/provider.go +++ b/internal/security/fileprovider/provider.go @@ -105,6 +105,10 @@ func (p *fileTokenProvider) Run() error { if serviceConfig.UseDefaults { p.logger.Info(fmt.Sprintf("using policy/token defaults for service %s", serviceName)) servicePolicy = makeDefaultTokenPolicy(serviceName) + defaultPolicyPaths := servicePolicy["path"].(map[string]interface{}) + for pathKey, policy := range defaultPolicyPaths { + servicePolicy["path"].(map[string]interface{})[pathKey] = policy + } createTokenParameters = makeDefaultTokenParameters(serviceName) } @@ -195,15 +199,8 @@ func (p *fileTokenProvider) Run() error { } } - encoder := json.NewEncoder(writeCloser) - if encoder == nil { - _ = writeCloser.Close() - err = fmt.Errorf("unable to create JSON output encoder") - return err - } - // Write resulting token - if err := encoder.Encode(createTokenResponse); err != nil { + if err := json.NewEncoder(writeCloser).Encode(createTokenResponse); err != nil { _ = writeCloser.Close() p.logger.Error(fmt.Sprintf("failed to write token file: %s", err.Error())) return err diff --git a/internal/security/fileprovider/provider_test.go b/internal/security/fileprovider/provider_test.go index 8c5c9aeff6..84322c1ea6 100644 --- a/internal/security/fileprovider/provider_test.go +++ b/internal/security/fileprovider/provider_test.go @@ -430,11 +430,22 @@ func runTokensWithDefault(serviceName string, additionalKeysEnv string, t *testi mockAuthTokenLoader := &loaderMock.AuthTokenLoader{} mockAuthTokenLoader.On("Load", privilegedTokenPath).Return("fake-priv-token", nil) - expectedService1Policy := `{"path":{"secret/edgex/` + serviceName + `/*":{"capabilities":["create","update","delete","list","read"]}}}` + policy := map[string]interface{}{ + "path": map[string]interface{}{ + "secret/edgex/" + serviceName + "/*": map[string]interface{}{ + "capabilities": []string{"create", "update", "delete", "list", "read"}, + }, + "consul/creds/" + serviceName: map[string]interface{}{ + "capabilities": []string{"read"}, + }, + }, + } + expectedService1Policy, err := json.Marshal(&policy) + require.NoError(t, err) expectedService1Parameters := makeDefaultTokenParameters(serviceName) expectedService1Parameters["meta"] = makeMetaServiceName(serviceName)["meta"] mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+serviceName, expectedService1Policy). + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+serviceName, string(expectedService1Policy)). Return(nil) mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters, mock.Anything). Return(createTokenResponse(), nil) @@ -442,12 +453,24 @@ func runTokensWithDefault(serviceName string, additionalKeysEnv string, t *testi // setup expected things for additional services from env if any for service := range expectedTokenConfigs { - expectedServicePolicy := `{"path":{"secret/edgex/` + service + `/*":{"capabilities":["create","update","delete","list","read"]}}}` + policy := map[string]interface{}{ + "path": map[string]interface{}{ + "secret/edgex/" + service + "/*": map[string]interface{}{ + "capabilities": []string{"create", "update", "delete", "list", "read"}, + }, + "consul/creds/" + service: map[string]interface{}{ + "capabilities": []string{"read"}, + }, + }, + } + expectedServicePolicy, err := json.Marshal(&policy) + require.NoError(t, err) + expectedServiceParameters := makeDefaultTokenParameters(service) expectedServiceParameters["meta"] = makeMetaServiceName(service)["meta"] - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+service, expectedServicePolicy). + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+service, string(expectedServicePolicy)). Return(nil) mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedServiceParameters, mock.Anything). Return(createTokenResponse(), nil) @@ -462,7 +485,7 @@ func runTokensWithDefault(serviceName string, additionalKeysEnv string, t *testi }) // Act - err := p.Run() + err = p.Run() if errFromEnv != nil { return errFromEnv