Files
teleport/lib/auth/access_request_test.go
T

687 lines
23 KiB
Go

/*
Copyright 2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package auth
import (
"context"
"crypto/tls"
"sort"
"strings"
"testing"
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type accessRequestTestPack struct {
tlsServer *TestTLSServer
clusterName string
roles map[string]types.RoleSpecV5
users map[string][]string
privKey []byte
pubKey []byte
}
func newAccessRequestTestPack(ctx context.Context, t *testing.T) *accessRequestTestPack {
testAuthServer, err := NewTestAuthServer(TestAuthServerConfig{
Dir: t.TempDir(),
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, testAuthServer.Close()) })
tlsServer, err := testAuthServer.NewTestTLSServer()
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, tlsServer.Close()) })
clusterName, err := tlsServer.Auth().GetClusterName()
require.NoError(t, err)
roles := map[string]types.RoleSpecV5{
// superadmins have access to all nodes
"superadmins": {
Allow: types.RoleConditions{
Logins: []string{"root"},
NodeLabels: types.Labels{
"*": []string{"*"},
},
},
},
// admins have access to nodes in prod and staging
"admins": {
Allow: types.RoleConditions{
Logins: []string{"root"},
NodeLabels: types.Labels{
"env": []string{"prod", "staging"},
},
ReviewRequests: &types.AccessReviewConditions{
Roles: []string{"admins"},
},
},
},
// operators can request the admins role
"operators": {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"admins"},
},
},
},
// responders can request the admins role but only with a
// resource request limited to specific resources
"responders": {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
SearchAsRoles: []string{"admins"},
},
},
},
// requesters can request everything possible
"requesters": {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"admins", "superadmins"},
SearchAsRoles: []string{"admins", "superadmins"},
},
},
},
"empty": {},
}
for roleName, roleSpec := range roles {
role, err := types.NewRole(roleName, roleSpec)
require.NoError(t, err)
err = tlsServer.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
}
users := map[string][]string{
"admin": {"admins"},
"responder": {"responders"},
"operator": {"operators"},
"requester": {"requesters"},
"nobody": {"empty"},
}
for name, roles := range users {
user, err := types.NewUser(name)
require.NoError(t, err)
user.SetRoles(roles)
err = tlsServer.Auth().UpsertUser(user)
require.NoError(t, err)
}
type nodeDesc struct {
labels map[string]string
}
nodes := map[string]nodeDesc{
"staging": {
labels: map[string]string{
"env": "staging",
},
},
"prod": {
labels: map[string]string{
"env": "prod",
},
},
}
for nodeName, desc := range nodes {
node, err := types.NewServerWithLabels(nodeName, types.KindNode, types.ServerSpecV2{}, desc.labels)
require.NoError(t, err)
_, err = tlsServer.Auth().UpsertNode(context.Background(), node)
require.NoError(t, err)
}
privKey, pubKey, err := testauthority.New().GenerateKeyPair()
require.NoError(t, err)
return &accessRequestTestPack{
tlsServer: tlsServer,
clusterName: clusterName.GetClusterName(),
roles: roles,
users: users,
privKey: privKey,
pubKey: pubKey,
}
}
func TestAccessRequest(t *testing.T) {
modules.SetTestModules(t, &modules.TestModules{TestBuildType: modules.BuildEnterprise})
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
testPack := newAccessRequestTestPack(ctx, t)
t.Run("single", func(t *testing.T) { testSingleAccessRequests(t, testPack) })
t.Run("multi", func(t *testing.T) { testMultiAccessRequests(t, testPack) })
}
func testSingleAccessRequests(t *testing.T, testPack *accessRequestTestPack) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
testCases := []struct {
desc string
requester string
reviewer string
expectRequestableRoles []string
requestRoles []string
expectRoles []string
expectNodes []string
requestResources []string
expectRequestError error
expectReviewError error
}{
{
desc: "role request",
requester: "operator",
reviewer: "admin",
expectRequestableRoles: []string{"admins"},
requestRoles: []string{"admins"},
expectRoles: []string{"operators", "admins"},
expectNodes: []string{"prod", "staging"},
},
{
desc: "no requestable roles",
requester: "nobody",
requestRoles: []string{"admins"},
expectRequestError: trace.BadParameter(`user "nobody" can not request role "admins"`),
},
{
desc: "role not allowed",
requester: "operator",
expectRequestableRoles: []string{"admins"},
requestRoles: []string{"super-admins"},
expectRequestError: trace.BadParameter(`user "operator" can not request role "super-admins"`),
},
{
desc: "review own request",
requester: "operator",
reviewer: "operator",
expectRequestableRoles: []string{"admins"},
requestRoles: []string{"admins"},
expectReviewError: trace.AccessDenied(`user "operator" cannot submit reviews`),
},
{
desc: "resource request for staging",
requester: "responder",
reviewer: "admin",
requestResources: []string{"staging"},
expectRoles: []string{"responders", "admins"},
expectNodes: []string{"staging"},
},
{
desc: "resource request for prod",
requester: "responder",
reviewer: "admin",
requestResources: []string{"prod"},
expectRoles: []string{"responders", "admins"},
expectNodes: []string{"prod"},
},
{
desc: "resource request for both",
requester: "responder",
reviewer: "admin",
requestResources: []string{"prod", "staging"},
expectRoles: []string{"responders", "admins"},
expectNodes: []string{"prod", "staging"},
},
{
desc: "no search_as_roles",
requester: "nobody",
requestResources: []string{"prod"},
expectRequestError: trace.BadParameter(`user attempted a resource request but does not have any "search_as_roles"`),
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
requester := TestUser(tc.requester)
requesterClient, err := testPack.tlsServer.NewClient(requester)
require.NoError(t, err)
// generateCerts executes a GenerateUserCerts request, optionally applying
// one or more access-requests to the certificate.
generateCerts := func(reqIDs ...string) (*proto.Certs, error) {
return requesterClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: testPack.pubKey,
Username: tc.requester,
Expires: time.Now().Add(time.Hour).UTC(),
Format: constants.CertificateFormatStandard,
AccessRequests: reqIDs,
})
}
// sanity check we can get certs with no access request
certs, err := generateCerts()
require.NoError(t, err)
// should have no logins, requests, or resources in ssh cert
checkCerts(t, certs, testPack.users[tc.requester], nil, nil, nil)
// should not be able to list any nodes
nodes, err := requesterClient.GetNodes(ctx, defaults.Namespace)
require.NoError(t, err)
require.Empty(t, nodes)
// requestable roles should be correct
caps, err := requesterClient.GetAccessCapabilities(ctx, types.AccessCapabilitiesRequest{
RequestableRoles: true,
})
require.NoError(t, err)
require.Equal(t, tc.expectRequestableRoles, caps.RequestableRoles)
// create the access request object
requestResourceIDs := []types.ResourceID{}
for _, nodeName := range tc.requestResources {
requestResourceIDs = append(requestResourceIDs, types.ResourceID{
ClusterName: testPack.clusterName,
Kind: types.KindNode,
Name: nodeName,
})
}
req, err := services.NewAccessRequestWithResources(tc.requester, tc.requestRoles, requestResourceIDs)
require.NoError(t, err)
// send the request to the auth server
err = requesterClient.CreateAccessRequest(ctx, req)
require.ErrorIs(t, err, tc.expectRequestError)
if tc.expectRequestError != nil {
return
}
// try logging in with request in PENDING state (should fail)
_, err = generateCerts(req.GetName())
require.ErrorIs(t, err, trace.AccessDenied("access request %q is awaiting approval", req.GetName()))
reviewer := TestUser(tc.reviewer)
reviewerClient, err := testPack.tlsServer.NewClient(reviewer)
require.NoError(t, err)
// approve the request
req, err = reviewerClient.SubmitAccessReview(ctx, types.AccessReviewSubmission{
RequestID: req.GetName(),
Review: types.AccessReview{
ProposedState: types.RequestState_APPROVED,
},
})
require.ErrorIs(t, err, tc.expectReviewError)
if tc.expectReviewError != nil {
return
}
require.NoError(t, reviewerClient.Close())
require.Equal(t, types.RequestState_APPROVED, req.GetState())
// log in now that request has been approved
certs, err = generateCerts(req.GetName())
require.NoError(t, err)
// cert should have login from requested role, the access request
// should be in the cert, and any requested resources should be
// encoded in the cert
checkCerts(t,
certs,
tc.expectRoles,
[]string{"root"},
[]string{req.GetName()},
requestResourceIDs)
elevatedCert, err := tls.X509KeyPair(certs.TLS, testPack.privKey)
require.NoError(t, err)
elevatedClient := testPack.tlsServer.NewClientWithCert(elevatedCert)
// should be able to list the expected nodes
nodes, err = elevatedClient.GetNodes(ctx, defaults.Namespace)
require.NoError(t, err)
gotNodes := []string{}
for _, node := range nodes {
gotNodes = append(gotNodes, node.GetName())
}
sort.Strings(gotNodes)
require.Equal(t, tc.expectNodes, gotNodes)
// renew elevated certs
newCerts, err := elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: testPack.pubKey,
Username: tc.requester,
Expires: time.Now().Add(time.Hour).UTC(),
// no new access requests
AccessRequests: nil,
})
require.NoError(t, err)
// in spite of providing no access requests, we still have elevated
// roles and the certicate shows the original access request
checkCerts(t,
newCerts,
tc.expectRoles,
[]string{"root"},
[]string{req.GetName()},
requestResourceIDs)
// attempt to apply request in DENIED state (should fail)
require.NoError(t, testPack.tlsServer.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{
RequestID: req.GetName(),
State: types.RequestState_DENIED,
}))
_, err = generateCerts(req.GetName())
require.ErrorIs(t, err, trace.AccessDenied("access request %q has been denied", req.GetName()))
// ensure that once in the DENIED state, a request cannot be set back to PENDING state.
require.Error(t, testPack.tlsServer.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{
RequestID: req.GetName(),
State: types.RequestState_PENDING,
}))
// ensure that once in the DENIED state, a request cannot be set back to APPROVED state.
require.Error(t, testPack.tlsServer.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{
RequestID: req.GetName(),
State: types.RequestState_APPROVED,
}))
// ensure that identities with requests in the DENIED state can't reissue new certs.
_, err = elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: testPack.pubKey,
Username: tc.requester,
Expires: time.Now().Add(time.Hour).UTC(),
// no new access requests
AccessRequests: nil,
})
require.ErrorIs(t, err, trace.AccessDenied("access request %q has been denied", req.GetName()))
})
}
}
func testMultiAccessRequests(t *testing.T, testPack *accessRequestTestPack) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
username := "requester"
prodResourceIDs := []types.ResourceID{{
ClusterName: testPack.clusterName,
Kind: types.KindNode,
Name: "prod",
}}
prodResourceRequest, err := services.NewAccessRequestWithResources(username, []string{"admins"}, prodResourceIDs)
require.NoError(t, err)
stagingResourceIDs := []types.ResourceID{{
ClusterName: testPack.clusterName,
Kind: types.KindNode,
Name: "staging",
}}
stagingResourceRequest, err := services.NewAccessRequestWithResources(username, []string{"admins"}, stagingResourceIDs)
require.NoError(t, err)
adminRequest, err := services.NewAccessRequest(username, "admins")
require.NoError(t, err)
superAdminRequest, err := services.NewAccessRequest(username, "superadmins")
require.NoError(t, err)
bothRolesRequest, err := services.NewAccessRequest(username, "admins", "superadmins")
require.NoError(t, err)
for _, request := range []types.AccessRequest{
prodResourceRequest,
stagingResourceRequest,
adminRequest,
superAdminRequest,
bothRolesRequest,
} {
request.SetState(types.RequestState_APPROVED)
request.SetAccessExpiry(time.Now().Add(time.Hour).UTC())
require.NoError(t, testPack.tlsServer.Auth().UpsertAccessRequest(ctx, request))
}
requester := TestUser(username)
requesterClient, err := testPack.tlsServer.NewClient(requester)
require.NoError(t, err)
type newClientFunc func(*testing.T, *Client, *proto.Certs) (*Client, *proto.Certs)
updateClientWithNewAndDroppedRequests := func(newRequests, dropRequests []string) newClientFunc {
return func(t *testing.T, clt *Client, _ *proto.Certs) (*Client, *proto.Certs) {
certs, err := clt.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: testPack.pubKey,
Username: username,
Expires: time.Now().Add(time.Hour).UTC(),
AccessRequests: newRequests,
DropAccessRequests: dropRequests,
})
require.NoError(t, err)
tlsCert, err := tls.X509KeyPair(certs.TLS, testPack.privKey)
require.NoError(t, err)
return testPack.tlsServer.NewClientWithCert(tlsCert), certs
}
}
applyAccessRequests := func(newRequests ...string) newClientFunc {
return updateClientWithNewAndDroppedRequests(newRequests, nil)
}
dropAccessRequests := func(dropRequests ...string) newClientFunc {
return updateClientWithNewAndDroppedRequests(nil, dropRequests)
}
failToApplyAccessRequests := func(reqs ...string) newClientFunc {
return func(t *testing.T, clt *Client, certs *proto.Certs) (*Client, *proto.Certs) {
// assert that this request fails
_, err := clt.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: testPack.pubKey,
Username: username,
Expires: time.Now().Add(time.Hour).UTC(),
AccessRequests: reqs,
})
assert.Error(t, err)
// return original client and certs unchanged
return clt, certs
}
}
for _, tc := range []struct {
desc string
steps []newClientFunc
expectRoles []string
expectResources []types.ResourceID
expectAccessRequests []string
expectLogins []string
}{
{
desc: "multi role requests",
steps: []newClientFunc{
applyAccessRequests(adminRequest.GetName()),
applyAccessRequests(superAdminRequest.GetName()),
},
expectRoles: []string{"requesters", "admins", "superadmins"},
expectLogins: []string{"root"},
expectAccessRequests: []string{adminRequest.GetName(), superAdminRequest.GetName()},
},
{
desc: "multi resource requests",
steps: []newClientFunc{
applyAccessRequests(prodResourceRequest.GetName()),
failToApplyAccessRequests(stagingResourceRequest.GetName()),
failToApplyAccessRequests(prodResourceRequest.GetName(), stagingResourceRequest.GetName()),
},
expectRoles: []string{"requesters", "admins"},
expectLogins: []string{"root"},
expectResources: prodResourceIDs,
expectAccessRequests: []string{prodResourceRequest.GetName()},
},
{
desc: "role then resource",
steps: []newClientFunc{
applyAccessRequests(adminRequest.GetName()),
applyAccessRequests(prodResourceRequest.GetName()),
},
expectRoles: []string{"requesters", "admins"},
expectLogins: []string{"root"},
expectResources: prodResourceIDs,
expectAccessRequests: []string{adminRequest.GetName(), prodResourceRequest.GetName()},
},
{
desc: "resource then role",
steps: []newClientFunc{
applyAccessRequests(prodResourceRequest.GetName()),
applyAccessRequests(adminRequest.GetName()),
},
expectRoles: []string{"requesters", "admins"},
expectLogins: []string{"root"},
expectResources: prodResourceIDs,
expectAccessRequests: []string{adminRequest.GetName(), prodResourceRequest.GetName()},
},
{
desc: "combined",
steps: []newClientFunc{
failToApplyAccessRequests(stagingResourceRequest.GetName(), prodResourceRequest.GetName()),
applyAccessRequests(prodResourceRequest.GetName(), adminRequest.GetName()),
failToApplyAccessRequests(stagingResourceRequest.GetName(), superAdminRequest.GetName()),
applyAccessRequests(superAdminRequest.GetName()),
},
expectRoles: []string{"requesters", "admins", "superadmins"},
expectLogins: []string{"root"},
expectResources: prodResourceIDs,
expectAccessRequests: []string{adminRequest.GetName(), prodResourceRequest.GetName(), superAdminRequest.GetName()},
},
{
desc: "drop resource request",
steps: []newClientFunc{
applyAccessRequests(prodResourceRequest.GetName(), adminRequest.GetName(), superAdminRequest.GetName()),
dropAccessRequests(prodResourceRequest.GetName()),
},
expectRoles: []string{"requesters", "admins", "superadmins"},
expectLogins: []string{"root"},
expectAccessRequests: []string{adminRequest.GetName(), superAdminRequest.GetName()},
},
{
desc: "drop role request",
steps: []newClientFunc{
applyAccessRequests(prodResourceRequest.GetName(), adminRequest.GetName(), superAdminRequest.GetName()),
dropAccessRequests(superAdminRequest.GetName()),
},
expectRoles: []string{"requesters", "admins"},
expectResources: prodResourceIDs,
expectLogins: []string{"root"},
expectAccessRequests: []string{adminRequest.GetName(), prodResourceRequest.GetName()},
},
{
desc: "drop all",
steps: []newClientFunc{
applyAccessRequests(prodResourceRequest.GetName(), adminRequest.GetName(), superAdminRequest.GetName()),
dropAccessRequests("*"),
},
expectRoles: []string{"requesters"},
},
{
desc: "switch resource requests",
steps: []newClientFunc{
applyAccessRequests(adminRequest.GetName()),
applyAccessRequests(prodResourceRequest.GetName()),
failToApplyAccessRequests(stagingResourceRequest.GetName()),
dropAccessRequests(prodResourceRequest.GetName()),
applyAccessRequests(stagingResourceRequest.GetName()),
failToApplyAccessRequests(prodResourceRequest.GetName()),
dropAccessRequests(stagingResourceRequest.GetName()),
},
expectRoles: []string{"requesters", "admins"},
expectLogins: []string{"root"},
expectAccessRequests: []string{adminRequest.GetName()},
},
} {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
client := requesterClient
var certs *proto.Certs
for _, step := range tc.steps {
client, certs = step(t, client, certs)
}
checkCerts(t, certs, tc.expectRoles, tc.expectLogins, tc.expectAccessRequests, tc.expectResources)
})
}
}
// checkCerts checks that the ssh and tls certs include the given roles, logins,
// accessRequests, and resourceIDs
func checkCerts(t *testing.T,
certs *proto.Certs,
roles []string,
logins []string,
accessRequests []string,
resourceIDs []types.ResourceID,
) {
t.Helper()
// Parse SSH cert.
sshCert, err := sshutils.ParseCertificate(certs.SSH)
require.NoError(t, err)
// Parse TLS cert.
tlsCert, err := tlsca.ParseCertificatePEM(certs.TLS)
require.NoError(t, err)
tlsIdentity, err := tlsca.FromSubject(tlsCert.Subject, tlsCert.NotAfter)
require.NoError(t, err)
// Make sure both certs have the expected roles.
rawSSHCertRoles := sshCert.Permissions.Extensions[teleport.CertExtensionTeleportRoles]
sshCertRoles, err := services.UnmarshalCertRoles(rawSSHCertRoles)
require.NoError(t, err)
assert.ElementsMatch(t, roles, sshCertRoles)
assert.ElementsMatch(t, roles, tlsIdentity.Groups)
// Make sure both certs have the expected logins/principals.
for _, certLogins := range [][]string{sshCert.ValidPrincipals, tlsIdentity.Principals} {
// filter out invalid logins placed in the cert
validCertLogins := []string{}
for _, certLogin := range certLogins {
if !strings.HasPrefix(certLogin, "-teleport") {
validCertLogins = append(validCertLogins, certLogin)
}
}
assert.ElementsMatch(t, logins, validCertLogins)
}
// Make sure both certs have the expected access requests, if any.
rawSSHCertAccessRequests := sshCert.Permissions.Extensions[teleport.CertExtensionTeleportActiveRequests]
sshCertAccessRequests := services.RequestIDs{}
if len(rawSSHCertAccessRequests) > 0 {
require.NoError(t, sshCertAccessRequests.Unmarshal([]byte(rawSSHCertAccessRequests)))
}
assert.ElementsMatch(t, accessRequests, sshCertAccessRequests.AccessRequests)
assert.ElementsMatch(t, accessRequests, tlsIdentity.ActiveRequests)
// Make sure both certs have the expected allowed resources, if any.
sshCertAllowedResources, err := types.ResourceIDsFromString(sshCert.Permissions.Extensions[teleport.CertExtensionAllowedResources])
require.NoError(t, err)
assert.ElementsMatch(t, resourceIDs, sshCertAllowedResources)
assert.ElementsMatch(t, resourceIDs, tlsIdentity.AllowedResourceIDs)
}