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.ACLListAuthMethodsRPCMethod'
|
||||||
- pattern-not: 'structs.ACLOIDCAuthURLRPCMethod'
|
- pattern-not: 'structs.ACLOIDCAuthURLRPCMethod'
|
||||||
- pattern-not: 'structs.ACLOIDCCompleteAuthRPCMethod'
|
- pattern-not: 'structs.ACLOIDCCompleteAuthRPCMethod'
|
||||||
|
- pattern-not: 'structs.ACLLoginRPCMethod'
|
||||||
- pattern-not: '"CSIPlugin.Get"'
|
- pattern-not: '"CSIPlugin.Get"'
|
||||||
- pattern-not: '"CSIPlugin.List"'
|
- pattern-not: '"CSIPlugin.List"'
|
||||||
- pattern-not: '"Status.Leader"'
|
- pattern-not: '"Status.Leader"'
|
||||||
|
|
|
@ -867,7 +867,7 @@ func (s *HTTPServer) ACLOIDCCompleteAuthRequest(resp http.ResponseWriter, req *h
|
||||||
return nil, CodedError(http.StatusBadRequest, err.Error())
|
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 {
|
if err := s.agent.RPC(structs.ACLOIDCCompleteAuthRPCMethod, &args, &out); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1122,7 +1122,7 @@ func TestHTTPServer_ACLAuthMethodListRequest(t *testing.T) {
|
||||||
|
|
||||||
// Upsert two auth-methods into state.
|
// Upsert two auth-methods into state.
|
||||||
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
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.
|
// Build the HTTP request.
|
||||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/auth-methods", nil)
|
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) {
|
testFn: func(srv *TestAgent) {
|
||||||
|
|
||||||
// Create a mock auth-method to use in the request body.
|
// Create a mock auth-method to use in the request body.
|
||||||
mockACLAuthMethod := mock.ACLAuthMethod()
|
mockACLAuthMethod := mock.ACLOIDCAuthMethod()
|
||||||
|
|
||||||
// Build the HTTP request.
|
// Build the HTTP request.
|
||||||
req, err := http.NewRequest(http.MethodPut, "/v1/acl/auth-method", encodeReq(mockACLAuthMethod))
|
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) {
|
testFn: func(srv *TestAgent) {
|
||||||
|
|
||||||
// Create a mock auth-method and put directly into state.
|
// Create a mock auth-method and put directly into state.
|
||||||
mockACLAuthMethod := mock.ACLAuthMethod()
|
mockACLAuthMethod := mock.ACLOIDCAuthMethod()
|
||||||
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
||||||
20, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
20, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
||||||
|
|
||||||
|
@ -1294,7 +1294,7 @@ func TestHTTPServer_ACLAuthMethodSpecificRequest(t *testing.T) {
|
||||||
testFn: func(srv *TestAgent) {
|
testFn: func(srv *TestAgent) {
|
||||||
|
|
||||||
// Create a mock auth-method and put directly into state.
|
// Create a mock auth-method and put directly into state.
|
||||||
mockACLAuthMethod := mock.ACLAuthMethod()
|
mockACLAuthMethod := mock.ACLOIDCAuthMethod()
|
||||||
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
||||||
20, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
20, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
||||||
|
|
||||||
|
@ -1499,7 +1499,7 @@ func TestHTTPServer_ACLBindingRuleRequest(t *testing.T) {
|
||||||
|
|
||||||
// Upsert the auth method that the binding rule will associate
|
// Upsert the auth method that the binding rule will associate
|
||||||
// with.
|
// with.
|
||||||
mockACLAuthMethod := mock.ACLAuthMethod()
|
mockACLAuthMethod := mock.ACLOIDCAuthMethod()
|
||||||
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
||||||
10, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
10, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
||||||
|
|
||||||
|
@ -1607,7 +1607,7 @@ func TestHTTPServer_ACLBindingRuleSpecificRequest(t *testing.T) {
|
||||||
|
|
||||||
// Upsert the auth method that the binding rule will associate
|
// Upsert the auth method that the binding rule will associate
|
||||||
// with.
|
// with.
|
||||||
mockACLAuthMethod := mock.ACLAuthMethod()
|
mockACLAuthMethod := mock.ACLOIDCAuthMethod()
|
||||||
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
|
||||||
10, []*structs.ACLAuthMethod{mockACLAuthMethod}))
|
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
|
// Generate and upsert an ACL auth method for use. Certain values must be
|
||||||
// taken from the cap OIDC provider just like real world use.
|
// 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.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
||||||
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
||||||
mockedAuthMethod.Config.SigningAlgs = []string{"ES256"}
|
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
|
// Generate and upsert an ACL auth method for use. Certain values must be
|
||||||
// taken from the cap OIDC provider just like real world use.
|
// taken from the cap OIDC provider just like real world use.
|
||||||
mockedAuthMethod := mock.ACLAuthMethod()
|
mockedAuthMethod := mock.ACLOIDCAuthMethod()
|
||||||
mockedAuthMethod.Config.BoundAudiences = []string{"mock"}
|
mockedAuthMethod.Config.BoundAudiences = []string{"mock"}
|
||||||
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
||||||
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package oidc
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -8,7 +8,6 @@ import (
|
||||||
"github.com/hashicorp/go-memdb"
|
"github.com/hashicorp/go-memdb"
|
||||||
"github.com/hashicorp/hil"
|
"github.com/hashicorp/hil"
|
||||||
"github.com/hashicorp/hil/ast"
|
"github.com/hashicorp/hil/ast"
|
||||||
|
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
"github.com/hashicorp/nomad/nomad/structs"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package oidc
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -19,7 +19,7 @@ func TestBinder_Bind(t *testing.T) {
|
||||||
testBind := NewBinder(testStore)
|
testBind := NewBinder(testStore)
|
||||||
|
|
||||||
// create an authMethod method and insert into the state store
|
// 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}))
|
must.NoError(t, testStore.UpsertACLAuthMethods(0, []*structs.ACLAuthMethod{authMethod}))
|
||||||
|
|
||||||
// create some roles and insert into the state store
|
// create some roles and insert into the state store
|
|
@ -1,4 +1,4 @@
|
||||||
package oidc
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
|
@ -1,4 +1,4 @@
|
||||||
package oidc
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
|
@ -1,4 +1,4 @@
|
||||||
package oidc
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
@ -1,9 +1,10 @@
|
||||||
package oidc
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/shoenig/test/must"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/shoenig/test/must"
|
||||||
|
|
||||||
"github.com/hashicorp/nomad/ci"
|
"github.com/hashicorp/nomad/ci"
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
"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"
|
policy "github.com/hashicorp/nomad/acl"
|
||||||
"github.com/hashicorp/nomad/helper"
|
"github.com/hashicorp/nomad/helper"
|
||||||
"github.com/hashicorp/nomad/helper/uuid"
|
"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/lib/auth/oidc"
|
||||||
"github.com/hashicorp/nomad/nomad/state"
|
"github.com/hashicorp/nomad/nomad/state"
|
||||||
"github.com/hashicorp/nomad/nomad/state/paginator"
|
"github.com/hashicorp/nomad/nomad/state/paginator"
|
||||||
|
@ -43,6 +45,10 @@ const (
|
||||||
// aclOIDCCallbackRequestExpiryTime is the deadline used when obtaining an
|
// aclOIDCCallbackRequestExpiryTime is the deadline used when obtaining an
|
||||||
// OIDC provider token. This is used for HTTP requests to external APIs.
|
// OIDC provider token. This is used for HTTP requests to external APIs.
|
||||||
aclOIDCCallbackRequestExpiryTime = 60 * time.Second
|
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
|
// 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
|
// provider token for a Nomad ACL token, using the configured ACL role and
|
||||||
// policy claims to provide authorization.
|
// policy claims to provide authorization.
|
||||||
func (a *ACL) OIDCCompleteAuth(
|
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.
|
// The OIDC flow can only be used when the Nomad cluster has ACL enabled.
|
||||||
if !a.srv.config.ACLEnabled {
|
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
|
// Generate the data used by the go-bexpr selector that is an internal
|
||||||
// representation of the claims that can be understood by Nomad.
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new binder object based on the current state snapshot to
|
// Create a new binder object based on the current state snapshot to
|
||||||
// provide consistency within the RPC handler.
|
// 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
|
// 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
|
// token. Ensure we have at least 1 role or policy, otherwise the RPC will
|
||||||
// fail anyway.
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2777,3 +2783,152 @@ func (a *ACL) OIDCCompleteAuth(
|
||||||
reply.ACLToken = tokenUpsertReply.Tokens[0]
|
reply.ACLToken = tokenUpsertReply.Tokens[0]
|
||||||
return nil
|
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"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
capOIDC "github.com/hashicorp/cap/oidc"
|
capOIDC "github.com/hashicorp/cap/oidc"
|
||||||
"github.com/hashicorp/go-memdb"
|
"github.com/hashicorp/go-memdb"
|
||||||
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||||
|
@ -2686,10 +2687,10 @@ func TestACLEndpoint_GetAuthMethod(t *testing.T) {
|
||||||
testutil.WaitForLeader(t, s1.RPC)
|
testutil.WaitForLeader(t, s1.RPC)
|
||||||
|
|
||||||
// Create the register request
|
// Create the register request
|
||||||
authMethod := mock.ACLAuthMethod()
|
authMethod := mock.ACLOIDCAuthMethod()
|
||||||
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{authMethod}))
|
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{authMethod}))
|
||||||
|
|
||||||
anonymousAuthMethod := mock.ACLAuthMethod()
|
anonymousAuthMethod := mock.ACLOIDCAuthMethod()
|
||||||
anonymousAuthMethod.Name = "anonymous"
|
anonymousAuthMethod.Name = "anonymous"
|
||||||
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1001, []*structs.ACLAuthMethod{anonymousAuthMethod}))
|
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)
|
testutil.WaitForLeader(t, s1.RPC)
|
||||||
|
|
||||||
// Create the authMethods
|
// Create the authMethods
|
||||||
am1 := mock.ACLAuthMethod()
|
am1 := mock.ACLOIDCAuthMethod()
|
||||||
am2 := mock.ACLAuthMethod()
|
am2 := mock.ACLOIDCAuthMethod()
|
||||||
|
|
||||||
// First create an unrelated authMethod
|
// First create an unrelated authMethod
|
||||||
time.AfterFunc(100*time.Millisecond, func() {
|
time.AfterFunc(100*time.Millisecond, func() {
|
||||||
|
@ -2782,8 +2783,8 @@ func TestACLEndpoint_GetAuthMethods(t *testing.T) {
|
||||||
testutil.WaitForLeader(t, s1.RPC)
|
testutil.WaitForLeader(t, s1.RPC)
|
||||||
|
|
||||||
// Create the register request
|
// Create the register request
|
||||||
authMethod := mock.ACLAuthMethod()
|
authMethod := mock.ACLOIDCAuthMethod()
|
||||||
authMethod2 := mock.ACLAuthMethod()
|
authMethod2 := mock.ACLOIDCAuthMethod()
|
||||||
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{authMethod, authMethod2}))
|
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{authMethod, authMethod2}))
|
||||||
|
|
||||||
// Lookup the authMethod
|
// Lookup the authMethod
|
||||||
|
@ -2819,8 +2820,8 @@ func TestACLEndpoint_GetAuthMethods_Blocking(t *testing.T) {
|
||||||
testutil.WaitForLeader(t, s1.RPC)
|
testutil.WaitForLeader(t, s1.RPC)
|
||||||
|
|
||||||
// Create the authMethods
|
// Create the authMethods
|
||||||
am1 := mock.ACLAuthMethod()
|
am1 := mock.ACLOIDCAuthMethod()
|
||||||
am2 := mock.ACLAuthMethod()
|
am2 := mock.ACLOIDCAuthMethod()
|
||||||
|
|
||||||
// First create an unrelated authMethod
|
// First create an unrelated authMethod
|
||||||
time.AfterFunc(100*time.Millisecond, func() {
|
time.AfterFunc(100*time.Millisecond, func() {
|
||||||
|
@ -2878,8 +2879,8 @@ func TestACLEndpoint_ListAuthMethods(t *testing.T) {
|
||||||
testutil.WaitForLeader(t, s1.RPC)
|
testutil.WaitForLeader(t, s1.RPC)
|
||||||
|
|
||||||
// Create the register request
|
// Create the register request
|
||||||
am1 := mock.ACLAuthMethod()
|
am1 := mock.ACLOIDCAuthMethod()
|
||||||
am2 := mock.ACLAuthMethod()
|
am2 := mock.ACLOIDCAuthMethod()
|
||||||
|
|
||||||
am1.Name = "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9"
|
am1.Name = "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9"
|
||||||
am2.Name = "aaaabbbb-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)
|
testutil.WaitForLeader(t, s1.RPC)
|
||||||
|
|
||||||
// Create the authMethod
|
// Create the authMethod
|
||||||
authMethod := mock.ACLAuthMethod()
|
authMethod := mock.ACLOIDCAuthMethod()
|
||||||
|
|
||||||
// Upsert auth method triggers watches
|
// Upsert auth method triggers watches
|
||||||
time.AfterFunc(100*time.Millisecond, func() {
|
time.AfterFunc(100*time.Millisecond, func() {
|
||||||
|
@ -2978,7 +2979,7 @@ func TestACLEndpoint_DeleteAuthMethods(t *testing.T) {
|
||||||
testutil.WaitForLeader(t, s1.RPC)
|
testutil.WaitForLeader(t, s1.RPC)
|
||||||
|
|
||||||
// Create the register request
|
// Create the register request
|
||||||
am1 := mock.ACLAuthMethod()
|
am1 := mock.ACLOIDCAuthMethod()
|
||||||
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{am1}))
|
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{am1}))
|
||||||
|
|
||||||
// Lookup the authMethods
|
// Lookup the authMethods
|
||||||
|
@ -3019,7 +3020,7 @@ func TestACLEndpoint_UpsertACLAuthMethods(t *testing.T) {
|
||||||
s1.config.ACLTokenMaxExpirationTTL = maxTTL
|
s1.config.ACLTokenMaxExpirationTTL = maxTTL
|
||||||
|
|
||||||
// Create the register request
|
// Create the register request
|
||||||
am1 := mock.ACLAuthMethod()
|
am1 := mock.ACLOIDCAuthMethod()
|
||||||
am1.Default = true // make sure it's going to be a default method
|
am1.Default = true // make sure it's going to be a default method
|
||||||
am1.SetHash()
|
am1.SetHash()
|
||||||
|
|
||||||
|
@ -3043,7 +3044,7 @@ func TestACLEndpoint_UpsertACLAuthMethods(t *testing.T) {
|
||||||
must.True(t, am1.Equal(resp.AuthMethods[0]))
|
must.True(t, am1.Equal(resp.AuthMethods[0]))
|
||||||
|
|
||||||
// Try to insert another default authMethod
|
// Try to insert another default authMethod
|
||||||
am2 := mock.ACLAuthMethod()
|
am2 := mock.ACLOIDCAuthMethod()
|
||||||
am2.Default = true
|
am2.Default = true
|
||||||
req = &structs.ACLAuthMethodUpsertRequest{
|
req = &structs.ACLAuthMethodUpsertRequest{
|
||||||
AuthMethods: []*structs.ACLAuthMethod{am2},
|
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")
|
must.EqError(t, err, "RPC Error:: 400,ACL auth method auth0 not found")
|
||||||
|
|
||||||
// Create the policies our ACL roles wants to link to.
|
// Create the policies our ACL roles wants to link to.
|
||||||
authMethod := mock.ACLAuthMethod()
|
authMethod := mock.ACLOIDCAuthMethod()
|
||||||
authMethod.Name = aclBindingRule1.AuthMethod
|
authMethod.Name = aclBindingRule1.AuthMethod
|
||||||
|
|
||||||
must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{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
|
// Generate and upsert an ACL auth method for use. Certain values must be
|
||||||
// taken from the cap OIDC provider just like real world use.
|
// 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.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
||||||
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
||||||
mockedAuthMethod.Config.SigningAlgs = []string{"ES256"}
|
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)
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq1, &completeAuthResp1)
|
||||||
must.Error(t, err)
|
must.Error(t, err)
|
||||||
must.ErrorContains(t, err, "400")
|
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)
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq2, &completeAuthResp2)
|
||||||
must.Error(t, err)
|
must.Error(t, err)
|
||||||
must.ErrorContains(t, err, "400")
|
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
|
// 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
|
// taken from the cap OIDC provider and these are validated. Others must
|
||||||
// match data we use later, such as the claims.
|
// match data we use later, such as the claims.
|
||||||
mockedAuthMethod := mock.ACLAuthMethod()
|
mockedAuthMethod := mock.ACLOIDCAuthMethod()
|
||||||
mockedAuthMethod.Config.BoundAudiences = []string{"mock"}
|
mockedAuthMethod.Config.BoundAudiences = []string{"mock"}
|
||||||
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
||||||
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
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)
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq3, &completeAuthResp3)
|
||||||
must.Error(t, err)
|
must.Error(t, err)
|
||||||
must.ErrorContains(t, err, "400")
|
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)
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq4, &completeAuthResp4)
|
||||||
must.NoError(t, err)
|
must.NoError(t, err)
|
||||||
must.NotNil(t, completeAuthResp4.ACLToken)
|
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)
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq5, &completeAuthResp5)
|
||||||
must.NoError(t, err)
|
must.NoError(t, err)
|
||||||
must.NotNil(t, completeAuthResp4.ACLToken)
|
must.NotNil(t, completeAuthResp4.ACLToken)
|
||||||
|
@ -3730,3 +3731,158 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
|
||||||
must.Len(t, 0, completeAuthResp5.ACLToken.Roles)
|
must.Len(t, 0, completeAuthResp5.ACLToken.Roles)
|
||||||
must.Eq(t, structs.ACLManagementToken, completeAuthResp5.ACLToken.Type)
|
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()
|
testState := fsm.State()
|
||||||
|
|
||||||
// Generate and upsert some ACL auth methods.
|
// 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))
|
must.NoError(t, testState.UpsertACLAuthMethods(10, authMethods))
|
||||||
|
|
||||||
// Perform a snapshot restore.
|
// Perform a snapshot restore.
|
||||||
|
@ -3540,8 +3540,8 @@ func TestFSM_UpsertACLAuthMethods(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
fsm := testFSM(t)
|
fsm := testFSM(t)
|
||||||
|
|
||||||
am1 := mock.ACLAuthMethod()
|
am1 := mock.ACLOIDCAuthMethod()
|
||||||
am2 := mock.ACLAuthMethod()
|
am2 := mock.ACLOIDCAuthMethod()
|
||||||
req := structs.ACLAuthMethodUpsertRequest{
|
req := structs.ACLAuthMethodUpsertRequest{
|
||||||
AuthMethods: []*structs.ACLAuthMethod{am1, am2},
|
AuthMethods: []*structs.ACLAuthMethod{am1, am2},
|
||||||
}
|
}
|
||||||
|
@ -3564,8 +3564,8 @@ func TestFSM_DeleteACLAuthMethods(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
fsm := testFSM(t)
|
fsm := testFSM(t)
|
||||||
|
|
||||||
am1 := mock.ACLAuthMethod()
|
am1 := mock.ACLOIDCAuthMethod()
|
||||||
am2 := mock.ACLAuthMethod()
|
am2 := mock.ACLOIDCAuthMethod()
|
||||||
must.Nil(t, fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{am1, am2}))
|
must.Nil(t, fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{am1, am2}))
|
||||||
|
|
||||||
req := structs.ACLAuthMethodDeleteRequest{
|
req := structs.ACLAuthMethodDeleteRequest{
|
||||||
|
@ -3591,7 +3591,7 @@ func TestFSM_UpsertACLBindingRules(t *testing.T) {
|
||||||
fsm := testFSM(t)
|
fsm := testFSM(t)
|
||||||
|
|
||||||
// Create an auth method and upsert so the binding rules can link to this.
|
// 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}))
|
must.NoError(t, fsm.state.UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{authMethod}))
|
||||||
|
|
||||||
aclBindingRule1 := mock.ACLBindingRule()
|
aclBindingRule1 := mock.ACLBindingRule()
|
||||||
|
|
|
@ -57,6 +57,14 @@ var minACLRoleVersion = version.Must(version.NewVersion("1.4.0"))
|
||||||
// meet before the feature can be used.
|
// meet before the feature can be used.
|
||||||
var minACLAuthMethodVersion = version.Must(version.NewVersion("1.5.0-beta.1"))
|
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
|
// minACLBindingRuleVersion is the Nomad version at which the ACL binding rules
|
||||||
// table was introduced. It forms the minimum version all federated servers
|
// table was introduced. It forms the minimum version all federated servers
|
||||||
// must meet before the feature can be used.
|
// must meet before the feature can be used.
|
||||||
|
|
|
@ -1301,10 +1301,10 @@ func Test_diffACLAuthMethods(t *testing.T) {
|
||||||
stateStore := state.TestStateStore(t)
|
stateStore := state.TestStateStore(t)
|
||||||
|
|
||||||
// Build an initial baseline of ACL auth-methods.
|
// Build an initial baseline of ACL auth-methods.
|
||||||
aclAuthMethod0 := mock.ACLAuthMethod()
|
aclAuthMethod0 := mock.ACLOIDCAuthMethod()
|
||||||
aclAuthMethod1 := mock.ACLAuthMethod()
|
aclAuthMethod1 := mock.ACLOIDCAuthMethod()
|
||||||
aclAuthMethod2 := mock.ACLAuthMethod()
|
aclAuthMethod2 := mock.ACLOIDCAuthMethod()
|
||||||
aclAuthMethod3 := mock.ACLAuthMethod()
|
aclAuthMethod3 := mock.ACLOIDCAuthMethod()
|
||||||
|
|
||||||
// Upsert these into our local state. Use copies, so we can alter the
|
// Upsert these into our local state. Use copies, so we can alter the
|
||||||
// auth-methods directly and use within the diff func.
|
// auth-methods directly and use within the diff func.
|
||||||
|
@ -1318,7 +1318,7 @@ func Test_diffACLAuthMethods(t *testing.T) {
|
||||||
aclAuthMethod2.ModifyIndex = 50
|
aclAuthMethod2.ModifyIndex = 50
|
||||||
aclAuthMethod3.ModifyIndex = 200
|
aclAuthMethod3.ModifyIndex = 200
|
||||||
aclAuthMethod3.Hash = []byte{0, 1, 2, 3}
|
aclAuthMethod3.Hash = []byte{0, 1, 2, 3}
|
||||||
aclAuthMethod4 := mock.ACLAuthMethod()
|
aclAuthMethod4 := mock.ACLOIDCAuthMethod()
|
||||||
|
|
||||||
// Run the diff function and test the output.
|
// Run the diff function and test the output.
|
||||||
toDelete, toUpdate := diffACLAuthMethods(stateStore, 50, []*structs.ACLAuthMethodStub{
|
toDelete, toUpdate := diffACLAuthMethods(stateStore, 50, []*structs.ACLAuthMethodStub{
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
package mock
|
package mock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/nomad/helper/uuid"
|
"github.com/golang-jwt/jwt/v4"
|
||||||
testing "github.com/mitchellh/go-testing-interface"
|
testing "github.com/mitchellh/go-testing-interface"
|
||||||
|
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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
|
// 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")
|
maxTokenTTL, _ := time.ParseDuration("3600s")
|
||||||
method := structs.ACLAuthMethod{
|
method := structs.ACLAuthMethod{
|
||||||
Name: fmt.Sprintf("acl-auth-method-%s", uuid.Short()),
|
Name: fmt.Sprintf("acl-auth-method-%s", uuid.Short()),
|
||||||
|
@ -232,10 +237,10 @@ func ACLAuthMethod() *structs.ACLAuthMethod {
|
||||||
OIDCClientID: "mock",
|
OIDCClientID: "mock",
|
||||||
OIDCClientSecret: "very secret secret",
|
OIDCClientSecret: "very secret secret",
|
||||||
OIDCScopes: []string{"groups"},
|
OIDCScopes: []string{"groups"},
|
||||||
BoundAudiences: []string{"audience1", "audience2"},
|
BoundAudiences: []string{"sales", "engineering"},
|
||||||
AllowedRedirectURIs: []string{"foo", "bar"},
|
AllowedRedirectURIs: []string{"foo", "bar"},
|
||||||
DiscoveryCaPem: []string{"foo"},
|
DiscoveryCaPem: []string{"foo"},
|
||||||
SigningAlgs: []string{"bar"},
|
SigningAlgs: []string{"RS256"},
|
||||||
ClaimMappings: map[string]string{"foo": "bar"},
|
ClaimMappings: map[string]string{"foo": "bar"},
|
||||||
ListClaimMappings: map[string]string{"foo": "bar"},
|
ListClaimMappings: map[string]string{"foo": "bar"},
|
||||||
},
|
},
|
||||||
|
@ -248,6 +253,73 @@ func ACLAuthMethod() *structs.ACLAuthMethod {
|
||||||
return &method
|
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 {
|
func ACLBindingRule() *structs.ACLBindingRule {
|
||||||
return &structs.ACLBindingRule{
|
return &structs.ACLBindingRule{
|
||||||
ID: uuid.Short(),
|
ID: uuid.Short(),
|
||||||
|
|
|
@ -1058,7 +1058,7 @@ func Test_eventsFromChanges_ACLAuthMethod(t *testing.T) {
|
||||||
defer testState.StopEventBroker()
|
defer testState.StopEventBroker()
|
||||||
|
|
||||||
// Generate a test ACL auth method
|
// Generate a test ACL auth method
|
||||||
authMethod := mock.ACLAuthMethod()
|
authMethod := mock.ACLOIDCAuthMethod()
|
||||||
|
|
||||||
// Upsert the auth method straight into state
|
// Upsert the auth method straight into state
|
||||||
writeTxn := testState.db.WriteTxn(10)
|
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
|
// Create an auth method and ensure the binding rule is updated, so it is
|
||||||
// related to it.
|
// related to it.
|
||||||
authMethod := mock.ACLAuthMethod()
|
authMethod := mock.ACLOIDCAuthMethod()
|
||||||
mockedACLBindingRules[0].AuthMethod = authMethod.Name
|
mockedACLBindingRules[0].AuthMethod = authMethod.Name
|
||||||
|
|
||||||
must.NoError(t, testState.UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{authMethod}))
|
must.NoError(t, testState.UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{authMethod}))
|
||||||
|
|
|
@ -15,7 +15,7 @@ func TestStateStore_UpsertACLAuthMethods(t *testing.T) {
|
||||||
testState := testStateStore(t)
|
testState := testStateStore(t)
|
||||||
|
|
||||||
// Create mock auth methods
|
// 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))
|
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
|
// Try adding a new auth method, which has a name clash with an existing
|
||||||
// entry.
|
// entry.
|
||||||
dup := mock.ACLAuthMethod()
|
dup := mock.ACLOIDCAuthMethod()
|
||||||
dup.Name = mockedACLAuthMethods[0].Name
|
dup.Name = mockedACLAuthMethods[0].Name
|
||||||
dup.Type = mockedACLAuthMethods[0].Type
|
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
|
// Generate some mocked ACL auth methods for testing and upsert these
|
||||||
// straight into state.
|
// straight into state.
|
||||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLAuthMethod(), mock.ACLAuthMethod()}
|
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLOIDCAuthMethod(), mock.ACLOIDCAuthMethod()}
|
||||||
must.NoError(t, testState.UpsertACLAuthMethods(10, mockedACLAuthMethods))
|
must.NoError(t, testState.UpsertACLAuthMethods(10, mockedACLAuthMethods))
|
||||||
|
|
||||||
// Try and delete a method using a name that doesn't exist. This should
|
// 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
|
// Generate a some mocked ACL auth methods for testing and upsert these
|
||||||
// straight into state.
|
// straight into state.
|
||||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLAuthMethod(), mock.ACLAuthMethod()}
|
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLOIDCAuthMethod(), mock.ACLOIDCAuthMethod()}
|
||||||
must.NoError(t, testState.UpsertACLAuthMethods(10, mockedACLAuthMethods))
|
must.NoError(t, testState.UpsertACLAuthMethods(10, mockedACLAuthMethods))
|
||||||
|
|
||||||
// List the auth methods and ensure they are exactly as we expect.
|
// 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
|
// Generate a some mocked ACL auth methods for testing and upsert these
|
||||||
// straight into state.
|
// straight into state.
|
||||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLAuthMethod(), mock.ACLAuthMethod()}
|
mockedACLAuthMethods := []*structs.ACLAuthMethod{mock.ACLOIDCAuthMethod(), mock.ACLOIDCAuthMethod()}
|
||||||
must.NoError(t, testState.UpsertACLAuthMethods(10, mockedACLAuthMethods))
|
must.NoError(t, testState.UpsertACLAuthMethods(10, mockedACLAuthMethods))
|
||||||
|
|
||||||
ws := memdb.NewWatchSet()
|
ws := memdb.NewWatchSet()
|
||||||
|
@ -232,9 +232,9 @@ func TestStateStore_GetDefaultACLAuthMethod(t *testing.T) {
|
||||||
testState := testStateStore(t)
|
testState := testStateStore(t)
|
||||||
|
|
||||||
// Generate 2 auth methods, make one of them default
|
// Generate 2 auth methods, make one of them default
|
||||||
am1 := mock.ACLAuthMethod()
|
am1 := mock.ACLOIDCAuthMethod()
|
||||||
am1.Default = true
|
am1.Default = true
|
||||||
am2 := mock.ACLAuthMethod()
|
am2 := mock.ACLOIDCAuthMethod()
|
||||||
|
|
||||||
// upsert
|
// upsert
|
||||||
mockedACLAuthMethods := []*structs.ACLAuthMethod{am1, am2}
|
mockedACLAuthMethods := []*structs.ACLAuthMethod{am1, am2}
|
||||||
|
|
|
@ -635,7 +635,7 @@ func TestStateStore_ACLAuthMethodRestore(t *testing.T) {
|
||||||
|
|
||||||
// Set up our test registrations and index.
|
// Set up our test registrations and index.
|
||||||
expectedIndex := uint64(13)
|
expectedIndex := uint64(13)
|
||||||
authMethod := mock.ACLAuthMethod()
|
authMethod := mock.ACLOIDCAuthMethod()
|
||||||
authMethod.CreateIndex = expectedIndex
|
authMethod.CreateIndex = expectedIndex
|
||||||
authMethod.ModifyIndex = expectedIndex
|
authMethod.ModifyIndex = expectedIndex
|
||||||
|
|
||||||
|
|
|
@ -169,6 +169,14 @@ const (
|
||||||
// Args: ACLOIDCCompleteAuthRequest
|
// Args: ACLOIDCCompleteAuthRequest
|
||||||
// Reply: ACLOIDCCompleteAuthResponse
|
// Reply: ACLOIDCCompleteAuthResponse
|
||||||
ACLOIDCCompleteAuthRPCMethod = "ACL.OIDCCompleteAuth"
|
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 (
|
const (
|
||||||
|
@ -185,6 +193,23 @@ const (
|
||||||
// maxACLBindingRuleDescriptionLength limits an ACL binding rules
|
// maxACLBindingRuleDescriptionLength limits an ACL binding rules
|
||||||
// description length and should be used to validate the object.
|
// description length and should be used to validate the object.
|
||||||
maxACLBindingRuleDescriptionLength = 256
|
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 (
|
var (
|
||||||
|
@ -193,6 +218,9 @@ var (
|
||||||
|
|
||||||
// ValidACLAuthMethod is used to validate an ACL auth method name.
|
// ValidACLAuthMethod is used to validate an ACL auth method name.
|
||||||
ValidACLAuthMethod = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
|
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]
|
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))
|
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 = append(
|
||||||
mErr.Errors, fmt.Errorf("invalid token type '%s'", a.Type))
|
mErr.Errors, fmt.Errorf("invalid token type '%s'", a.Type))
|
||||||
}
|
}
|
||||||
|
@ -991,6 +1019,93 @@ func (a *ACLAuthMethodConfig) Copy() *ACLAuthMethodConfig {
|
||||||
return c
|
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
|
// 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
|
// can be used with go-bexpr. This structure is used during rule binding
|
||||||
// evaluation.
|
// evaluation.
|
||||||
|
@ -1480,9 +1595,39 @@ func (a *ACLOIDCCompleteAuthRequest) Validate() error {
|
||||||
return mErr.ErrorOrNil()
|
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.
|
// completed successfully.
|
||||||
type ACLOIDCCompleteAuthResponse struct {
|
type ACLLoginResponse struct {
|
||||||
ACLToken *ACLToken
|
ACLToken *ACLToken
|
||||||
WriteMeta
|
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