diff --git a/.semgrep/rpc_endpoint.yml b/.semgrep/rpc_endpoint.yml index f32b327e7..95f01881f 100644 --- a/.semgrep/rpc_endpoint.yml +++ b/.semgrep/rpc_endpoint.yml @@ -62,6 +62,12 @@ rules: ... return $A.deregister(...) ... + # Pattern used by Authenticate method. + # TODO: add authorization steps as well. + - pattern-not-inside: | + ... + ... := $A.$B.Authenticate($A.ctx, args.AuthToken) + ... - metavariable-pattern: metavariable: $METHOD patterns: diff --git a/helper/tlsutil/testdata/nomad-foo-client-key.pem b/helper/tlsutil/testdata/nomad-foo-client-key.pem new file mode 100644 index 000000000..56ad2d3b3 --- /dev/null +++ b/helper/tlsutil/testdata/nomad-foo-client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIBxaGxJxJXnAXVmb8E3ALsWqva9F01R0cr/1Ap75YyeAoAoGCCqGSM49 +AwEHoUQDQgAEXSLJPcA7b9P6y0Ls7zR4997+F3251hwEUn8qR01AEVGjYrAjk/ns +qaq7P9y/w4k9TvhWaq9/L6id468a0/VWCw== +-----END EC PRIVATE KEY----- diff --git a/helper/tlsutil/testdata/nomad-foo-client.pem b/helper/tlsutil/testdata/nomad-foo-client.pem new file mode 100644 index 000000000..27c3c94e8 --- /dev/null +++ b/helper/tlsutil/testdata/nomad-foo-client.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICWTCCAgCgAwIBAgIQOW7/CDB2IhlMyfh16erD/jAKBggqhkjOPQQDAjB4MQsw +CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy +YW5jaXNjbzESMBAGA1UEChMJSGFzaGlDb3JwMQ4wDAYDVQQLEwVOb21hZDEYMBYG +A1UEAxMPbm9tYWQuaGFzaGljb3JwMCAXDTIyMTEyOTE5MjY0MloYDzIxMjIxMTA1 +MTkyNjQyWjAhMR8wHQYDVQQDExZjbGllbnQucmVnaW9uRm9vLm5vbWFkMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEXSLJPcA7b9P6y0Ls7zR4997+F3251hwEUn8q +R01AEVGjYrAjk/nsqaq7P9y/w4k9TvhWaq9/L6id468a0/VWC6OBwDCBvTAOBgNV +HQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAwGA1Ud +EwEB/wQCMAAwKQYDVR0OBCIEII1J2DmAAcPAaNLFlxFpdBzjhRFRd9E9fedoz9I8 +vHPPMB8GA1UdIwQYMBaAFKJkNK006jVs/eYf4w00jciQj2MEMDIGA1UdEQQrMCmC +FmNsaWVudC5yZWdpb25Gb28ubm9tYWSCCWxvY2FsaG9zdIcEfwAAATAKBggqhkjO +PQQDAgNHADBEAiAXzlb98iqyXvtlkThR13ojgjwjP25JBysDKf4vnXjQuwIgFpkB +0B7bPy5VNIAVsw6n5ocvsB7w0rgBPJyS3I2YCi0= +-----END CERTIFICATE----- diff --git a/nomad/acl.go b/nomad/acl.go index 7c14bfa1c..ab73ea705 100644 --- a/nomad/acl.go +++ b/nomad/acl.go @@ -1,16 +1,133 @@ package nomad import ( + "errors" "fmt" + "net" "time" metrics "github.com/armon/go-metrics" lru "github.com/hashicorp/golang-lru" "github.com/hashicorp/nomad/acl" + "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/nomad/state" "github.com/hashicorp/nomad/nomad/structs" ) +// Authenticate extracts an AuthenticatedIdentity from the request context or +// provided token. The caller can extract an acl.ACL, WorkloadIdentity, or other +// identifying token to use for authorization. +// +// Note: when called on the follower we'll be making stale queries, so it's +// possible if the follower is behind that the leader will get a different value +// if an ACL token or allocation's WI has just been created. +func (s *Server) Authenticate(ctx *RPCContext, secretID string) (*structs.AuthenticatedIdentity, error) { + + // Previously-connected clients will have a NodeID set and will be a large + // number of the RPCs sent, so we can fast path this case + if ctx != nil && ctx.NodeID != "" { + return &structs.AuthenticatedIdentity{ClientID: ctx.NodeID}, nil + } + + // get the user ACLToken or anonymous token + aclToken, err := s.ResolveSecretToken(secretID) + + switch { + case err == nil: + // If ACLs are disabled or we have a non-anonymous token, return that. + if aclToken == nil || aclToken != structs.AnonymousACLToken { + return &structs.AuthenticatedIdentity{ACLToken: aclToken}, nil + } + + case errors.Is(err, structs.ErrTokenExpired): + return nil, err + + case errors.Is(err, structs.ErrTokenInvalid): + // if it's not a UUID it might be an identity claim + claims, err := s.VerifyClaim(secretID) + if err != nil { + // we already know the token wasn't valid for an ACL in the state + // store, so if we get an error at this point we have an invalid + // token and there are no other options but to bail out + return nil, err + } + + return &structs.AuthenticatedIdentity{Claims: claims}, nil + + case errors.Is(err, structs.ErrTokenNotFound): + // Check if the secret ID is the leader's secret ID, in which case treat + // it as a management token. + leaderAcl := s.getLeaderAcl() + if leaderAcl != "" && secretID == leaderAcl { + aclToken = structs.LeaderACLToken + } else { + // Otherwise, see if the secret ID belongs to a node. We should + // reach this point only on first connection. + node, err := s.State().NodeBySecretID(nil, secretID) + if err != nil { + // this is a go-memdb error; shouldn't happen + return nil, fmt.Errorf("could not resolve node secret: %w", err) + } + if node != nil { + return &structs.AuthenticatedIdentity{ClientID: node.ID}, nil + } + } + + default: // any other error + return nil, fmt.Errorf("could not resolve user: %w", err) + + } + + // If there's no context we're in a "static" handler which only happens for + // cases where the leader is making RPCs internally (volumewatcher and + // deploymentwatcher) + if ctx == nil { + return &structs.AuthenticatedIdentity{ACLToken: aclToken}, nil + } + + // At this point we either have an anonymous token or an invalid one. + // Unlike clients that provide their Node ID on first connection, server + // RPCs don't include an ID for the server so we identify servers by cert + // and IP address. + identity := &structs.AuthenticatedIdentity{ACLToken: aclToken} + if ctx.TLS { + identity.TLSName = ctx.Certificate().Subject.CommonName + } + + var remoteAddr *net.TCPAddr + var ok bool + if ctx.Session != nil { + remoteAddr, ok = ctx.Session.RemoteAddr().(*net.TCPAddr) + if !ok { + return nil, errors.New("session address was not a TCP address") + } + } + if remoteAddr == nil && ctx.Conn != nil { + remoteAddr, ok = ctx.Conn.RemoteAddr().(*net.TCPAddr) + if !ok { + return nil, errors.New("session address was not a TCP address") + } + } + if remoteAddr != nil { + identity.RemoteIP = remoteAddr.IP + return identity, nil + } + + s.logger.Error("could not authenticate RPC request or determine remote address") + return nil, structs.ErrPermissionDenied +} + +func (s *Server) ResolveACL(aclToken *structs.ACLToken) (*acl.ACL, error) { + if !s.config.ACLEnabled { + return nil, nil + } + snap, err := s.fsm.State().Snapshot() + if err != nil { + return nil, err + } + return resolveACLFromToken(snap, s.aclCache, aclToken) +} + // ResolveToken is used to translate an ACL Token Secret ID into // an ACL object, nil if ACLs are disabled, or an error. func (s *Server) ResolveToken(secretID string) (*acl.ACL, error) { @@ -106,6 +223,12 @@ func resolveTokenFromSnapshotCache(snap *state.StateSnapshot, cache *lru.TwoQueu } } + return resolveACLFromToken(snap, cache, token) + +} + +func resolveACLFromToken(snap *state.StateSnapshot, cache *lru.TwoQueueCache, token *structs.ACLToken) (*acl.ACL, error) { + // Check if this is a management token if token.Type == structs.ACLManagementToken { return acl.ManagementACL, nil @@ -185,27 +308,28 @@ func (s *Server) ResolveSecretToken(secretID string) (*structs.ACLToken, error) } defer metrics.MeasureSince([]string{"nomad", "acl", "resolveSecretToken"}, time.Now()) + if secretID == "" { + return structs.AnonymousACLToken, nil + } + if !helper.IsUUID(secretID) { + return nil, structs.ErrTokenInvalid + } + snap, err := s.fsm.State().Snapshot() if err != nil { return nil, err } // Lookup the ACL Token - var token *structs.ACLToken - // Handle anonymous requests - if secretID == "" { - token = structs.AnonymousACLToken - } else { - token, err = snap.ACLTokenBySecretID(nil, secretID) - if err != nil { - return nil, err - } - if token == nil { - return nil, structs.ErrTokenNotFound - } - if token.IsExpired(time.Now().UTC()) { - return nil, structs.ErrTokenExpired - } + token, err := snap.ACLTokenBySecretID(nil, secretID) + if err != nil { + return nil, err + } + if token == nil { + return nil, structs.ErrTokenNotFound + } + if token.IsExpired(time.Now().UTC()) { + return nil, structs.ErrTokenExpired } return token, nil diff --git a/nomad/acl_endpoint.go b/nomad/acl_endpoint.go index 7d0288ad9..f2de31bcf 100644 --- a/nomad/acl_endpoint.go +++ b/nomad/acl_endpoint.go @@ -1964,3 +1964,25 @@ func (a *ACL) GetAuthMethods( }}, ) } + +// WhoAmI is a RPC for debugging authentication. This endpoint returns the same +// AuthenticatedIdentity that will be used by RPC handlers. +// +// TODO: At some point we might want to give this an equivalent HTTP endpoint +// once other Workload Identity work is solidified +func (a *ACL) WhoAmI(args *structs.GenericRequest, reply *structs.ACLWhoAmIResponse) error { + + identity, err := a.srv.Authenticate(a.ctx, args.AuthToken) + if err != nil { + return err + } + args.SetIdentity(identity) + + if done, err := a.srv.forward("ACL.WhoAmI", args, args, reply); done { + return err + } + defer metrics.MeasureSince([]string{"nomad", "acl", "whoami"}, time.Now()) + + reply.Identity = args.GetIdentity() + return nil +} diff --git a/nomad/acl_test.go b/nomad/acl_test.go index c9f46245b..9b7590794 100644 --- a/nomad/acl_test.go +++ b/nomad/acl_test.go @@ -1,20 +1,302 @@ package nomad import ( + "path" "testing" "time" + msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc" "github.com/hashicorp/nomad/acl" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/helper/uuid" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" + "github.com/hashicorp/nomad/nomad/structs/config" "github.com/hashicorp/nomad/testutil" + "github.com/shoenig/test" "github.com/shoenig/test/must" "github.com/stretchr/testify/require" ) +func TestAuthenticate_mTLS(t *testing.T) { + ci.Parallel(t) + + // Set up a cluster with mTLS and ACLs + + dir := t.TempDir() + + tlsCfg := &config.TLSConfig{ + EnableHTTP: true, + EnableRPC: true, + VerifyServerHostname: true, + CAFile: "../helper/tlsutil/testdata/ca.pem", + CertFile: "../helper/tlsutil/testdata/nomad-foo.pem", + KeyFile: "../helper/tlsutil/testdata/nomad-foo-key.pem", + } + clientTLSCfg := tlsCfg.Copy() + clientTLSCfg.CertFile = "../helper/tlsutil/testdata/nomad-foo-client.pem" + clientTLSCfg.KeyFile = "../helper/tlsutil/testdata/nomad-foo-client-key.pem" + + setCfg := func(name string, bootstrapExpect int) func(*Config) { + return func(c *Config) { + c.Region = "regionFoo" + c.AuthoritativeRegion = "regionFoo" + c.ACLEnabled = true + c.BootstrapExpect = bootstrapExpect + c.NumSchedulers = 0 + c.DevMode = false + c.DataDir = path.Join(dir, name) + c.TLSConfig = tlsCfg + } + } + + leader, cleanupLeader := TestServer(t, setCfg("node1", 1)) + defer cleanupLeader() + testutil.WaitForLeader(t, leader.RPC) + + follower, cleanupFollower := TestServer(t, setCfg("node2", 0)) + defer cleanupFollower() + + TestJoin(t, leader, follower) + testutil.WaitForLeader(t, leader.RPC) + + testutil.Wait(t, func() (bool, error) { + keyset, err := follower.encrypter.activeKeySet() + return keyset != nil, err + }) + + rootToken := uuid.Generate() + var bootstrapResp *structs.ACLTokenUpsertResponse + + codec := rpcClientWithTLS(t, follower, tlsCfg) + must.NoError(t, msgpackrpc.CallWithCodec(codec, + "ACL.Bootstrap", &structs.ACLTokenBootstrapRequest{ + BootstrapSecret: rootToken, + WriteRequest: structs.WriteRequest{Region: "regionFoo"}, + }, &bootstrapResp)) + must.NotNil(t, bootstrapResp) + must.Len(t, 1, bootstrapResp.Tokens) + rootAccessor := bootstrapResp.Tokens[0].AccessorID + + // create some ACL tokens directly into raft so we can bypass RPC validation + // around expiration times + + token1 := mock.ACLToken() + token2 := mock.ACLToken() + expireTime := time.Now().Add(time.Second * -10) + token2.ExpirationTime = &expireTime + + _, _, err := leader.raftApply(structs.ACLTokenUpsertRequestType, + &structs.ACLTokenUpsertRequest{Tokens: []*structs.ACLToken{token1, token2}}) + must.NoError(t, err) + + // create a node so we can test client RPCs + + node := mock.Node() + nodeRegisterReq := &structs.NodeRegisterRequest{ + Node: node, + WriteRequest: structs.WriteRequest{Region: "regionFoo"}, + } + var nodeRegisterResp structs.NodeUpdateResponse + + must.NoError(t, msgpackrpc.CallWithCodec(codec, + "Node.Register", nodeRegisterReq, &nodeRegisterResp)) + must.NotNil(t, bootstrapResp) + + // create some allocations so we can test WorkloadIdentity claims. we'll + // create directly into raft so we can bypass RPC validation and the whole + // eval, plan, etc. workflow. + job := mock.Job() + + _, _, err = leader.raftApply(structs.JobRegisterRequestType, + &structs.JobRegisterRequest{Job: job}) + must.NoError(t, err) + + alloc1 := mock.Alloc() + alloc1.NodeID = node.ID + alloc1.ClientStatus = structs.AllocClientStatusFailed + alloc1.Job = job + alloc1.JobID = job.ID + + alloc2 := mock.Alloc() + alloc2.NodeID = node.ID + alloc2.Job = job + alloc2.JobID = job.ID + alloc2.ClientStatus = structs.AllocClientStatusRunning + + claims1 := alloc1.ToTaskIdentityClaims(nil, "web") + claims1Token, _, err := leader.encrypter.SignClaims(claims1) + must.NoError(t, err, must.Sprint("could not sign claims")) + + claims2 := alloc2.ToTaskIdentityClaims(nil, "web") + claims2Token, _, err := leader.encrypter.SignClaims(claims2) + must.NoError(t, err, must.Sprint("could not sign claims")) + + planReq := &structs.ApplyPlanResultsRequest{ + AllocUpdateRequest: structs.AllocUpdateRequest{ + Alloc: []*structs.Allocation{alloc1, alloc2}, + Job: job, + }, + } + _, _, err = leader.raftApply(structs.ApplyPlanResultsRequestType, planReq) + must.NoError(t, err) + + testutil.WaitForResult(func() (bool, error) { + store := follower.fsm.State() + alloc, err := store.AllocByID(nil, alloc1.ID) + return alloc != nil, err + }, func(err error) { + t.Fatalf("alloc was not replicated via raft: %v", err) // should never happen + }) + + testCases := []struct { + name string + tlsCfg *config.TLSConfig + stale bool + testToken string + expectAccessor string + expectClientID string + expectAllocID string + expectTLSName string + expectIP string + expectErr string + sendFromPeer *Server + }{ + { + name: "root token", + tlsCfg: clientTLSCfg, // TODO: this is a mixed use cert + testToken: rootToken, + expectAccessor: rootAccessor, + }, + { + name: "from peer to leader without token", // ex. Eval.Dequeue + tlsCfg: tlsCfg, + expectTLSName: "regionFoo.nomad", + expectAccessor: "anonymous", + expectIP: follower.GetConfig().RPCAddr.IP.String(), + sendFromPeer: follower, + }, + { + // note: this test is somewhat bogus because under test all the + // servers share the same IP address with the RPC client + name: "anonymous forwarded from peer to leader", + tlsCfg: tlsCfg, + expectAccessor: "anonymous", + expectTLSName: "regionFoo.nomad", + expectIP: "127.0.0.1", + }, + { + name: "invalid token", + tlsCfg: clientTLSCfg, + testToken: uuid.Generate(), + expectTLSName: "regionFoo.nomad", + expectIP: follower.GetConfig().RPCAddr.IP.String(), + }, + { + name: "expired token", + tlsCfg: clientTLSCfg, + testToken: uuid.Generate(), + expectTLSName: "regionFoo.nomad", + expectIP: follower.GetConfig().RPCAddr.IP.String(), + }, + { + name: "from peer to leader with leader ACL", // ex. core job GC + tlsCfg: tlsCfg, + testToken: leader.getLeaderAcl(), + expectTLSName: "regionFoo.nomad", + expectAccessor: "leader", + expectIP: follower.GetConfig().RPCAddr.IP.String(), + sendFromPeer: follower, + }, + { + name: "from client", // ex. Node.GetAllocs + tlsCfg: clientTLSCfg, + testToken: node.SecretID, + expectClientID: node.ID, + }, + { + name: "from failed workload", // ex. Variables.List + tlsCfg: clientTLSCfg, + testToken: claims1Token, + expectErr: "rpc error: allocation is terminal", + }, + { + name: "from running workload", // ex. Variables.List + tlsCfg: clientTLSCfg, + testToken: claims2Token, + expectAllocID: alloc2.ID, + }, + { + name: "valid user token", + tlsCfg: clientTLSCfg, + testToken: token1.SecretID, + expectAccessor: token1.AccessorID, + }, + { + name: "expired user token", + tlsCfg: clientTLSCfg, + testToken: token2.SecretID, + expectErr: "rpc error: ACL token expired", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + req := &structs.GenericRequest{ + QueryOptions: structs.QueryOptions{ + Region: "regionFoo", + AllowStale: tc.stale, + AuthToken: tc.testToken, + }, + } + var resp structs.ACLWhoAmIResponse + var err error + + if tc.sendFromPeer != nil { + aclEndpoint := NewACLEndpoint(tc.sendFromPeer, nil) + err = aclEndpoint.WhoAmI(req, &resp) + } else { + err = msgpackrpc.CallWithCodec(codec, "ACL.WhoAmI", req, &resp) + } + + if tc.expectErr != "" { + must.EqError(t, err, tc.expectErr) + return + } + + must.NoError(t, err) + must.NotNil(t, resp) + must.NotNil(t, resp.Identity) + + if tc.expectAccessor != "" { + must.NotNil(t, resp.Identity.ACLToken, must.Sprint("expected ACL token")) + test.Eq(t, tc.expectAccessor, resp.Identity.ACLToken.AccessorID, + must.Sprint("expected ACL token accessor ID")) + } + + test.Eq(t, tc.expectClientID, resp.Identity.ClientID, + must.Sprint("expected client ID")) + + if tc.expectAllocID != "" { + must.NotNil(t, resp.Identity.Claims, must.Sprint("expected claims")) + test.Eq(t, tc.expectAllocID, resp.Identity.Claims.AllocationID, + must.Sprint("expected workload identity")) + } + + test.Eq(t, tc.expectTLSName, resp.Identity.TLSName, must.Sprint("expected TLS name")) + + if tc.expectIP == "" { + test.Nil(t, resp.Identity.RemoteIP, must.Sprint("expected no remote IP")) + } else { + test.Eq(t, tc.expectIP, resp.Identity.RemoteIP.String()) + } + + }) + } +} + func TestResolveACLToken(t *testing.T) { ci.Parallel(t) diff --git a/nomad/namespace_endpoint.go b/nomad/namespace_endpoint.go index 0a6a77800..1b7f29760 100644 --- a/nomad/namespace_endpoint.go +++ b/nomad/namespace_endpoint.go @@ -25,6 +25,13 @@ func NewNamespaceEndpoint(srv *Server, ctx *RPCContext) *Namespace { // UpsertNamespaces is used to upsert a set of namespaces func (n *Namespace) UpsertNamespaces(args *structs.NamespaceUpsertRequest, reply *structs.GenericResponse) error { + + identity, err := n.srv.Authenticate(n.ctx, args.AuthToken) + if err != nil { + return err + } + args.SetIdentity(identity) + args.Region = n.srv.config.AuthoritativeRegion if done, err := n.srv.forward("Namespace.UpsertNamespaces", args, args, reply); done { return err @@ -32,7 +39,7 @@ func (n *Namespace) UpsertNamespaces(args *structs.NamespaceUpsertRequest, defer metrics.MeasureSince([]string{"nomad", "namespace", "upsert_namespaces"}, time.Now()) // Check management permissions - if aclObj, err := n.srv.ResolveToken(args.AuthToken); err != nil { + if aclObj, err := n.srv.ResolveACL(args.GetIdentity().GetACLToken()); err != nil { return err } else if aclObj != nil && !aclObj.IsManagement() { return structs.ErrPermissionDenied diff --git a/nomad/rpc_test.go b/nomad/rpc_test.go index 61397547c..af6e89dae 100644 --- a/nomad/rpc_test.go +++ b/nomad/rpc_test.go @@ -31,6 +31,7 @@ import ( "github.com/hashicorp/nomad/testutil" "github.com/hashicorp/raft" "github.com/hashicorp/yamux" + "github.com/shoenig/test/must" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,6 +39,7 @@ import ( // rpcClient is a test helper method to return a ClientCodec to use to make rpc // calls to the passed server. func rpcClient(t *testing.T, s *Server) rpc.ClientCodec { + t.Helper() addr := s.config.RPCAddr conn, err := net.DialTimeout("tcp", addr.String(), time.Second) if err != nil { @@ -48,6 +50,36 @@ func rpcClient(t *testing.T, s *Server) rpc.ClientCodec { return pool.NewClientCodec(conn) } +// rpcClientWithTLS is a test helper method to return a ClientCodec to use to +// make RPC calls to the passed server via mTLS +func rpcClientWithTLS(t *testing.T, srv *Server, cfg *config.TLSConfig) rpc.ClientCodec { + t.Helper() + + // configure TLS, ignoring client-side validation + tlsConf, err := tlsutil.NewTLSConfiguration(cfg, true, true) + must.NoError(t, err) + outTLSConf, err := tlsConf.OutgoingTLSConfig() + must.NoError(t, err) + outTLSConf.InsecureSkipVerify = true + + // make the TCP connection + conn, err := net.DialTimeout("tcp", srv.config.RPCAddr.String(), time.Second) + + // write the TLS byte to set the mode + _, err = conn.Write([]byte{byte(pool.RpcTLS)}) + must.NoError(t, err) + + // connect w/ TLS + tlsConn := tls.Client(conn, outTLSConf) + must.NoError(t, tlsConn.Handshake()) + + // write the Nomad RPC byte to set the mode + _, err = tlsConn.Write([]byte{byte(pool.RpcNomad)}) + must.NoError(t, err) + + return pool.NewClientCodec(tlsConn) +} + func TestRPC_forwardLeader(t *testing.T) { ci.Parallel(t) diff --git a/nomad/structs/acl.go b/nomad/structs/acl.go index 702e560bc..cb194cfb7 100644 --- a/nomad/structs/acl.go +++ b/nomad/structs/acl.go @@ -843,3 +843,8 @@ type ACLAuthMethodDeleteRequest struct { type ACLAuthMethodDeleteResponse struct { WriteMeta } + +type ACLWhoAmIResponse struct { + Identity *AuthenticatedIdentity + QueryMeta +} diff --git a/nomad/structs/errors.go b/nomad/structs/errors.go index c96a5e9d0..b46d32505 100644 --- a/nomad/structs/errors.go +++ b/nomad/structs/errors.go @@ -13,6 +13,7 @@ const ( errNoRegionPath = "No path to region" errTokenNotFound = "ACL token not found" errTokenExpired = "ACL token expired" + errTokenInvalid = "ACL token is invalid" // not a UUID errPermissionDenied = "Permission denied" errJobRegistrationDisabled = "Job registration, dispatch, and scale are disabled by the scheduler configuration" errNoNodeConn = "No path to node" @@ -50,6 +51,7 @@ var ( ErrNoRegionPath = errors.New(errNoRegionPath) ErrTokenNotFound = errors.New(errTokenNotFound) ErrTokenExpired = errors.New(errTokenExpired) + ErrTokenInvalid = errors.New(errTokenInvalid) ErrPermissionDenied = errors.New(errPermissionDenied) ErrJobRegistrationDisabled = errors.New(errJobRegistrationDisabled) ErrNoNodeConn = errors.New(errNoNodeConn) diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 3ebeecbec..b5d28cd38 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -293,6 +293,8 @@ type QueryOptions struct { // Reverse is used to reverse the default order of list results. Reverse bool + identity *AuthenticatedIdentity + InternalRpcInfo } @@ -339,6 +341,14 @@ func (q QueryOptions) AllowStaleRead() bool { return q.AllowStale } +func (q *QueryOptions) SetIdentity(identity *AuthenticatedIdentity) { + q.identity = identity +} + +func (q QueryOptions) GetIdentity() *AuthenticatedIdentity { + return q.identity +} + // AgentPprofRequest is used to request a pprof report for a given node. type AgentPprofRequest struct { // ReqType specifies the profile to use @@ -399,6 +409,8 @@ type WriteRequest struct { // IdempotencyToken can be used to ensure the write is idempotent. IdempotencyToken string + identity *AuthenticatedIdentity + InternalRpcInfo } @@ -435,6 +447,41 @@ func (w WriteRequest) AllowStaleRead() bool { return false } +func (w *WriteRequest) SetIdentity(identity *AuthenticatedIdentity) { + w.identity = identity +} + +func (w WriteRequest) GetIdentity() *AuthenticatedIdentity { + return w.identity +} + +// AuthenticatedIdentity is returned by the Authenticate method on server to +// return a wrapper around the various elements that can be resolved as an +// identity. RPC handlers will use the relevant fields for performing +// authorization. +type AuthenticatedIdentity struct { + ACLToken *ACLToken + Claims *IdentityClaims + ClientID string + ServerID string + TLSName string + RemoteIP net.IP +} + +func (ai *AuthenticatedIdentity) GetACLToken() *ACLToken { + if ai == nil { + return nil + } + return ai.ACLToken +} + +func (ai *AuthenticatedIdentity) GetClaims() *IdentityClaims { + if ai == nil { + return nil + } + return ai.Claims +} + // QueryMeta allows a query response to include potentially // useful metadata about a query type QueryMeta struct { @@ -12086,6 +12133,14 @@ var ( Policies: []string{"anonymous"}, Global: false, } + + // LeaderACLToken is used to represent a leader's own token; this object + // never gets used except on the leader + LeaderACLToken = &ACLToken{ + AccessorID: "leader", + Name: "Leader Token", + Type: ACLManagementToken, + } ) type ACLTokenListStub struct {