forked from edgexfoundry/edgex-go
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(security): Implementation for adding ACL policies and roles
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 - 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
1 parent
4dfaf16
commit 1563183
Showing
16 changed files
with
1,208 additions
and
445 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
206 changes: 206 additions & 0 deletions
206
internal/security/bootstrapper/command/setupacl/aclpolicies.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
85 changes: 85 additions & 0 deletions
85
internal/security/bootstrapper/command/setupacl/aclpolicies_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.