Skip to content

Commit

Permalink
feat(security): Implementation for adding ACL policies and roles
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
 - Add logic to check whehter the ACL policy is already pre-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
 - Add implementation to create registry management token and store it into a file
 - Update token-file-provider on edgex's default policy to add the permission for calling /consul/creds/"service-key" endpoint

Closes: edgexfoundry#3158, edgexfoundry#3254, edgexfoundry#3160

Signed-off-by: Jim Wang <[email protected]>
  • Loading branch information
jim-wang-intel committed Mar 18, 2021
1 parent 4dfaf16 commit 81908f6
Show file tree
Hide file tree
Showing 16 changed files with 1,208 additions and 445 deletions.
23 changes: 22 additions & 1 deletion cmd/security-bootstrapper/res/configuration.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,32 @@ 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.Registry.ACL.Roles.edgex-application-service]
Description = "role for application-service"
[StageGate.Registry.ACL.Roles.edgex-security-proxy-setup]
Description = "role for security-proxy-setup"

[StageGate.KongDb]
Host = "kong-db"
Port = 5432
Expand Down
206 changes: 206 additions & 0 deletions internal/security/bootstrapper/command/setupacl/aclpolicies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*******************************************************************************
* 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"
"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"
)

// createRegistryPolicy 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) createRegistryPolicy(tokenID, policyName, policyRules string) (*Policy, error) {
if len(tokenID) == 0 {
return nil, errors.New("bootstrap ACL token is required for creating policy")
}

if len(policyName) == 0 {
return nil, errors.New("policy name is required for creating policy")
}

// 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 CreatPolicy 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 creat 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 TestCreatePolicy(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: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.createRegistryPolicy(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 81908f6

Please sign in to comment.