mirror of https://github.com/status-im/consul.git
[NET-5325] ACL templated policies support in tokens and roles (#18708)
* [NET-5325] ACL templated policies support in tokens and roles - Add API support for creating tokens/roles with templated-policies - Add CLI support for creating tokens/roles with templated-policies * adding changelog
This commit is contained in:
parent
993fe9a6a6
commit
bbef879f85
|
@ -0,0 +1,7 @@
|
|||
```release-note:feature
|
||||
acl: Added ACL Templated policies to simplify getting the right ACL token.
|
||||
```
|
||||
|
||||
```release-note:improvement
|
||||
cli: Added `-templated-policy`, `-templated-policy-file`, `-replace-templated-policy`, `-append-templated-policy`, `-replace-templated-policy-file`, `-append-templated-policy-file` and `-var` flags for creating or updating tokens/roles.
|
||||
```
|
|
@ -102,6 +102,10 @@ func (id *missingIdentity) NodeIdentityList() []*structs.ACLNodeIdentity {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (id *missingIdentity) TemplatedPolicyList() []*structs.ACLTemplatedPolicy {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (id *missingIdentity) IsExpired(asOf time.Time) bool {
|
||||
return false
|
||||
}
|
||||
|
@ -596,9 +600,11 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
|
|||
roleIDs = identity.RoleIDs()
|
||||
serviceIdentities = structs.ACLServiceIdentities(identity.ServiceIdentityList())
|
||||
nodeIdentities = structs.ACLNodeIdentities(identity.NodeIdentityList())
|
||||
templatedPolicies = structs.ACLTemplatedPolicies(identity.TemplatedPolicyList())
|
||||
)
|
||||
|
||||
if len(policyIDs) == 0 && len(serviceIdentities) == 0 && len(roleIDs) == 0 && len(nodeIdentities) == 0 {
|
||||
if len(policyIDs) == 0 && len(serviceIdentities) == 0 &&
|
||||
len(roleIDs) == 0 && len(nodeIdentities) == 0 && len(templatedPolicies) == 0 {
|
||||
// In this case the default policy will be all that is in effect.
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -616,16 +622,19 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
|
|||
}
|
||||
serviceIdentities = append(serviceIdentities, role.ServiceIdentities...)
|
||||
nodeIdentities = append(nodeIdentities, role.NodeIdentityList()...)
|
||||
templatedPolicies = append(templatedPolicies, role.TemplatedPolicyList()...)
|
||||
}
|
||||
|
||||
// Now deduplicate any policies or service identities that occur more than once.
|
||||
policyIDs = dedupeStringSlice(policyIDs)
|
||||
serviceIdentities = serviceIdentities.Deduplicate()
|
||||
nodeIdentities = nodeIdentities.Deduplicate()
|
||||
templatedPolicies = templatedPolicies.Deduplicate()
|
||||
|
||||
// Generate synthetic policies for all service identities in effect.
|
||||
syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities, identity.EnterpriseMetadata())
|
||||
syntheticPolicies = append(syntheticPolicies, r.synthesizePoliciesForNodeIdentities(nodeIdentities, identity.EnterpriseMetadata())...)
|
||||
syntheticPolicies = append(syntheticPolicies, r.synthesizePoliciesForTemplatedPolicies(templatedPolicies, identity.EnterpriseMetadata())...)
|
||||
|
||||
// For the new ACLs policy replication is mandatory for correct operation on servers. Therefore
|
||||
// we only attempt to resolve policies locally
|
||||
|
@ -669,6 +678,24 @@ func (r *ACLResolver) synthesizePoliciesForNodeIdentities(nodeIdentities []*stru
|
|||
return syntheticPolicies
|
||||
}
|
||||
|
||||
func (r *ACLResolver) synthesizePoliciesForTemplatedPolicies(templatedPolicies []*structs.ACLTemplatedPolicy, entMeta *acl.EnterpriseMeta) []*structs.ACLPolicy {
|
||||
if len(templatedPolicies) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
syntheticPolicies := make([]*structs.ACLPolicy, 0, len(templatedPolicies))
|
||||
for _, tp := range templatedPolicies {
|
||||
policy, err := tp.SyntheticPolicy(entMeta)
|
||||
if err != nil {
|
||||
r.logger.Warn(fmt.Sprintf("could not generate synthetic policy for templated policy: %q", tp.TemplateName), "error", err)
|
||||
continue
|
||||
}
|
||||
syntheticPolicies = append(syntheticPolicies, policy)
|
||||
}
|
||||
|
||||
return syntheticPolicies
|
||||
}
|
||||
|
||||
func mergeStringSlice(a, b []string) []string {
|
||||
out := make([]string, 0, len(a)+len(b))
|
||||
out = append(out, a...)
|
||||
|
|
|
@ -350,9 +350,10 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
|
|||
policyIDs := make(map[string]struct{})
|
||||
roleIDs := make(map[string]struct{})
|
||||
identityPolicies := make(map[string]*structs.ACLPolicy)
|
||||
templatedPolicies := make(map[string]*structs.ACLPolicy)
|
||||
tokenInfo := structs.ExpandedTokenInfo{}
|
||||
|
||||
// Add the token's policies and node/service identity policies
|
||||
// Add the token's policies, templated policies and node/service identity policies
|
||||
for _, policy := range token.Policies {
|
||||
policyIDs[policy.ID] = struct{}{}
|
||||
}
|
||||
|
@ -368,6 +369,14 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
|
|||
policy := identity.SyntheticPolicy(&token.EnterpriseMeta)
|
||||
identityPolicies[policy.ID] = policy
|
||||
}
|
||||
for _, templatedPolicy := range token.TemplatedPolicies {
|
||||
policy, err := templatedPolicy.SyntheticPolicy(&token.EnterpriseMeta)
|
||||
if err != nil {
|
||||
a.logger.Warn(fmt.Sprintf("could not generate synthetic policy for templated policy: %q", templatedPolicy.TemplateName), "error", err)
|
||||
continue
|
||||
}
|
||||
templatedPolicies[policy.ID] = policy
|
||||
}
|
||||
|
||||
// Get any namespace default roles/policies to look up
|
||||
nsPolicies, nsRoles, err := getTokenNamespaceDefaults(ws, state, &token.EnterpriseMeta)
|
||||
|
@ -405,6 +414,14 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
|
|||
policy := identity.SyntheticPolicy(&role.EnterpriseMeta)
|
||||
identityPolicies[policy.ID] = policy
|
||||
}
|
||||
for _, templatedPolicy := range role.TemplatedPolicies {
|
||||
policy, err := templatedPolicy.SyntheticPolicy(&role.EnterpriseMeta)
|
||||
if err != nil {
|
||||
a.logger.Warn(fmt.Sprintf("could not generate synthetic policy for templated policy: %q", templatedPolicy.TemplateName), "error", err)
|
||||
continue
|
||||
}
|
||||
templatedPolicies[policy.ID] = policy
|
||||
}
|
||||
|
||||
tokenInfo.ExpandedRoles = append(tokenInfo.ExpandedRoles, role)
|
||||
}
|
||||
|
@ -423,6 +440,9 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
|
|||
for _, policy := range identityPolicies {
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
for _, policy := range templatedPolicies {
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
|
||||
tokenInfo.ExpandedPolicies = policies
|
||||
tokenInfo.AgentACLDefaultPolicy = a.srv.config.ACLResolverSettings.ACLDefaultPolicy
|
||||
|
@ -486,6 +506,7 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
|
|||
Roles: token.Roles,
|
||||
ServiceIdentities: token.ServiceIdentities,
|
||||
NodeIdentities: token.NodeIdentities,
|
||||
TemplatedPolicies: token.TemplatedPolicies,
|
||||
Local: token.Local,
|
||||
Description: token.Description,
|
||||
ExpirationTime: token.ExpirationTime,
|
||||
|
@ -1364,6 +1385,27 @@ func (a *ACL) RoleSet(args *structs.ACLRoleSetRequest, reply *structs.ACLRole) e
|
|||
}
|
||||
role.NodeIdentities = role.NodeIdentities.Deduplicate()
|
||||
|
||||
for _, templatedPolicy := range role.TemplatedPolicies {
|
||||
if templatedPolicy.TemplateName == "" {
|
||||
return fmt.Errorf("templated policy is missing the template name field on this role")
|
||||
}
|
||||
|
||||
baseTemplate, ok := structs.GetACLTemplatedPolicyBase(templatedPolicy.TemplateName)
|
||||
if !ok {
|
||||
return fmt.Errorf("templated policy with an invalid templated name: %s for this role", templatedPolicy.TemplateName)
|
||||
}
|
||||
|
||||
if templatedPolicy.TemplateID == "" {
|
||||
templatedPolicy.TemplateID = baseTemplate.TemplateID
|
||||
}
|
||||
|
||||
err := templatedPolicy.ValidateTemplatedPolicy(baseTemplate.Schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encountered role with invalid templated policy: %w", err)
|
||||
}
|
||||
}
|
||||
role.TemplatedPolicies = role.TemplatedPolicies.Deduplicate()
|
||||
|
||||
// calculate the hash for this role
|
||||
role.SetHash(true)
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
|
@ -376,7 +377,7 @@ func TestACLEndpoint_TokenRead(t *testing.T) {
|
|||
require.ElementsMatch(t, []*structs.ACLRole{r1, r2}, resp.ExpandedRoles)
|
||||
})
|
||||
|
||||
t.Run("expanded output with node/service identities", func(t *testing.T) {
|
||||
t.Run("expanded output with node/service identities and templated policies", func(t *testing.T) {
|
||||
setReq := structs.ACLTokenSetRequest{
|
||||
Datacenter: "dc1",
|
||||
ACLToken: structs.ACLToken{
|
||||
|
@ -401,6 +402,22 @@ func TestACLEndpoint_TokenRead(t *testing.T) {
|
|||
Datacenter: "dc1",
|
||||
},
|
||||
},
|
||||
TemplatedPolicies: []*structs.ACLTemplatedPolicy{
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
|
||||
Name: "web",
|
||||
},
|
||||
Datacenters: []string{"dc1"},
|
||||
},
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
|
||||
Name: "foo",
|
||||
},
|
||||
Datacenters: []string{"dc1"},
|
||||
},
|
||||
},
|
||||
Local: false,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
|
||||
|
@ -414,6 +431,11 @@ func TestACLEndpoint_TokenRead(t *testing.T) {
|
|||
for _, serviceIdentity := range setReq.ACLToken.NodeIdentities {
|
||||
expectedPolicies = append(expectedPolicies, serviceIdentity.SyntheticPolicy(entMeta))
|
||||
}
|
||||
for _, templatedPolicy := range setReq.ACLToken.TemplatedPolicies {
|
||||
pol, tmplError := templatedPolicy.SyntheticPolicy(entMeta)
|
||||
require.NoError(t, tmplError)
|
||||
expectedPolicies = append(expectedPolicies, pol)
|
||||
}
|
||||
|
||||
setResp := structs.ACLToken{}
|
||||
err := msgpackrpc.CallWithCodec(codec, "ACL.TokenSet", &setReq, &setResp)
|
||||
|
@ -468,6 +490,10 @@ func TestACLEndpoint_TokenClone(t *testing.T) {
|
|||
t.NodeIdentities = []*structs.ACLNodeIdentity{
|
||||
{NodeName: "foo", Datacenter: "bar"},
|
||||
}
|
||||
t.TemplatedPolicies = []*structs.ACLTemplatedPolicy{
|
||||
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "foo"}, Datacenters: []string{"bar"}},
|
||||
{TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "node"}},
|
||||
}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -490,6 +516,7 @@ func TestACLEndpoint_TokenClone(t *testing.T) {
|
|||
require.Equal(t, t1.Roles, t2.Roles)
|
||||
require.Equal(t, t1.ServiceIdentities, t2.ServiceIdentities)
|
||||
require.Equal(t, t1.NodeIdentities, t2.NodeIdentities)
|
||||
require.Equal(t, t1.TemplatedPolicies, t2.TemplatedPolicies)
|
||||
require.Equal(t, t1.Local, t2.Local)
|
||||
require.NotEqual(t, t1.AccessorID, t2.AccessorID)
|
||||
require.NotEqual(t, t1.SecretID, t2.SecretID)
|
||||
|
@ -548,6 +575,10 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
|||
Datacenter: "dc1",
|
||||
},
|
||||
},
|
||||
TemplatedPolicies: []*structs.ACLTemplatedPolicy{
|
||||
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "foo"}, Datacenters: []string{"bar"}},
|
||||
{TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "node"}},
|
||||
},
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
|
||||
}
|
||||
|
@ -570,6 +601,19 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
|||
require.Equal(t, "foo", token.NodeIdentities[0].NodeName)
|
||||
require.Equal(t, "dc1", token.NodeIdentities[0].Datacenter)
|
||||
|
||||
require.Len(t, token.TemplatedPolicies, 2)
|
||||
require.Contains(t, token.TemplatedPolicies, &structs.ACLTemplatedPolicy{
|
||||
TemplateID: structs.ACLTemplatedPolicyServiceID,
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "foo"},
|
||||
Datacenters: []string{"bar"},
|
||||
})
|
||||
require.Contains(t, token.TemplatedPolicies, &structs.ACLTemplatedPolicy{
|
||||
TemplateID: structs.ACLTemplatedPolicyNodeID,
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "node"},
|
||||
})
|
||||
|
||||
accessorID = token.AccessorID
|
||||
})
|
||||
|
||||
|
@ -2183,6 +2227,39 @@ func TestACLEndpoint_PolicySet_CustomID(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestACLEndpoint_TemplatedPolicySet_UnknownTemplateName(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
_, srv, _ := testACLServerWithConfig(t, nil, false)
|
||||
waitForLeaderEstablishment(t, srv)
|
||||
|
||||
aclEp := ACL{srv: srv}
|
||||
|
||||
t.Run("unknown template name", func(t *testing.T) {
|
||||
req := structs.ACLTokenSetRequest{
|
||||
Datacenter: "dc1",
|
||||
ACLToken: structs.ACLToken{
|
||||
Description: "foobar",
|
||||
Policies: nil,
|
||||
Local: false,
|
||||
TemplatedPolicies: []*structs.ACLTemplatedPolicy{{TemplateName: "fake-builtin"}},
|
||||
},
|
||||
Create: true,
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
|
||||
}
|
||||
|
||||
resp := structs.ACLToken{}
|
||||
|
||||
err := aclEp.TokenSet(&req, &resp)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "no such ACL templated policy with Name \"fake-builtin\"")
|
||||
})
|
||||
}
|
||||
|
||||
func TestACLEndpoint_PolicySet_builtins(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/hashicorp/consul/acl/resolver"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/token"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
)
|
||||
|
@ -1978,6 +1979,48 @@ func testACLResolver_variousTokens(t *testing.T, delegate *ACLResolverTestDelega
|
|||
},
|
||||
},
|
||||
},
|
||||
&structs.ACLToken{
|
||||
AccessorID: "359b9927-25fd-46b9-84c2-3470f848ec65",
|
||||
SecretID: "found-synthetic-policy-5",
|
||||
TemplatedPolicies: []*structs.ACLTemplatedPolicy{
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
|
||||
Name: "templated-test-node1",
|
||||
},
|
||||
Datacenters: []string{"dc1"},
|
||||
},
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
|
||||
Name: "templated-test-node2",
|
||||
},
|
||||
// as the resolver is in dc1 this identity should be ignored
|
||||
Datacenters: []string{"dc2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
&structs.ACLToken{
|
||||
AccessorID: "359b9927-25fd-46b9-84c2-3470f848ec65",
|
||||
SecretID: "found-synthetic-policy-6",
|
||||
TemplatedPolicies: []*structs.ACLTemplatedPolicy{
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
|
||||
Name: "templated-test-node3",
|
||||
},
|
||||
Datacenters: []string{"dc1"},
|
||||
},
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
|
||||
Name: "templated-test-node4",
|
||||
},
|
||||
// as the resolver is in dc1 this identity should be ignored
|
||||
Datacenters: []string{"dc2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// We resolve these tokens in the same cache session
|
||||
|
@ -2043,6 +2086,22 @@ func testACLResolver_variousTokens(t *testing.T, delegate *ACLResolverTestDelega
|
|||
// ensure node identity for other DC is ignored
|
||||
require.Equal(t, acl.Deny, authz.NodeWrite("test-node-dc2", nil))
|
||||
})
|
||||
t.Run("synthetic-policy-6", func(t *testing.T) { // templated policy
|
||||
authz, err := r.ResolveToken("found-synthetic-policy-6")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, authz)
|
||||
|
||||
// spot check some random perms
|
||||
require.Equal(t, acl.Deny, authz.ACLRead(nil))
|
||||
require.Equal(t, acl.Deny, authz.NodeWrite("foo", nil))
|
||||
// ensure we didn't bleed over to the other synthetic policy
|
||||
require.Equal(t, acl.Deny, authz.NodeWrite("templated-test-node1", nil))
|
||||
// check our own synthetic policy
|
||||
require.Equal(t, acl.Allow, authz.ServiceRead("literally-anything", nil))
|
||||
require.Equal(t, acl.Allow, authz.NodeWrite("templated-test-node3", nil))
|
||||
// ensure template identity for other DC is ignored
|
||||
require.Equal(t, acl.Deny, authz.NodeWrite("templated-test-node4", nil))
|
||||
})
|
||||
})
|
||||
|
||||
runTwiceAndReset("Anonymous", func(t *testing.T) {
|
||||
|
|
|
@ -309,6 +309,12 @@ func (w *TokenWriter) write(token, existing *structs.ACLToken, fromLogin bool) (
|
|||
}
|
||||
token.NodeIdentities = nodeIdentities
|
||||
|
||||
templatedPolicies, err := w.normalizeTemplatedPolicies(token.TemplatedPolicies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token.TemplatedPolicies = templatedPolicies
|
||||
|
||||
if err := w.enterpriseValidation(token, existing); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -442,3 +448,32 @@ func (w *TokenWriter) normalizeNodeIdentities(nodeIDs structs.ACLNodeIdentities)
|
|||
}
|
||||
return nodeIDs.Deduplicate(), nil
|
||||
}
|
||||
|
||||
func (w *TokenWriter) normalizeTemplatedPolicies(templatedPolicies structs.ACLTemplatedPolicies) (structs.ACLTemplatedPolicies, error) {
|
||||
if len(templatedPolicies) == 0 {
|
||||
return templatedPolicies, nil
|
||||
}
|
||||
|
||||
finalPolicies := make(structs.ACLTemplatedPolicies, 0, len(templatedPolicies))
|
||||
for _, templatedPolicy := range templatedPolicies {
|
||||
if templatedPolicy.TemplateName == "" {
|
||||
return nil, errors.New("templated policy is missing the template name field on this token")
|
||||
}
|
||||
|
||||
tmp, ok := structs.GetACLTemplatedPolicyBase(templatedPolicy.TemplateName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no such ACL templated policy with Name %q", templatedPolicy.TemplateName)
|
||||
}
|
||||
|
||||
out := templatedPolicy.Clone()
|
||||
out.TemplateID = tmp.TemplateID
|
||||
|
||||
err := templatedPolicy.ValidateTemplatedPolicy(tmp.Schema)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validation error for templated policy %q: %w", templatedPolicy.TemplateName, err)
|
||||
}
|
||||
finalPolicies = append(finalPolicies, out)
|
||||
}
|
||||
|
||||
return finalPolicies.Deduplicate(), nil
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
func TestTokenWriter_Create_Validation(t *testing.T) {
|
||||
|
@ -357,6 +358,59 @@ func TestTokenWriter_NodeIdentities(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTokenWriter_TemplatedPolicies(t *testing.T) {
|
||||
aclCache := &MockACLCache{}
|
||||
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
|
||||
|
||||
store := testStateStore(t)
|
||||
|
||||
writer := buildTokenWriter(store, aclCache)
|
||||
|
||||
testCases := map[string]struct {
|
||||
input []*structs.ACLTemplatedPolicy
|
||||
output []*structs.ACLTemplatedPolicy
|
||||
errorContains string
|
||||
}{
|
||||
"missing templated policy name": {
|
||||
input: []*structs.ACLTemplatedPolicy{{TemplateName: ""}},
|
||||
errorContains: "templated policy is missing the template name field on this token",
|
||||
},
|
||||
"invalid templated policy name": {
|
||||
input: []*structs.ACLTemplatedPolicy{{TemplateName: "faketemplate"}},
|
||||
errorContains: "no such ACL templated policy with Name \"faketemplate\"",
|
||||
},
|
||||
"missing required template variable: name": {
|
||||
input: []*structs.ACLTemplatedPolicy{{TemplateName: api.ACLTemplatedPolicyServiceName, Datacenters: []string{"dc1"}}},
|
||||
errorContains: "validation error for templated policy \"builtin/service\"",
|
||||
},
|
||||
"duplicate templated policies are removed and ids are set": {
|
||||
input: []*structs.ACLTemplatedPolicy{
|
||||
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "web"}},
|
||||
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "web"}},
|
||||
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "api"}},
|
||||
{TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "nodename"}},
|
||||
},
|
||||
output: []*structs.ACLTemplatedPolicy{
|
||||
{TemplateID: structs.ACLTemplatedPolicyServiceID, TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "web"}},
|
||||
{TemplateID: structs.ACLTemplatedPolicyServiceID, TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "api"}},
|
||||
{TemplateID: structs.ACLTemplatedPolicyNodeID, TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "nodename"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
for desc, tc := range testCases {
|
||||
t.Run(desc, func(t *testing.T) {
|
||||
updated, err := writer.Create(&structs.ACLToken{TemplatedPolicies: tc.input}, false)
|
||||
if tc.errorContains == "" {
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, tc.output, updated.TemplatedPolicies)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.errorContains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenWriter_Create_Expiration(t *testing.T) {
|
||||
aclCache := &MockACLCache{}
|
||||
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
|
||||
|
|
|
@ -1177,6 +1177,26 @@ func aclRoleSetTxn(tx WriteTxn, idx uint64, role *structs.ACLRole, allowMissing
|
|||
}
|
||||
}
|
||||
|
||||
for _, templatedPolicy := range role.TemplatedPolicies {
|
||||
if templatedPolicy.TemplateName == "" {
|
||||
return fmt.Errorf("encountered a Role %s (%s) with an empty templated policy name in the state store", role.Name, role.ID)
|
||||
}
|
||||
|
||||
baseTemplate, ok := structs.GetACLTemplatedPolicyBase(templatedPolicy.TemplateName)
|
||||
if !ok {
|
||||
return fmt.Errorf("encountered a Role %s (%s) with an invalid templated policy name %q", role.Name, role.ID, templatedPolicy.TemplateName)
|
||||
}
|
||||
|
||||
if templatedPolicy.TemplateID == "" {
|
||||
templatedPolicy.TemplateID = baseTemplate.TemplateID
|
||||
}
|
||||
|
||||
err := templatedPolicy.ValidateTemplatedPolicy(baseTemplate.Schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encountered a Role %s (%s) with an invalid templated policy: %w", role.Name, role.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := aclRoleUpsertValidateEnterprise(tx, role, existing); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -126,6 +126,7 @@ type ACLIdentity interface {
|
|||
RoleIDs() []string
|
||||
ServiceIdentityList() []*ACLServiceIdentity
|
||||
NodeIdentityList() []*ACLNodeIdentity
|
||||
TemplatedPolicyList() []*ACLTemplatedPolicy
|
||||
IsExpired(asOf time.Time) bool
|
||||
IsLocal() bool
|
||||
EnterpriseMetadata() *acl.EnterpriseMeta
|
||||
|
@ -314,6 +315,9 @@ type ACLToken struct {
|
|||
// The node identities that this token should be allowed to manage.
|
||||
NodeIdentities ACLNodeIdentities `json:",omitempty"`
|
||||
|
||||
// The templated policies to generate synthetic policies for.
|
||||
TemplatedPolicies ACLTemplatedPolicies `json:",omitempty"`
|
||||
|
||||
// Whether this token is DC local. This means that it will not be synced
|
||||
// to the ACL datacenter and replicated to others.
|
||||
Local bool
|
||||
|
@ -394,6 +398,7 @@ func (t *ACLToken) Clone() *ACLToken {
|
|||
t2.Roles = nil
|
||||
t2.ServiceIdentities = nil
|
||||
t2.NodeIdentities = nil
|
||||
t2.TemplatedPolicies = nil
|
||||
|
||||
if len(t.Policies) > 0 {
|
||||
t2.Policies = make([]ACLTokenPolicyLink, len(t.Policies))
|
||||
|
@ -415,6 +420,12 @@ func (t *ACLToken) Clone() *ACLToken {
|
|||
t2.NodeIdentities[i] = n.Clone()
|
||||
}
|
||||
}
|
||||
if len(t.TemplatedPolicies) > 0 {
|
||||
t2.TemplatedPolicies = make([]*ACLTemplatedPolicy, len(t.TemplatedPolicies))
|
||||
for idx, tp := range t.TemplatedPolicies {
|
||||
t2.TemplatedPolicies[idx] = tp.Clone()
|
||||
}
|
||||
}
|
||||
|
||||
return &t2
|
||||
}
|
||||
|
@ -523,6 +534,10 @@ func (t *ACLToken) SetHash(force bool) []byte {
|
|||
nodeID.AddToHash(hash)
|
||||
}
|
||||
|
||||
for _, templatedPolicy := range t.TemplatedPolicies {
|
||||
templatedPolicy.AddToHash(hash)
|
||||
}
|
||||
|
||||
t.EnterpriseMeta.AddToHash(hash, false)
|
||||
|
||||
// Finalize the hash
|
||||
|
@ -549,6 +564,9 @@ func (t *ACLToken) EstimateSize() int {
|
|||
for _, nodeID := range t.NodeIdentities {
|
||||
size += nodeID.EstimateSize()
|
||||
}
|
||||
for _, templatedPolicy := range t.TemplatedPolicies {
|
||||
size += templatedPolicy.EstimateSize()
|
||||
}
|
||||
return size + t.EnterpriseMeta.EstimateSize()
|
||||
}
|
||||
|
||||
|
@ -563,6 +581,7 @@ type ACLTokenListStub struct {
|
|||
Roles []ACLTokenRoleLink `json:",omitempty"`
|
||||
ServiceIdentities ACLServiceIdentities `json:",omitempty"`
|
||||
NodeIdentities ACLNodeIdentities `json:",omitempty"`
|
||||
TemplatedPolicies ACLTemplatedPolicies `json:",omitempty"`
|
||||
Local bool
|
||||
AuthMethod string `json:",omitempty"`
|
||||
ExpirationTime *time.Time `json:",omitempty"`
|
||||
|
@ -585,6 +604,7 @@ func (token *ACLToken) Stub() *ACLTokenListStub {
|
|||
Roles: token.Roles,
|
||||
ServiceIdentities: token.ServiceIdentities,
|
||||
NodeIdentities: token.NodeIdentities,
|
||||
TemplatedPolicies: token.TemplatedPolicies,
|
||||
Local: token.Local,
|
||||
AuthMethod: token.AuthMethod,
|
||||
ExpirationTime: token.ExpirationTime,
|
||||
|
@ -870,6 +890,9 @@ type ACLRole struct {
|
|||
// List of nodes to generate synthetic policies for.
|
||||
NodeIdentities ACLNodeIdentities `json:",omitempty"`
|
||||
|
||||
// List of templated policies to generate synthethic policies for.
|
||||
TemplatedPolicies ACLTemplatedPolicies `json:",omitempty"`
|
||||
|
||||
// Hash of the contents of the role
|
||||
// This does not take into account the ID (which is immutable)
|
||||
// nor the raft metadata.
|
||||
|
@ -909,6 +932,7 @@ func (r *ACLRole) Clone() *ACLRole {
|
|||
r2.Policies = nil
|
||||
r2.ServiceIdentities = nil
|
||||
r2.NodeIdentities = nil
|
||||
r2.TemplatedPolicies = nil
|
||||
|
||||
if len(r.Policies) > 0 {
|
||||
r2.Policies = make([]ACLRolePolicyLink, len(r.Policies))
|
||||
|
@ -926,6 +950,12 @@ func (r *ACLRole) Clone() *ACLRole {
|
|||
r2.NodeIdentities[i] = n.Clone()
|
||||
}
|
||||
}
|
||||
if len(r.TemplatedPolicies) > 0 {
|
||||
r2.TemplatedPolicies = make([]*ACLTemplatedPolicy, len(r.TemplatedPolicies))
|
||||
for i, n := range r.TemplatedPolicies {
|
||||
r2.TemplatedPolicies[i] = n.Clone()
|
||||
}
|
||||
}
|
||||
return &r2
|
||||
}
|
||||
|
||||
|
@ -957,6 +987,9 @@ func (r *ACLRole) SetHash(force bool) []byte {
|
|||
for _, nodeID := range r.NodeIdentities {
|
||||
nodeID.AddToHash(hash)
|
||||
}
|
||||
for _, templatedPolicy := range r.TemplatedPolicies {
|
||||
templatedPolicy.AddToHash(hash)
|
||||
}
|
||||
|
||||
r.EnterpriseMeta.AddToHash(hash, false)
|
||||
|
||||
|
@ -984,6 +1017,9 @@ func (r *ACLRole) EstimateSize() int {
|
|||
for _, nodeID := range r.NodeIdentities {
|
||||
size += nodeID.EstimateSize()
|
||||
}
|
||||
for _, templatedPolicy := range r.TemplatedPolicies {
|
||||
size += templatedPolicy.EstimateSize()
|
||||
}
|
||||
|
||||
return size + r.EnterpriseMeta.EstimateSize()
|
||||
}
|
||||
|
@ -1845,6 +1881,10 @@ func (id *AgentRecoveryTokenIdentity) NodeIdentityList() []*ACLNodeIdentity {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (id *AgentRecoveryTokenIdentity) TemplatedPolicyList() []*ACLTemplatedPolicy {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (id *AgentRecoveryTokenIdentity) IsExpired(asOf time.Time) bool {
|
||||
return false
|
||||
}
|
||||
|
@ -1893,6 +1933,10 @@ func (i *ACLServerIdentity) NodeIdentityList() []*ACLNodeIdentity {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (i *ACLServerIdentity) TemplatedPolicyList() []*ACLTemplatedPolicy {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *ACLServerIdentity) IsExpired(asOf time.Time) bool {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package structs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"hash"
|
||||
"hash/fnv"
|
||||
"html/template"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/lib/stringslice"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type ACLTemplatedPolicies []*ACLTemplatedPolicy
|
||||
|
||||
const (
|
||||
ACLTemplatedPolicyNodeID = "00000000-0000-0000-0000-000000000004"
|
||||
ACLTemplatedPolicyServiceID = "00000000-0000-0000-0000-000000000003"
|
||||
ACLTemplatedPolicyIdentitiesSchema = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "$ref": "#/definitions/min-length-one" }
|
||||
},
|
||||
"required": ["name"],
|
||||
"definitions": {
|
||||
"min-length-one": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
ACLTemplatedPolicyDNSID = "00000000-0000-0000-0000-000000000005"
|
||||
ACLTemplatedPolicyDNSSchema = "" // empty schema as it does not require variables
|
||||
)
|
||||
|
||||
// ACLTemplatedPolicyBase contains basic information about builtin templated policies
|
||||
// template name, id, template code and schema
|
||||
type ACLTemplatedPolicyBase struct {
|
||||
TemplateName string
|
||||
TemplateID string
|
||||
Schema string
|
||||
Template string
|
||||
}
|
||||
|
||||
var (
|
||||
// TODO(Ronald): add other templates
|
||||
// This supports: node, service and dns templates
|
||||
aclTemplatedPoliciesList = map[string]*ACLTemplatedPolicyBase{
|
||||
api.ACLTemplatedPolicyServiceName: {
|
||||
TemplateID: ACLTemplatedPolicyServiceID,
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
Schema: ACLTemplatedPolicyIdentitiesSchema,
|
||||
Template: ACLTemplatedPolicyService,
|
||||
},
|
||||
api.ACLTemplatedPolicyNodeName: {
|
||||
TemplateID: ACLTemplatedPolicyNodeID,
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
Schema: ACLTemplatedPolicyIdentitiesSchema,
|
||||
Template: ACLTemplatedPolicyNode,
|
||||
},
|
||||
api.ACLTemplatedPolicyDNSName: {
|
||||
TemplateID: ACLTemplatedPolicyDNSID,
|
||||
TemplateName: api.ACLTemplatedPolicyDNSName,
|
||||
Schema: ACLTemplatedPolicyDNSSchema,
|
||||
Template: ACLTemplatedPolicyDNS,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// ACLTemplatedPolicy represents a template used to generate a `synthetic` policy
|
||||
// given some input variables.
|
||||
type ACLTemplatedPolicy struct {
|
||||
// TemplateID are hidden from all displays and should not be exposed to the users.
|
||||
TemplateID string `json:",omitempty"`
|
||||
|
||||
// TemplateName is used for display purposes mostly and should not be used for policy rendering.
|
||||
TemplateName string `json:",omitempty"`
|
||||
|
||||
// TemplateVariables are input variables required to render templated policies.
|
||||
TemplateVariables *ACLTemplatedPolicyVariables `json:",omitempty"`
|
||||
|
||||
// Datacenters that the synthetic policy will be valid within.
|
||||
// - No wildcards allowed
|
||||
// - If empty then the synthetic policy is valid within all datacenters
|
||||
//
|
||||
// This is kept for legacy reasons to enable us to replace Node/Service Identities by templated policies.
|
||||
//
|
||||
// Only valid for global tokens. It is an error to specify this for local tokens.
|
||||
Datacenters []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ACLTemplatedPolicyVariables are input variables required to render templated policies.
|
||||
type ACLTemplatedPolicyVariables struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
func (tp *ACLTemplatedPolicy) Clone() *ACLTemplatedPolicy {
|
||||
tp2 := *tp
|
||||
|
||||
tp2.TemplateVariables = nil
|
||||
if tp.TemplateVariables != nil {
|
||||
tp2.TemplateVariables = tp.TemplateVariables.Clone()
|
||||
}
|
||||
tp2.Datacenters = stringslice.CloneStringSlice(tp.Datacenters)
|
||||
|
||||
return &tp2
|
||||
}
|
||||
|
||||
func (tp *ACLTemplatedPolicy) AddToHash(h hash.Hash) {
|
||||
h.Write([]byte(tp.TemplateID))
|
||||
h.Write([]byte(tp.TemplateName))
|
||||
|
||||
if tp.TemplateVariables != nil {
|
||||
tp.TemplateVariables.AddToHash(h)
|
||||
}
|
||||
for _, dc := range tp.Datacenters {
|
||||
h.Write([]byte(dc))
|
||||
}
|
||||
}
|
||||
|
||||
func (tv *ACLTemplatedPolicyVariables) AddToHash(h hash.Hash) {
|
||||
h.Write([]byte(tv.Name))
|
||||
}
|
||||
|
||||
func (tv *ACLTemplatedPolicyVariables) Clone() *ACLTemplatedPolicyVariables {
|
||||
tv2 := *tv
|
||||
return &tv2
|
||||
}
|
||||
|
||||
// validates templated policy variables against schema.
|
||||
func (tp *ACLTemplatedPolicy) ValidateTemplatedPolicy(schema string) error {
|
||||
if schema == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
loader := gojsonschema.NewStringLoader(schema)
|
||||
dataloader := gojsonschema.NewGoLoader(tp.TemplateVariables)
|
||||
res, err := gojsonschema.Validate(loader, dataloader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load json schema for validation %w", err)
|
||||
}
|
||||
|
||||
if res.Valid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var merr *multierror.Error
|
||||
|
||||
for _, resultError := range res.Errors() {
|
||||
merr = multierror.Append(merr, fmt.Errorf(resultError.Description()))
|
||||
}
|
||||
return merr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (tp *ACLTemplatedPolicy) EstimateSize() int {
|
||||
size := len(tp.TemplateName) + len(tp.TemplateID) + tp.TemplateVariables.EstimateSize()
|
||||
for _, dc := range tp.Datacenters {
|
||||
size += len(dc)
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
func (tv *ACLTemplatedPolicyVariables) EstimateSize() int {
|
||||
return len(tv.Name)
|
||||
}
|
||||
|
||||
// SyntheticPolicy generates a policy based on templated policies' ID and variables
|
||||
//
|
||||
// Given that we validate this string name before persisting, we do not
|
||||
// have to escape it before doing the following interpolation.
|
||||
func (tp *ACLTemplatedPolicy) SyntheticPolicy(entMeta *acl.EnterpriseMeta) (*ACLPolicy, error) {
|
||||
rules, err := tp.aclTemplatedPolicyRules(entMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasher := fnv.New128a()
|
||||
hashID := fmt.Sprintf("%x", hasher.Sum([]byte(rules)))
|
||||
|
||||
policy := &ACLPolicy{
|
||||
Rules: rules,
|
||||
ID: hashID,
|
||||
Name: fmt.Sprintf("synthetic-policy-%s", hashID),
|
||||
Datacenters: tp.Datacenters,
|
||||
Description: fmt.Sprintf("synthetic policy generated from templated policy: %s", tp.TemplateName),
|
||||
}
|
||||
policy.EnterpriseMeta.Merge(entMeta)
|
||||
policy.SetHash(true)
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (tp *ACLTemplatedPolicy) aclTemplatedPolicyRules(entMeta *acl.EnterpriseMeta) (string, error) {
|
||||
if entMeta == nil {
|
||||
entMeta = DefaultEnterpriseMetaInDefaultPartition()
|
||||
}
|
||||
entMeta.Normalize()
|
||||
|
||||
tpl := template.New(tp.TemplateName)
|
||||
tmplCode, ok := aclTemplatedPoliciesList[tp.TemplateName]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("acl templated policy does not exist: %s", tp.TemplateName)
|
||||
}
|
||||
|
||||
parsedTpl, err := tpl.Parse(tmplCode.Template)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("an error occured when parsing template structs: %w", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = parsedTpl.Execute(&buf, struct {
|
||||
*ACLTemplatedPolicyVariables
|
||||
Namespace string
|
||||
Partition string
|
||||
}{
|
||||
Namespace: entMeta.NamespaceOrDefault(),
|
||||
Partition: entMeta.PartitionOrDefault(),
|
||||
ACLTemplatedPolicyVariables: tp.TemplateVariables,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("an error occured when executing on templated policy variables: %w", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Deduplicate returns a new list of templated policies without duplicates.
|
||||
// compares values of template variables to ensure no duplicates
|
||||
func (tps ACLTemplatedPolicies) Deduplicate() ACLTemplatedPolicies {
|
||||
list := make(map[string][]ACLTemplatedPolicyVariables)
|
||||
var out ACLTemplatedPolicies
|
||||
|
||||
for _, tp := range tps {
|
||||
// checks if template name already in the unique list
|
||||
_, found := list[tp.TemplateName]
|
||||
if !found {
|
||||
list[tp.TemplateName] = make([]ACLTemplatedPolicyVariables, 0)
|
||||
}
|
||||
templateSchema := aclTemplatedPoliciesList[tp.TemplateName].Schema
|
||||
|
||||
// if schema is empty, template does not require variables
|
||||
if templateSchema == "" {
|
||||
if !found {
|
||||
out = append(out, tp)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !slices.Contains(list[tp.TemplateName], *tp.TemplateVariables) {
|
||||
list[tp.TemplateName] = append(list[tp.TemplateName], *tp.TemplateVariables)
|
||||
out = append(out, tp)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func GetACLTemplatedPolicyBase(templateName string) (*ACLTemplatedPolicyBase, bool) {
|
||||
baseTemplate, found := aclTemplatedPoliciesList[templateName]
|
||||
|
||||
return baseTemplate, found
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !consulent
|
||||
|
||||
package structs
|
||||
|
||||
const (
|
||||
ACLTemplatedPolicyService = `
|
||||
service "{{.Name}}" {
|
||||
policy = "write"
|
||||
}
|
||||
service "{{.Name}}-sidecar-proxy" {
|
||||
policy = "write"
|
||||
}
|
||||
service_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
node_prefix "" {
|
||||
policy = "read"
|
||||
}`
|
||||
|
||||
ACLTemplatedPolicyNode = `
|
||||
node "{{.Name}}" {
|
||||
policy = "write"
|
||||
}
|
||||
service_prefix "" {
|
||||
policy = "read"
|
||||
}`
|
||||
|
||||
ACLTemplatedPolicyDNS = `
|
||||
node_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
service_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
query_prefix "" {
|
||||
policy = "read"
|
||||
}`
|
||||
)
|
||||
|
||||
func (t *ACLToken) TemplatedPolicyList() []*ACLTemplatedPolicy {
|
||||
if len(t.TemplatedPolicies) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]*ACLTemplatedPolicy, 0, len(t.TemplatedPolicies))
|
||||
for _, n := range t.TemplatedPolicies {
|
||||
out = append(out, n.Clone())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (t *ACLRole) TemplatedPolicyList() []*ACLTemplatedPolicy {
|
||||
if len(t.TemplatedPolicies) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]*ACLTemplatedPolicy, 0, len(t.TemplatedPolicies))
|
||||
for _, n := range t.TemplatedPolicies {
|
||||
out = append(out, n.Clone())
|
||||
}
|
||||
return out
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !consulent
|
||||
|
||||
package structs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStructs_ACLTemplatedPolicy_SyntheticPolicy(t *testing.T) {
|
||||
type testCase struct {
|
||||
templatedPolicy *ACLTemplatedPolicy
|
||||
expectedPolicy *ACLPolicy
|
||||
}
|
||||
|
||||
testCases := map[string]testCase{
|
||||
"service-identity-template": {
|
||||
templatedPolicy: &ACLTemplatedPolicy{
|
||||
TemplateID: ACLTemplatedPolicyServiceID,
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
expectedPolicy: &ACLPolicy{
|
||||
Description: "synthetic policy generated from templated policy: builtin/service",
|
||||
Rules: `
|
||||
service "api" {
|
||||
policy = "write"
|
||||
}
|
||||
service "api-sidecar-proxy" {
|
||||
policy = "write"
|
||||
}
|
||||
service_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
node_prefix "" {
|
||||
policy = "read"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
"node-identity-template": {
|
||||
templatedPolicy: &ACLTemplatedPolicy{
|
||||
TemplateID: ACLTemplatedPolicyNodeID,
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "web",
|
||||
},
|
||||
},
|
||||
expectedPolicy: &ACLPolicy{
|
||||
Description: "synthetic policy generated from templated policy: builtin/node",
|
||||
Rules: `
|
||||
node "web" {
|
||||
policy = "write"
|
||||
}
|
||||
service_prefix "" {
|
||||
policy = "read"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
"dns-template": {
|
||||
templatedPolicy: &ACLTemplatedPolicy{
|
||||
TemplateID: ACLTemplatedPolicyDNSID,
|
||||
TemplateName: api.ACLTemplatedPolicyDNSName,
|
||||
},
|
||||
expectedPolicy: &ACLPolicy{
|
||||
Description: "synthetic policy generated from templated policy: builtin/dns",
|
||||
Rules: `
|
||||
node_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
service_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
query_prefix "" {
|
||||
policy = "read"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tcase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
policy, err := tcase.templatedPolicy.SyntheticPolicy(nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tcase.expectedPolicy.Description, policy.Description)
|
||||
require.Equal(t, tcase.expectedPolicy.Rules, policy.Rules)
|
||||
require.Contains(t, policy.Name, "synthetic-policy-")
|
||||
require.NotEmpty(t, policy.Hash)
|
||||
require.NotEmpty(t, policy.ID)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package structs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDeduplicate(t *testing.T) {
|
||||
type testCase struct {
|
||||
templatedPolicies ACLTemplatedPolicies
|
||||
expectedCount int
|
||||
}
|
||||
tcases := map[string]testCase{
|
||||
"multiple-of-the-same-template": {
|
||||
templatedPolicies: ACLTemplatedPolicies{
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedCount: 1,
|
||||
},
|
||||
"separate-templates-with-matching-variables": {
|
||||
templatedPolicies: ACLTemplatedPolicies{
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedCount: 2,
|
||||
},
|
||||
"separate-templates-with-multiple-matching-variables": {
|
||||
templatedPolicies: ACLTemplatedPolicies{
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "web",
|
||||
},
|
||||
},
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyDNSName,
|
||||
},
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &ACLTemplatedPolicyVariables{
|
||||
Name: "web",
|
||||
},
|
||||
},
|
||||
&ACLTemplatedPolicy{
|
||||
TemplateName: api.ACLTemplatedPolicyDNSName,
|
||||
},
|
||||
},
|
||||
expectedCount: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tcase := range tcases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
policies := tcase.templatedPolicies.Deduplicate()
|
||||
|
||||
require.Equal(t, tcase.expectedCount, len(policies))
|
||||
})
|
||||
}
|
||||
}
|
23
api/acl.go
23
api/acl.go
|
@ -19,6 +19,11 @@ const (
|
|||
|
||||
// ACLManagementType is the management type token
|
||||
ACLManagementType = "management"
|
||||
|
||||
// ACLTemplatedPolicy names
|
||||
ACLTemplatedPolicyServiceName = "builtin/service"
|
||||
ACLTemplatedPolicyNodeName = "builtin/node"
|
||||
ACLTemplatedPolicyDNSName = "builtin/dns"
|
||||
)
|
||||
|
||||
type ACLLink struct {
|
||||
|
@ -40,6 +45,7 @@ type ACLToken struct {
|
|||
Roles []*ACLTokenRoleLink `json:",omitempty"`
|
||||
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
|
||||
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
|
||||
TemplatedPolicies []*ACLTemplatedPolicy `json:",omitempty"`
|
||||
Local bool
|
||||
AuthMethod string `json:",omitempty"`
|
||||
ExpirationTTL time.Duration `json:",omitempty"`
|
||||
|
@ -88,6 +94,7 @@ type ACLTokenListEntry struct {
|
|||
Roles []*ACLTokenRoleLink `json:",omitempty"`
|
||||
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
|
||||
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
|
||||
TemplatedPolicies []*ACLTemplatedPolicy `json:",omitempty"`
|
||||
Local bool
|
||||
AuthMethod string `json:",omitempty"`
|
||||
ExpirationTime *time.Time `json:",omitempty"`
|
||||
|
@ -148,6 +155,21 @@ type ACLNodeIdentity struct {
|
|||
Datacenter string
|
||||
}
|
||||
|
||||
// ACLTemplatedPolicy represents a template used to generate a `synthetic` policy
|
||||
// given some input variables.
|
||||
type ACLTemplatedPolicy struct {
|
||||
TemplateName string
|
||||
TemplateVariables *ACLTemplatedPolicyVariables `json:",omitempty"`
|
||||
|
||||
// Datacenters are an artifact of Nodeidentity & ServiceIdentity.
|
||||
// It is used to facilitate the future migration away from both
|
||||
Datacenters []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ACLTemplatedPolicyVariables struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// ACLPolicy represents an ACL Policy.
|
||||
type ACLPolicy struct {
|
||||
ID string
|
||||
|
@ -196,6 +218,7 @@ type ACLRole struct {
|
|||
Policies []*ACLRolePolicyLink `json:",omitempty"`
|
||||
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
|
||||
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
|
||||
TemplatedPolicies []*ACLTemplatedPolicy `json:",omitempty"`
|
||||
Hash []byte
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
|
|
|
@ -10,6 +10,9 @@ import (
|
|||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/command/helpers"
|
||||
"github.com/hashicorp/hcl"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
func GetTokenAccessorIDFromPartial(client *api.Client, partialAccessorID string) (string, error) {
|
||||
|
@ -218,6 +221,70 @@ func ExtractNodeIdentities(nodeIdents []string) ([]*api.ACLNodeIdentity, error)
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func ExtractTemplatedPolicies(templatedPolicy string, templatedPolicyFile string, templatedPolicyVariables []string) ([]*api.ACLTemplatedPolicy, error) {
|
||||
var out []*api.ACLTemplatedPolicy
|
||||
if templatedPolicy == "" && templatedPolicyFile == "" {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
if templatedPolicy != "" {
|
||||
parsedVariables, err := getTemplatedPolicyVariables(templatedPolicyVariables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out = append(out, &api.ACLTemplatedPolicy{
|
||||
TemplateName: templatedPolicy,
|
||||
TemplateVariables: parsedVariables,
|
||||
})
|
||||
}
|
||||
|
||||
if templatedPolicyFile != "" {
|
||||
fileData, err := helpers.LoadFromFile(templatedPolicyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config map[string]map[string][]api.ACLTemplatedPolicyVariables
|
||||
err = hcl.Decode(&config, fileData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for templateName, templateVariables := range config["TemplatedPolicy"] {
|
||||
for _, tp := range templateVariables {
|
||||
out = append(out, &api.ACLTemplatedPolicy{
|
||||
TemplateName: templateName,
|
||||
TemplateVariables: &api.ACLTemplatedPolicyVariables{
|
||||
Name: tp.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func getTemplatedPolicyVariables(variables []string) (*api.ACLTemplatedPolicyVariables, error) {
|
||||
if len(variables) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
out := &api.ACLTemplatedPolicyVariables{}
|
||||
jsonVariables := make(map[string]string)
|
||||
|
||||
for _, variable := range variables {
|
||||
parts := strings.Split(variable, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("malformed -var argument: %q, expecting VariableName:Value", variable)
|
||||
}
|
||||
jsonVariables[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
err := mapstructure.Decode(jsonVariables, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// TestKubernetesJWT_A is a valid service account jwt extracted from a minikube setup.
|
||||
//
|
||||
// {
|
||||
|
|
|
@ -71,7 +71,7 @@ func (c *cmd) Run(args []string) int {
|
|||
}
|
||||
|
||||
if c.name == "" {
|
||||
c.UI.Error(fmt.Sprintf("Missing require '-name' flag"))
|
||||
c.UI.Error(fmt.Sprintf("Missing required '-name' flag"))
|
||||
c.UI.Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
|
|
@ -34,6 +34,9 @@ type cmd struct {
|
|||
policyNames []string
|
||||
serviceIdents []string
|
||||
nodeIdents []string
|
||||
templatedPolicy string
|
||||
templatedPolicyFile string
|
||||
templatedPolicyVariables []string
|
||||
|
||||
showMeta bool
|
||||
format string
|
||||
|
@ -55,6 +58,12 @@ func (c *cmd) init() {
|
|||
c.flags.Var((*flags.AppendSliceValue)(&c.nodeIdents), "node-identity", "Name of a "+
|
||||
"node identity to use for this role. May be specified multiple times. Format is "+
|
||||
"NODENAME:DATACENTER")
|
||||
c.flags.Var((*flags.AppendSliceValue)(&c.templatedPolicyVariables), "var", "Templated policy variables."+
|
||||
" Must be used in combination with -templated-policy flag to specify required variables."+
|
||||
" May be specified multiple times with different variables."+
|
||||
" Format is VariableName:Value")
|
||||
c.flags.StringVar(&c.templatedPolicy, "templated-policy", "", "The templated policy name. Use -var flag to specify variables when required.")
|
||||
c.flags.StringVar(&c.templatedPolicyFile, "templated-policy-file", "", "Path to a file containing templated policy names and variables.")
|
||||
c.flags.StringVar(
|
||||
&c.format,
|
||||
"format",
|
||||
|
@ -74,13 +83,14 @@ func (c *cmd) Run(args []string) int {
|
|||
}
|
||||
|
||||
if c.name == "" {
|
||||
c.UI.Error(fmt.Sprintf("Missing require '-name' flag"))
|
||||
c.UI.Error("Missing required '-name' flag")
|
||||
c.UI.Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 && len(c.serviceIdents) == 0 && len(c.nodeIdents) == 0 {
|
||||
c.UI.Error(fmt.Sprintf("Cannot create a role without specifying -policy-name, -policy-id, -service-identity, or -node-identity at least once"))
|
||||
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 && len(c.serviceIdents) == 0 && len(c.nodeIdents) == 0 &&
|
||||
len(c.templatedPolicy) == 0 && len(c.templatedPolicyFile) == 0 {
|
||||
c.UI.Error("Cannot create a role without specifying -policy-name, -policy-id, -service-identity, -node-identity, -templated-policy-file or -templated-policy at least once")
|
||||
return 1
|
||||
}
|
||||
|
||||
|
@ -124,6 +134,13 @@ func (c *cmd) Run(args []string) int {
|
|||
}
|
||||
newRole.NodeIdentities = parsedNodeIdents
|
||||
|
||||
parsedTemplatedPolicies, err := acl.ExtractTemplatedPolicies(c.templatedPolicy, c.templatedPolicyFile, c.templatedPolicyVariables)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
newRole.TemplatedPolicies = parsedTemplatedPolicies
|
||||
|
||||
r, _, err := client.ACL().RoleCreate(newRole, nil)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Failed to create new role: %v", err))
|
||||
|
@ -167,5 +184,7 @@ Usage: consul acl role create -name NAME [options]
|
|||
-policy-id b52fc3de-5 \
|
||||
-policy-name "acl-replication" \
|
||||
-service-identity "web" \
|
||||
-service-identity "db:east,west"
|
||||
-service-identity "db:east,west" \
|
||||
-templated-policy "builtin/service" \
|
||||
-var "name:api"
|
||||
`
|
||||
|
|
|
@ -89,6 +89,20 @@ func (f *prettyFormatter) FormatRole(role *api.ACLRole) (string, error) {
|
|||
buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter))
|
||||
}
|
||||
}
|
||||
if len(role.TemplatedPolicies) > 0 {
|
||||
buffer.WriteString(fmt.Sprintln("Templated Policies:"))
|
||||
for _, templatedPolicy := range role.TemplatedPolicies {
|
||||
buffer.WriteString(fmt.Sprintf(" %s\n", templatedPolicy.TemplateName))
|
||||
if templatedPolicy.TemplateVariables != nil && templatedPolicy.TemplateVariables.Name != "" {
|
||||
buffer.WriteString(fmt.Sprintf(" Name: %s\n", templatedPolicy.TemplateVariables.Name))
|
||||
}
|
||||
if len(templatedPolicy.Datacenters) > 0 {
|
||||
buffer.WriteString(fmt.Sprintf(" Datacenters: %s\n", strings.Join(templatedPolicy.Datacenters, ", ")))
|
||||
} else {
|
||||
buffer.WriteString(" Datacenters: all\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
@ -144,6 +158,21 @@ func (f *prettyFormatter) formatRoleListEntry(role *api.ACLRole) string {
|
|||
}
|
||||
}
|
||||
|
||||
if len(role.TemplatedPolicies) > 0 {
|
||||
buffer.WriteString(fmt.Sprintln(" Templated Policies:"))
|
||||
for _, templatedPolicy := range role.TemplatedPolicies {
|
||||
buffer.WriteString(fmt.Sprintf(" %s\n", templatedPolicy.TemplateName))
|
||||
if templatedPolicy.TemplateVariables != nil && templatedPolicy.TemplateVariables.Name != "" {
|
||||
buffer.WriteString(fmt.Sprintf(" Name: %s\n", templatedPolicy.TemplateVariables.Name))
|
||||
}
|
||||
if len(templatedPolicy.Datacenters) > 0 {
|
||||
buffer.WriteString(fmt.Sprintf(" Datacenters: %s\n", strings.Join(templatedPolicy.Datacenters, ", ")))
|
||||
} else {
|
||||
buffer.WriteString(" Datacenters: all\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,14 @@ func TestFormatRole(t *testing.T) {
|
|||
Datacenter: "middleearth-northwest",
|
||||
},
|
||||
},
|
||||
TemplatedPolicies: []*api.ACLTemplatedPolicy{
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "gardener"},
|
||||
Datacenters: []string{"middleearth-northwest", "somewhere-east"},
|
||||
},
|
||||
{TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "bagend"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -114,7 +122,7 @@ func TestFormatRole(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFormatTokenList(t *testing.T) {
|
||||
func TestFormatRoleList(t *testing.T) {
|
||||
type testCase struct {
|
||||
roles []*api.ACLRole
|
||||
overrideGoldenName string
|
||||
|
@ -165,6 +173,10 @@ func TestFormatTokenList(t *testing.T) {
|
|||
Datacenter: "middleearth-northwest",
|
||||
},
|
||||
},
|
||||
TemplatedPolicies: []*api.ACLTemplatedPolicy{
|
||||
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "gardener"}},
|
||||
{TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "bagend"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -26,6 +26,24 @@
|
|||
"Datacenter": "middleearth-northwest"
|
||||
}
|
||||
],
|
||||
"TemplatedPolicies": [
|
||||
{
|
||||
"TemplateName": "builtin/service",
|
||||
"TemplateVariables": {
|
||||
"Name": "gardener"
|
||||
},
|
||||
"Datacenters": [
|
||||
"middleearth-northwest",
|
||||
"somewhere-east"
|
||||
]
|
||||
},
|
||||
{
|
||||
"TemplateName": "builtin/node",
|
||||
"TemplateVariables": {
|
||||
"Name": "bagend"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Hash": "YWJjZGVmZ2g=",
|
||||
"CreateIndex": 5,
|
||||
"ModifyIndex": 10,
|
||||
|
|
|
@ -12,3 +12,10 @@ Service Identities:
|
|||
gardener (Datacenters: middleearth-northwest)
|
||||
Node Identities:
|
||||
bagend (Datacenter: middleearth-northwest)
|
||||
Templated Policies:
|
||||
builtin/service
|
||||
Name: gardener
|
||||
Datacenters: middleearth-northwest, somewhere-east
|
||||
builtin/node
|
||||
Name: bagend
|
||||
Datacenters: all
|
||||
|
|
|
@ -9,3 +9,10 @@ Service Identities:
|
|||
gardener (Datacenters: middleearth-northwest)
|
||||
Node Identities:
|
||||
bagend (Datacenter: middleearth-northwest)
|
||||
Templated Policies:
|
||||
builtin/service
|
||||
Name: gardener
|
||||
Datacenters: middleearth-northwest, somewhere-east
|
||||
builtin/node
|
||||
Name: bagend
|
||||
Datacenters: all
|
||||
|
|
|
@ -27,6 +27,20 @@
|
|||
"Datacenter": "middleearth-northwest"
|
||||
}
|
||||
],
|
||||
"TemplatedPolicies": [
|
||||
{
|
||||
"TemplateName": "builtin/service",
|
||||
"TemplateVariables": {
|
||||
"Name": "gardener"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TemplateName": "builtin/node",
|
||||
"TemplateVariables": {
|
||||
"Name": "bagend"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Hash": "YWJjZGVmZ2g=",
|
||||
"CreateIndex": 5,
|
||||
"ModifyIndex": 10,
|
||||
|
|
|
@ -12,3 +12,10 @@ complex:
|
|||
gardener (Datacenters: middleearth-northwest)
|
||||
Node Identities:
|
||||
bagend (Datacenter: middleearth-northwest)
|
||||
Templated Policies:
|
||||
builtin/service
|
||||
Name: gardener
|
||||
Datacenters: all
|
||||
builtin/node
|
||||
Name: bagend
|
||||
Datacenters: all
|
||||
|
|
|
@ -9,3 +9,10 @@ complex:
|
|||
gardener (Datacenters: middleearth-northwest)
|
||||
Node Identities:
|
||||
bagend (Datacenter: middleearth-northwest)
|
||||
Templated Policies:
|
||||
builtin/service
|
||||
Name: gardener
|
||||
Datacenters: all
|
||||
builtin/node
|
||||
Name: bagend
|
||||
Datacenters: all
|
||||
|
|
|
@ -35,6 +35,11 @@ type cmd struct {
|
|||
policyNames []string
|
||||
serviceIdents []string
|
||||
nodeIdents []string
|
||||
appendTemplatedPolicy string
|
||||
replaceTemplatedPolicy string
|
||||
appendTemplatedPolicyFile string
|
||||
replaceTemplatedPolicyFile string
|
||||
templatedPolicyVariables []string
|
||||
|
||||
noMerge bool
|
||||
showMeta bool
|
||||
|
@ -69,6 +74,16 @@ func (c *cmd) init() {
|
|||
role.PrettyFormat,
|
||||
fmt.Sprintf("Output format {%s}", strings.Join(role.GetSupportedFormats(), "|")),
|
||||
)
|
||||
c.flags.Var((*flags.AppendSliceValue)(&c.templatedPolicyVariables), "var", "Templated policy variables."+
|
||||
" Must be used in combination with -append-templated-policy or -replace-templated-policy flags to specify required variables."+
|
||||
" May be specified multiple times with different variables."+
|
||||
" Format is VariableName:Value")
|
||||
c.flags.StringVar(&c.appendTemplatedPolicy, "append-templated-policy", "", "The templated policy name to attach to the role's existing templated policies list. Use -var flag to specify variables when required."+
|
||||
" The role retains existing templated policies.")
|
||||
c.flags.StringVar(&c.replaceTemplatedPolicy, "replace-templated-policy", "", "The templated policy name to replace the existing templated policies list with. Use -var flag to specify variables when required."+
|
||||
" Overwrites the role's existing templated policies.")
|
||||
c.flags.StringVar(&c.appendTemplatedPolicyFile, "append-templated-policy-file", "", "Path to a file containing templated policies and variables. Works like `-append-templated-policy`. The role retains existing templated policies.")
|
||||
c.flags.StringVar(&c.replaceTemplatedPolicyFile, "replace-templated-policy-file", "", "Path to a file containing templated policies and variables. Works like `-replace-templated-policy`. Overwrites the role's existing templated policies.")
|
||||
|
||||
c.http = &flags.HTTPFlags{}
|
||||
flags.Merge(c.flags, c.http.ClientFlags())
|
||||
|
@ -111,6 +126,24 @@ func (c *cmd) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
hasAppendTemplatedPolicies := len(c.appendTemplatedPolicy) > 0 || len(c.appendTemplatedPolicyFile) > 0
|
||||
hasReplaceTemplatedPolicies := len(c.replaceTemplatedPolicy) > 0 || len(c.replaceTemplatedPolicyFile) > 0
|
||||
|
||||
if hasReplaceTemplatedPolicies && hasAppendTemplatedPolicies {
|
||||
c.UI.Error("Cannot combine the use of append-templated-policy flags with replace-templated-policy. " +
|
||||
"To set or overwrite existing templated policies, use -replace-templated-policy or -replace-templated-policy-file. " +
|
||||
"To append to existing templated policies, use -append-templated-policy or -append-templated-policy-file.")
|
||||
return 1
|
||||
}
|
||||
parsedTemplatedPolicies, err := acl.ExtractTemplatedPolicies(c.replaceTemplatedPolicy, c.replaceTemplatedPolicyFile, c.templatedPolicyVariables)
|
||||
if hasAppendTemplatedPolicies {
|
||||
parsedTemplatedPolicies, err = acl.ExtractTemplatedPolicies(c.appendTemplatedPolicy, c.appendTemplatedPolicyFile, c.templatedPolicyVariables)
|
||||
}
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Read the current role in both cases so we can fail better if not found.
|
||||
currentRole, _, err := client.ACL().RoleRead(roleID, nil)
|
||||
if err != nil {
|
||||
|
@ -129,6 +162,7 @@ func (c *cmd) Run(args []string) int {
|
|||
Description: c.description,
|
||||
ServiceIdentities: parsedServiceIdents,
|
||||
NodeIdentities: parsedNodeIdents,
|
||||
TemplatedPolicies: parsedTemplatedPolicies,
|
||||
}
|
||||
|
||||
for _, policyName := range c.policyNames {
|
||||
|
@ -221,6 +255,12 @@ func (c *cmd) Run(args []string) int {
|
|||
r.NodeIdentities = append(r.NodeIdentities, nodeid)
|
||||
}
|
||||
}
|
||||
|
||||
if hasReplaceTemplatedPolicies {
|
||||
r.TemplatedPolicies = parsedTemplatedPolicies
|
||||
} else {
|
||||
r.TemplatedPolicies = append(r.TemplatedPolicies, parsedTemplatedPolicies...)
|
||||
}
|
||||
}
|
||||
|
||||
r, _, err = client.ACL().RoleUpdate(r, nil)
|
||||
|
@ -273,6 +313,8 @@ Usage: consul acl role update [options]
|
|||
-name "better-name" \
|
||||
-description "replication" \
|
||||
-policy-name "token-replication" \
|
||||
-service-identity "web"
|
||||
-service-identity "web" \
|
||||
-templated-policy "builtin/service" \
|
||||
-var "name:api"
|
||||
`
|
||||
)
|
||||
|
|
|
@ -68,6 +68,9 @@ func TestRoleUpdateCommand(t *testing.T) {
|
|||
ServiceName: "fake",
|
||||
},
|
||||
},
|
||||
TemplatedPolicies: []*api.ACLTemplatedPolicy{
|
||||
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "fake"}},
|
||||
},
|
||||
},
|
||||
&api.WriteOptions{Token: "root"},
|
||||
)
|
||||
|
@ -122,6 +125,7 @@ func TestRoleUpdateCommand(t *testing.T) {
|
|||
require.Equal(t, "test role edited", role.Description)
|
||||
require.Len(t, role.Policies, 1)
|
||||
require.Len(t, role.ServiceIdentities, 1)
|
||||
require.Len(t, role.TemplatedPolicies, 1)
|
||||
})
|
||||
|
||||
t.Run("update with policy by id", func(t *testing.T) {
|
||||
|
@ -198,6 +202,47 @@ func TestRoleUpdateCommand(t *testing.T) {
|
|||
require.Len(t, role.ServiceIdentities, 3)
|
||||
require.Len(t, role.NodeIdentities, 1)
|
||||
})
|
||||
t.Run("update with append templated policies", func(t *testing.T) {
|
||||
_ = run(t, []string{
|
||||
"-id=" + role.ID,
|
||||
"-token=root",
|
||||
"-append-templated-policy=builtin/service",
|
||||
"-var=name:api",
|
||||
})
|
||||
|
||||
role, _, err := client.ACL().RoleRead(
|
||||
role.ID,
|
||||
&api.QueryOptions{Token: "root"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, role)
|
||||
require.Equal(t, "test role edited", role.Description)
|
||||
require.Len(t, role.Policies, 2)
|
||||
require.Len(t, role.ServiceIdentities, 3)
|
||||
require.Len(t, role.NodeIdentities, 1)
|
||||
require.Len(t, role.TemplatedPolicies, 2)
|
||||
})
|
||||
|
||||
t.Run("update with replace templated policies", func(t *testing.T) {
|
||||
_ = run(t, []string{
|
||||
"-id=" + role.ID,
|
||||
"-token=root",
|
||||
"-replace-templated-policy=builtin/service",
|
||||
"-var=name:api",
|
||||
})
|
||||
|
||||
role, _, err := client.ACL().RoleRead(
|
||||
role.ID,
|
||||
&api.QueryOptions{Token: "root"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, role)
|
||||
require.Equal(t, "test role edited", role.Description)
|
||||
require.Len(t, role.Policies, 2)
|
||||
require.Len(t, role.ServiceIdentities, 3)
|
||||
require.Len(t, role.NodeIdentities, 1)
|
||||
require.Len(t, role.TemplatedPolicies, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleUpdateCommand_JSON(t *testing.T) {
|
||||
|
@ -335,6 +380,9 @@ func TestRoleUpdateCommand_noMerge(t *testing.T) {
|
|||
ServiceName: "fake",
|
||||
},
|
||||
},
|
||||
TemplatedPolicies: []*api.ACLTemplatedPolicy{
|
||||
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "fake"}},
|
||||
},
|
||||
Policies: []*api.ACLRolePolicyLink{
|
||||
{
|
||||
ID: policy3.ID,
|
||||
|
@ -482,4 +530,64 @@ func TestRoleUpdateCommand_noMerge(t *testing.T) {
|
|||
require.Len(t, role.Policies, 0)
|
||||
require.Len(t, role.ServiceIdentities, 1)
|
||||
})
|
||||
|
||||
t.Run("update with templated policy append", func(t *testing.T) {
|
||||
role := createRole(t)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := New(ui)
|
||||
args := []string{
|
||||
"-http-addr=" + a.HTTPAddr(),
|
||||
"-id=" + role.ID,
|
||||
"-name=" + role.Name,
|
||||
"-token=root",
|
||||
"-no-merge",
|
||||
"-append-templated-policy=builtin/service",
|
||||
"-var=name:api",
|
||||
}
|
||||
|
||||
code := cmd.Run(args)
|
||||
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
|
||||
require.Empty(t, ui.ErrorWriter.String())
|
||||
|
||||
role, _, err := client.ACL().RoleRead(
|
||||
role.ID,
|
||||
&api.QueryOptions{Token: "root"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, role)
|
||||
require.Equal(t, "", role.Description)
|
||||
require.Len(t, role.Policies, 0)
|
||||
require.Len(t, role.TemplatedPolicies, 1)
|
||||
})
|
||||
|
||||
t.Run("update with replace templated policy", func(t *testing.T) {
|
||||
role := createRole(t)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := New(ui)
|
||||
args := []string{
|
||||
"-http-addr=" + a.HTTPAddr(),
|
||||
"-id=" + role.ID,
|
||||
"-name=" + role.Name,
|
||||
"-token=root",
|
||||
"-no-merge",
|
||||
"-replace-templated-policy=builtin/service",
|
||||
"-var=name:api",
|
||||
}
|
||||
|
||||
code := cmd.Run(args)
|
||||
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
|
||||
require.Empty(t, ui.ErrorWriter.String())
|
||||
|
||||
role, _, err := client.ACL().RoleRead(
|
||||
role.ID,
|
||||
&api.QueryOptions{Token: "root"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, role)
|
||||
require.Equal(t, "", role.Description)
|
||||
require.Len(t, role.Policies, 0)
|
||||
require.Len(t, role.TemplatedPolicies, 1)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -165,6 +165,7 @@ func TestTokenCloneCommand_Pretty(t *testing.T) {
|
|||
require.Equal(t, cloned.Description, apiToken.Description)
|
||||
require.Equal(t, cloned.Local, apiToken.Local)
|
||||
require.Equal(t, cloned.Policies, apiToken.Policies)
|
||||
require.Equal(t, cloned.TemplatedPolicies, apiToken.TemplatedPolicies)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -198,7 +199,13 @@ func TestTokenCloneCommand_JSON(t *testing.T) {
|
|||
|
||||
// create a token
|
||||
token, _, err := client.ACL().TokenCreate(
|
||||
&api.ACLToken{Description: "test", Policies: []*api.ACLTokenPolicyLink{{Name: "test-policy"}}},
|
||||
&api.ACLToken{
|
||||
Description: "test",
|
||||
Policies: []*api.ACLTokenPolicyLink{{Name: "test-policy"}},
|
||||
TemplatedPolicies: []*api.ACLTemplatedPolicy{
|
||||
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "web"}},
|
||||
},
|
||||
},
|
||||
&api.WriteOptions{Token: "root"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -38,6 +38,9 @@ type cmd struct {
|
|||
roleNames []string
|
||||
serviceIdents []string
|
||||
nodeIdents []string
|
||||
templatedPolicy string
|
||||
templatedPolicyFile string
|
||||
templatedPolicyVariables []string
|
||||
expirationTTL time.Duration
|
||||
local bool
|
||||
showMeta bool
|
||||
|
@ -76,6 +79,12 @@ func (c *cmd) init() {
|
|||
token.PrettyFormat,
|
||||
fmt.Sprintf("Output format {%s}", strings.Join(token.GetSupportedFormats(), "|")),
|
||||
)
|
||||
c.flags.Var((*flags.AppendSliceValue)(&c.templatedPolicyVariables), "var", "Templated policy variables."+
|
||||
" Must be used in combination with -templated-policy flag to specify required variables."+
|
||||
" May be specified multiple times with different variables."+
|
||||
" Format is VariableName:Value")
|
||||
c.flags.StringVar(&c.templatedPolicy, "templated-policy", "", "The templated policy name. Use -var flag to specify variables when required.")
|
||||
c.flags.StringVar(&c.templatedPolicyFile, "templated-policy-file", "", "Path to a file containing templated policies and variables.")
|
||||
c.http = &flags.HTTPFlags{}
|
||||
flags.Merge(c.flags, c.http.ClientFlags())
|
||||
flags.Merge(c.flags, c.http.ServerFlags())
|
||||
|
@ -90,8 +99,9 @@ func (c *cmd) Run(args []string) int {
|
|||
|
||||
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 &&
|
||||
len(c.roleNames) == 0 && len(c.roleIDs) == 0 &&
|
||||
len(c.serviceIdents) == 0 && len(c.nodeIdents) == 0 {
|
||||
c.UI.Error(fmt.Sprintf("Cannot create a token without specifying -policy-name, -policy-id, -role-name, -role-id, -service-identity, or -node-identity at least once"))
|
||||
len(c.serviceIdents) == 0 && len(c.nodeIdents) == 0 &&
|
||||
len(c.templatedPolicy) == 0 && len(c.templatedPolicyFile) == 0 {
|
||||
c.UI.Error("Cannot create a token without specifying -policy-name, -policy-id, -role-name, -role-id, -service-identity, -node-identity, -templated-policy, or -templated-policy-file at least once")
|
||||
return 1
|
||||
}
|
||||
|
||||
|
@ -125,6 +135,13 @@ func (c *cmd) Run(args []string) int {
|
|||
}
|
||||
newToken.NodeIdentities = parsedNodeIdents
|
||||
|
||||
parsedTemplatedPolicies, err := acl.ExtractTemplatedPolicies(c.templatedPolicy, c.templatedPolicyFile, c.templatedPolicyVariables)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
newToken.TemplatedPolicies = parsedTemplatedPolicies
|
||||
|
||||
for _, policyName := range c.policyNames {
|
||||
// We could resolve names to IDs here but there isn't any reason why its would be better
|
||||
// than allowing the agent to do it.
|
||||
|
@ -203,6 +220,8 @@ Usage: consul acl token create [options]
|
|||
-role-id c630d4ef-6 \
|
||||
-role-name "db-updater" \
|
||||
-service-identity "web" \
|
||||
-service-identity "db:east,west"
|
||||
-service-identity "db:east,west" \
|
||||
-templated-policy "builtin/service" \
|
||||
-var "name:web"
|
||||
`
|
||||
)
|
||||
|
|
|
@ -107,6 +107,27 @@ func TestTokenCreateCommand_Pretty(t *testing.T) {
|
|||
require.Equal(t, a.Config.NodeName, nodes[0].Node)
|
||||
})
|
||||
|
||||
// templated policy
|
||||
t.Run("templated-policy", func(t *testing.T) {
|
||||
token := run(t, []string{
|
||||
"-http-addr=" + a.HTTPAddr(),
|
||||
"-token=root",
|
||||
"-templated-policy=builtin/node",
|
||||
"-var=name:" + a.Config.NodeName,
|
||||
})
|
||||
|
||||
conf := api.DefaultConfig()
|
||||
conf.Address = a.HTTPAddr()
|
||||
conf.Token = token.SecretID
|
||||
client, err := api.NewClient(conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
nodes, _, err := client.Catalog().Nodes(nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, nodes, 1)
|
||||
require.Equal(t, a.Config.NodeName, nodes[0].Node)
|
||||
})
|
||||
|
||||
// create with accessor and secret
|
||||
t.Run("predefined-ids", func(t *testing.T) {
|
||||
token := run(t, []string{
|
||||
|
|
|
@ -109,6 +109,20 @@ func (f *prettyFormatter) FormatToken(token *api.ACLToken) (string, error) {
|
|||
buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter))
|
||||
}
|
||||
}
|
||||
if len(token.TemplatedPolicies) > 0 {
|
||||
buffer.WriteString(fmt.Sprintln("Templated Policies:"))
|
||||
for _, templatedPolicy := range token.TemplatedPolicies {
|
||||
buffer.WriteString(fmt.Sprintf(" %s\n", templatedPolicy.TemplateName))
|
||||
if templatedPolicy.TemplateVariables != nil && templatedPolicy.TemplateVariables.Name != "" {
|
||||
buffer.WriteString(fmt.Sprintf(" Name: %s\n", templatedPolicy.TemplateVariables.Name))
|
||||
}
|
||||
if len(templatedPolicy.Datacenters) > 0 {
|
||||
buffer.WriteString(fmt.Sprintf(" Datacenters: %s\n", strings.Join(templatedPolicy.Datacenters, ", ")))
|
||||
} else {
|
||||
buffer.WriteString(" Datacenters: all\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
@ -174,10 +188,7 @@ func (f *prettyFormatter) FormatTokenExpanded(token *api.ACLTokenExpanded) (stri
|
|||
}
|
||||
identity := structs.ACLServiceIdentity{ServiceName: svcIdentity.ServiceName, Datacenters: svcIdentity.Datacenters}
|
||||
policy := identity.SyntheticPolicy(&entMeta)
|
||||
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"Description: %s\n", policy.Description))
|
||||
buffer.WriteString(indent + WHITESPACE_2 + "Rules:")
|
||||
buffer.WriteString(strings.ReplaceAll(policy.Rules, "\n", "\n"+indent+WHITESPACE_4))
|
||||
buffer.WriteString("\n\n")
|
||||
displaySyntheticPolicy(policy, &buffer, indent)
|
||||
}
|
||||
if len(token.ACLToken.ServiceIdentities) > 0 {
|
||||
buffer.WriteString("Service Identities:\n")
|
||||
|
@ -190,10 +201,7 @@ func (f *prettyFormatter) FormatTokenExpanded(token *api.ACLTokenExpanded) (stri
|
|||
buffer.WriteString(fmt.Sprintf(indent+"Name: %s (Datacenter: %s)\n", nodeIdentity.NodeName, nodeIdentity.Datacenter))
|
||||
identity := structs.ACLNodeIdentity{NodeName: nodeIdentity.NodeName, Datacenter: nodeIdentity.Datacenter}
|
||||
policy := identity.SyntheticPolicy(&entMeta)
|
||||
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"Description: %s\n", policy.Description))
|
||||
buffer.WriteString(indent + WHITESPACE_2 + "Rules:")
|
||||
buffer.WriteString(strings.ReplaceAll(policy.Rules, "\n", "\n"+indent+WHITESPACE_4))
|
||||
buffer.WriteString("\n\n")
|
||||
displaySyntheticPolicy(policy, &buffer, indent)
|
||||
}
|
||||
if len(token.ACLToken.NodeIdentities) > 0 {
|
||||
buffer.WriteString("Node Identities:\n")
|
||||
|
@ -202,6 +210,34 @@ func (f *prettyFormatter) FormatTokenExpanded(token *api.ACLTokenExpanded) (stri
|
|||
}
|
||||
}
|
||||
|
||||
formatTemplatedPolicy := func(templatedPolicy *api.ACLTemplatedPolicy, indent string) {
|
||||
buffer.WriteString(fmt.Sprintf(indent+"%s\n", templatedPolicy.TemplateName))
|
||||
tp := structs.ACLTemplatedPolicy{
|
||||
TemplateName: templatedPolicy.TemplateName,
|
||||
Datacenters: templatedPolicy.Datacenters,
|
||||
}
|
||||
if templatedPolicy.TemplateVariables != nil && templatedPolicy.TemplateVariables.Name != "" {
|
||||
tp.TemplateVariables = &structs.ACLTemplatedPolicyVariables{
|
||||
Name: templatedPolicy.TemplateVariables.Name,
|
||||
}
|
||||
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"Name: %s\n", templatedPolicy.TemplateVariables.Name))
|
||||
}
|
||||
if len(templatedPolicy.Datacenters) > 0 {
|
||||
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"Datacenters: %s\n", strings.Join(templatedPolicy.Datacenters, ", ")))
|
||||
} else {
|
||||
buffer.WriteString(fmt.Sprintf(indent + WHITESPACE_2 + "Datacenters: all\n"))
|
||||
}
|
||||
policy, _ := tp.SyntheticPolicy(&entMeta)
|
||||
displaySyntheticPolicy(policy, &buffer, indent)
|
||||
}
|
||||
if len(token.ACLToken.TemplatedPolicies) > 0 {
|
||||
buffer.WriteString("Templated Policies:\n")
|
||||
|
||||
for _, templatedPolicy := range token.ACLToken.TemplatedPolicies {
|
||||
formatTemplatedPolicy(templatedPolicy, WHITESPACE_2)
|
||||
}
|
||||
}
|
||||
|
||||
formatRole := func(role api.ACLRole, indent string) {
|
||||
buffer.WriteString(fmt.Sprintf(indent+"Role Name: %s\n", role.Name))
|
||||
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"ID: %s\n", role.ID))
|
||||
|
@ -266,6 +302,13 @@ func (f *prettyFormatter) FormatTokenExpanded(token *api.ACLTokenExpanded) (stri
|
|||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
func displaySyntheticPolicy(policy *structs.ACLPolicy, buffer *bytes.Buffer, indent string) {
|
||||
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"Description: %s\n", policy.Description))
|
||||
buffer.WriteString(indent + WHITESPACE_2 + "Rules:")
|
||||
buffer.WriteString(strings.ReplaceAll(policy.Rules, "\n", "\n"+indent+WHITESPACE_4))
|
||||
buffer.WriteString("\n\n")
|
||||
}
|
||||
|
||||
func (f *prettyFormatter) FormatTokenList(tokens []*api.ACLTokenListEntry) (string, error) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
|
@ -335,6 +378,21 @@ func (f *prettyFormatter) formatTokenListEntry(token *api.ACLTokenListEntry) str
|
|||
buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter))
|
||||
}
|
||||
}
|
||||
|
||||
if len(token.TemplatedPolicies) > 0 {
|
||||
buffer.WriteString(fmt.Sprintln("Templated Policies:"))
|
||||
for _, templatedPolicy := range token.TemplatedPolicies {
|
||||
buffer.WriteString(fmt.Sprintf(" %s\n", templatedPolicy.TemplateName))
|
||||
if templatedPolicy.TemplateVariables != nil && templatedPolicy.TemplateVariables.Name != "" {
|
||||
buffer.WriteString(fmt.Sprintf(" Name: %s\n", templatedPolicy.TemplateVariables.Name))
|
||||
}
|
||||
if len(templatedPolicy.Datacenters) > 0 {
|
||||
buffer.WriteString(fmt.Sprintf(" Datacenters: %s\n", strings.Join(templatedPolicy.Datacenters, ", ")))
|
||||
} else {
|
||||
buffer.WriteString(" Datacenters: all\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
|
|
|
@ -106,6 +106,21 @@ func TestFormatToken(t *testing.T) {
|
|||
Datacenter: "middleearth-northwest",
|
||||
},
|
||||
},
|
||||
TemplatedPolicies: []*api.ACLTemplatedPolicy{
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &api.ACLTemplatedPolicyVariables{
|
||||
Name: "web",
|
||||
},
|
||||
Datacenters: []string{"middleearth-northwest", "somewhere-east"},
|
||||
},
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &api.ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -209,6 +224,21 @@ func TestFormatTokenList(t *testing.T) {
|
|||
Datacenter: "middleearth-northwest",
|
||||
},
|
||||
},
|
||||
TemplatedPolicies: []*api.ACLTemplatedPolicy{
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &api.ACLTemplatedPolicyVariables{
|
||||
Name: "web",
|
||||
},
|
||||
Datacenters: []string{"middleearth-northwest"},
|
||||
},
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &api.ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -444,6 +474,21 @@ var expandedTokenTestCases = map[string]testCase{
|
|||
Datacenter: "middleearth-northwest",
|
||||
},
|
||||
},
|
||||
TemplatedPolicies: []*api.ACLTemplatedPolicy{
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyServiceName,
|
||||
TemplateVariables: &api.ACLTemplatedPolicyVariables{
|
||||
Name: "web",
|
||||
},
|
||||
Datacenters: []string{"middleearth-northwest"},
|
||||
},
|
||||
{
|
||||
TemplateName: api.ACLTemplatedPolicyNodeName,
|
||||
TemplateVariables: &api.ACLTemplatedPolicyVariables{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -38,6 +38,24 @@
|
|||
"Datacenter": "middleearth-northwest"
|
||||
}
|
||||
],
|
||||
"TemplatedPolicies": [
|
||||
{
|
||||
"TemplateName": "builtin/service",
|
||||
"TemplateVariables": {
|
||||
"Name": "web"
|
||||
},
|
||||
"Datacenters": [
|
||||
"middleearth-northwest",
|
||||
"somewhere-east"
|
||||
]
|
||||
},
|
||||
{
|
||||
"TemplateName": "builtin/node",
|
||||
"TemplateVariables": {
|
||||
"Name": "api"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Local": false,
|
||||
"AuthMethod": "bar",
|
||||
"ExpirationTime": "2020-05-22T19:52:31Z",
|
||||
|
|
|
@ -19,3 +19,10 @@ Service Identities:
|
|||
gardener (Datacenters: middleearth-northwest)
|
||||
Node Identities:
|
||||
bagend (Datacenter: middleearth-northwest)
|
||||
Templated Policies:
|
||||
builtin/service
|
||||
Name: web
|
||||
Datacenters: middleearth-northwest, somewhere-east
|
||||
builtin/node
|
||||
Name: api
|
||||
Datacenters: all
|
||||
|
|
|
@ -16,3 +16,10 @@ Service Identities:
|
|||
gardener (Datacenters: middleearth-northwest)
|
||||
Node Identities:
|
||||
bagend (Datacenter: middleearth-northwest)
|
||||
Templated Policies:
|
||||
builtin/service
|
||||
Name: web
|
||||
Datacenters: middleearth-northwest, somewhere-east
|
||||
builtin/node
|
||||
Name: api
|
||||
Datacenters: all
|
||||
|
|
|
@ -181,6 +181,23 @@
|
|||
"Datacenter": "middleearth-northwest"
|
||||
}
|
||||
],
|
||||
"TemplatedPolicies": [
|
||||
{
|
||||
"TemplateName": "builtin/service",
|
||||
"TemplateVariables": {
|
||||
"Name": "web"
|
||||
},
|
||||
"Datacenters": [
|
||||
"middleearth-northwest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"TemplateName": "builtin/node",
|
||||
"TemplateVariables": {
|
||||
"Name": "api"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Local": false,
|
||||
"AuthMethod": "bar",
|
||||
"ExpirationTime": "2020-05-22T19:52:31Z",
|
||||
|
|
|
@ -52,6 +52,37 @@ Node Identities:
|
|||
policy = "read"
|
||||
}
|
||||
|
||||
Templated Policies:
|
||||
builtin/service
|
||||
Name: web
|
||||
Datacenters: middleearth-northwest
|
||||
Description: synthetic policy generated from templated policy: builtin/service
|
||||
Rules:
|
||||
service "web" {
|
||||
policy = "write"
|
||||
}
|
||||
service "web-sidecar-proxy" {
|
||||
policy = "write"
|
||||
}
|
||||
service_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
node_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
|
||||
builtin/node
|
||||
Name: api
|
||||
Datacenters: all
|
||||
Description: synthetic policy generated from templated policy: builtin/node
|
||||
Rules:
|
||||
node "api" {
|
||||
policy = "write"
|
||||
}
|
||||
service_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
|
||||
Roles:
|
||||
Role Name: shire
|
||||
ID: 3b0a78fe-b9c3-40de-b8ea-7d4d6674b366
|
||||
|
|
|
@ -49,6 +49,37 @@ Node Identities:
|
|||
policy = "read"
|
||||
}
|
||||
|
||||
Templated Policies:
|
||||
builtin/service
|
||||
Name: web
|
||||
Datacenters: middleearth-northwest
|
||||
Description: synthetic policy generated from templated policy: builtin/service
|
||||
Rules:
|
||||
service "web" {
|
||||
policy = "write"
|
||||
}
|
||||
service "web-sidecar-proxy" {
|
||||
policy = "write"
|
||||
}
|
||||
service_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
node_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
|
||||
builtin/node
|
||||
Name: api
|
||||
Datacenters: all
|
||||
Description: synthetic policy generated from templated policy: builtin/node
|
||||
Rules:
|
||||
node "api" {
|
||||
policy = "write"
|
||||
}
|
||||
service_prefix "" {
|
||||
policy = "read"
|
||||
}
|
||||
|
||||
Roles:
|
||||
Role Name: shire
|
||||
ID: 3b0a78fe-b9c3-40de-b8ea-7d4d6674b366
|
||||
|
|
|
@ -39,6 +39,23 @@
|
|||
"Datacenter": "middleearth-northwest"
|
||||
}
|
||||
],
|
||||
"TemplatedPolicies": [
|
||||
{
|
||||
"TemplateName": "builtin/service",
|
||||
"TemplateVariables": {
|
||||
"Name": "web"
|
||||
},
|
||||
"Datacenters": [
|
||||
"middleearth-northwest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"TemplateName": "builtin/node",
|
||||
"TemplateVariables": {
|
||||
"Name": "api"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Local": false,
|
||||
"AuthMethod": "bar",
|
||||
"ExpirationTime": "2020-05-22T19:52:31Z",
|
||||
|
|
|
@ -19,3 +19,10 @@ Service Identities:
|
|||
gardener (Datacenters: middleearth-northwest)
|
||||
Node Identities:
|
||||
bagend (Datacenter: middleearth-northwest)
|
||||
Templated Policies:
|
||||
builtin/service
|
||||
Name: web
|
||||
Datacenters: middleearth-northwest
|
||||
builtin/node
|
||||
Name: api
|
||||
Datacenters: all
|
||||
|
|
|
@ -16,3 +16,10 @@ Service Identities:
|
|||
gardener (Datacenters: middleearth-northwest)
|
||||
Node Identities:
|
||||
bagend (Datacenter: middleearth-northwest)
|
||||
Templated Policies:
|
||||
builtin/service
|
||||
Name: web
|
||||
Datacenters: middleearth-northwest
|
||||
builtin/node
|
||||
Name: api
|
||||
Datacenters: all
|
||||
|
|
|
@ -41,6 +41,11 @@ type cmd struct {
|
|||
nodeIdents []string
|
||||
appendNodeIdents []string
|
||||
appendServiceIdents []string
|
||||
appendTemplatedPolicy string
|
||||
replaceTemplatedPolicy string
|
||||
appendTemplatedPolicyFile string
|
||||
replaceTemplatedPolicyFile string
|
||||
templatedPolicyVariables []string
|
||||
description string
|
||||
showMeta bool
|
||||
format string
|
||||
|
@ -89,6 +94,19 @@ func (c *cmd) init() {
|
|||
c.flags.Var((*flags.AppendSliceValue)(&c.appendNodeIdents), "append-node-identity", "Name of a "+
|
||||
"node identity to use for this token. This token retains existing node identities. May be "+
|
||||
"specified multiple times. Format is NODENAME:DATACENTER")
|
||||
c.flags.Var((*flags.AppendSliceValue)(&c.templatedPolicyVariables), "var", "Templated policy variables."+
|
||||
" Must be used in combination with -replace-templated-policy or -append-templated-policy flags to specify required variables."+
|
||||
" May be specified multiple times with different variables."+
|
||||
" Format is VariableName:Value")
|
||||
c.flags.StringVar(&c.appendTemplatedPolicy, "append-templated-policy", "", "The templated policy name to attach to the token's existing templated policies."+
|
||||
"Use -var flag to specify variables when required. Token retains existing templated policies.")
|
||||
c.flags.StringVar(&c.replaceTemplatedPolicy, "replace-templated-policy", "", "The templated policy name to use replace token's existing templated policies."+
|
||||
" Use -var flag to specify variables when required. Overwrites token's existing templated policies.")
|
||||
c.flags.StringVar(&c.appendTemplatedPolicyFile, "append-templated-policy-file", "", "Path to a file containing templated policy names and variables."+
|
||||
" Works similarly to `-append-templated-policy`. The token retains existing templated policies.")
|
||||
c.flags.StringVar(&c.replaceTemplatedPolicyFile, "replace-templated-policy-file", "", "Path to a file containing templated policy names and variables."+
|
||||
" Works similarly to `-replace-templated-policy`. Overwrites the token's existing templated policies.")
|
||||
|
||||
c.flags.StringVar(
|
||||
&c.format,
|
||||
"format",
|
||||
|
@ -195,6 +213,24 @@ func (c *cmd) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
hasAppendTemplatedPolicies := len(c.appendTemplatedPolicy) > 0 || len(c.appendTemplatedPolicyFile) > 0
|
||||
hasReplaceTemplatedPolicies := len(c.replaceTemplatedPolicy) > 0 || len(c.replaceTemplatedPolicyFile) > 0
|
||||
|
||||
if hasReplaceTemplatedPolicies && hasAppendTemplatedPolicies {
|
||||
c.UI.Error("Cannot combine the use of -append-templated-policy flags with replace-templated-policy. " +
|
||||
"To set or overwrite existing templated policies, use -replace-templated-policy or -replace-templated-policy-file. " +
|
||||
"To append to existing templated policies, use -append-templated-policy or -append-templated-policy-file.")
|
||||
return 1
|
||||
}
|
||||
parsedTemplatedPolicies, err := acl.ExtractTemplatedPolicies(c.replaceTemplatedPolicy, c.replaceTemplatedPolicyFile, c.templatedPolicyVariables)
|
||||
if hasAppendTemplatedPolicies {
|
||||
parsedTemplatedPolicies, err = acl.ExtractTemplatedPolicies(c.appendTemplatedPolicy, c.appendTemplatedPolicyFile, c.templatedPolicyVariables)
|
||||
}
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if c.mergePolicies {
|
||||
c.UI.Warn("merge-policies is deprecated and will be removed in a future Consul version. " +
|
||||
"Use `append-policy-name` or `append-policy-id` instead.")
|
||||
|
@ -384,6 +420,12 @@ func (c *cmd) Run(args []string) int {
|
|||
t.NodeIdentities = parsedNodeIdents
|
||||
}
|
||||
|
||||
if hasReplaceTemplatedPolicies {
|
||||
t.TemplatedPolicies = parsedTemplatedPolicies
|
||||
} else {
|
||||
t.TemplatedPolicies = append(t.TemplatedPolicies, parsedTemplatedPolicies...)
|
||||
}
|
||||
|
||||
t, _, err = client.ACL().TokenUpdate(t, nil)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Failed to update token %s: %v", tok, err))
|
||||
|
@ -432,6 +474,8 @@ Usage: consul acl token update [options]
|
|||
$ consul acl token update -accessor-id abcd \
|
||||
-description "replication" \
|
||||
-policy-name "token-replication" \
|
||||
-role-name "db-updater"
|
||||
-role-name "db-updater" \
|
||||
-templated-policy "builtin/service" \
|
||||
-var "name:web"
|
||||
`
|
||||
)
|
||||
|
|
|
@ -120,6 +120,51 @@ func TestTokenUpdateCommand(t *testing.T) {
|
|||
require.ElementsMatch(t, expected, responseToken.NodeIdentities)
|
||||
})
|
||||
|
||||
t.Run("replace-templated-policy", func(t *testing.T) {
|
||||
token := create_token(
|
||||
t,
|
||||
client, &api.ACLToken{Description: "test", TemplatedPolicies: []*api.ACLTemplatedPolicy{
|
||||
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "api"}},
|
||||
}},
|
||||
&api.WriteOptions{Token: "root"},
|
||||
)
|
||||
|
||||
responseToken := run(t, []string{
|
||||
"-http-addr=" + a.HTTPAddr(),
|
||||
"-accessor-id=" + token.AccessorID,
|
||||
"-token=root",
|
||||
"-replace-templated-policy=builtin/node",
|
||||
"-description=test token",
|
||||
"-var=name:web",
|
||||
})
|
||||
|
||||
require.Len(t, responseToken.TemplatedPolicies, 1)
|
||||
require.Equal(t, api.ACLTemplatedPolicyNodeName, responseToken.TemplatedPolicies[0].TemplateName)
|
||||
require.Equal(t, "web", responseToken.TemplatedPolicies[0].TemplateVariables.Name)
|
||||
})
|
||||
t.Run("append-templated-policy", func(t *testing.T) {
|
||||
templatedPolicy := &api.ACLTemplatedPolicy{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "api"}}
|
||||
token := create_token(
|
||||
t,
|
||||
client, &api.ACLToken{Description: "test", TemplatedPolicies: []*api.ACLTemplatedPolicy{
|
||||
templatedPolicy,
|
||||
}},
|
||||
&api.WriteOptions{Token: "root"},
|
||||
)
|
||||
|
||||
responseToken := run(t, []string{
|
||||
"-http-addr=" + a.HTTPAddr(),
|
||||
"-accessor-id=" + token.AccessorID,
|
||||
"-token=root",
|
||||
"-append-templated-policy=builtin/node",
|
||||
"-description=test token",
|
||||
"-var=name:web",
|
||||
})
|
||||
|
||||
require.Len(t, responseToken.TemplatedPolicies, 2)
|
||||
require.ElementsMatch(t, responseToken.TemplatedPolicies,
|
||||
[]*api.ACLTemplatedPolicy{templatedPolicy, {TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "web"}}})
|
||||
})
|
||||
// update with policy by name
|
||||
t.Run("policy-name", func(t *testing.T) {
|
||||
token := create_token(t, client, &api.ACLToken{Description: "test"}, &api.WriteOptions{Token: "root"})
|
||||
|
|
|
@ -18,7 +18,7 @@ import (
|
|||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
func loadFromFile(path string) (string, error) {
|
||||
func LoadFromFile(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to read file: %v", err)
|
||||
|
@ -47,7 +47,7 @@ func LoadDataSource(data string, testStdin io.Reader) (string, error) {
|
|||
|
||||
switch data[0] {
|
||||
case '@':
|
||||
return loadFromFile(data[1:])
|
||||
return LoadFromFile(data[1:])
|
||||
case '-':
|
||||
if len(data) > 1 {
|
||||
return data, nil
|
||||
|
@ -67,7 +67,7 @@ func LoadDataSourceNoRaw(data string, testStdin io.Reader) (string, error) {
|
|||
return loadFromStdin(testStdin)
|
||||
}
|
||||
|
||||
return loadFromFile(data)
|
||||
return LoadFromFile(data)
|
||||
}
|
||||
|
||||
func ParseConfigEntry(data string) (api.ConfigEntry, error) {
|
||||
|
|
2
go.mod
2
go.mod
|
@ -97,6 +97,7 @@ require (
|
|||
github.com/ryanuber/columnize v2.1.2+incompatible
|
||||
github.com/shirou/gopsutil/v3 v3.22.8
|
||||
github.com/stretchr/testify v1.8.3
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/zclconf/go-cty v1.2.0
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
go.opentelemetry.io/otel v1.16.0
|
||||
|
@ -245,6 +246,7 @@ require (
|
|||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect
|
||||
github.com/vmware/govmomi v0.18.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.mongodb.org/mongo-driver v1.11.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
|
|
5
go.sum
5
go.sum
|
@ -937,8 +937,13 @@ github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
|
|||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
|
|
|
@ -190,6 +190,8 @@ require (
|
|||
github.com/testcontainers/testcontainers-go v0.22.0 // indirect
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/zclconf/go-cty v1.12.1 // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
go.mongodb.org/mongo-driver v1.11.0 // indirect
|
||||
|
|
|
@ -774,8 +774,13 @@ github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
|
|||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
|
|
@ -186,6 +186,8 @@ require (
|
|||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
go.mongodb.org/mongo-driver v1.11.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
|
|
|
@ -763,8 +763,13 @@ github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
|
|||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
|
Loading…
Reference in New Issue