Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(security): Implementation for adding Consul ACL policies, roles #3273

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
`
lenny-goodell marked this conversation as resolved.
Show resolved Hide resolved

// 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
}
jim-wang-intel marked this conversation as resolved.
Show resolved Hide resolved

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 {
jim-wang-intel marked this conversation as resolved.
Show resolved Hide resolved
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 {
jim-wang-intel marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("failed to decode Policy json data: %v", err)
}

return &existing, nil
case http.StatusForbidden:
jim-wang-intel marked this conversation as resolved.
Show resolved Hide resolved
// 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)
}
})
}
}
jim-wang-intel marked this conversation as resolved.
Show resolved Hide resolved
Loading