sso: allow binding rules to create management ACL tokens. (#15860)

* sso: allow binding rules to create management ACL tokens.

* docs: update binding rule docs to detail management type addition.
This commit is contained in:
James Rasell 2023-01-26 09:57:44 +01:00 committed by GitHub
parent 0bb408bc10
commit 5d33891910
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 280 additions and 114 deletions

View File

@ -799,8 +799,8 @@ type ACLBindingRule struct {
Selector string
// BindType adjusts how this binding rule is applied at login time. The
// valid values are ACLBindingRuleBindTypeRole and
// ACLBindingRuleBindTypePolicy.
// valid values are ACLBindingRuleBindTypeRole,
// ACLBindingRuleBindTypePolicy, and ACLBindingRuleBindTypeManagement.
BindType string
// BindName is the target of the binding. Can be lightly templated using
@ -826,6 +826,10 @@ const (
// within the ACLBindingRule.BindName parameter, and will be the policy
// name.
ACLBindingRuleBindTypePolicy = "policy"
// ACLBindingRuleBindTypeManagement is the ACL binding rule bind type that
// will generate management ACL tokens when matched.
ACLBindingRuleBindTypeManagement = "management"
)
// ACLBindingRuleListStub is the stub object returned when performing a listing

View File

@ -53,11 +53,12 @@ Create Options:
-bind-type
Specifies adjusts how this binding rule is applied at login time to internal
Nomad objects. Valid options are "role" and "policy".
Nomad objects. Valid options are "role", "policy", or "management".
-bind-name
Specifies is the target of the binding used on selector match. This can be
lightly templated using HIL ${foo} syntax.
lightly templated using HIL ${foo} syntax. If the bind type is set to
"management", this should not be set.
-json
Output the ACL binding rule in a JSON format.
@ -74,10 +75,14 @@ func (a *ACLBindingRuleCreateCommand) AutocompleteFlags() complete.Flags {
"-description": complete.PredictAnything,
"-auth-method": complete.PredictAnything,
"-selector": complete.PredictAnything,
"-bind-type": complete.PredictSet("role", "policy"),
"-bind-name": complete.PredictAnything,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
"-bind-type": complete.PredictSet(
api.ACLBindingRuleBindTypeRole,
api.ACLBindingRuleBindTypePolicy,
api.ACLBindingRuleBindTypeManagement,
),
"-bind-name": complete.PredictAnything,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
})
}
@ -120,10 +125,6 @@ func (a *ACLBindingRuleCreateCommand) Run(args []string) int {
a.Ui.Error("ACL binding rule auth method must be specified using the -auth-method flag")
return 1
}
if a.bindName == "" {
a.Ui.Error("ACL binding rule bind name must be specified using the -bind-name flag")
return 1
}
if a.bindType == "" {
a.Ui.Error("ACL binding rule bind type must be specified using the -bind-type flag")
return 1

View File

@ -48,13 +48,6 @@ func TestACLBindingRuleCreateCommand_Run(t *testing.T) {
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
must.Eq(t, 1, cmd.Run([]string{"-address=" + url, "-auth-method=auth0"}))
must.StrContains(t, ui.ErrorWriter.String(),
"ACL binding rule bind name must be specified using the -bind-name flag")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
must.Eq(t, 1, cmd.Run([]string{"-address=" + url, "-auth-method=auth0", "-bind-name=engineering"}))
must.StrContains(t, ui.ErrorWriter.String(),
"ACL binding rule bind type must be specified using the -bind-type flag")

View File

@ -53,11 +53,12 @@ Update Options:
-bind-type
Specifies adjusts how this binding rule is applied at login time to internal
Nomad objects. Valid options are "role" and "policy".
Nomad objects. Valid options are "role", "policy", or "management".
-bind-name
Specifies is the target of the binding used on selector match. This can be
lightly templated using HIL ${foo} syntax.
lightly templated using HIL ${foo} syntax. If the bind type is set to
management, this should not be set.
-json
Output the ACL binding rule in a JSON format.
@ -74,10 +75,14 @@ func (a *ACLBindingRuleUpdateCommand) AutocompleteFlags() complete.Flags {
complete.Flags{
"-description": complete.PredictAnything,
"-selector": complete.PredictAnything,
"-bind-type": complete.PredictSet("role", "policy"),
"-bind-name": complete.PredictAnything,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
"-bind-type": complete.PredictSet(
api.ACLBindingRuleBindTypeRole,
api.ACLBindingRuleBindTypePolicy,
api.ACLBindingRuleBindTypeManagement,
),
"-bind-name": complete.PredictAnything,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
})
}
@ -138,10 +143,6 @@ func (a *ACLBindingRuleUpdateCommand) Run(args []string) int {
switch a.noMerge {
case true:
if a.bindName == "" {
a.Ui.Error("ACL binding rule bind name must be specified using the -bind-name flag")
return 1
}
if a.bindType == "" {
a.Ui.Error("ACL binding rule bind type must be specified using the -bind-type flag")
return 1
@ -159,7 +160,7 @@ func (a *ACLBindingRuleUpdateCommand) Run(args []string) int {
// Check that the operator specified at least one flag to update the ACL
// binding rule with.
if a.description == "" && a.selector == "" && a.bindType == "" && a.bindName == "" {
a.Ui.Error("Please provide all required flags to update the ACL binding rule")
a.Ui.Error("Please provide at least one update for the ACL binding rule")
a.Ui.Error(commandErrorText(a))
return 1
}

View File

@ -76,7 +76,7 @@ func TestACLBindingRuleUpdateCommand_Run(t *testing.T) {
// Try a merge update without setting any parameters to update.
code = cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, aclBindingRule.ID})
must.Eq(t, 1, code)
must.StrContains(t, ui.ErrorWriter.String(), "Please provide all required flags to update the ACL binding rule")
must.StrContains(t, ui.ErrorWriter.String(), "Please provide at least one update for the ACL binding rule")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
@ -95,15 +95,6 @@ func TestACLBindingRuleUpdateCommand_Run(t *testing.T) {
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Try updating the role using no-merge without setting the required flags.
code = cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", aclBindingRule.ID})
must.Eq(t, 1, code)
must.StrContains(t, ui.ErrorWriter.String(),
"ACL binding rule bind name must be specified using the -bind-name flag")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
code = cmd.Run([]string{
"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", "-bind-name=engineering-updated", aclBindingRule.ID})
must.Eq(t, 1, code)

View File

@ -35,8 +35,9 @@ type BinderStateStore interface {
// Bindings contains the ACL roles and policies to be assigned to the created
// token.
type Bindings struct {
Roles []*structs.ACLTokenRoleLink
Policies []string
Management bool
Roles []*structs.ACLTokenRoleLink
Policies []string
}
// None indicates that the resulting bindings would not give the created token
@ -110,6 +111,11 @@ func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, identity *Identity) (*B
if policy != nil {
bindings.Policies = append(bindings.Policies, policy.Name)
}
case structs.ACLBindingRuleBindTypeManagement:
bindings.Management = true
bindings.Policies = nil
bindings.Roles = nil
return &bindings, nil
}
}
@ -134,6 +140,8 @@ func computeBindName(bindType, bindName string, claimMappings map[string]string)
valid = structs.ValidPolicyName.MatchString(bindName)
case structs.ACLBindingRuleBindTypeRole:
valid = structs.ValidACLRoleName.MatchString(bindName)
case structs.ACLManagementToken:
valid = true
default:
return "", false, fmt.Errorf("unknown binding rule bind type: %s", bindType)
}

View File

@ -58,6 +58,13 @@ func TestBinder_Bind(t *testing.T) {
BindName: otherRole.Name,
AuthMethod: authMethod.Name,
},
{
ID: uuid.Generate(),
Selector: "role==admin",
BindType: structs.ACLBindingRuleBindTypeManagement,
BindName: "",
AuthMethod: authMethod.Name,
},
}
must.NoError(t, testStore.UpsertACLBindingRules(0, bindingRules, true))
@ -69,16 +76,16 @@ func TestBinder_Bind(t *testing.T) {
wantErr bool
}{
{
"empty identity",
authMethod,
&Identity{},
&Bindings{},
false,
name: "empty identity",
authMethod: authMethod,
identity: &Identity{},
want: &Bindings{},
wantErr: false,
},
{
"role",
authMethod,
&Identity{
name: "role",
authMethod: authMethod,
identity: &Identity{
Claims: map[string]string{
"role": "engineer",
"language": "go",
@ -87,8 +94,20 @@ func TestBinder_Bind(t *testing.T) {
"editor": "vim",
},
},
&Bindings{Roles: []*structs.ACLTokenRoleLink{{ID: targetRole.ID}}},
false,
want: &Bindings{Roles: []*structs.ACLTokenRoleLink{{ID: targetRole.ID}}},
wantErr: false,
},
{
name: "management",
authMethod: authMethod,
identity: &Identity{
Claims: map[string]string{
"role": "admin",
},
ClaimMappings: map[string]string{},
},
want: &Bindings{Management: true},
wantErr: false,
},
}
for _, tt := range tests {
@ -116,22 +135,31 @@ func Test_computeBindName(t *testing.T) {
wantErr bool
}{
{
"valid bind name and type",
structs.ACLBindingRuleBindTypeRole,
"cluster-admin",
map[string]string{"cluster-admin": "root"},
"cluster-admin",
true,
false,
name: "valid bind name and type",
bindType: structs.ACLBindingRuleBindTypeRole,
bindName: "cluster-admin",
claimMappings: map[string]string{"cluster-admin": "root"},
wantName: "cluster-admin",
wantTrue: true,
wantErr: false,
},
{
"invalid type",
"amazing",
"cluster-admin",
map[string]string{"cluster-admin": "root"},
"",
false,
true,
name: "valid management",
bindType: structs.ACLBindingRuleBindTypeManagement,
bindName: "",
claimMappings: map[string]string{"cluster-admin": "root"},
wantName: "",
wantTrue: true,
wantErr: false,
},
{
name: "invalid type",
bindType: "amazing",
bindName: "cluster-admin",
claimMappings: map[string]string{"cluster-admin": "root"},
wantName: "",
wantTrue: false,
wantErr: true,
},
}
for _, tt := range tests {

View File

@ -2693,7 +2693,7 @@ func (a *ACL) OIDCCompleteAuth(
if err != nil {
return err
}
if tokenBindings.None() {
if tokenBindings.None() && !tokenBindings.Management {
return structs.NewErrRPCCoded(http.StatusBadRequest, "no role or policy bindings matched")
}
@ -2701,17 +2701,22 @@ func (a *ACL) OIDCCompleteAuth(
// logic, so we do not want to call Raft directly or copy that here. In the
// future we should try and extract out the logic into an interface, or at
// least a separate function.
token := structs.ACLToken{
Name: "OIDC-" + authMethod.Name,
Global: authMethod.TokenLocalityIsGlobal(),
ExpirationTTL: authMethod.MaxTokenTTL,
}
if tokenBindings.Management {
token.Type = structs.ACLManagementToken
} else {
token.Type = structs.ACLClientToken
token.Policies = tokenBindings.Policies
token.Roles = tokenBindings.Roles
}
tokenUpsertRequest := structs.ACLTokenUpsertRequest{
Tokens: []*structs.ACLToken{
{
Name: "OIDC-" + authMethod.Name,
Type: structs.ACLClientToken,
Policies: tokenBindings.Policies,
Roles: tokenBindings.Roles,
Global: authMethod.TokenLocalityIsGlobal(),
ExpirationTTL: authMethod.MaxTokenTTL,
},
},
Tokens: []*structs.ACLToken{&token},
WriteRequest: structs.WriteRequest{
Region: a.srv.Region(),
AuthToken: a.srv.getLeaderAcl(),

View File

@ -3701,4 +3701,35 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
must.Len(t, 1, completeAuthResp4.ACLToken.Roles)
must.Eq(t, mockACLRole.Name, completeAuthResp4.ACLToken.Roles[0].Name)
must.Eq(t, mockACLRole.ID, completeAuthResp4.ACLToken.Roles[0].ID)
// Create a binding rule which generates management tokens. This should
// override the other rules, giving us a management token when we next
// log in.
mockBindingRule3 := mock.ACLBindingRule()
mockBindingRule3.AuthMethod = mockedAuthMethod.Name
mockBindingRule3.BindType = structs.ACLBindingRuleBindTypeManagement
mockBindingRule3.Selector = "engineering in list.policies"
mockBindingRule3.BindName = ""
must.NoError(t, testServer.fsm.State().UpsertACLBindingRules(
50, []*structs.ACLBindingRule{mockBindingRule3}, true))
completeAuthReq5 := structs.ACLOIDCCompleteAuthRequest{
AuthMethodName: mockedAuthMethod.Name,
ClientNonce: "fsSPuaodKevKfDU3IeXa",
State: "st_someweirdstateid",
Code: "codeABC",
RedirectURI: mockedAuthMethod.Config.AllowedRedirectURIs[0],
WriteRequest: structs.WriteRequest{
Region: DefaultRegion,
},
}
var completeAuthResp5 structs.ACLOIDCCompleteAuthResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq5, &completeAuthResp5)
must.NoError(t, err)
must.NotNil(t, completeAuthResp4.ACLToken)
must.Len(t, 0, completeAuthResp5.ACLToken.Policies)
must.Len(t, 0, completeAuthResp5.ACLToken.Roles)
must.Eq(t, structs.ACLManagementToken, completeAuthResp5.ACLToken.Type)
}

View File

@ -966,8 +966,8 @@ type ACLBindingRule struct {
Selector string
// BindType adjusts how this binding rule is applied at login time. The
// valid values are ACLBindingRuleBindTypeRole and
// ACLBindingRuleBindTypePolicy.
// valid values are ACLBindingRuleBindTypeRole,
// ACLBindingRuleBindTypePolicy, and ACLBindingRuleBindTypeManagement.
BindType string
// BindName is the target of the binding. Can be lightly templated using
@ -998,6 +998,10 @@ const (
// within the ACLBindingRule.BindName parameter, and will be the policy
// name.
ACLBindingRuleBindTypePolicy = "policy"
// ACLBindingRuleBindTypeManagement is the ACL binding rule bind type that
// will generate management ACL tokens when matched.
ACLBindingRuleBindTypeManagement = "management"
)
// Canonicalize performs basic canonicalization on the ACL token object. It is
@ -1028,21 +1032,26 @@ func (a *ACLBindingRule) Validate() error {
if a.AuthMethod == "" {
mErr.Errors = append(mErr.Errors, errors.New("auth method is missing"))
}
if a.BindName == "" {
mErr.Errors = append(mErr.Errors, errors.New("bind name is missing"))
}
if len(a.Description) > maxACLBindingRuleDescriptionLength {
mErr.Errors = append(mErr.Errors, fmt.Errorf("description longer than %d", maxACLRoleDescriptionLength))
}
if a.BindType == "" {
// Depending on the bind type, we have some specific validation. Catching
// the empty string also provides easier to understand feedback to the
// user.
switch a.BindType {
case "":
mErr.Errors = append(mErr.Errors, errors.New("bind type is missing"))
} else {
switch a.BindType {
case ACLBindingRuleBindTypeRole, ACLBindingRuleBindTypePolicy: // fall-through.
default:
mErr.Errors = append(mErr.Errors, fmt.Errorf("unsupported bind type: %q", a.BindType))
case ACLBindingRuleBindTypeRole, ACLBindingRuleBindTypePolicy:
if a.BindName == "" {
mErr.Errors = append(mErr.Errors, errors.New("bind name is missing"))
}
case ACLBindingRuleBindTypeManagement:
if a.BindName != "" {
mErr.Errors = append(mErr.Errors, errors.New("bind name should be empty"))
}
default:
mErr.Errors = append(mErr.Errors, fmt.Errorf("unsupported bind type: %q", a.BindType))
}
return mErr.ErrorOrNil()

View File

@ -1,10 +1,12 @@
package structs
import (
"errors"
"fmt"
"testing"
"time"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/hashicorp/nomad/helper/uuid"
@ -1192,25 +1194,115 @@ func TestACLBindingRule_Canonicalize(t *testing.T) {
func TestACLBindingRule_Validate(t *testing.T) {
ci.Parallel(t)
// Quite possibly the most invalid binding rule to have ever existed.
totallyInvalidACLBindingRule := ACLBindingRule{
Description: uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate() +
uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate(),
AuthMethod: "",
BindType: "",
BindName: "",
testCases := []struct {
name string
inputACLBindingRule *ACLBindingRule
expectedError error
}{
{
name: "valid policy type rule",
inputACLBindingRule: &ACLBindingRule{
Description: "some short description",
AuthMethod: "auth0",
Selector: "group-name in list.groups",
BindType: ACLBindingRuleBindTypePolicy,
BindName: "some-policy-name",
},
expectedError: nil,
},
{
name: "invalid policy type rule",
inputACLBindingRule: &ACLBindingRule{
Description: "some short description",
AuthMethod: "auth0",
Selector: "group-name in list.groups",
BindType: ACLBindingRuleBindTypePolicy,
BindName: "",
},
expectedError: &multierror.Error{
Errors: []error{
errors.New("bind name is missing"),
},
},
},
{
name: "valid role type rule",
inputACLBindingRule: &ACLBindingRule{
Description: "some short description",
AuthMethod: "auth0",
Selector: "group-name in list.groups",
BindType: ACLBindingRuleBindTypeRole,
BindName: "some-role-name",
},
expectedError: nil,
},
{
name: "invalid role type rule",
inputACLBindingRule: &ACLBindingRule{
Description: "some short description",
AuthMethod: "auth0",
Selector: "group-name in list.groups",
BindType: ACLBindingRuleBindTypeRole,
BindName: "",
},
expectedError: &multierror.Error{
Errors: []error{
errors.New("bind name is missing"),
},
},
},
{
name: "valid management type rule",
inputACLBindingRule: &ACLBindingRule{
Description: "some short description",
AuthMethod: "auth0",
Selector: "group-name in list.groups",
BindType: ACLBindingRuleBindTypeManagement,
BindName: "",
},
expectedError: nil,
},
{
name: "invalid management type rule",
inputACLBindingRule: &ACLBindingRule{
Description: "some short description",
AuthMethod: "auth0",
Selector: "group-name in list.groups",
BindType: ACLBindingRuleBindTypeManagement,
BindName: "some-name",
},
expectedError: &multierror.Error{
Errors: []error{
errors.New("bind name should be empty"),
},
},
},
{
name: "invalid all",
inputACLBindingRule: &ACLBindingRule{
Description: uuid.Generate() + uuid.Generate() + uuid.Generate() +
uuid.Generate() + uuid.Generate() + uuid.Generate() +
uuid.Generate() + uuid.Generate(),
AuthMethod: "",
Selector: "group-name in list.groups",
BindType: "",
BindName: "",
},
expectedError: &multierror.Error{
Errors: []error{
errors.New("auth method is missing"),
errors.New("description longer than 256"),
errors.New("bind type is missing"),
},
},
},
}
err := totallyInvalidACLBindingRule.Validate()
must.StrContains(t, err.Error(), "auth method is missing")
must.StrContains(t, err.Error(), "bind name is missing")
must.StrContains(t, err.Error(), "description longer than 256")
must.StrContains(t, err.Error(), "bind type is missing")
// Update the bind type, so we get the alternative error when this is not
// empty, but incorrectly set.
totallyInvalidACLBindingRule.BindType = "service"
err = totallyInvalidACLBindingRule.Validate()
must.StrContains(t, err.Error(), `unsupported bind type: "service"`)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
must.Eq(t, tc.expectedError, tc.inputACLBindingRule.Validate())
})
}
}
func TestACLBindingRule_Merge(t *testing.T) {

View File

@ -130,11 +130,12 @@ The table below shows this endpoint's support for
optional and when not set, provides a catch-all rule.
- `BindType` `(string: <required>)` - Adjusts how this binding rule is applied
at login time. Valid values are `role` and `policy`.
at login time. Valid values are `role`, `policy`, and `management`.
- `BindName` `(string: <required>)` - Target of the binding. Can be lightly
templated using HIL ${foo} syntax from available field names. How it is used
depends on the BindType.
templated using HIL ${foo} syntax from available field names. If the bind
type is set to `management`, this should not be set. How it is used depends
on the BindType.
### Sample Payload
@ -202,11 +203,11 @@ queries](/nomad/api-docs#blocking-queries) and [required ACLs](/nomad/api-docs#a
optional and when not set, provides a catch-all rule.
- `BindType` `(string: "")` - Adjusts how this binding rule is applied at login
time. Valid values are `role` and `policy`.
time. Valid values are `role`, `policy`, and `management`.
- `BindName` `(string: "")` - Target of the binding. Can be lightly templated
using HIL ${foo} syntax from available field names. How it is used depends on
the BindType.
the BindType.
### Sample Payload

View File

@ -33,10 +33,11 @@ via flags detailed below.
attributes returned from the auth method during login.
- `-bind-type`: Specifies adjusts how this binding rule is applied at login time
to internal Nomad objects. Valid options are `role` and `policy`.
to internal Nomad objects. Valid options are `role`, `policy`, and `management`.
- `-bind-name`: Specifies is the target of the binding used on selector match.
This can be lightly templated using HIL `${foo}` syntax.
This can be lightly templated using HIL `${foo}` syntax. If the bind type is
set to `management`, this should not be set.
- `-json`: Output the ACL binding-rule in a JSON format.

View File

@ -29,10 +29,11 @@ The `acl binding-rule update` command requires an existing rule's ID.
attributes returned from the binding rule during login.
- `-bind-type`: Specifies adjusts how this binding rule is applied at login time
to internal Nomad objects. Valid options are `role` and `policy`.
to internal Nomad objects. Valid options are `role`, `policy`, and `management`.
- `-bind-name`: Specifies is the target of the binding used on selector match.
This can be lightly templated using HIL `${foo}` syntax.
This can be lightly templated using HIL `${foo}` syntax. If the bind type is
set to `management`, this should not be set.
- `-json`: Output the ACL binding-rule in a JSON format.