Files
teleport/lib/auth/access_request_test.go
Lion Chen 8d643b2967 Search Access Requests by Requester Display Values (#68120)
* search access requests by requester display values

* Refactor ListUsers request and response handling in user search functions

* Refactor user search functions to use ListUsers API and improve test coverage

* Enhance user search functionality by adding display traits and refining search logic

* Refactor user search functionality by removing deprecated access request search files and updating related tests

* reexport function

* Refactor access request and user search functionality to normalize search keywords

* update user search logging and add pagination handling in findUsernamesBySearchKeywords

* use Set
2026-07-27 17:55:35 +00:00

2800 lines
90 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package auth_test
import (
"cmp"
"context"
"crypto/tls"
"maps"
"slices"
"sort"
"strings"
"testing"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/defaults"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
"github.com/gravitational/teleport/api/types/header"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/authtest"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/modules/modulestest"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/tlsca"
)
type accessRequestTestPack struct {
tlsServer *authtest.TLSServer
clusterName string
roles map[string]types.RoleSpecV6
users map[string][]string
tlsPrivKey []byte
tlsPubKey []byte
sshPrivKey []byte
sshPubKey []byte
}
func newAccessRequestTestPack(ctx context.Context, t *testing.T) *accessRequestTestPack {
testAuthServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
Modules: modulestest.EnterpriseModules(),
})
require.NoError(t, err, "%s", trace.DebugReport(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(ctx)
require.NoError(t, err)
roles := map[string]types.RoleSpecV6{
// 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"},
MaxDuration: types.Duration(services.MaxAccessDuration),
},
},
},
// requesters-threshold can request everything possible, with threshold of 2 approvals
"requesters-threshold": {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"admins", "superadmins"},
SearchAsRoles: []string{"admins", "superadmins"},
MaxDuration: types.Duration(services.MaxAccessDuration),
Thresholds: []types.AccessReviewThreshold{
{Approve: 2},
},
},
},
},
// plugin-reviewers can submit reviews for every user
"plugin-reviewers": {
Allow: types.RoleConditions{
ReviewRequests: &types.AccessReviewConditions{
SubmitForUsers: []string{"*"},
},
Rules: []types.Rule{
types.NewRule(types.KindUser, services.RO()),
},
},
},
"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"},
"admin2": {"admins"},
"requester-threshold": {"requesters-threshold"},
"plugin-reviewer": {"plugin-reviewers", "requesters"}, // "requesters" tests edge case where plugin reviewer applys a review on its own request
}
for name, roles := range users {
user, err := types.NewUser(name)
require.NoError(t, err)
user.SetRoles(roles)
_, err = tlsServer.Auth().UpsertUser(ctx, 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)
}
sshPrivKey, sshPubKey, tlsPrivKey, tlsPubKey := newSSHAndTLSKeyPairs(t)
return &accessRequestTestPack{
tlsServer: tlsServer,
clusterName: clusterName.GetClusterName(),
roles: roles,
users: users,
sshPrivKey: sshPrivKey,
sshPubKey: sshPubKey,
tlsPrivKey: tlsPrivKey,
tlsPubKey: tlsPubKey,
}
}
func TestAccessRequest(t *testing.T) {
t.Parallel()
ctx := t.Context()
testPack := newAccessRequestTestPack(ctx, t)
t.Run("single", func(t *testing.T) { testSingleAccessRequests(t, testPack) })
t.Run("multi", func(t *testing.T) { testMultiAccessRequests(t, testPack) })
t.Run("role refresh with bogus request ID", func(t *testing.T) { testRoleRefreshWithBogusRequestID(t, testPack) })
t.Run("drop requests keeps access list grants", func(t *testing.T) { testDropRequestsKeepsAccessListGrants(t, testPack) })
t.Run("bot user approver", func(t *testing.T) { testBotAccessRequestReview(t, testPack) })
t.Run("deny", func(t *testing.T) { testAccessRequestDenyRules(t, testPack) })
t.Run("cert extension resource IDs", func(t *testing.T) { testCertExtensionResourceIDs(t, testPack) })
t.Run("submit_for_users review", func(t *testing.T) { testSubmitAccessReview_SubmitForUsers(t, testPack) })
}
// waitForAccessRequests is a helper for writing access request tests that need to wait for access request CRUD. the supplied condition is
// repeatedly called with the contents of the access request cache until it returns true or a reasonably long timeout is exceeded. this is
// similar to require.Eventually except that it is safe to use normal (test-failing) assertions within the supplied condition closure.
func waitForAccessRequests(t *testing.T, ctx context.Context, getter services.AccessRequestGetter, condition func([]*types.AccessRequestV3) bool) {
t.Helper()
timeout := time.After(time.Second * 30)
for {
var reqs []*types.AccessRequestV3
var nextKey string
Paginate:
for {
rsp, err := getter.ListAccessRequests(ctx, &proto.ListAccessRequestsRequest{
Limit: 1_000,
StartKey: nextKey,
})
require.NoError(t, err, "ListAccessRequests API call should succeed")
reqs = append(reqs, rsp.AccessRequests...)
nextKey = rsp.NextKey
if nextKey == "" {
break Paginate
}
}
if condition(reqs) {
return
}
select {
case <-time.After(time.Millisecond * 150):
case <-timeout:
require.FailNow(t, "timeout waiting for access request condition to pass")
}
}
}
func upsertUser(t *testing.T, ctx context.Context, authServer *auth.Server, name string, roles []string, traits map[string][]string) {
t.Helper()
user, err := types.NewUser(name)
require.NoError(t, err)
user.SetRoles(roles)
user.SetTraits(traits)
_, err = authServer.UpsertUser(ctx, user)
require.NoError(t, err)
}
func upsertAccessRequest(t *testing.T, ctx context.Context, authServer *auth.Server, name string, user string, roles []string, mutate func(types.AccessRequest)) {
t.Helper()
req, err := types.NewAccessRequest(name, user, roles...)
require.NoError(t, err)
if mutate != nil {
mutate(req)
}
require.NoError(t, authServer.UpsertAccessRequest(ctx, req))
}
func waitForUserSearchMatch(t *testing.T, ctx context.Context, users services.UserSearchLister, searchKeywords []string, username string) {
t.Helper()
timeout := time.After(time.Second * 30)
for {
rsp, err := users.ListUsers(ctx, userspb.ListUsersRequest_builder{
PageSize: defaults.DefaultChunkSize,
Filter: &types.UserFilter{
SearchKeywords: searchKeywords,
SkipSystemUsers: true,
},
}.Build())
require.NoError(t, err)
for _, user := range rsp.GetUsers() {
if user.GetName() == username {
return
}
}
select {
case <-time.After(time.Millisecond * 150):
case <-timeout:
require.FailNow(t, "timeout waiting for user search match")
}
}
}
func collectAccessRequestNames(t *testing.T, ctx context.Context, getter services.AccessRequestGetter, req proto.ListAccessRequestsRequest) []string {
t.Helper()
var names []string
for {
rsp, err := getter.ListAccessRequests(ctx, &req)
require.NoError(t, err)
for _, accessRequest := range rsp.AccessRequests {
names = append(names, accessRequest.GetName())
}
req.StartKey = rsp.NextKey
if req.StartKey == "" {
return names
}
}
}
// TestAccessRequestResourceRBACLimits verifies the special constraint conditions put on resource-level access
// request permissions (create/update) to mitigate their power.
func TestAccessRequestResourceRBACLimits(t *testing.T) {
const (
staticRoleName = "static-role"
dynamicRoleName = "dynamic-role"
userName = "alice@example.com"
otherUserName = "bob@example.com"
)
t.Parallel()
clock := clockwork.NewFakeClock()
authServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
Clock: clock,
})
require.NoError(t, err)
defer authServer.Close()
tlsServer, err := authServer.NewTestTLSServer()
require.NoError(t, err)
defer tlsServer.Close()
ctx := t.Context()
staticRole, err := types.NewRole(staticRoleName, types.RoleSpecV6{
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{dynamicRoleName},
},
Rules: []types.Rule{
types.NewRule(types.KindAccessRequest, services.RW()),
},
},
})
require.NoError(t, err)
dynamicRole, err := types.NewRole(dynamicRoleName, types.RoleSpecV6{})
require.NoError(t, err)
_, err = tlsServer.Auth().UpsertRole(ctx, staticRole)
require.NoError(t, err)
_, err = tlsServer.Auth().UpsertRole(ctx, dynamicRole)
require.NoError(t, err)
user, err := types.NewUser(userName)
require.NoError(t, err)
user.SetRoles([]string{staticRoleName})
_, err = tlsServer.Auth().UpsertUser(ctx, user)
require.NoError(t, err)
otherUser, err := types.NewUser(otherUserName)
require.NoError(t, err)
otherUser.SetRoles([]string{staticRoleName})
_, err = tlsServer.Auth().UpsertUser(ctx, otherUser)
require.NoError(t, err)
clt, err := tlsServer.NewClient(authtest.TestUser(userName))
require.NoError(t, err)
defer clt.Close()
// try to create a pre-approved request for self
req, err := services.NewAccessRequest(userName, dynamicRoleName)
require.NoError(t, err)
req.SetState(types.RequestState_APPROVED)
_, err = clt.CreateAccessRequestV2(ctx, req)
require.Error(t, err)
// verify that we got the expected rejection
require.Equal(t, "cannot create access request for self in non-pending state", err.Error())
// verify that creating pre-approved requests for others still works
// (note: we'd like to eventually deprecate ability too).
req, err = services.NewAccessRequest(otherUserName, dynamicRoleName)
require.NoError(t, err)
req.SetState(types.RequestState_APPROVED)
_, err = clt.CreateAccessRequestV2(ctx, req)
require.NoError(t, err)
// create a pending request for self
req, err = services.NewAccessRequest(userName, dynamicRoleName)
require.NoError(t, err)
req.SetState(types.RequestState_PENDING)
req, err = clt.CreateAccessRequestV2(ctx, req)
require.NoError(t, err)
// attempt to self-approve
err = clt.SetAccessRequestState(ctx, types.AccessRequestUpdate{
RequestID: req.GetName(),
State: types.RequestState_APPROVED,
})
require.Error(t, err)
// verify that we got the expected rejection
require.Equal(t, "directly updating the state of your own access requests is not permitted", err.Error())
req, err = services.NewAccessRequest(otherUserName, dynamicRoleName)
require.NoError(t, err)
req.SetState(types.RequestState_PENDING)
req, err = clt.CreateAccessRequestV2(ctx, req)
require.NoError(t, err)
// approve other
err = clt.SetAccessRequestState(ctx, types.AccessRequestUpdate{
RequestID: req.GetName(),
State: types.RequestState_APPROVED,
})
require.NoError(t, err)
}
// TestListAccessRequests tests some basic functionality of the ListAccessRequests API, including access-control,
// filtering, sort, and pagination.
func TestListAccessRequests(t *testing.T) {
const (
requestsPerUser = 200
pageSize = 7
)
t.Parallel()
clock := clockwork.NewFakeClock()
authServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
Clock: clock,
})
require.NoError(t, err)
defer authServer.Close()
tlsServer, err := authServer.NewTestTLSServer()
require.NoError(t, err)
defer tlsServer.Close()
ctx := t.Context()
userA, userB := "lister-a", "lister-b"
roleA, roleB := userA+"-role", userB+"-role"
rroleA, rroleB := userA+"-rrole", userB+"-rrole"
roles := map[string]types.RoleSpecV6{
roleA: {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{rroleA},
},
},
},
roleB: {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{rroleB},
},
},
},
rroleA: {},
rroleB: {},
}
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)
}
for userName, roleName := range map[string]string{userA: roleA, userB: roleB} {
user, err := types.NewUser(userName)
require.NoError(t, err)
user.SetRoles([]string{roleName})
_, err = tlsServer.Auth().UpsertUser(ctx, user)
require.NoError(t, err)
}
clientA, err := tlsServer.NewClient(authtest.TestUser(userA))
require.NoError(t, err)
defer clientA.Close()
clientB, err := tlsServer.NewClient(authtest.TestUser(userB))
require.NoError(t, err)
defer clientB.Close()
// orderedIDs is a list of all access request IDs in order of creation (used to
// verify sort order).
var orderedIDs []string
for range requestsPerUser {
clock.Advance(time.Second)
reqA, err := services.NewAccessRequest(userA, rroleA)
require.NoError(t, err)
rr, err := clientA.CreateAccessRequestV2(ctx, reqA)
require.NoError(t, err)
orderedIDs = append(orderedIDs, rr.GetName())
}
for range requestsPerUser {
clock.Advance(time.Second)
reqB, err := services.NewAccessRequest(userB, rroleB)
require.NoError(t, err)
rr, err := clientB.CreateAccessRequestV2(ctx, reqB)
require.NoError(t, err)
orderedIDs = append(orderedIDs, rr.GetName())
}
// wait for all written requests to propagate to cache
waitForAccessRequests(t, ctx, tlsServer.Auth(), func(reqs []*types.AccessRequestV3) bool {
return len(reqs) == len(orderedIDs)
})
var reqs []*types.AccessRequestV3
var observedIDs []string
var nextKey string
for {
rsp, err := tlsServer.Auth().ListAccessRequests(ctx, &proto.ListAccessRequestsRequest{
Limit: pageSize,
StartKey: nextKey,
Sort: proto.AccessRequestSort_CREATED,
})
require.NoError(t, err)
for _, r := range rsp.AccessRequests {
observedIDs = append(observedIDs, r.GetName())
}
reqs = append(reqs, rsp.AccessRequests...)
nextKey = rsp.NextKey
if nextKey == "" {
break
}
require.Len(t, rsp.AccessRequests, pageSize)
}
// verify that we observed the requests in the same order that they were created (i.e. that the
// default sort order is ascending and time-based).
require.Equal(t, orderedIDs, observedIDs)
// verify that time-based sorting can be checked via creation time field as expected (relied upon later)
require.True(t, slices.IsSortedFunc(reqs, func(a, b *types.AccessRequestV3) int {
return a.GetCreationTime().Compare(b.GetCreationTime())
}))
reqs = nil
nextKey = ""
for {
rsp, err := tlsServer.Auth().ListAccessRequests(ctx, &proto.ListAccessRequestsRequest{
Filter: &types.AccessRequestFilter{
User: userB,
},
Limit: pageSize,
StartKey: nextKey,
})
require.NoError(t, err)
reqs = append(reqs, rsp.AccessRequests...)
nextKey = rsp.NextKey
if nextKey == "" {
break
}
require.Len(t, rsp.AccessRequests, pageSize)
}
require.Len(t, reqs, requestsPerUser)
// verify that access-control filtering is applied and exercise a different combination of sort params
for _, clt := range []authclient.ClientI{clientA, clientB} {
reqs = nil
nextKey = ""
for {
rsp, err := clt.ListAccessRequests(ctx, &proto.ListAccessRequestsRequest{
Limit: pageSize,
StartKey: nextKey,
Sort: proto.AccessRequestSort_CREATED,
Descending: true,
})
require.NoError(t, err)
reqs = append(reqs, rsp.AccessRequests...)
nextKey = rsp.NextKey
if nextKey == "" {
break
}
require.Len(t, rsp.AccessRequests, pageSize)
}
require.Len(t, reqs, requestsPerUser)
require.True(t, slices.IsSortedFunc(reqs, func(a, b *types.AccessRequestV3) int {
// note that we flip `a` and `b` to assert Descending ordering.
return b.GetCreationTime().Compare(a.GetCreationTime())
}))
}
// set requests to a variety of states so that state-based ordering
// is distinctly different from time-based ordering.
var deny bool
expectStates := make(map[string]types.RequestState)
for i, id := range observedIDs {
if i%2 == 0 {
// leave half the requests as pending
continue
}
state := types.RequestState_APPROVED
if deny {
state = types.RequestState_DENIED
}
deny = !deny // toggle next target state
require.NoError(t, tlsServer.Auth().SetAccessRequestState(ctx, types.AccessRequestUpdate{
RequestID: id,
State: state,
}))
expectStates[id] = state
}
// wait until all requests in cache to present the expected state
waitForAccessRequests(t, ctx, tlsServer.Auth(), func(reqs []*types.AccessRequestV3) bool {
for _, r := range reqs {
if expected, ok := expectStates[r.GetName()]; ok && r.GetState() != expected {
return false
}
}
return true
})
// aggregate requests by descending state ordering
reqs = nil
nextKey = ""
for {
rsp, err := clientA.ListAccessRequests(ctx, &proto.ListAccessRequestsRequest{
Sort: proto.AccessRequestSort_STATE,
Descending: true,
Limit: pageSize,
StartKey: nextKey,
})
require.NoError(t, err)
reqs = append(reqs, rsp.AccessRequests...)
nextKey = rsp.NextKey
if nextKey == "" {
break
}
require.Len(t, rsp.AccessRequests, pageSize)
}
require.Len(t, reqs, requestsPerUser)
// verify that requests are sorted by state
require.True(t, slices.IsSortedFunc(reqs, func(a, b *types.AccessRequestV3) int {
// state sort index sorts by the string representation of state. note that we
// flip `a` and `b` to assert Descending ordering.
return cmp.Compare(b.GetState().String(), a.GetState().String())
}))
// sanity-check to ensure that we did force a custom state ordering
require.False(t, slices.IsSortedFunc(reqs, func(a, b *types.AccessRequestV3) int {
return b.GetCreationTime().Compare(a.GetCreationTime())
}))
}
func TestListAccessRequestsRequesterDisplaySearch(t *testing.T) {
t.Parallel()
const (
existingRequestID = "00000000-0000-0000-0000-000000000001"
displayRequestID = "00000000-0000-0000-0000-000000000002"
displayRoleName = "display-role"
storedRoleName = "stored-role"
displayUserName = "123456"
storedUserName = "stored-user"
)
authServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
})
require.NoError(t, err)
defer authServer.Close()
tlsServer, err := authServer.NewTestTLSServer()
require.NoError(t, err)
defer tlsServer.Close()
ctx := t.Context()
authServerClient := tlsServer.Auth()
upsertUser(t, ctx, authServerClient, displayUserName, nil, map[string][]string{
"okta/displayName": {"Jane Garcia"},
"okta/email": {"jane.garcia@example.com"},
})
upsertUser(t, ctx, authServerClient, storedUserName, nil, nil)
waitForUserSearchMatch(t, ctx, authServerClient, []string{"Jane"}, displayUserName)
upsertAccessRequest(t, ctx, authServerClient, existingRequestID, storedUserName, []string{storedRoleName}, func(req types.AccessRequest) {
req.SetStaticLabels(map[string]string{"owner": "Jane"})
})
upsertAccessRequest(t, ctx, authServerClient, displayRequestID, displayUserName, []string{displayRoleName}, func(req types.AccessRequest) {
req.SetState(types.RequestState_APPROVED)
})
waitForAccessRequests(t, ctx, authServerClient, func(reqs []*types.AccessRequestV3) bool {
return len(reqs) == 2
})
for _, tc := range []struct {
name string
filter *types.AccessRequestFilter
limit int32
expected []string
}{
{
name: "display primary partial first name",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{"Jane"},
},
// existingRequestID has a label with "Jane" in it,
// displayRequestID has a requester whose display name contains "Jane", so both should match
expected: []string{existingRequestID, displayRequestID},
},
{
name: "search keywords are normalized",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{" Jane "},
},
expected: []string{existingRequestID, displayRequestID},
},
{
name: "blank search keywords are ignored",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{"", " "},
},
expected: []string{existingRequestID, displayRequestID},
},
{
name: "display secondary",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{"jane.garcia@example.com"},
},
// displayRequestID has a requester whose email is "jane.garcia@example.com", so it should match
expected: []string{displayRequestID},
},
{
name: "display primary multi token",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{"Jane", "Garcia"},
},
// displayRequestID has a requester whose display name contains both "Jane" and "Garcia", so it should match
expected: []string{displayRequestID},
},
{
name: "requester username search still works",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{displayUserName},
},
expected: []string{displayRequestID},
},
{
name: "mixed display and role terms",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{"Jane", displayRoleName},
},
expected: []string{displayRequestID},
},
{
name: "existing role search still works",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{storedRoleName},
},
expected: []string{existingRequestID},
},
{
name: "state filter still ANDs with display search",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{"Jane", "Garcia"},
State: types.RequestState_PENDING,
},
// No requests are set up to be PENDING so this should return no results
expected: nil,
},
{
name: "approved state filter still returns display search match",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{"Jane", "Garcia"},
State: types.RequestState_APPROVED,
},
// displayRequestID is set up to be APPROVED and has a requester whose display name contains both "Jane" and "Garcia", so it should match
expected: []string{displayRequestID},
},
{
name: "no match",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{"NoSuchPerson"},
},
expected: nil,
},
{
name: "mixed existing and display pagination",
filter: &types.AccessRequestFilter{
SearchKeywords: []string{"Jane"},
},
// force pagination to be exercised with search
limit: 1,
expected: []string{existingRequestID, displayRequestID},
},
} {
t.Run(tc.name, func(t *testing.T) {
req := proto.ListAccessRequestsRequest{
Filter: tc.filter,
Limit: tc.limit,
}
require.Equal(t, tc.expected, collectAccessRequestNames(t, ctx, authServerClient, req))
})
}
}
func TestListAccessRequestsRequesterDisplaySearchPreservesRBAC(t *testing.T) {
t.Parallel()
const (
reviewableRequestID = "00000000-0000-0000-0000-000000000011"
displayUserName = "123456"
reviewerUserName = "reviewer"
reviewableRole = "reviewable-role"
unreviewableRole = "unreviewable-role"
reviewerRole = "reviewer-role"
)
authServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
})
require.NoError(t, err)
defer authServer.Close()
tlsServer, err := authServer.NewTestTLSServer()
require.NoError(t, err)
defer tlsServer.Close()
ctx := t.Context()
authServerClient := tlsServer.Auth()
for roleName, roleSpec := range map[string]types.RoleSpecV6{
reviewableRole: {},
unreviewableRole: {},
reviewerRole: {
Allow: types.RoleConditions{
ReviewRequests: &types.AccessReviewConditions{
Roles: []string{reviewableRole},
},
},
},
} {
role, err := types.NewRole(roleName, roleSpec)
require.NoError(t, err)
_, err = authServerClient.UpsertRole(ctx, role)
require.NoError(t, err)
}
upsertUser(t, ctx, authServerClient, displayUserName, nil, map[string][]string{
"okta/displayName": {"Jane Garcia"},
"department": {"classified-department"},
})
upsertUser(t, ctx, authServerClient, reviewerUserName, []string{reviewerRole}, nil)
waitForUserSearchMatch(t, ctx, authServerClient, []string{"Jane"}, displayUserName)
upsertAccessRequest(t, ctx, authServerClient, reviewableRequestID, displayUserName, []string{reviewableRole}, nil)
upsertAccessRequest(t, ctx, authServerClient, "00000000-0000-0000-0000-000000000012", displayUserName, []string{unreviewableRole}, nil)
waitForAccessRequests(t, ctx, authServerClient, func(reqs []*types.AccessRequestV3) bool {
return len(reqs) == 2
})
reviewerClient, err := tlsServer.NewClient(authtest.TestUser(reviewerUserName))
require.NoError(t, err)
defer reviewerClient.Close()
names := collectAccessRequestNames(t, ctx, reviewerClient, proto.ListAccessRequestsRequest{
Filter: &types.AccessRequestFilter{
SearchKeywords: []string{"Jane"},
},
})
require.Equal(t, []string{reviewableRequestID}, names)
names = collectAccessRequestNames(t, ctx, reviewerClient, proto.ListAccessRequestsRequest{
Filter: &types.AccessRequestFilter{
SearchKeywords: []string{"classified-department"},
},
})
require.Empty(t, names)
}
func TestListAccessRequestsUserDisplays(t *testing.T) {
t.Parallel()
authServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
})
require.NoError(t, err)
defer authServer.Close()
tlsServer, err := authServer.NewTestTLSServer()
require.NoError(t, err)
defer tlsServer.Close()
ctx := t.Context()
const (
requester = "display-requester"
reviewer = "display-reviewer"
suggestedReviewer = "display-suggested-reviewer"
plainUser = "display-plain-user"
missingUser = "display-missing-user"
roleName = "display-role"
)
role, err := types.NewRole(roleName, types.RoleSpecV6{})
require.NoError(t, err)
_, err = tlsServer.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
for _, user := range []types.User{
newAccessRequestDisplayUser(t, requester, "Request User", "requester@example.com", roleName),
newAccessRequestDisplayUser(t, reviewer, "Review User", "reviewer@example.com", roleName),
newAccessRequestDisplayUser(t, suggestedReviewer, "Suggested User", "suggested@example.com", roleName),
newAccessRequestDisplayUser(t, plainUser, "", "", roleName),
} {
_, err := tlsServer.Auth().UpsertUser(ctx, user)
require.NoError(t, err)
}
req, err := services.NewAccessRequest(requester, roleName)
require.NoError(t, err)
req.SetReviews([]types.AccessReview{{Author: reviewer}})
req.SetSuggestedReviewers([]string{suggestedReviewer, plainUser, missingUser})
require.NoError(t, tlsServer.Auth().UpsertAccessRequest(ctx, req))
waitForAccessRequests(t, ctx, tlsServer.Auth(), func(reqs []*types.AccessRequestV3) bool {
return len(reqs) == 1
})
adminClient, err := tlsServer.NewClient(authtest.TestAdmin())
require.NoError(t, err)
defer adminClient.Close()
rsp, err := adminClient.ListAccessRequests(ctx, &proto.ListAccessRequestsRequest{
Filter: &types.AccessRequestFilter{
ID: req.GetName(),
},
})
require.NoError(t, err)
require.Len(t, rsp.AccessRequests, 1)
require.Equal(t, map[string]*proto.UserDisplay{
requester: {
Primary: "Request User",
Secondary: "requester@example.com",
},
reviewer: {
Primary: "Review User",
Secondary: "reviewer@example.com",
},
suggestedReviewer: {
Primary: "Suggested User",
Secondary: "suggested@example.com",
},
plainUser: {},
}, rsp.UserDisplays)
require.NotContains(t, rsp.UserDisplays, missingUser)
}
func newAccessRequestDisplayUser(t *testing.T, name, primary, secondary, role string) types.User {
t.Helper()
user, err := types.NewUser(name)
require.NoError(t, err)
user.SetRoles([]string{role})
traits := make(map[string][]string)
if primary != "" {
traits["displayName"] = []string{primary}
}
if secondary != "" {
traits["email"] = []string{secondary}
}
user.SetTraits(traits)
return user
}
func TestCreateAccessRequestV2DryRunUserDisplays(t *testing.T) {
t.Parallel()
const (
requester = "dry-run-display-requester"
policySuggestedReviewer = "dry-run-display-policy-reviewer"
dynamicSuggestedReviewer = "dry-run-display-dynamic-reviewer"
callerSuggestedReviewer = "dry-run-display-caller-reviewer"
missingUser = "dry-run-display-missing-user"
requestableRole = "dry-run-display-requestable-role"
requesterRole = "dry-run-display-requester-role"
)
testModules := modulestest.EnterpriseModules()
testModules.GenerateAccessRequestSuggestedReviewersFn = func(context.Context, modules.AccessResourcesGetter, types.AccessRequest) ([]string, error) {
return []string{dynamicSuggestedReviewer}, nil
}
authServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
Modules: testModules,
})
require.NoError(t, err)
defer authServer.Close()
tlsServer, err := authServer.NewTestTLSServer()
require.NoError(t, err)
defer tlsServer.Close()
ctx := t.Context()
requestable, err := types.NewRole(requestableRole, types.RoleSpecV6{})
require.NoError(t, err)
_, err = tlsServer.Auth().UpsertRole(ctx, requestable)
require.NoError(t, err)
// The requester can request the requestable role but cannot read other
// users, so displays must come from server-side resolution.
role, err := types.NewRole(requesterRole, types.RoleSpecV6{
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{requestableRole},
SuggestedReviewers: []string{policySuggestedReviewer},
},
},
})
require.NoError(t, err)
_, err = tlsServer.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
for _, user := range []types.User{
newAccessRequestDisplayUser(t, requester, "Request User", "requester@example.com", requesterRole),
newAccessRequestDisplayUser(t, policySuggestedReviewer, "Policy Reviewer", "policy@example.com", requestableRole),
newAccessRequestDisplayUser(t, dynamicSuggestedReviewer, "Dynamic Reviewer", "dynamic@example.com", requestableRole),
newAccessRequestDisplayUser(t, callerSuggestedReviewer, "Caller Reviewer", "caller@example.com", requestableRole),
} {
_, err := tlsServer.Auth().UpsertUser(ctx, user)
require.NoError(t, err)
}
clt, err := tlsServer.NewClient(authtest.TestUser(requester))
require.NoError(t, err)
defer clt.Close()
t.Run("server suggested reviewers", func(t *testing.T) {
req, err := services.NewAccessRequest(requester, requestableRole)
require.NoError(t, err)
req.SetDryRun(true)
resp, err := clt.CreateAccessRequestV2(ctx, req)
require.NoError(t, err)
enrichment := resp.GetDryRunEnrichment()
require.NotNil(t, enrichment)
require.Equal(t, types.RequestReasonModeOptional, enrichment.ReasonMode)
require.Equal(t, map[string]types.AccessRequestUserDisplay{
requester: {
Primary: "Request User",
Secondary: "requester@example.com",
},
policySuggestedReviewer: {
Primary: "Policy Reviewer",
Secondary: "policy@example.com",
},
dynamicSuggestedReviewer: {
Primary: "Dynamic Reviewer",
Secondary: "dynamic@example.com",
},
}, enrichment.UserDisplays)
})
t.Run("caller suggested reviewers", func(t *testing.T) {
req, err := services.NewAccessRequest(requester, requestableRole)
require.NoError(t, err)
req.SetDryRun(true)
req.SetSuggestedReviewers([]string{callerSuggestedReviewer, missingUser})
resp, err := clt.CreateAccessRequestV2(ctx, req)
require.NoError(t, err)
enrichment := resp.GetDryRunEnrichment()
require.NotNil(t, enrichment)
require.Equal(t, map[string]types.AccessRequestUserDisplay{
requester: {
Primary: "Request User",
Secondary: "requester@example.com",
},
dynamicSuggestedReviewer: {
Primary: "Dynamic Reviewer",
Secondary: "dynamic@example.com",
},
}, enrichment.UserDisplays)
require.Contains(t, resp.GetSuggestedReviewers(), callerSuggestedReviewer)
require.NotContains(t, enrichment.UserDisplays, callerSuggestedReviewer)
require.NotContains(t, enrichment.UserDisplays, missingUser)
})
}
func testAccessRequestDenyRules(t *testing.T, testPack *accessRequestTestPack) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
userName := "denied"
accessRequest, err := services.NewAccessRequest(userName, "admins")
require.NoError(t, err)
for _, tc := range []struct {
desc string
roles map[string]types.RoleSpecV6
expectGetDenied bool
expectCreateDenied bool
}{
{
desc: "all allowed",
roles: map[string]types.RoleSpecV6{
"allow": {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"admins"},
},
ReviewRequests: &types.AccessReviewConditions{
Roles: []string{"admins"},
},
},
},
},
},
{
desc: "all denied",
roles: map[string]types.RoleSpecV6{
"allow": {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"admins"},
},
ReviewRequests: &types.AccessReviewConditions{
Roles: []string{"admins"},
},
},
},
"deny": {
Deny: types.RoleConditions{
Rules: []types.Rule{
{
Resources: []string{"access_request"},
Verbs: []string{"read", "create", "list"},
},
},
},
},
},
expectGetDenied: true,
expectCreateDenied: true,
},
{
desc: "create denied",
roles: map[string]types.RoleSpecV6{
"allow": {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"admins"},
},
ReviewRequests: &types.AccessReviewConditions{
Roles: []string{"admins"},
},
},
},
"deny": {
Deny: types.RoleConditions{
Rules: []types.Rule{
{
Resources: []string{"access_request"},
Verbs: []string{"create"},
},
},
},
},
},
expectCreateDenied: true,
},
{
desc: "get denied",
roles: map[string]types.RoleSpecV6{
"allow": {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"admins"},
},
ReviewRequests: &types.AccessReviewConditions{
Roles: []string{"admins"},
},
},
},
"deny": {
Deny: types.RoleConditions{
Rules: []types.Rule{
{
Resources: []string{"access_request"},
Verbs: []string{"read"},
},
},
},
},
},
expectGetDenied: true,
},
{
desc: "list denied",
roles: map[string]types.RoleSpecV6{
"allow": {
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"admins"},
},
ReviewRequests: &types.AccessReviewConditions{
Roles: []string{"admins"},
},
},
},
"deny": {
Deny: types.RoleConditions{
Rules: []types.Rule{
{
Resources: []string{"access_request"},
Verbs: []string{"list"},
},
},
},
},
},
expectGetDenied: true,
},
} {
t.Run(tc.desc, func(t *testing.T) {
for roleName, roleSpec := range tc.roles {
role, err := types.NewRole(roleName, roleSpec)
require.NoError(t, err)
_, err = testPack.tlsServer.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
}
user, err := types.NewUser(userName)
require.NoError(t, err)
user.SetRoles(slices.Collect(maps.Keys(tc.roles)))
_, err = testPack.tlsServer.Auth().UpsertUser(ctx, user)
require.NoError(t, err)
client, err := testPack.tlsServer.NewClient(authtest.TestUser(userName))
require.NoError(t, err)
_, err = client.GetAccessRequests(ctx, types.AccessRequestFilter{})
if tc.expectGetDenied {
assert.True(t, trace.IsAccessDenied(err), "want access denied, got %v", err)
} else {
assert.NoError(t, err)
}
_, err = client.CreateAccessRequestV2(ctx, accessRequest)
if tc.expectCreateDenied {
assert.True(t, trace.IsAccessDenied(err), "want access denied, got %v", err)
} else {
assert.NoError(t, err)
}
})
}
}
func testSingleAccessRequests(t *testing.T, testPack *accessRequestTestPack) {
t.Parallel()
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.AccessDenied(`Resource Access Requests require usable "search_as_roles", none found for user "nobody"`),
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
requester := authtest.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(t.Context(), proto.UserCertsRequest{
SSHPublicKey: testPack.sshPubKey,
TLSPublicKey: testPack.tlsPubKey,
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(t.Context(), defaults.Namespace)
require.NoError(t, err)
require.Empty(t, nodes)
// requestable roles should be correct
caps, err := requesterClient.GetAccessCapabilities(t.Context(), types.AccessCapabilitiesRequest{
RequestableRoles: true,
})
require.NoError(t, err)
require.Equal(t, tc.expectRequestableRoles, caps.RequestableRoles)
// create the access request object
requestResourceIDs := []types.ResourceAccessID{}
for _, nodeName := range tc.requestResources {
requestResourceIDs = append(requestResourceIDs, types.ResourceAccessID{
Id: 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
req, err = requesterClient.CreateAccessRequestV2(t.Context(), 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 := authtest.TestUser(tc.reviewer)
reviewerClient, err := testPack.tlsServer.NewClient(reviewer)
require.NoError(t, err)
// approve the request
req, err = reviewerClient.SubmitAccessReview(t.Context(), 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.tlsPrivKey)
require.NoError(t, err)
elevatedClient, err := testPack.tlsServer.NewClientWithCert(elevatedCert)
require.NoError(t, err)
// should be able to list the expected nodes
nodes, err = elevatedClient.GetNodes(t.Context(), defaults.Namespace)
require.NoError(t, err)
gotNodes := make([]string, 0, len(nodes))
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(t.Context(), proto.UserCertsRequest{
SSHPublicKey: testPack.sshPubKey,
TLSPublicKey: testPack.tlsPubKey,
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 certificate shows the original access request
checkCerts(t,
newCerts,
tc.expectRoles,
[]string{"root"},
[]string{req.GetName()},
requestResourceIDs)
// ensure that once in the APPROVED state, a request cannot be set to DENIED.
err = testPack.tlsServer.Auth().SetAccessRequestState(t.Context(), types.AccessRequestUpdate{
RequestID: req.GetName(),
State: types.RequestState_DENIED,
})
require.True(t, trace.IsBadParameter(err), "unexpected error: %v", err)
// ensure that once in the APPROVED state, a request cannot be updated again.
err = testPack.tlsServer.Auth().SetAccessRequestState(t.Context(), types.AccessRequestUpdate{
RequestID: req.GetName(),
State: types.RequestState_APPROVED,
})
require.True(t, trace.IsBadParameter(err), "unexpected error: %v", err)
deniedReq, err := services.NewAccessRequestWithResources(tc.requester, tc.requestRoles, requestResourceIDs)
require.NoError(t, err)
deniedReq, err = requesterClient.CreateAccessRequestV2(t.Context(), deniedReq)
require.NoError(t, err)
// attempt to use a request in DENIED state (should fail)
require.NoError(t, testPack.tlsServer.Auth().SetAccessRequestState(t.Context(), types.AccessRequestUpdate{
RequestID: deniedReq.GetName(),
State: types.RequestState_DENIED,
}))
// ensure that identities with requests in the DENIED state can't reissue new certs.
_, err = generateCerts(deniedReq.GetName())
require.ErrorIs(t, err, trace.AccessDenied("access request %q has been denied", deniedReq.GetName()))
// ensure that once in the DENIED state, a request cannot be set back to PENDING state.
err = testPack.tlsServer.Auth().SetAccessRequestState(t.Context(), types.AccessRequestUpdate{
RequestID: deniedReq.GetName(),
State: types.RequestState_PENDING,
})
require.True(t, trace.IsBadParameter(err), "unexpected error: %v", err)
// ensure that once in the DENIED state, a request cannot be set back to APPROVED state.
err = testPack.tlsServer.Auth().SetAccessRequestState(t.Context(), types.AccessRequestUpdate{
RequestID: deniedReq.GetName(),
State: types.RequestState_APPROVED,
})
require.True(t, trace.IsBadParameter(err), "unexpected error: %v", err)
// ensure that once in the DENIED state, a request cannot be updated again.
err = testPack.tlsServer.Auth().SetAccessRequestState(t.Context(), types.AccessRequestUpdate{
RequestID: deniedReq.GetName(),
State: types.RequestState_DENIED,
})
require.True(t, trace.IsBadParameter(err), "unexpected error: %v", err)
})
}
}
// testBotAccessRequestReview specifically ensures that a bots output cert
// can be used to review a access request. This is because there's a special
// case to handle their role impersonated certs correctly.
func testBotAccessRequestReview(t *testing.T, testPack *accessRequestTestPack) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
// Create the bot
adminClient, err := testPack.tlsServer.NewClient(authtest.TestAdmin())
require.NoError(t, err)
defer adminClient.Close()
bot, err := adminClient.BotServiceClient().CreateBot(ctx, machineidv1pb.CreateBotRequest_builder{
Bot: machineidv1pb.Bot_builder{
Kind: types.KindBot,
Version: types.V1,
Metadata: headerv1.Metadata_builder{
Name: "request-approver",
}.Build(),
Spec: machineidv1pb.BotSpec_builder{
Roles: []string{
// Grants the ability to approve requests
"admins",
},
}.Build(),
}.Build(),
}.Build())
require.NoError(t, err)
// Use the bot user to generate some certs using role impersonation.
// This mimics what the bot actually does.
botClient, err := testPack.tlsServer.NewClient(authtest.TestUser(bot.GetStatus().GetUserName()))
require.NoError(t, err)
defer botClient.Close()
certRes, err := botClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
Username: bot.GetStatus().GetUserName(),
TLSPublicKey: testPack.tlsPubKey,
Expires: time.Now().Add(time.Hour),
RoleRequests: []string{"admins"},
UseRoleRequests: true,
})
require.NoError(t, err)
tlsCert, err := tls.X509KeyPair(certRes.TLS, testPack.tlsPrivKey)
require.NoError(t, err)
impersonatedBotClient, err := testPack.tlsServer.NewClientWithCert(tlsCert)
require.NoError(t, err)
defer impersonatedBotClient.Close()
// Create an access request for the bot to approve
requesterClient, err := testPack.tlsServer.NewClient(authtest.TestUser("requester"))
require.NoError(t, err)
defer requesterClient.Close()
accessRequest, err := services.NewAccessRequest("requester", "admins")
require.NoError(t, err)
accessRequest, err = requesterClient.CreateAccessRequestV2(ctx, accessRequest)
require.NoError(t, err)
// Approve the access request with the bot
accessRequest, err = impersonatedBotClient.SubmitAccessReview(ctx, types.AccessReviewSubmission{
RequestID: accessRequest.GetName(),
Review: types.AccessReview{
ProposedState: types.RequestState_APPROVED,
},
})
require.NoError(t, err)
// Check the final state of the request
require.Equal(t, bot.GetStatus().GetUserName(), accessRequest.GetReviews()[0].Author)
require.Equal(t, types.RequestState_APPROVED, accessRequest.GetState())
}
func testMultiAccessRequests(t *testing.T, testPack *accessRequestTestPack) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
username := "requester"
prodResourceIDs := []types.ResourceAccessID{{
Id: types.ResourceID{
ClusterName: testPack.clusterName,
Kind: types.KindNode,
Name: "prod",
},
}}
prodResourceRequest, err := services.NewAccessRequestWithResources(username, []string{"admins"}, prodResourceIDs)
require.NoError(t, err)
stagingResourceIDs := []types.ResourceAccessID{{
Id: 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 := authtest.TestUser(username)
requesterClient, err := testPack.tlsServer.NewClient(requester)
require.NoError(t, err)
type newClientFunc func(*testing.T, *authclient.Client, *proto.Certs) (*authclient.Client, *proto.Certs)
updateClientWithNewAndDroppedRequests := func(newRequests, dropRequests []string) newClientFunc {
return func(t *testing.T, clt *authclient.Client, _ *proto.Certs) (*authclient.Client, *proto.Certs) {
certs, err := clt.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: testPack.sshPubKey,
TLSPublicKey: testPack.tlsPubKey,
Username: username,
Expires: time.Now().Add(time.Hour).UTC(),
AccessRequests: newRequests,
DropAccessRequests: dropRequests,
})
require.NoError(t, err)
tlsCert, err := tls.X509KeyPair(certs.TLS, testPack.tlsPrivKey)
require.NoError(t, err)
client, err := testPack.tlsServer.NewClientWithCert(tlsCert)
require.NoError(t, err)
return client, 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 *authclient.Client, certs *proto.Certs) (*authclient.Client, *proto.Certs) {
// assert that this request fails
_, err := clt.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: testPack.sshPubKey,
TLSPublicKey: testPack.tlsPubKey,
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.ResourceAccessID
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()},
},
} {
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)
})
}
}
// testRoleRefreshWithBogusRequestID verifies that GenerateUserCerts refreshes the role list based
// on the server state, even when supplied an ID for a nonexistent access request.
//
// Teleport Connect depends on this behavior when setting up roles for Connect My Computer.
// See [teleterm.connectmycomputer.RoleSetup].
func testRoleRefreshWithBogusRequestID(t *testing.T, testPack *accessRequestTestPack) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
username := "user-for-role-refresh"
auth := testPack.tlsServer.Auth()
// Create a user.
user, err := types.NewUser(username)
require.NoError(t, err)
user.AddRole("requesters")
user, err = auth.UpsertUser(ctx, user)
require.NoError(t, err)
// Create a client with the old set of roles.
clt, err := testPack.tlsServer.NewClient(authtest.TestUser(username))
require.NoError(t, err)
// Add a new role to the user on the server.
user.AddRole("operators")
_, err = auth.UpdateUser(ctx, user)
require.NoError(t, err)
certs, err := clt.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: testPack.sshPubKey,
TLSPublicKey: testPack.tlsPubKey,
Username: username,
Expires: time.Now().Add(time.Hour).UTC(),
DropAccessRequests: []string{"bogus-request-id"},
})
require.NoError(t, err)
// Verify that the new certs issued for the old client have the new role.
checkCerts(t, certs, []string{"requesters", "operators"}, nil, nil, nil)
}
// testDropRequestsKeepsAccessListGrants verifies that dropping access requests
// resets the certificate roles and traits to the user login state, which
// includes roles and traits granted by access lists, rather than to the bare
// backend user. See https://github.com/gravitational/teleport/issues/45797.
func testDropRequestsKeepsAccessListGrants(t *testing.T, testPack *accessRequestTestPack) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
username := "requester-with-access-list"
authServer := testPack.tlsServer.Auth()
// Create a user with the base "requesters" role.
user, err := types.NewUser(username)
require.NoError(t, err)
user.AddRole("requesters")
_, err = authServer.UpsertUser(ctx, user)
require.NoError(t, err)
// Create a role that is granted only through an access list.
aclRole, err := types.NewRole("access-list-granted", types.RoleSpecV6{
Allow: types.RoleConditions{
Logins: []string{"acl-login"},
},
})
require.NoError(t, err)
_, err = authServer.UpsertRole(ctx, aclRole)
require.NoError(t, err)
// Grant the role through a real access list. Dropping access requests
// regenerates the user login state from the backend user and access
// lists, so the grant must be backed by an actual list membership.
acl, err := accesslist.NewAccessList(
header.Metadata{
Name: "requester-acl",
},
accesslist.Spec{
Title: "requester-acl",
Owners: []accesslist.Owner{{Name: "acl-owner"}},
Audit: accesslist.Audit{NextAuditDate: time.Now().Add(24 * time.Hour).UTC()},
Grants: accesslist.Grants{
Roles: []string{aclRole.GetName()},
},
},
)
require.NoError(t, err)
_, err = authServer.UpsertAccessList(ctx, acl)
require.NoError(t, err)
member, err := accesslist.NewAccessListMember(
header.Metadata{
Name: username,
},
accesslist.AccessListMemberSpec{
AccessList: acl.GetName(),
Name: username,
Joined: time.Now().UTC(),
AddedBy: "acl-owner",
Expires: time.Now().Add(24 * time.Hour).UTC(),
},
)
require.NoError(t, err)
_, err = authServer.UpsertAccessListMember(ctx, member)
require.NoError(t, err)
// Create an approved access request for the "admins" role.
accessRequest, err := services.NewAccessRequest(username, "admins")
require.NoError(t, err)
accessRequest.SetState(types.RequestState_APPROVED)
accessRequest.SetAccessExpiry(time.Now().Add(time.Hour).UTC())
require.NoError(t, authServer.UpsertAccessRequest(ctx, accessRequest))
clt, err := testPack.tlsServer.NewClient(authtest.TestUser(username))
require.NoError(t, err)
defer clt.Close()
// Assume the access request.
certs, err := clt.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: testPack.sshPubKey,
TLSPublicKey: testPack.tlsPubKey,
Username: username,
Expires: time.Now().Add(time.Hour).UTC(),
AccessRequests: []string{accessRequest.GetName()},
})
require.NoError(t, err)
tlsCert, err := tls.X509KeyPair(certs.TLS, testPack.tlsPrivKey)
require.NoError(t, err)
elevatedClient, err := testPack.tlsServer.NewClientWithCert(tlsCert)
require.NoError(t, err)
defer elevatedClient.Close()
// Drop all access requests. The reissued certs must keep the roles and
// traits granted by access lists via the user login state.
certs, err = elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: testPack.sshPubKey,
TLSPublicKey: testPack.tlsPubKey,
Username: username,
Expires: time.Now().Add(time.Hour).UTC(),
DropAccessRequests: []string{"*"},
})
require.NoError(t, err)
checkCerts(t, certs, []string{"requesters", aclRole.GetName()}, []string{"acl-login"}, nil, nil)
// Dropping a single request by ID must behave the same way.
certs, err = elevatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: testPack.sshPubKey,
TLSPublicKey: testPack.tlsPubKey,
Username: username,
Expires: time.Now().Add(time.Hour).UTC(),
DropAccessRequests: []string{accessRequest.GetName()},
})
require.NoError(t, err)
checkCerts(t, certs, []string{"requesters", aclRole.GetName()}, []string{"acl-login"}, nil, nil)
}
// 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,
resourceAccessIDs []types.ResourceAccessID,
) {
t.Helper()
// Parse SSH cert.
sshCert, err := sshutils.ParseCertificate(certs.SSH)
require.NoError(t, err)
sshIdentity, err := sshca.DecodeIdentity(sshCert)
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.
assert.ElementsMatch(t, roles, sshIdentity.Roles)
assert.ElementsMatch(t, roles, tlsIdentity.Groups)
// Make sure both certs have the expected logins/principals.
for _, certLogins := range [][]string{sshIdentity.Principals, 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.
assert.ElementsMatch(t, accessRequests, sshIdentity.ActiveRequests)
assert.ElementsMatch(t, accessRequests, tlsIdentity.ActiveRequests)
// Make sure both certs have the expected allowed resources, if any.
sshCertAllowedResources, err := types.ResourceAccessIDsFromString(sshCert.Permissions.Extensions[teleport.CertExtensionAllowedResourceAccessIDs])
require.NoError(t, err)
assert.ElementsMatch(t, resourceAccessIDs, sshCertAllowedResources)
assert.ElementsMatch(t, resourceAccessIDs, sshIdentity.AllowedResourceAccessIDs)
assert.ElementsMatch(t, resourceAccessIDs, tlsIdentity.AllowedResourceAccessIDs)
// Verify the legacy AllowedResourceIDs extension contains the expected values.
// Plain (unconstrained) resource IDs should appear in the old extension so that older
// agents/proxies that don't understand AllowedResourceAccessIDs can still enforce
// resource-level restrictions. The sentinel should only appear when all requested
// resources carry constraints that old agents can't enforce.
sshCertLegacyResources, err := types.ResourceIDsFromString(sshCert.Permissions.Extensions[teleport.CertExtensionAllowedResources])
require.NoError(t, err)
plainIDs, constrainedOnly := types.UnwrapResourceAccessIDs(resourceAccessIDs)
if len(plainIDs) > 0 {
// Plain resources should be in the old extension.
assert.ElementsMatch(t, plainIDs, sshCertLegacyResources, "SSH cert legacy extension should contain plain resource IDs")
// Sentinel should not be present.
for _, rid := range sshCertLegacyResources {
assert.False(t, types.IsSentinelResourceID(rid), "SSH cert legacy extension should not contain sentinel when plain resources are present")
}
} else if len(constrainedOnly) > 0 {
// Constraint-only requests should have the sentinel in the old extension.
require.Len(t, sshCertLegacyResources, 1, "SSH cert legacy extension should contain only sentinel")
assert.True(t, types.IsSentinelResourceID(sshCertLegacyResources[0]), "SSH cert legacy extension should be sentinel")
}
}
func TestCreateSuggestions(t *testing.T) {
t.Parallel()
testAuthServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, testAuthServer.Close()) })
const username = "admin"
// Create the access request, so we can attach the promotions to it.
adminRequest, err := services.NewAccessRequest(username, "admins")
require.NoError(t, err)
authSrvClient := testAuthServer.AuthServer
err = authSrvClient.UpsertAccessRequest(context.Background(), adminRequest)
require.NoError(t, err)
// Create the promotions.
err = authSrvClient.CreateAccessRequestAllowedPromotions(context.Background(), adminRequest, &types.AccessRequestAllowedPromotions{
Promotions: []*types.AccessRequestAllowedPromotion{
{AccessListName: "a"},
{AccessListName: "b"},
{AccessListName: "c"}},
})
require.NoError(t, err)
// Get the promotions and verify them.
promotions, err := authSrvClient.GetAccessRequestAllowedPromotions(context.Background(), adminRequest)
require.NoError(t, err)
require.Len(t, promotions.Promotions, 3)
require.Equal(t, []string{"a", "b", "c"},
[]string{
promotions.Promotions[0].AccessListName,
promotions.Promotions[1].AccessListName,
promotions.Promotions[2].AccessListName,
})
}
func TestPromotedRequest(t *testing.T) {
t.Parallel()
ctx := t.Context()
testPack := newAccessRequestTestPack(ctx, t)
const requesterUserName = "requester"
requester := authtest.TestUser(requesterUserName)
requesterClient, err := testPack.tlsServer.NewClient(requester)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, requesterClient.Close()) })
// create the access request object
req, err := services.NewAccessRequest(requesterUserName, "admins")
require.NoError(t, err)
// send the request to the auth server
createdReq, err := requesterClient.CreateAccessRequestV2(ctx, req)
require.NoError(t, err)
const adminUser = "admin"
approveAs := func(reviewerName string) (types.AccessRequest, error) {
reviewer := authtest.TestUser(reviewerName)
reviewerClient, err := testPack.tlsServer.NewClient(reviewer)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, reviewerClient.Close()) })
// try to promote the request
return reviewerClient.SubmitAccessReview(ctx, types.AccessReviewSubmission{
RequestID: req.GetName(),
Review: types.AccessReview{
ProposedState: types.RequestState_PROMOTED,
Author: adminUser,
AccessList: &types.PromotedAccessList{
Title: "ACL title",
Name: "0000-00-00-0000",
},
},
})
}
t.Run("try promoting using access request API", func(t *testing.T) {
// Access request promotion is prohibited for everyone, including admins.
// An access request can be only approved by using Ent AccessRequestPromote API
// operator can't promote the request
_, err = approveAs("operator")
require.Error(t, err)
// admin can't promote the request
_, err = approveAs(adminUser)
require.Error(t, err)
req2, err := requesterClient.GetAccessRequests(ctx, types.AccessRequestFilter{
ID: createdReq.GetMetadata().Name,
})
require.NoError(t, err)
require.Len(t, req2, 1)
// the state should be still pending
require.Equal(t, types.RequestState_PENDING, req2[0].GetState())
})
t.Run("promote without access list data fails", func(t *testing.T) {
// The only way to promote the request is to use Ent AccessRequestPromote API
// which is not available in OSS. As a workaround, we can use the access request
// server API.
_, err := testPack.tlsServer.AuthServer.AuthServer.SubmitAccessReview(ctx, types.AccessReviewSubmission{
RequestID: createdReq.GetName(),
Review: types.AccessReview{
ProposedState: types.RequestState_PROMOTED,
},
})
// Promoting without access list information is prohibited.
require.Error(t, err)
})
t.Run("promote", func(t *testing.T) {
promotedRequest, err := testPack.tlsServer.AuthServer.AuthServer.SubmitAccessReview(ctx, types.AccessReviewSubmission{
RequestID: createdReq.GetName(),
Review: types.AccessReview{
ProposedState: types.RequestState_PROMOTED,
Author: adminUser,
AccessList: &types.PromotedAccessList{
Title: "ACL title",
Name: "0000-00-00-0000",
},
},
})
require.NoError(t, err)
// verify promotion related fields
require.Equal(t, types.RequestState_PROMOTED, promotedRequest.GetState())
require.Equal(t, "0000-00-00-0000", promotedRequest.GetPromotedAccessListName())
require.Equal(t, "ACL title", promotedRequest.GetPromotedAccessListTitle())
})
}
func TestUpdateAccessRequestWithAdditionalReviewers(t *testing.T) {
t.Parallel()
mustRequest := func(suggestedReviewers ...string) types.AccessRequest {
req, err := services.NewAccessRequest("test-user", "admins")
require.NoError(t, err)
req.SetSuggestedReviewers(suggestedReviewers)
return req
}
tests := []struct {
name string
req types.AccessRequest
suggestedReviewers []string
expectedReviewers []string
}{
{
name: "nil additional reviewers",
req: mustRequest("rev1", "rev2"),
expectedReviewers: []string{"rev1", "rev2"},
},
{
name: "a few additional reviewers",
req: mustRequest("rev1", "rev2"),
suggestedReviewers: []string{"name1", "name2", "name3"},
expectedReviewers: []string{"rev1", "rev2", "name1", "name2", "name3"},
},
{
name: "no additional reviewers",
req: mustRequest("rev1", "rev2"),
suggestedReviewers: []string{},
expectedReviewers: []string{"rev1", "rev2"},
},
{
name: "duplicate additional reviewers",
req: mustRequest("rev1", "rev2"),
suggestedReviewers: []string{"rev2", "name1", "name2", "name1"},
expectedReviewers: []string{"rev1", "rev2", "name1", "name2"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
req := test.req.Copy()
auth.UpdateAccessRequestWithAdditionalReviewers(ctx, req, test.suggestedReviewers)
require.ElementsMatch(t, test.expectedReviewers, req.GetSuggestedReviewers())
})
}
}
func TestAssumeStartTime_CreateAccessRequestV2(t *testing.T) {
ctx := context.Background()
s := createAccessRequestWithStartTime(t)
testCases := []struct {
name string
startTime time.Time
errCheck require.ErrorAssertionFunc
}{
{
name: "too far in the future",
startTime: s.invalidMaxedAssumeStartTime,
errCheck: func(tt require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
require.ErrorContains(t, err, "assume start time is too far in the future")
},
},
{
name: "after access expiry time",
startTime: s.invalidExpiredAssumeStartTime,
errCheck: func(tt require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
require.ErrorContains(t, err, "assume start time must be prior to access expiry time")
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req, err := services.NewAccessRequest(s.requesterUserName, "admins")
require.NoError(t, err)
req.SetMaxDuration(s.maxDuration)
req.SetAssumeStartTime(tc.startTime)
_, err = s.requesterClient.CreateAccessRequestV2(ctx, req)
tc.errCheck(t, err)
})
}
}
func TestAssumeStartTime_SubmitAccessReview(t *testing.T) {
ctx := context.Background()
s := createAccessRequestWithStartTime(t)
testCases := []struct {
name string
startTime time.Time
errCheck require.ErrorAssertionFunc
}{
{
name: "too far in the future",
startTime: s.invalidMaxedAssumeStartTime,
errCheck: func(tt require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
require.ErrorContains(t, err, "assume start time is too far in the future")
},
},
{
name: "after access expiry time",
startTime: s.invalidExpiredAssumeStartTime,
errCheck: func(tt require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
require.ErrorContains(t, err, "assume start time must be prior to access expiry time")
},
},
{
name: "valid submission",
startTime: s.validStartTime,
errCheck: require.NoError,
},
}
review := types.AccessReviewSubmission{
RequestID: s.createdRequest.GetName(),
Review: types.AccessReview{
Author: "admin",
ProposedState: types.RequestState_APPROVED,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
review.Review.AssumeStartTime = &tc.startTime
resp, err := s.testPack.tlsServer.AuthServer.AuthServer.SubmitAccessReview(ctx, review)
tc.errCheck(t, err)
if err == nil {
require.Equal(t, tc.startTime, *resp.GetAssumeStartTime())
}
})
}
}
func TestAssumeStartTime_SetAccessRequestState(t *testing.T) {
ctx := context.Background()
s := createAccessRequestWithStartTime(t)
testCases := []struct {
name string
startTime time.Time
errCheck require.ErrorAssertionFunc
}{
{
name: "too far in the future",
startTime: s.invalidMaxedAssumeStartTime,
errCheck: func(tt require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
require.ErrorContains(t, err, "assume start time is too far in the future")
},
},
{
name: "after access expiry time",
startTime: s.invalidExpiredAssumeStartTime,
errCheck: func(tt require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
require.ErrorContains(t, err, "assume start time must be prior to access expiry time")
},
},
{
name: "valid set state",
startTime: s.validStartTime,
errCheck: require.NoError,
},
}
update := types.AccessRequestUpdate{
RequestID: s.createdRequest.GetName(),
State: types.RequestState_APPROVED,
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
update.AssumeStartTime = &tc.startTime
err := s.testPack.tlsServer.Auth().SetAccessRequestState(ctx, update)
tc.errCheck(t, err)
if err == nil {
resp, err := s.testPack.tlsServer.AuthServer.AuthServer.GetAccessRequests(ctx, types.AccessRequestFilter{})
require.NoError(t, err)
require.Len(t, resp, 1)
require.Equal(t, tc.startTime, *resp[0].GetAssumeStartTime())
}
})
}
}
type accessRequestWithStartTime struct {
testPack *accessRequestTestPack
requesterClient *authclient.Client
invalidMaxedAssumeStartTime time.Time
invalidExpiredAssumeStartTime time.Time
validStartTime time.Time
maxDuration time.Time
requesterUserName string
createdRequest types.AccessRequest
}
func createAccessRequestWithStartTime(t *testing.T) accessRequestWithStartTime {
t.Helper()
ctx := t.Context()
testPack := newAccessRequestTestPack(ctx, t)
const requesterUserName = "requester"
requester := authtest.TestUser(requesterUserName)
requesterClient, err := testPack.tlsServer.NewClient(requester)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, requesterClient.Close()) })
now := time.Now().UTC()
day := 24 * time.Hour
maxDuration := time.Now().UTC().Add(12 * day)
invalidMaxedAssumeStartTime := now.Add(constants.MaxAssumeStartDuration + (1 * day))
invalidExpiredAssumeStartTime := now.Add(100 * day)
validStartTime := now.Add(6 * day)
// create the access request object
req, err := services.NewAccessRequest(requesterUserName, "admins")
require.NoError(t, err)
req.SetMaxDuration(maxDuration)
req.SetAssumeStartTime(validStartTime)
createdReq, err := requesterClient.CreateAccessRequestV2(ctx, req)
require.NoError(t, err)
require.Equal(t, validStartTime, *createdReq.GetAssumeStartTime())
return accessRequestWithStartTime{
testPack: testPack,
requesterClient: requesterClient,
invalidMaxedAssumeStartTime: invalidMaxedAssumeStartTime,
invalidExpiredAssumeStartTime: invalidExpiredAssumeStartTime,
validStartTime: validStartTime,
maxDuration: maxDuration,
requesterUserName: requesterUserName,
createdRequest: createdReq,
}
}
// testCertExtensionResourceIDs verifies that both the legacy AllowedResourceIDs
// and the new AllowedResourceAccessIDs cert extensions are correctly populated
// for access requests with constrained resources only, unconstrained resources
// only, and a mix of both.
//
// Plain (unconstrained) resources should appear in both extensions.
// Constrained resources appear only in the new extension. When all resources
// are constrained, the legacy extension receives a sentinel value to prevent
// older agents from interpreting an empty extension as "unrestricted access".
// When a mix of plain and constrained resources is requested, only the plain
// resource IDs appear in the legacy extension (no sentinel).
func testCertExtensionResourceIDs(t *testing.T, testPack *accessRequestTestPack) {
t.Parallel()
ctx := t.Context()
username := "requester"
constrainedResourceIDs := []types.ResourceAccessID{{
Id: types.ResourceID{
ClusterName: testPack.clusterName,
Kind: types.KindNode,
Name: "staging",
},
Constraints: &types.ResourceConstraints{
Details: &types.ResourceConstraints_Ssh{
Ssh: &types.SSHResourceConstraints{Logins: []string{"root"}},
},
},
}}
plainResourceIDs := []types.ResourceAccessID{{
Id: types.ResourceID{
ClusterName: testPack.clusterName,
Kind: types.KindNode,
Name: "prod",
},
}}
mixedResourceIDs := append(plainResourceIDs, constrainedResourceIDs...)
constrainedRequest, err := services.NewAccessRequestWithResources(username, []string{"admins"}, constrainedResourceIDs)
require.NoError(t, err)
plainRequest, err := services.NewAccessRequestWithResources(username, []string{"admins"}, plainResourceIDs)
require.NoError(t, err)
mixedRequest, err := services.NewAccessRequestWithResources(username, []string{"admins"}, mixedResourceIDs)
require.NoError(t, err)
for _, req := range []types.AccessRequest{constrainedRequest, plainRequest, mixedRequest} {
req.SetState(types.RequestState_APPROVED)
req.SetAccessExpiry(time.Now().Add(time.Hour).UTC())
require.NoError(t, testPack.tlsServer.Auth().UpsertAccessRequest(ctx, req))
}
requester := authtest.TestUser(username)
requesterClient, err := testPack.tlsServer.NewClient(requester)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, requesterClient.Close()) })
generateCerts := func(t *testing.T, requestName string) *proto.Certs {
t.Helper()
certs, err := requesterClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: testPack.sshPubKey,
TLSPublicKey: testPack.tlsPubKey,
Username: username,
Expires: time.Now().Add(time.Hour).UTC(),
AccessRequests: []string{requestName},
})
require.NoError(t, err)
return certs
}
type certResources struct {
AllowedResourceIDs []types.ResourceID
AllowedResourceAccessIDs []types.ResourceAccessID
}
parseCertResources := func(t *testing.T, certs *proto.Certs) (ssh, tls certResources) {
t.Helper()
sshCert, err := sshutils.ParseCertificate(certs.SSH)
require.NoError(t, err)
ssh.AllowedResourceIDs, err = types.ResourceIDsFromString(sshCert.Permissions.Extensions[teleport.CertExtensionAllowedResources])
require.NoError(t, err)
ssh.AllowedResourceAccessIDs, err = types.ResourceAccessIDsFromString(sshCert.Permissions.Extensions[teleport.CertExtensionAllowedResourceAccessIDs])
require.NoError(t, err)
tlsCert, err := tlsca.ParseCertificatePEM(certs.TLS)
require.NoError(t, err)
for _, attr := range tlsCert.Subject.Names {
switch {
case attr.Type.Equal(tlsca.AllowedResourcesASN1ExtensionOID):
tls.AllowedResourceIDs, err = types.ResourceIDsFromString(attr.Value.(string))
require.NoError(t, err)
case attr.Type.Equal(tlsca.AllowedResourceAccessIDsASN1ExtensionOID):
tls.AllowedResourceAccessIDs, err = types.ResourceAccessIDsFromString(attr.Value.(string))
require.NoError(t, err)
}
}
return ssh, tls
}
t.Run("constrained", func(t *testing.T) {
sshCert, tlsCert := parseCertResources(t, generateCerts(t, constrainedRequest.GetName()))
// All resources are constrained, so the legacy extension gets the
// sentinel and the new extension carries the full access IDs.
want := certResources{
AllowedResourceIDs: []types.ResourceID{types.CreateSentinelResourceID()},
AllowedResourceAccessIDs: constrainedResourceIDs,
}
require.Equal(t, want, sshCert)
require.Equal(t, want, tlsCert)
})
t.Run("without constraints", func(t *testing.T) {
sshCert, tlsCert := parseCertResources(t, generateCerts(t, plainRequest.GetName()))
// No constraints, so the legacy extension carries the actual
// resource IDs and no sentinel is needed.
want := certResources{
AllowedResourceIDs: []types.ResourceID{plainResourceIDs[0].Id},
AllowedResourceAccessIDs: plainResourceIDs,
}
require.Equal(t, want, sshCert)
require.Equal(t, want, tlsCert)
})
t.Run("mixed constrained and unconstrained", func(t *testing.T) {
sshCert, tlsCert := parseCertResources(t, generateCerts(t, mixedRequest.GetName()))
// The legacy extension should contain only the plain resource IDs
// (no sentinel) because older agents can still enforce those.
// The new extension carries all resources including constrained ones.
want := certResources{
AllowedResourceIDs: []types.ResourceID{plainResourceIDs[0].Id},
AllowedResourceAccessIDs: mixedResourceIDs,
}
require.Equal(t, want, sshCert)
require.Equal(t, want, tlsCert)
})
}
type reviewState struct {
author string
wantState types.RequestState
wantErr error
}
// testSubmitAccessReview_SubmitForUsers tests if plugin users with the `review_requests.submit_for_users` rule
// can submit reviews for other users.
func testSubmitAccessReview_SubmitForUsers(t *testing.T, testPack *accessRequestTestPack) {
t.Parallel()
ctx := t.Context()
// Create access plugin reviewer without "submit for" review permissions.
_, err := authtest.CreateUser(ctx, testPack.tlsServer.Auth(), "plugin-no-review", services.NewPresetAccessPluginRole())
require.NoError(t, err)
tests := []struct {
name string
reviewStates []reviewState
requester string
reviewer string // (plugin) identity submitting the review request to Auth Service
}{
{
name: "access-plugin without review",
reviewer: "plugin-no-review",
reviewStates: []reviewState{
{
author: "admin",
wantErr: trace.AccessDenied("user %q cannot submit reviews for %q", "plugin-no-review", "admin"),
},
},
},
{
name: "access-plugin with review; submitted for admin",
reviewStates: []reviewState{
{
author: "admin",
wantState: types.RequestState_APPROVED,
},
},
},
{
name: "access-plugin with review; submitted for nobody",
reviewStates: []reviewState{
{
author: "nobody",
wantErr: trace.AccessDenied("user %q cannot submit reviews", "nobody"),
},
},
},
{
name: "access-plugin with review; submitted for non-existent user",
reviewStates: []reviewState{
{
author: "fake-user",
wantErr: trace.AccessDenied("user %q cannot submit reviews for %q, user could not be fetched from local store",
"plugin-reviewer",
"fake-user",
),
},
},
},
{
name: "access-plugin with review; submitted for same user",
requester: "requester-threshold",
reviewStates: []reviewState{
{
author: "admin",
wantState: types.RequestState_PENDING,
},
{
author: "admin",
wantErr: trace.AlreadyExists("user %q has already reviewed this request", "admin"),
},
},
},
{
name: "access-plugin with review; submitted for multiple users",
requester: "requester-threshold",
reviewStates: []reviewState{
{
author: "admin",
wantState: types.RequestState_PENDING,
},
{
author: "admin2",
wantState: types.RequestState_APPROVED,
},
},
},
{
name: "access-plugin with review; submitted for multiple users, but already approved",
reviewStates: []reviewState{
{
author: "admin",
wantState: types.RequestState_APPROVED,
},
{
author: "admin2",
wantErr: trace.AccessDenied("the access request has been already approved"),
},
},
},
{
name: "access-plugin with review; cannot apply on own request",
requester: "plugin-reviewer",
reviewStates: []reviewState{
{
author: "admin2",
wantErr: trace.AccessDenied("review submitter %q cannot apply a review on their own request", "plugin-reviewer"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if tt.requester == "" {
tt.requester = "requester"
}
if tt.reviewer == "" {
tt.reviewer = "plugin-reviewer"
}
// Create requester client.
requesterClient, err := testPack.tlsServer.NewClient(authtest.TestUser(tt.requester))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, requesterClient.Close()) })
// Create access request.
request, err := services.NewAccessRequest(tt.requester, "admins")
require.NoError(t, err)
request, err = requesterClient.CreateAccessRequestV2(ctx, request)
require.NoError(t, err)
// Create plugin reviewer client.
reviewerClient, err := testPack.tlsServer.NewClient(authtest.TestUser(tt.reviewer))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, reviewerClient.Close()) })
// Plugin reviewer should be able to submit for multiple human authors.
for _, r := range tt.reviewStates {
review := types.AccessReviewSubmission{
RequestID: request.GetName(),
Review: types.AccessReview{
Author: r.author,
SubmittedOnBehalfOfAuthor: true,
ProposedState: types.RequestState_APPROVED,
},
}
updatedRequest, err := reviewerClient.SubmitAccessReview(ctx, review)
if r.wantErr != nil {
require.ErrorIs(t, err, r.wantErr)
continue
}
require.NoError(t, err)
require.Equal(t, r.wantState, updatedRequest.GetState())
}
})
}
}