Skip to content

Commit

Permalink
feat(security): Implementation for adding ACL policies and roles (#3273)
Browse files Browse the repository at this point in the history
New addition for implementing for Consul's ACL policies creation and roles for Consul tokens generated later on via go-mod-secret
 - Add logic to check whether the ACL policy is already per-existing before creation of new policy
 - Add implementation to create a new ACL policy
 - Add implementation to create a role for EdgeX's services via Vault's /consul/roles/* APIs: this sets the stage for creating role-based Consul tokens used by EdgeX services
 - Add logic for creating token roles based on EdgeX service keys from configuration file
 - Update token-file-provider on edgex's default policy to add the permission for calling /consul/creds/"service-key" endpoint

Closes: #3254, #3160

Signed-off-by: Jim Wang <[email protected]>
  • Loading branch information
jim-wang-intel authored Mar 22, 2021
1 parent eeaee6b commit 8b8c045
Show file tree
Hide file tree
Showing 16 changed files with 1,156 additions and 471 deletions.
19 changes: 18 additions & 1 deletion cmd/security-bootstrapper/res/configuration.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
197 changes: 197 additions & 0 deletions internal/security/bootstrapper/command/setupacl/aclpolicies.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
Loading

0 comments on commit 8b8c045

Please sign in to comment.