acl: RPC endpoints for JWT auth (#15918)
This commit is contained in:
parent
a9230fb0b7
commit
e48c48e89b
|
@ -99,6 +99,7 @@ rules:
|
|||
- pattern-not: 'structs.ACLListAuthMethodsRPCMethod'
|
||||
- pattern-not: 'structs.ACLOIDCAuthURLRPCMethod'
|
||||
- pattern-not: 'structs.ACLOIDCCompleteAuthRPCMethod'
|
||||
- pattern-not: 'structs.ACLLoginRPCMethod'
|
||||
- pattern-not: '"CSIPlugin.Get"'
|
||||
- pattern-not: '"CSIPlugin.List"'
|
||||
- pattern-not: '"Status.Leader"'
|
||||
|
|
|
@ -867,7 +867,7 @@ func (s *HTTPServer) ACLOIDCCompleteAuthRequest(resp http.ResponseWriter, req *h
|
|||
return nil, CodedError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
var out structs.ACLOIDCCompleteAuthResponse
|
||||
var out structs.ACLLoginResponse
|
||||
if err := s.agent.RPC(structs.ACLOIDCCompleteAuthRPCMethod, &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1122,7 +1122,7 @@ func TestHTTPServer_ACLAuthMethodListRequest(t *testing.T) {
|
|||
|
||||
// Upsert two auth-methods into state.
|
||||
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
||||
10, []*structs.ACLAuthMethod{mock.ACLAuthMethod(), mock.ACLAuthMethod()}))
|
||||
10, []*structs.ACLAuthMethod{mock.ACLOIDCAuthMethod(), mock.ACLOIDCAuthMethod()}))
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/auth-methods", nil)
|
||||
|
@ -1198,7 +1198,7 @@ func TestHTTPServer_ACLAuthMethodRequest(t *testing.T) {
|
|||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create a mock auth-method to use in the request body.
|
||||
mockACLAuthMethod := mock.ACLAuthMethod()
|
||||
mockACLAuthMethod := mock.ACLOIDCAuthMethod()
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodPut, "/v1/acl/auth-method", encodeReq(mockACLAuthMethod))
|
||||
|
@ -1269,7 +1269,7 @@ func TestHTTPServer_ACLAuthMethodSpecificRequest(t *testing.T) {
|
|||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create a mock auth-method and put directly into state.
|
||||
mockACLAuthMethod := mock.ACLAuthMethod()
|
||||
mockACLAuthMethod := mock.ACLOIDCAuthMethod()
|
||||
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
||||
20, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
||||
|
||||
|
@ -1294,7 +1294,7 @@ func TestHTTPServer_ACLAuthMethodSpecificRequest(t *testing.T) {
|
|||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create a mock auth-method and put directly into state.
|
||||
mockACLAuthMethod := mock.ACLAuthMethod()
|
||||
mockACLAuthMethod := mock.ACLOIDCAuthMethod()
|
||||
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
||||
20, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
||||
|
||||
|
@ -1499,7 +1499,7 @@ func TestHTTPServer_ACLBindingRuleRequest(t *testing.T) {
|
|||
|
||||
// Upsert the auth method that the binding rule will associate
|
||||
// with.
|
||||
mockACLAuthMethod := mock.ACLAuthMethod()
|
||||
mockACLAuthMethod := mock.ACLOIDCAuthMethod()
|
||||
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
||||
10, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
||||
|
||||
|
@ -1607,7 +1607,7 @@ func TestHTTPServer_ACLBindingRuleSpecificRequest(t *testing.T) {
|
|||
|
||||
// Upsert the auth method that the binding rule will associate
|
||||
// with.
|
||||
mockACLAuthMethod := mock.ACLAuthMethod()
|
||||
mockACLAuthMethod := mock.ACLOIDCAuthMethod()
|
||||
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
||||
10, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
||||
|
||||
|
@ -1716,7 +1716,7 @@ func TestHTTPServer_ACLOIDCAuthURLRequest(t *testing.T) {
|
|||
|
||||
// Generate and upsert an ACL auth method for use. Certain values must be
|
||||
// taken from the cap OIDC provider just like real world use.
|
||||
mockedAuthMethod := mock.ACLAuthMethod()
|
||||
mockedAuthMethod := mock.ACLOIDCAuthMethod()
|
||||
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
||||
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
||||
mockedAuthMethod.Config.SigningAlgs = []string{"ES256"}
|
||||
|
@ -1799,7 +1799,7 @@ func TestHTTPServer_ACLOIDCCompleteAuthRequest(t *testing.T) {
|
|||
|
||||
// Generate and upsert an ACL auth method for use. Certain values must be
|
||||
// taken from the cap OIDC provider just like real world use.
|
||||
mockedAuthMethod := mock.ACLAuthMethod()
|
||||
mockedAuthMethod := mock.ACLOIDCAuthMethod()
|
||||
mockedAuthMethod.Config.BoundAudiences = []string{"mock"}
|
||||
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
||||
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package oidc
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/hil"
|
||||
"github.com/hashicorp/hil/ast"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package oidc
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -19,7 +19,7 @@ func TestBinder_Bind(t *testing.T) {
|
|||
testBind := NewBinder(testStore)
|
||||
|
||||
// create an authMethod method and insert into the state store
|
||||
authMethod := mock.ACLAuthMethod()
|
||||
authMethod := mock.ACLOIDCAuthMethod()
|
||||
must.NoError(t, testStore.UpsertACLAuthMethods(0, []*structs.ACLAuthMethod{authMethod}))
|
||||
|
||||
// create some roles and insert into the state store
|
|
@ -1,4 +1,4 @@
|
|||
package oidc
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
|
@ -1,4 +1,4 @@
|
|||
package oidc
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
|
@ -1,4 +1,4 @@
|
|||
package oidc
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
|
@ -1,9 +1,10 @@
|
|||
package oidc
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/shoenig/test/must"
|
||||
"testing"
|
||||
|
||||
"github.com/shoenig/test/must"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
|
@ -0,0 +1,125 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/cap/jwt"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
// Validate performs token signature verification and JWT header validation,
|
||||
// and returns a list of claims or an error in case any validation or signature
|
||||
// verification fails.
|
||||
func Validate(ctx context.Context, token string, methodConf *structs.ACLAuthMethodConfig) (map[string]any, error) {
|
||||
var (
|
||||
keySet jwt.KeySet
|
||||
err error
|
||||
)
|
||||
|
||||
// JWT validation can happen in 3 ways:
|
||||
// - via embedded public keys, locally
|
||||
// - via JWKS
|
||||
// - or via OIDC provider
|
||||
if len(methodConf.JWTValidationPubKeys) != 0 {
|
||||
keySet, err = usingStaticKeys(methodConf.JWTValidationPubKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if methodConf.JWKSURL != "" {
|
||||
keySet, err = usingJWKS(ctx, methodConf.JWKSURL, methodConf.JWKSCACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if methodConf.OIDCDiscoveryURL != "" {
|
||||
keySet, err = usingOIDC(ctx, methodConf.OIDCDiscoveryURL, methodConf.DiscoveryCaPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// SigningAlgs field is a string, we need to convert it to a type the go-jwt
|
||||
// accepts in order to validate.
|
||||
toAlgFn := func(m string) jwt.Alg { return jwt.Alg(m) }
|
||||
algorithms := helper.ConvertSlice(methodConf.SigningAlgs, toAlgFn)
|
||||
|
||||
expected := jwt.Expected{
|
||||
Audiences: methodConf.BoundAudiences,
|
||||
SigningAlgorithms: algorithms,
|
||||
NotBeforeLeeway: methodConf.NotBeforeLeeway,
|
||||
ExpirationLeeway: methodConf.ExpirationLeeway,
|
||||
ClockSkewLeeway: methodConf.ClockSkewLeeway,
|
||||
}
|
||||
|
||||
validator, err := jwt.NewValidator(keySet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, err := validator.Validate(ctx, token, expected)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to verify signature of JWT token: %v", err)
|
||||
}
|
||||
|
||||
// validate issuer manually, because we allow users to specify an array
|
||||
if len(methodConf.BoundIssuer) > 0 {
|
||||
if _, ok := claims["iss"]; !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"auth method specifies BoundIssuers but the provided token does not contain issuer information",
|
||||
)
|
||||
}
|
||||
if iss, ok := claims["iss"].(string); !ok {
|
||||
return nil, fmt.Errorf("unable to read iss property of provided token")
|
||||
} else if !slices.Contains(methodConf.BoundIssuer, iss) {
|
||||
return nil, fmt.Errorf("invalid JWT issuer: %v", claims["iss"])
|
||||
}
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func usingStaticKeys(keys []string) (jwt.KeySet, error) {
|
||||
var parsedKeys []crypto.PublicKey
|
||||
for _, v := range keys {
|
||||
key, err := jwt.ParsePublicKeyPEM([]byte(v))
|
||||
parsedKeys = append(parsedKeys, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse public key for JWT auth: %v", err)
|
||||
}
|
||||
}
|
||||
return jwt.NewStaticKeySet(parsedKeys)
|
||||
}
|
||||
|
||||
func usingJWKS(ctx context.Context, jwksurl, jwkscapem string) (jwt.KeySet, error) {
|
||||
// Measure the JWKS endpoint performance.
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "jwt", "jwks"}, time.Now())
|
||||
|
||||
keySet, err := jwt.NewJSONWebKeySet(ctx, jwksurl, jwkscapem)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get validation keys from JWKS: %v", err)
|
||||
}
|
||||
return keySet, nil
|
||||
}
|
||||
|
||||
func usingOIDC(ctx context.Context, oidcurl string, oidccapem []string) (jwt.KeySet, error) {
|
||||
// Measure the OIDC endpoint performance.
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "jwt", "oidc_jwt"}, time.Now())
|
||||
|
||||
// TODO why do we have DiscoverCaPem as an array but JWKSCaPem as a single string?
|
||||
pem := ""
|
||||
if len(oidccapem) > 0 {
|
||||
pem = oidccapem[0]
|
||||
}
|
||||
|
||||
keySet, err := jwt.NewOIDCDiscoveryKeySet(ctx, oidcurl, pem)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get validation keys from OIDC provider: %v", err)
|
||||
}
|
||||
return keySet, nil
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/hashicorp/cap/oidc"
|
||||
"github.com/shoenig/test/must"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
iat := time.Now().Unix()
|
||||
nbf := time.Now().Unix()
|
||||
exp := time.Now().Add(time.Hour).Unix()
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"foo": "bar",
|
||||
"issuer": "test suite",
|
||||
"float": 3.14,
|
||||
"iat": iat,
|
||||
"nbf": nbf,
|
||||
"exp": exp,
|
||||
}
|
||||
|
||||
wantedClaims := map[string]any{
|
||||
"foo": "bar",
|
||||
"issuer": "test suite",
|
||||
"float": 3.14,
|
||||
"iat": float64(iat),
|
||||
"nbf": float64(nbf),
|
||||
"exp": float64(exp),
|
||||
}
|
||||
|
||||
// appended to JWKS test server URL
|
||||
wellKnownJWKS := "/.well-known/jwks.json"
|
||||
|
||||
// generate a key pair, so that we can use it for consistent signing and
|
||||
// set it as our test server key
|
||||
rsaKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
must.NoError(t, err)
|
||||
|
||||
token, _, err := mock.SampleJWTokenWithKeys(claims, rsaKey)
|
||||
must.NoError(t, err)
|
||||
tokenWithNoClaims, pubKeyPem, err := mock.SampleJWTokenWithKeys(nil, rsaKey)
|
||||
must.NoError(t, err)
|
||||
|
||||
// make an expired token...
|
||||
expired := time.Now().Add(-time.Hour).Unix()
|
||||
expiredClaims := jwt.MapClaims{"iat": iat, "nbf": nbf, "exp": expired}
|
||||
expiredToken, _, err := mock.SampleJWTokenWithKeys(expiredClaims, rsaKey)
|
||||
must.NoError(t, err)
|
||||
|
||||
// ...and one with invalid issuer, too
|
||||
invalidIssuer := jwt.MapClaims{"iat": iat, "nbf": nbf, "exp": exp, "iss": "hashicorp vault"}
|
||||
invalidIssuerToken, _, err := mock.SampleJWTokenWithKeys(invalidIssuer, rsaKey)
|
||||
must.NoError(t, err)
|
||||
|
||||
testServer := oidc.StartTestProvider(t)
|
||||
defer testServer.Stop()
|
||||
|
||||
keyID := strconv.Itoa(int(time.Now().Unix()))
|
||||
testServer.SetSigningKeys(rsaKey, rsaKey.Public(), oidc.RS256, keyID)
|
||||
tokenSignedWithRemoteServerKeys, _, err := mock.SampleJWTokenWithKeys(claims, rsaKey)
|
||||
must.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
conf *structs.ACLAuthMethodConfig
|
||||
want map[string]interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid signature, local verification",
|
||||
token: token,
|
||||
conf: &structs.ACLAuthMethodConfig{JWTValidationPubKeys: []string{pubKeyPem}},
|
||||
want: wantedClaims,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid signature, local verification, no claims",
|
||||
token: tokenWithNoClaims,
|
||||
conf: &structs.ACLAuthMethodConfig{JWTValidationPubKeys: []string{pubKeyPem}},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid signature, JWKS verification",
|
||||
token: tokenSignedWithRemoteServerKeys,
|
||||
conf: &structs.ACLAuthMethodConfig{
|
||||
JWKSURL: testServer.Addr() + wellKnownJWKS,
|
||||
JWKSCACert: testServer.CACert(),
|
||||
},
|
||||
want: wantedClaims,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid signature, OIDC verification",
|
||||
token: tokenSignedWithRemoteServerKeys,
|
||||
conf: &structs.ACLAuthMethodConfig{
|
||||
OIDCDiscoveryURL: testServer.Addr(),
|
||||
DiscoveryCaPem: []string{testServer.CACert()},
|
||||
},
|
||||
want: wantedClaims,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "expired token, local verification",
|
||||
token: expiredToken,
|
||||
conf: &structs.ACLAuthMethodConfig{JWTValidationPubKeys: []string{pubKeyPem}},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid issuer, local verification",
|
||||
token: invalidIssuerToken,
|
||||
conf: &structs.ACLAuthMethodConfig{
|
||||
JWTValidationPubKeys: []string{pubKeyPem},
|
||||
BoundIssuer: []string{"test suite"},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Validate(context.Background(), tt.token, tt.conf)
|
||||
if !tt.wantErr {
|
||||
must.Nil(t, err, must.Sprint(err))
|
||||
}
|
||||
must.Eq(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -19,6 +19,8 @@ import (
|
|||
policy "github.com/hashicorp/nomad/acl"
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/lib/auth"
|
||||
"github.com/hashicorp/nomad/lib/auth/jwt"
|
||||
"github.com/hashicorp/nomad/lib/auth/oidc"
|
||||
"github.com/hashicorp/nomad/nomad/state"
|
||||
"github.com/hashicorp/nomad/nomad/state/paginator"
|
||||
|
@ -43,6 +45,10 @@ const (
|
|||
// aclOIDCCallbackRequestExpiryTime is the deadline used when obtaining an
|
||||
// OIDC provider token. This is used for HTTP requests to external APIs.
|
||||
aclOIDCCallbackRequestExpiryTime = 60 * time.Second
|
||||
|
||||
// aclLoginRequestExpiryTime is the deadline used when performing HTTP
|
||||
// requests to external APIs during the validation of bearer tokens.
|
||||
aclLoginRequestExpiryTime = 60 * time.Second
|
||||
)
|
||||
|
||||
// ACL endpoint is used for manipulating ACL tokens and policies
|
||||
|
@ -2612,7 +2618,7 @@ func (a *ACL) OIDCAuthURL(args *structs.ACLOIDCAuthURLRequest, reply *structs.AC
|
|||
// provider token for a Nomad ACL token, using the configured ACL role and
|
||||
// policy claims to provide authorization.
|
||||
func (a *ACL) OIDCCompleteAuth(
|
||||
args *structs.ACLOIDCCompleteAuthRequest, reply *structs.ACLOIDCCompleteAuthResponse) error {
|
||||
args *structs.ACLOIDCCompleteAuthRequest, reply *structs.ACLLoginResponse) error {
|
||||
|
||||
// The OIDC flow can only be used when the Nomad cluster has ACL enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
|
@ -2719,19 +2725,19 @@ func (a *ACL) OIDCCompleteAuth(
|
|||
|
||||
// Generate the data used by the go-bexpr selector that is an internal
|
||||
// representation of the claims that can be understood by Nomad.
|
||||
oidcInternalClaims, err := oidc.SelectorData(authMethod, idTokenClaims, userClaims)
|
||||
oidcInternalClaims, err := auth.SelectorData(authMethod, idTokenClaims, userClaims)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new binder object based on the current state snapshot to
|
||||
// provide consistency within the RPC handler.
|
||||
oidcBinder := oidc.NewBinder(stateSnapshot)
|
||||
oidcBinder := auth.NewBinder(stateSnapshot)
|
||||
|
||||
// Generate the role and policy bindings that will be assigned to the ACL
|
||||
// token. Ensure we have at least 1 role or policy, otherwise the RPC will
|
||||
// fail anyway.
|
||||
tokenBindings, err := oidcBinder.Bind(authMethod, oidc.NewIdentity(authMethod.Config, oidcInternalClaims))
|
||||
tokenBindings, err := oidcBinder.Bind(authMethod, auth.NewIdentity(authMethod.Config, oidcInternalClaims))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -2777,3 +2783,152 @@ func (a *ACL) OIDCCompleteAuth(
|
|||
reply.ACLToken = tokenUpsertReply.Tokens[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
// Login RPC performs non-interactive auth using a given AuthMethod. This method
|
||||
// can not be used for OIDC login flow.
|
||||
func (a *ACL) Login(args *structs.ACLLoginRequest, reply *structs.ACLLoginResponse) error {
|
||||
|
||||
// The login flow can only be used when the Nomad cluster has ACL enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
// Perform the initial forwarding within the region. This ensures we
|
||||
// respect stale queries.
|
||||
if done, err := a.srv.forward(structs.ACLLoginRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// Measure the login endpoint performance.
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "login"}, time.Now())
|
||||
|
||||
// This endpoint can only be used once all servers in all federated regions
|
||||
// have been upgraded to 1.5.2 or greater, since JWT Auth method was
|
||||
// introduced then.
|
||||
if !ServersMeetMinimumVersion(a.srv.Members(), AllRegions, minACLJWTAuthMethodVersion, false) {
|
||||
return fmt.Errorf("all servers should be running version %v or later to use JWT ACL auth methods",
|
||||
minACLJWTAuthMethodVersion)
|
||||
}
|
||||
|
||||
// Validate the request arguments to ensure it contains all the data it
|
||||
// needs.
|
||||
if err := args.Validate(); err != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "invalid login request: %v", err)
|
||||
}
|
||||
|
||||
// Grab a snapshot of the state, so we can query it safely.
|
||||
stateSnapshot, err := a.srv.fsm.State().Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Lookup the auth method from state, so we have the entire object
|
||||
// available to us. It's important to check for nil on the auth method
|
||||
// object, as it is possible the request was made with an incorrectly named
|
||||
// auth method.
|
||||
authMethod, err := stateSnapshot.GetACLAuthMethodByName(nil, args.AuthMethodName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if authMethod == nil {
|
||||
return structs.NewErrRPCCodedf(
|
||||
http.StatusBadRequest,
|
||||
"auth-method %q not found",
|
||||
args.AuthMethodName,
|
||||
)
|
||||
}
|
||||
|
||||
// If the authentication method generates global ACL tokens, we need to
|
||||
// forward the request onto the authoritative regional leader.
|
||||
if authMethod.TokenLocalityIsGlobal() {
|
||||
args.Region = a.srv.config.AuthoritativeRegion
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLLoginRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a context with a deadline. This is used when making remote HTTP
|
||||
// requests.
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(aclLoginRequestExpiryTime))
|
||||
defer cancel()
|
||||
|
||||
var claims map[string]interface{}
|
||||
|
||||
// Validate the token depending on its method type
|
||||
switch authMethod.Type {
|
||||
case structs.ACLAuthMethodTypeJWT:
|
||||
claims, err = jwt.Validate(ctx, args.LoginToken, authMethod.Config)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(
|
||||
http.StatusUnauthorized,
|
||||
"unable to validate provided token: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
default:
|
||||
return structs.NewErrRPCCodedf(
|
||||
http.StatusBadRequest,
|
||||
"unsupported auth-method type: %s",
|
||||
authMethod.Type,
|
||||
)
|
||||
}
|
||||
|
||||
// Create a new binder object based on the current state snapshot to
|
||||
// provide consistency within the RPC handler.
|
||||
jwtBinder := auth.NewBinder(stateSnapshot)
|
||||
|
||||
// Generate the data used by the go-bexpr selector that is an internal
|
||||
// representation of the claims that can be understood by Nomad.
|
||||
jwtClaims, err := auth.SelectorData(authMethod, claims, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenBindings, err := jwtBinder.Bind(authMethod, auth.NewIdentity(authMethod.Config, jwtClaims))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tokenBindings.None() && !tokenBindings.Management {
|
||||
return structs.NewErrRPCCoded(http.StatusBadRequest, "no role or policy bindings matched")
|
||||
}
|
||||
|
||||
// Build our token RPC request. The RPC handler includes a lot of specific
|
||||
// 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: "JWT-" + 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{&token},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: a.srv.Region(),
|
||||
AuthToken: a.srv.getLeaderAcl(),
|
||||
},
|
||||
}
|
||||
|
||||
var tokenUpsertReply structs.ACLTokenUpsertResponse
|
||||
|
||||
if err := a.srv.RPC(structs.ACLUpsertTokensRPCMethod, &tokenUpsertRequest, &tokenUpsertReply); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The way the UpsertTokens RPC currently works, if we get no error, then
|
||||
// we will have exactly the same number of tokens returned as we sent. It
|
||||
// is therefore safe to assume we have 1 token.
|
||||
reply.ACLToken = tokenUpsertReply.Tokens[0]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
capOIDC "github.com/hashicorp/cap/oidc"
|
||||
"github.com/hashicorp/go-memdb"
|
||||
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||
|
@ -2686,10 +2687,10 @@ func TestACLEndpoint_GetAuthMethod(t *testing.T) {
|
|||
testutil.WaitForLeader(t, s1.RPC)
|
||||
|
||||
// Create the register request
|
||||
authMethod := mock.ACLAuthMethod()
|
||||
authMethod := mock.ACLOIDCAuthMethod()
|
||||
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{authMethod}))
|
||||
|
||||
anonymousAuthMethod := mock.ACLAuthMethod()
|
||||
anonymousAuthMethod := mock.ACLOIDCAuthMethod()
|
||||
anonymousAuthMethod.Name = "anonymous"
|
||||
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1001, []*structs.ACLAuthMethod{anonymousAuthMethod}))
|
||||
|
||||
|
@ -2723,8 +2724,8 @@ func TestACLEndpoint_GetAuthMethod_Blocking(t *testing.T) {
|
|||
testutil.WaitForLeader(t, s1.RPC)
|
||||
|
||||
// Create the authMethods
|
||||
am1 := mock.ACLAuthMethod()
|
||||
am2 := mock.ACLAuthMethod()
|
||||
am1 := mock.ACLOIDCAuthMethod()
|
||||
am2 := mock.ACLOIDCAuthMethod()
|
||||
|
||||
// First create an unrelated authMethod
|
||||
time.AfterFunc(100*time.Millisecond, func() {
|
||||
|
@ -2782,8 +2783,8 @@ func TestACLEndpoint_GetAuthMethods(t *testing.T) {
|
|||
testutil.WaitForLeader(t, s1.RPC)
|
||||
|
||||
// Create the register request
|
||||
authMethod := mock.ACLAuthMethod()
|
||||
authMethod2 := mock.ACLAuthMethod()
|
||||
authMethod := mock.ACLOIDCAuthMethod()
|
||||
authMethod2 := mock.ACLOIDCAuthMethod()
|
||||
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{authMethod, authMethod2}))
|
||||
|
||||
// Lookup the authMethod
|
||||
|
@ -2819,8 +2820,8 @@ func TestACLEndpoint_GetAuthMethods_Blocking(t *testing.T) {
|
|||
testutil.WaitForLeader(t, s1.RPC)
|
||||
|
||||
// Create the authMethods
|
||||
am1 := mock.ACLAuthMethod()
|
||||
am2 := mock.ACLAuthMethod()
|
||||
am1 := mock.ACLOIDCAuthMethod()
|
||||
am2 := mock.ACLOIDCAuthMethod()
|
||||
|
||||
// First create an unrelated authMethod
|
||||
time.AfterFunc(100*time.Millisecond, func() {
|
||||
|
@ -2878,8 +2879,8 @@ func TestACLEndpoint_ListAuthMethods(t *testing.T) {
|
|||
testutil.WaitForLeader(t, s1.RPC)
|
||||
|
||||
// Create the register request
|
||||
am1 := mock.ACLAuthMethod()
|
||||
am2 := mock.ACLAuthMethod()
|
||||
am1 := mock.ACLOIDCAuthMethod()
|
||||
am2 := mock.ACLOIDCAuthMethod()
|
||||
|
||||
am1.Name = "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9"
|
||||
am2.Name = "aaaabbbb-3350-4b4b-d185-0e1992ed43e9"
|
||||
|
@ -2927,7 +2928,7 @@ func TestACLEndpoint_ListAuthMethods_Blocking(t *testing.T) {
|
|||
testutil.WaitForLeader(t, s1.RPC)
|
||||
|
||||
// Create the authMethod
|
||||
authMethod := mock.ACLAuthMethod()
|
||||
authMethod := mock.ACLOIDCAuthMethod()
|
||||
|
||||
// Upsert auth method triggers watches
|
||||
time.AfterFunc(100*time.Millisecond, func() {
|
||||
|
@ -2978,7 +2979,7 @@ func TestACLEndpoint_DeleteAuthMethods(t *testing.T) {
|
|||
testutil.WaitForLeader(t, s1.RPC)
|
||||
|
||||
// Create the register request
|
||||
am1 := mock.ACLAuthMethod()
|
||||
am1 := mock.ACLOIDCAuthMethod()
|
||||
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{am1}))
|
||||
|
||||
// Lookup the authMethods
|
||||
|
@ -3019,7 +3020,7 @@ func TestACLEndpoint_UpsertACLAuthMethods(t *testing.T) {
|
|||
s1.config.ACLTokenMaxExpirationTTL = maxTTL
|
||||
|
||||
// Create the register request
|
||||
am1 := mock.ACLAuthMethod()
|
||||
am1 := mock.ACLOIDCAuthMethod()
|
||||
am1.Default = true // make sure it's going to be a default method
|
||||
am1.SetHash()
|
||||
|
||||
|
@ -3043,7 +3044,7 @@ func TestACLEndpoint_UpsertACLAuthMethods(t *testing.T) {
|
|||
must.True(t, am1.Equal(resp.AuthMethods[0]))
|
||||
|
||||
// Try to insert another default authMethod
|
||||
am2 := mock.ACLAuthMethod()
|
||||
am2 := mock.ACLOIDCAuthMethod()
|
||||
am2.Default = true
|
||||
req = &structs.ACLAuthMethodUpsertRequest{
|
||||
AuthMethods: []*structs.ACLAuthMethod{am2},
|
||||
|
@ -3107,7 +3108,7 @@ func TestACL_UpsertBindingRules(t *testing.T) {
|
|||
must.EqError(t, err, "RPC Error:: 400,ACL auth method auth0 not found")
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
authMethod := mock.ACLAuthMethod()
|
||||
authMethod := mock.ACLOIDCAuthMethod()
|
||||
authMethod.Name = aclBindingRule1.AuthMethod
|
||||
|
||||
must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{authMethod}))
|
||||
|
@ -3524,7 +3525,7 @@ func TestACL_OIDCAuthURL(t *testing.T) {
|
|||
|
||||
// Generate and upsert an ACL auth method for use. Certain values must be
|
||||
// taken from the cap OIDC provider just like real world use.
|
||||
mockedAuthMethod := mock.ACLAuthMethod()
|
||||
mockedAuthMethod := mock.ACLOIDCAuthMethod()
|
||||
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
||||
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
||||
mockedAuthMethod.Config.SigningAlgs = []string{"ES256"}
|
||||
|
@ -3579,7 +3580,7 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
var completeAuthResp1 structs.ACLOIDCCompleteAuthResponse
|
||||
var completeAuthResp1 structs.ACLLoginResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq1, &completeAuthResp1)
|
||||
must.Error(t, err)
|
||||
must.ErrorContains(t, err, "400")
|
||||
|
@ -3598,7 +3599,7 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
var completeAuthResp2 structs.ACLOIDCCompleteAuthResponse
|
||||
var completeAuthResp2 structs.ACLLoginResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq2, &completeAuthResp2)
|
||||
must.Error(t, err)
|
||||
must.ErrorContains(t, err, "400")
|
||||
|
@ -3607,7 +3608,7 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
|
|||
// Generate and upsert an ACL auth method for use. Certain values must be
|
||||
// taken from the cap OIDC provider and these are validated. Others must
|
||||
// match data we use later, such as the claims.
|
||||
mockedAuthMethod := mock.ACLAuthMethod()
|
||||
mockedAuthMethod := mock.ACLOIDCAuthMethod()
|
||||
mockedAuthMethod.Config.BoundAudiences = []string{"mock"}
|
||||
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
||||
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
||||
|
@ -3646,7 +3647,7 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
var completeAuthResp3 structs.ACLOIDCCompleteAuthResponse
|
||||
var completeAuthResp3 structs.ACLLoginResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq3, &completeAuthResp3)
|
||||
must.Error(t, err)
|
||||
must.ErrorContains(t, err, "400")
|
||||
|
@ -3689,7 +3690,7 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
var completeAuthResp4 structs.ACLOIDCCompleteAuthResponse
|
||||
var completeAuthResp4 structs.ACLLoginResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq4, &completeAuthResp4)
|
||||
must.NoError(t, err)
|
||||
must.NotNil(t, completeAuthResp4.ACLToken)
|
||||
|
@ -3722,7 +3723,7 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
var completeAuthResp5 structs.ACLOIDCCompleteAuthResponse
|
||||
var completeAuthResp5 structs.ACLLoginResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq5, &completeAuthResp5)
|
||||
must.NoError(t, err)
|
||||
must.NotNil(t, completeAuthResp4.ACLToken)
|
||||
|
@ -3730,3 +3731,158 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
|
|||
must.Len(t, 0, completeAuthResp5.ACLToken.Roles)
|
||||
must.Eq(t, structs.ACLManagementToken, completeAuthResp5.ACLToken.Type)
|
||||
}
|
||||
|
||||
func TestACL_Login(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testServer, _, testServerCleanupFn := TestACLServer(t, nil)
|
||||
defer testServerCleanupFn()
|
||||
codec := rpcClient(t, testServer)
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// create a sample JWT and a pub key for verification
|
||||
iat := time.Now().Unix()
|
||||
nbf := time.Now().Unix()
|
||||
exp := time.Now().Add(time.Hour).Unix()
|
||||
testToken, testPubKey, err := mock.SampleJWTokenWithKeys(jwt.MapClaims{
|
||||
"http://nomad.internal/policies": []string{"engineering"},
|
||||
"http://nomad.internal/roles": []string{"engineering"},
|
||||
"iat": iat,
|
||||
"nbf": nbf,
|
||||
"exp": exp,
|
||||
"iss": "nomad test suite",
|
||||
"aud": []string{"sales", "engineering"},
|
||||
}, nil)
|
||||
must.Nil(t, err)
|
||||
|
||||
// send empty req to test validation
|
||||
loginReq1 := structs.ACLLoginRequest{
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
},
|
||||
}
|
||||
|
||||
var completeAuthResp1 structs.ACLLoginResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLLoginRPCMethod, &loginReq1, &completeAuthResp1)
|
||||
must.ErrorContains(t, err, "missing auth method name")
|
||||
must.ErrorContains(t, err, "missing login token")
|
||||
|
||||
// Send a request that passes initial validation. The auth method does not
|
||||
// exist meaning it will fail.
|
||||
loginReq2 := structs.ACLLoginRequest{
|
||||
AuthMethodName: "test-oidc-auth-method",
|
||||
LoginToken: testToken,
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
},
|
||||
}
|
||||
|
||||
var completeAuthResp2 structs.ACLLoginResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLLoginRPCMethod, &loginReq2, &completeAuthResp2)
|
||||
must.Error(t, err)
|
||||
must.ErrorContains(t, err, "400")
|
||||
must.ErrorContains(t, err, "auth-method \"test-oidc-auth-method\" not found")
|
||||
|
||||
// Generate and upsert a JWT ACL auth method for use.
|
||||
mockedAuthMethod := mock.ACLJWTAuthMethod()
|
||||
mockedAuthMethod.Config.BoundAudiences = []string{"engineering"}
|
||||
mockedAuthMethod.Config.JWTValidationPubKeys = []string{testPubKey}
|
||||
mockedAuthMethod.Config.BoundIssuer = []string{"nomad test suite"}
|
||||
mockedAuthMethod.Config.ExpirationLeeway = time.Duration(3600)
|
||||
mockedAuthMethod.Config.ClockSkewLeeway = time.Duration(3600)
|
||||
mockedAuthMethod.Config.ClaimMappings = map[string]string{}
|
||||
mockedAuthMethod.Config.ListClaimMappings = map[string]string{
|
||||
"http://nomad.internal/roles": "roles",
|
||||
"http://nomad.internal/policies": "policies",
|
||||
}
|
||||
|
||||
must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{mockedAuthMethod}))
|
||||
|
||||
// We should now be able to authenticate, however, we do not have any rule
|
||||
// bindings that will match.
|
||||
loginReq3 := structs.ACLLoginRequest{
|
||||
AuthMethodName: mockedAuthMethod.Name,
|
||||
LoginToken: testToken,
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
},
|
||||
}
|
||||
|
||||
var completeAuthResp3 structs.ACLLoginResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLLoginRPCMethod, &loginReq3, &completeAuthResp3)
|
||||
must.Error(t, err)
|
||||
must.ErrorContains(t, err, "400")
|
||||
must.ErrorContains(t, err, "no role or policy bindings matched")
|
||||
|
||||
// Upsert an ACL policy and role, so that we can reference this within our
|
||||
// JWT claims.
|
||||
mockACLPolicy := mock.ACLPolicy()
|
||||
must.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLPolicy{mockACLPolicy}))
|
||||
|
||||
mockACLRole := mock.ACLRole()
|
||||
mockACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: mockACLPolicy.Name}}
|
||||
must.NoError(t, testServer.fsm.State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 30, []*structs.ACLRole{mockACLRole}, true))
|
||||
|
||||
// Generate and upsert two binding rules, so we can test both ACL Policy
|
||||
// and Role claim mapping.
|
||||
mockBindingRule1 := mock.ACLBindingRule()
|
||||
mockBindingRule1.AuthMethod = mockedAuthMethod.Name
|
||||
mockBindingRule1.BindType = structs.ACLBindingRuleBindTypePolicy
|
||||
mockBindingRule1.Selector = "engineering in list.policies"
|
||||
mockBindingRule1.BindName = mockACLPolicy.Name
|
||||
|
||||
mockBindingRule2 := mock.ACLBindingRule()
|
||||
mockBindingRule2.AuthMethod = mockedAuthMethod.Name
|
||||
mockBindingRule2.BindName = mockACLRole.Name
|
||||
|
||||
must.NoError(t, testServer.fsm.State().UpsertACLBindingRules(
|
||||
40, []*structs.ACLBindingRule{mockBindingRule1, mockBindingRule2}, true))
|
||||
|
||||
loginReq4 := structs.ACLLoginRequest{
|
||||
AuthMethodName: mockedAuthMethod.Name,
|
||||
LoginToken: testToken,
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
},
|
||||
}
|
||||
|
||||
var completeAuthResp4 structs.ACLLoginResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLLoginRPCMethod, &loginReq4, &completeAuthResp4)
|
||||
must.NoError(t, err)
|
||||
must.NotNil(t, completeAuthResp4.ACLToken)
|
||||
must.Len(t, 1, completeAuthResp4.ACLToken.Policies)
|
||||
must.Eq(t, mockACLPolicy.Name, completeAuthResp4.ACLToken.Policies[0])
|
||||
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))
|
||||
|
||||
loginReq5 := structs.ACLLoginRequest{
|
||||
AuthMethodName: mockedAuthMethod.Name,
|
||||
LoginToken: testToken,
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
},
|
||||
}
|
||||
|
||||
var completeAuthResp5 structs.ACLLoginResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLLoginRPCMethod, &loginReq5, &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)
|
||||
}
|
||||
|
|
|
@ -2691,7 +2691,7 @@ func TestFSM_SnapshotRestore_ACLAuthMethods(t *testing.T) {
|
|||
testState := fsm.State()
|
||||
|
||||
// Generate and upsert some ACL auth methods.
|
||||
authMethods := []*structs.ACLAuthMethod{mock.ACLAuthMethod(), mock.ACLAuthMethod()}
|
||||
authMethods := []*structs.ACLAuthMethod{mock.ACLOIDCAuthMethod(), mock.ACLOIDCAuthMethod()}
|
||||
must.NoError(t, testState.UpsertACLAuthMethods(10, authMethods))
|
||||
|
||||
// Perform a snapshot restore.
|
||||
|
@ -3540,8 +3540,8 @@ func TestFSM_UpsertACLAuthMethods(t *testing.T) {
|
|||
ci.Parallel(t)
|
||||
fsm := testFSM(t)
|
||||
|
||||
am1 := mock.ACLAuthMethod()
|
||||
am2 := mock.ACLAuthMethod()
|
||||
am1 := mock.ACLOIDCAuthMethod()
|
||||
am2 := mock.ACLOIDCAuthMethod()
|
||||
req := structs.ACLAuthMethodUpsertRequest{
|
||||
AuthMethods: []*structs.ACLAuthMethod{am1, am2},
|
||||
}
|
||||
|
@ -3564,8 +3564,8 @@ func TestFSM_DeleteACLAuthMethods(t *testing.T) {
|
|||
ci.Parallel(t)
|
||||
fsm := testFSM(t)
|
||||
|
||||
am1 := mock.ACLAuthMethod()
|
||||
am2 := mock.ACLAuthMethod()
|
||||
am1 := mock.ACLOIDCAuthMethod()
|
||||
am2 := mock.ACLOIDCAuthMethod()
|
||||
must.Nil(t, fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{am1, am2}))
|
||||
|
||||
req := structs.ACLAuthMethodDeleteRequest{
|
||||
|
@ -3591,7 +3591,7 @@ func TestFSM_UpsertACLBindingRules(t *testing.T) {
|
|||
fsm := testFSM(t)
|
||||
|
||||
// Create an auth method and upsert so the binding rules can link to this.
|
||||
authMethod := mock.ACLAuthMethod()
|
||||
authMethod := mock.ACLOIDCAuthMethod()
|
||||
must.NoError(t, fsm.state.UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{authMethod}))
|
||||
|
||||
aclBindingRule1 := mock.ACLBindingRule()
|
||||
|
|
|
@ -57,6 +57,14 @@ var minACLRoleVersion = version.Must(version.NewVersion("1.4.0"))
|
|||
// meet before the feature can be used.
|
||||
var minACLAuthMethodVersion = version.Must(version.NewVersion("1.5.0-beta.1"))
|
||||
|
||||
// minACLJWTAuthMethodVersion is the Nomad version at which the ACL JWT auth method type
|
||||
// was introduced. It forms the minimum version all federated servers must
|
||||
// meet before the feature can be used.
|
||||
//
|
||||
// TODO: version constraint will be updated for until we reach 1.5.2, otherwise
|
||||
// it's hard to test the functionality
|
||||
var minACLJWTAuthMethodVersion = version.Must(version.NewVersion("1.4.4-dev"))
|
||||
|
||||
// minACLBindingRuleVersion is the Nomad version at which the ACL binding rules
|
||||
// table was introduced. It forms the minimum version all federated servers
|
||||
// must meet before the feature can be used.
|
||||
|
|
|
@ -1301,10 +1301,10 @@ func Test_diffACLAuthMethods(t *testing.T) {
|
|||
stateStore := state.TestStateStore(t)
|
||||
|
||||
// Build an initial baseline of ACL auth-methods.
|
||||
aclAuthMethod0 := mock.ACLAuthMethod()
|
||||
aclAuthMethod1 := mock.ACLAuthMethod()
|
||||
aclAuthMethod2 := mock.ACLAuthMethod()
|
||||
aclAuthMethod3 := mock.ACLAuthMethod()
|
||||
aclAuthMethod0 := mock.ACLOIDCAuthMethod()
|
||||
aclAuthMethod1 := mock.ACLOIDCAuthMethod()
|
||||
aclAuthMethod2 := mock.ACLOIDCAuthMethod()
|
||||
aclAuthMethod3 := mock.ACLOIDCAuthMethod()
|
||||
|
||||
// Upsert these into our local state. Use copies, so we can alter the
|
||||
// auth-methods directly and use within the diff func.
|
||||
|
@ -1318,7 +1318,7 @@ func Test_diffACLAuthMethods(t *testing.T) {
|
|||
aclAuthMethod2.ModifyIndex = 50
|
||||
aclAuthMethod3.ModifyIndex = 200
|
||||
aclAuthMethod3.Hash = []byte{0, 1, 2, 3}
|
||||
aclAuthMethod4 := mock.ACLAuthMethod()
|
||||
aclAuthMethod4 := mock.ACLOIDCAuthMethod()
|
||||
|
||||
// Run the diff function and test the output.
|
||||
toDelete, toUpdate := diffACLAuthMethods(stateStore, 50, []*structs.ACLAuthMethodStub{
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
package mock
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
testing "github.com/mitchellh/go-testing-interface"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
// StateStore defines the methods required from state.StateStore but avoids a
|
||||
|
@ -219,7 +224,7 @@ func ACLManagementToken() *structs.ACLToken {
|
|||
}
|
||||
}
|
||||
|
||||
func ACLAuthMethod() *structs.ACLAuthMethod {
|
||||
func ACLOIDCAuthMethod() *structs.ACLAuthMethod {
|
||||
maxTokenTTL, _ := time.ParseDuration("3600s")
|
||||
method := structs.ACLAuthMethod{
|
||||
Name: fmt.Sprintf("acl-auth-method-%s", uuid.Short()),
|
||||
|
@ -232,10 +237,10 @@ func ACLAuthMethod() *structs.ACLAuthMethod {
|
|||
OIDCClientID: "mock",
|
||||
OIDCClientSecret: "very secret secret",
|
||||
OIDCScopes: []string{"groups"},
|
||||
BoundAudiences: []string{"audience1", "audience2"},
|
||||
BoundAudiences: []string{"sales", "engineering"},
|
||||
AllowedRedirectURIs: []string{"foo", "bar"},
|
||||
DiscoveryCaPem: []string{"foo"},
|
||||
SigningAlgs: []string{"bar"},
|
||||
SigningAlgs: []string{"RS256"},
|
||||
ClaimMappings: map[string]string{"foo": "bar"},
|
||||
ListClaimMappings: map[string]string{"foo": "bar"},
|
||||
},
|
||||
|
@ -248,6 +253,73 @@ func ACLAuthMethod() *structs.ACLAuthMethod {
|
|||
return &method
|
||||
}
|
||||
|
||||
func ACLJWTAuthMethod() *structs.ACLAuthMethod {
|
||||
maxTokenTTL, _ := time.ParseDuration("3600s")
|
||||
method := structs.ACLAuthMethod{
|
||||
Name: fmt.Sprintf("acl-auth-method-%s", uuid.Short()),
|
||||
Type: "JWT",
|
||||
TokenLocality: "local",
|
||||
MaxTokenTTL: maxTokenTTL,
|
||||
Default: false,
|
||||
Config: &structs.ACLAuthMethodConfig{
|
||||
JWTValidationPubKeys: []string{},
|
||||
OIDCDiscoveryURL: "http://example.com",
|
||||
BoundAudiences: []string{"sales", "engineering"},
|
||||
DiscoveryCaPem: []string{"foo"},
|
||||
SigningAlgs: []string{"RS256"},
|
||||
ClaimMappings: map[string]string{"foo": "bar"},
|
||||
ListClaimMappings: map[string]string{"foo": "bar"},
|
||||
},
|
||||
CreateTime: time.Now().UTC(),
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
}
|
||||
method.SetHash()
|
||||
method.Canonicalize()
|
||||
return &method
|
||||
}
|
||||
|
||||
// SampleJWTokenWithKeys takes a set of claims (can be nil) and optionally
|
||||
// a private RSA key that should be used for signing the JWT, and returns:
|
||||
// - a JWT signed with a randomly generated RSA key
|
||||
// - PEM string of the public part of that key that can be used for validation.
|
||||
func SampleJWTokenWithKeys(claims jwt.Claims, rsaKey *rsa.PrivateKey) (string, string, error) {
|
||||
var token, pubkeyPem string
|
||||
|
||||
if rsaKey == nil {
|
||||
var err error
|
||||
rsaKey, err = rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return token, pubkeyPem, err
|
||||
}
|
||||
}
|
||||
|
||||
pubkeyBytes, err := x509.MarshalPKIXPublicKey(rsaKey.Public())
|
||||
if err != nil {
|
||||
return token, pubkeyPem, err
|
||||
}
|
||||
pubkeyPem = string(pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: pubkeyBytes,
|
||||
},
|
||||
))
|
||||
|
||||
var rawToken *jwt.Token
|
||||
if claims != nil {
|
||||
rawToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
} else {
|
||||
rawToken = jwt.New(jwt.SigningMethodRS256)
|
||||
}
|
||||
|
||||
token, err = rawToken.SignedString(rsaKey)
|
||||
if err != nil {
|
||||
return token, pubkeyPem, err
|
||||
}
|
||||
|
||||
return token, pubkeyPem, nil
|
||||
}
|
||||
|
||||
func ACLBindingRule() *structs.ACLBindingRule {
|
||||
return &structs.ACLBindingRule{
|
||||
ID: uuid.Short(),
|
||||
|
|
|
@ -1058,7 +1058,7 @@ func Test_eventsFromChanges_ACLAuthMethod(t *testing.T) {
|
|||
defer testState.StopEventBroker()
|
||||
|
||||
// Generate a test ACL auth method
|
||||
authMethod := mock.ACLAuthMethod()
|
||||
authMethod := mock.ACLOIDCAuthMethod()
|
||||
|
||||
// Upsert the auth method straight into state
|
||||
writeTxn := testState.db.WriteTxn(10)
|
||||
|
|
|
@ -24,7 +24,7 @@ func TestStateStore_UpsertACLBindingRules(t *testing.T) {
|
|||
|
||||
// Create an auth method and ensure the binding rule is updated, so it is
|
||||
// related to it.
|
||||
authMethod := mock.ACLAuthMethod()
|
||||
authMethod := mock.ACLOIDCAuthMethod()
|
||||
mockedACLBindingRules[0].AuthMethod = authMethod.Name
|
||||
|
||||
must.NoError(t, testState.UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{authMethod}))
|
||||
|
|
|
@ -15,7 +15,7 @@ func TestStateStore_UpsertACLAuthMethods(t *testing.T) {
|
|||
testState := testStateStore(t)
|
||||
|
||||
// Create mock auth methods
|
||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLAuthMethod(), mock.ACLAuthMethod()}
|
||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLOIDCAuthMethod(), mock.ACLOIDCAuthMethod()}
|
||||
|
||||
must.NoError(t, testState.UpsertACLAuthMethods(10, mockedACLAuthMethods))
|
||||
|
||||
|
@ -88,7 +88,7 @@ func TestStateStore_UpsertACLAuthMethods(t *testing.T) {
|
|||
|
||||
// Try adding a new auth method, which has a name clash with an existing
|
||||
// entry.
|
||||
dup := mock.ACLAuthMethod()
|
||||
dup := mock.ACLOIDCAuthMethod()
|
||||
dup.Name = mockedACLAuthMethods[0].Name
|
||||
dup.Type = mockedACLAuthMethods[0].Type
|
||||
|
||||
|
@ -115,7 +115,7 @@ func TestStateStore_DeleteACLAuthMethods(t *testing.T) {
|
|||
|
||||
// Generate some mocked ACL auth methods for testing and upsert these
|
||||
// straight into state.
|
||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLAuthMethod(), mock.ACLAuthMethod()}
|
||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLOIDCAuthMethod(), mock.ACLOIDCAuthMethod()}
|
||||
must.NoError(t, testState.UpsertACLAuthMethods(10, mockedACLAuthMethods))
|
||||
|
||||
// Try and delete a method using a name that doesn't exist. This should
|
||||
|
@ -178,7 +178,7 @@ func TestStateStore_GetACLAuthMethods(t *testing.T) {
|
|||
|
||||
// Generate a some mocked ACL auth methods for testing and upsert these
|
||||
// straight into state.
|
||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLAuthMethod(), mock.ACLAuthMethod()}
|
||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLOIDCAuthMethod(), mock.ACLOIDCAuthMethod()}
|
||||
must.NoError(t, testState.UpsertACLAuthMethods(10, mockedACLAuthMethods))
|
||||
|
||||
// List the auth methods and ensure they are exactly as we expect.
|
||||
|
@ -207,7 +207,7 @@ func TestStateStore_GetACLAuthMethodByName(t *testing.T) {
|
|||
|
||||
// Generate a some mocked ACL auth methods for testing and upsert these
|
||||
// straight into state.
|
||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLAuthMethod(), mock.ACLAuthMethod()}
|
||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLOIDCAuthMethod(), mock.ACLOIDCAuthMethod()}
|
||||
must.NoError(t, testState.UpsertACLAuthMethods(10, mockedACLAuthMethods))
|
||||
|
||||
ws := memdb.NewWatchSet()
|
||||
|
@ -232,9 +232,9 @@ func TestStateStore_GetDefaultACLAuthMethod(t *testing.T) {
|
|||
testState := testStateStore(t)
|
||||
|
||||
// Generate 2 auth methods, make one of them default
|
||||
am1 := mock.ACLAuthMethod()
|
||||
am1 := mock.ACLOIDCAuthMethod()
|
||||
am1.Default = true
|
||||
am2 := mock.ACLAuthMethod()
|
||||
am2 := mock.ACLOIDCAuthMethod()
|
||||
|
||||
// upsert
|
||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{am1, am2}
|
||||
|
|
|
@ -635,7 +635,7 @@ func TestStateStore_ACLAuthMethodRestore(t *testing.T) {
|
|||
|
||||
// Set up our test registrations and index.
|
||||
expectedIndex := uint64(13)
|
||||
authMethod := mock.ACLAuthMethod()
|
||||
authMethod := mock.ACLOIDCAuthMethod()
|
||||
authMethod.CreateIndex = expectedIndex
|
||||
authMethod.ModifyIndex = expectedIndex
|
||||
|
||||
|
|
|
@ -169,6 +169,14 @@ const (
|
|||
// Args: ACLOIDCCompleteAuthRequest
|
||||
// Reply: ACLOIDCCompleteAuthResponse
|
||||
ACLOIDCCompleteAuthRPCMethod = "ACL.OIDCCompleteAuth"
|
||||
|
||||
// ACLLoginRPCMethod is the RPC method for performing a non-OIDC login
|
||||
// workflow. It exchanges the provided token for a Nomad ACL token with
|
||||
// roles as defined within the remote provider.
|
||||
//
|
||||
// Args: ACLLoginRequest
|
||||
// Reply: ACLLoginResponse
|
||||
ACLLoginRPCMethod = "ACL.Login"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -185,6 +193,23 @@ const (
|
|||
// maxACLBindingRuleDescriptionLength limits an ACL binding rules
|
||||
// description length and should be used to validate the object.
|
||||
maxACLBindingRuleDescriptionLength = 256
|
||||
|
||||
// ACLAuthMethodTokenLocalityLocal is the ACLAuthMethod.TokenLocality that
|
||||
// will generate ACL tokens which can only be used on the local cluster the
|
||||
// request was made.
|
||||
ACLAuthMethodTokenLocalityLocal = "local"
|
||||
|
||||
// ACLAuthMethodTokenLocalityGlobal is the ACLAuthMethod.TokenLocality that
|
||||
// will generate ACL tokens which can be used on all federated clusters.
|
||||
ACLAuthMethodTokenLocalityGlobal = "global"
|
||||
|
||||
// ACLAuthMethodTypeOIDC the ACLAuthMethod.Type and represents an
|
||||
// auth-method which uses the OIDC protocol.
|
||||
ACLAuthMethodTypeOIDC = "OIDC"
|
||||
|
||||
// ACLAuthMethodTypeJWT the ACLAuthMethod.Type and represents an auth-method
|
||||
// which uses the JWT type.
|
||||
ACLAuthMethodTypeJWT = "JWT"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -193,6 +218,9 @@ var (
|
|||
|
||||
// ValidACLAuthMethod is used to validate an ACL auth method name.
|
||||
ValidACLAuthMethod = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
|
||||
|
||||
// ValitACLAuthMethodTypes lists supported auth method types.
|
||||
ValidACLAuthMethodTypes = []string{ACLAuthMethodTypeOIDC, ACLAuthMethodTypeJWT}
|
||||
)
|
||||
|
||||
type ACLCacheEntry[T any] lang.Pair[T, time.Time]
|
||||
|
@ -895,7 +923,7 @@ func (a *ACLAuthMethod) Validate(minTTL, maxTTL time.Duration) error {
|
|||
mErr.Errors, fmt.Errorf("invalid token locality '%s'", a.TokenLocality))
|
||||
}
|
||||
|
||||
if a.Type != "OIDC" {
|
||||
if !slices.Contains(ValidACLAuthMethodTypes, a.Type) {
|
||||
mErr.Errors = append(
|
||||
mErr.Errors, fmt.Errorf("invalid token type '%s'", a.Type))
|
||||
}
|
||||
|
@ -991,6 +1019,93 @@ func (a *ACLAuthMethodConfig) Copy() *ACLAuthMethodConfig {
|
|||
return c
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface and allows
|
||||
// time.Diration fields to be marshaled correctly.
|
||||
func (a *ACLAuthMethodConfig) MarshalJSON() ([]byte, error) {
|
||||
type Alias ACLAuthMethodConfig
|
||||
exported := &struct {
|
||||
ExpirationLeeway string
|
||||
NotBeforeLeeway string
|
||||
ClockSkewLeeway string
|
||||
*Alias
|
||||
}{
|
||||
ExpirationLeeway: a.ExpirationLeeway.String(),
|
||||
NotBeforeLeeway: a.NotBeforeLeeway.String(),
|
||||
ClockSkewLeeway: a.ClockSkewLeeway.String(),
|
||||
Alias: (*Alias)(a),
|
||||
}
|
||||
if a.ExpirationLeeway == 0 {
|
||||
exported.ExpirationLeeway = ""
|
||||
}
|
||||
if a.NotBeforeLeeway == 0 {
|
||||
exported.NotBeforeLeeway = ""
|
||||
}
|
||||
if a.ClockSkewLeeway == 0 {
|
||||
exported.ClockSkewLeeway = ""
|
||||
}
|
||||
return json.Marshal(exported)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface and allows
|
||||
// time.Duration fields to be unmarshalled correctly.
|
||||
func (a *ACLAuthMethodConfig) UnmarshalJSON(data []byte) (err error) {
|
||||
type Alias ACLAuthMethodConfig
|
||||
aux := &struct {
|
||||
ExpirationLeeway any
|
||||
NotBeforeLeeway any
|
||||
ClockSkewLeeway any
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(a),
|
||||
}
|
||||
if err = json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
if aux.ExpirationLeeway != nil {
|
||||
switch v := aux.ExpirationLeeway.(type) {
|
||||
case string:
|
||||
if v != "" {
|
||||
if a.ExpirationLeeway, err = time.ParseDuration(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case float64:
|
||||
a.ExpirationLeeway = time.Duration(v)
|
||||
default:
|
||||
return fmt.Errorf("unexpected ExpirationLeeway type: %v", v)
|
||||
}
|
||||
}
|
||||
if aux.NotBeforeLeeway != nil {
|
||||
switch v := aux.NotBeforeLeeway.(type) {
|
||||
case string:
|
||||
if v != "" {
|
||||
if a.NotBeforeLeeway, err = time.ParseDuration(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case float64:
|
||||
a.NotBeforeLeeway = time.Duration(v)
|
||||
default:
|
||||
return fmt.Errorf("unexpected NotBeforeLeeway type: %v", v)
|
||||
}
|
||||
}
|
||||
if aux.ClockSkewLeeway != nil {
|
||||
switch v := aux.ClockSkewLeeway.(type) {
|
||||
case string:
|
||||
if v != "" {
|
||||
if a.ClockSkewLeeway, err = time.ParseDuration(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case float64:
|
||||
a.ClockSkewLeeway = time.Duration(v)
|
||||
default:
|
||||
return fmt.Errorf("unexpected ClockSkewLeeway type: %v", v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ACLAuthClaims is the claim mapping of the OIDC auth method in a format that
|
||||
// can be used with go-bexpr. This structure is used during rule binding
|
||||
// evaluation.
|
||||
|
@ -1480,9 +1595,39 @@ func (a *ACLOIDCCompleteAuthRequest) Validate() error {
|
|||
return mErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
// ACLOIDCCompleteAuthResponse is the response when the OIDC auth flow has been
|
||||
// ACLLoginResponse is the response when the auth flow has been
|
||||
// completed successfully.
|
||||
type ACLOIDCCompleteAuthResponse struct {
|
||||
type ACLLoginResponse struct {
|
||||
ACLToken *ACLToken
|
||||
WriteMeta
|
||||
}
|
||||
|
||||
// ACLLoginRequest is the request object to begin auth with an external
|
||||
// token provider.
|
||||
type ACLLoginRequest struct {
|
||||
|
||||
// AuthMethodName is the name of the auth method being used to login. This
|
||||
// is a required parameter.
|
||||
AuthMethodName string
|
||||
|
||||
// LoginToken is the 3rd party token that we use to exchange for Nomad ACL
|
||||
// Token in order to authenticate. This is a required parameter.
|
||||
LoginToken string
|
||||
|
||||
WriteRequest
|
||||
}
|
||||
|
||||
// Validate ensures the request object contains all the required fields in
|
||||
// order to complete the authentication flow.
|
||||
func (a *ACLLoginRequest) Validate() error {
|
||||
|
||||
var mErr multierror.Error
|
||||
|
||||
if a.AuthMethodName == "" {
|
||||
mErr.Errors = append(mErr.Errors, errors.New("missing auth method name"))
|
||||
}
|
||||
if a.LoginToken == "" {
|
||||
mErr.Errors = append(mErr.Errors, errors.New("missing login token"))
|
||||
}
|
||||
return mErr.ErrorOrNil()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue