Files
teleport/lib/auth/access_request_test.go
T
Noah StrideandMarco Dinis 0586278cde Migrate GetClusterName to gRPC (#53164)
* Adjust GetClusterName method signature to include context.Context

* Start hacking on gRPC GetClusterName impl

* Fix test mocks to match signature

* Fix integration tests

* Fix integrations tests

* Missing ctx

* New GetClusterName RPC implementatin

* Add fallback cluster name

* Correct godoc comment

* Remove unused method

* Remove unused import

* Fix E2E test

* Fix tests

* Fix deferred context cancel in test conflicting with parallel subtests

* Fix import

* Fix imports

* Update lib/auth/auth_with_roles.go

Co-authored-by: Marco Dinis <marco.dinis@goteleport.com>

---------

Co-authored-by: Marco Dinis <marco.dinis@goteleport.com>
2025-03-20 10:07:02 +00:00

1963 lines
61 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
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"
"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/entitlements"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/backend/memory"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/tlsca"
)
type accessRequestTestPack struct {
tlsServer *TestTLSServer
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 := NewTestAuthServer(TestAuthServerConfig{
Dir: t.TempDir(),
})
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),
},
},
},
"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(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) {
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) })
t.Run("role refresh with bogus request ID", func(t *testing.T) { testRoleRefreshWithBogusRequestID(t, testPack) })
t.Run("bot user approver", func(t *testing.T) { testBotAccessRequestReview(t, testPack) })
t.Run("deny", func(t *testing.T) { testAccessRequestDenyRules(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")
}
}
}
// 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 := NewTestAuthServer(TestAuthServerConfig{
Dir: t.TempDir(),
Clock: clock,
})
require.NoError(t, err)
defer authServer.Close()
tlsServer, err := authServer.NewTestTLSServer()
require.NoError(t, err)
defer tlsServer.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
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(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 := NewTestAuthServer(TestAuthServerConfig{
Dir: t.TempDir(),
Clock: clock,
})
require.NoError(t, err)
defer authServer.Close()
tlsServer, err := authServer.NewTestTLSServer()
require.NoError(t, err)
defer tlsServer.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
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(TestUser(userA))
require.NoError(t, err)
defer clientA.Close()
clientB, err := tlsServer.NewClient(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 i := 0; i < requestsPerUser; i++ {
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 i := 0; i < requestsPerUser; i++ {
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 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(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()
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.AccessDenied(`Resource Access Requests require usable "search_as_roles", none found for user "nobody"`),
},
}
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{
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(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
req, err = requesterClient.CreateAccessRequestV2(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.tlsPrivKey)
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{
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 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{
SSHPublicKey: testPack.sshPubKey,
TLSPublicKey: testPack.tlsPubKey,
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()))
})
}
}
// 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(TestAdmin())
require.NoError(t, err)
defer adminClient.Close()
bot, err := adminClient.BotServiceClient().CreateBot(ctx, &machineidv1pb.CreateBotRequest{
Bot: &machineidv1pb.Bot{
Metadata: &headerv1.Metadata{
Name: "request-approver",
},
Spec: &machineidv1pb.BotSpec{
Roles: []string{
// Grants the ability to approve requests
"admins",
},
},
},
})
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(TestUser(bot.Status.UserName))
require.NoError(t, err)
defer botClient.Close()
certRes, err := botClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
Username: bot.Status.UserName,
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 := testPack.tlsServer.NewClientWithCert(tlsCert)
defer impersonatedBotClient.Close()
// Create an access request for the bot to approve
requesterClient, err := testPack.tlsServer.NewClient(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.Status.UserName, 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.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, *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)
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 *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.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)
})
}
}
// 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(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)
}
// 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)
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.ResourceIDsFromString(sshCert.Permissions.Extensions[teleport.CertExtensionAllowedResources])
require.NoError(t, err)
assert.ElementsMatch(t, resourceIDs, sshCertAllowedResources)
assert.ElementsMatch(t, resourceIDs, tlsIdentity.AllowedResourceIDs)
}
func TestCreateSuggestions(t *testing.T) {
t.Parallel()
testAuthServer, err := NewTestAuthServer(TestAuthServerConfig{
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) {
modules.SetTestModules(t, &modules.TestModules{TestBuildType: modules.BuildEnterprise})
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
testPack := newAccessRequestTestPack(ctx, t)
const requesterUserName = "requester"
requester := 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 := 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) {
clock := clockwork.NewFakeClock()
modules.SetTestModules(t, &modules.TestModules{
TestFeatures: modules.Features{
Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{
entitlements.Identity: {Enabled: true},
},
},
})
mustRequest := func(suggestedReviewers ...string) types.AccessRequest {
req, err := services.NewAccessRequest("test-user", "admins")
require.NoError(t, err)
req.SetSuggestedReviewers(suggestedReviewers)
return req
}
type testAccessListOwner struct {
name string
kind string
}
mustAccessListWithMembershipKind := func(name string, owners ...testAccessListOwner) *accesslist.AccessList {
ownersSpec := make([]accesslist.Owner, len(owners))
for i, owner := range owners {
ownersSpec[i] = accesslist.Owner{
Name: owner.name,
MembershipKind: owner.kind,
}
}
accessList, err := accesslist.NewAccessList(header.Metadata{
Name: name,
}, accesslist.Spec{
Title: "simple",
Grants: accesslist.Grants{
Roles: []string{"grant-role"},
},
Audit: accesslist.Audit{
NextAuditDate: clock.Now().AddDate(1, 0, 0),
},
Owners: ownersSpec,
})
require.NoError(t, err)
return accessList
}
mustAccessList := func(name string, owners ...string) *accesslist.AccessList {
ownersStruct := make([]testAccessListOwner, 0, len(owners))
for _, owner := range owners {
ownersStruct = append(ownersStruct, testAccessListOwner{owner, accesslist.MembershipKindUser})
}
return mustAccessListWithMembershipKind(name, ownersStruct...)
}
tests := []struct {
name string
req types.AccessRequest
accessLists []*accesslist.AccessList
accessListMembers []struct {
Header header.Metadata
Spec accesslist.AccessListMemberSpec
}
promotions *types.AccessRequestAllowedPromotions
expectedReviewers []string
}{
{
name: "nil promotions",
req: mustRequest("rev1", "rev2"),
expectedReviewers: []string{"rev1", "rev2"},
},
{
name: "a few promotions",
req: mustRequest("rev1", "rev2"),
accessLists: []*accesslist.AccessList{
mustAccessList("name1", "owner1", "owner2"),
mustAccessList("name2", "owner1", "owner3"),
mustAccessList("name3", "owner4", "owner5"),
},
promotions: &types.AccessRequestAllowedPromotions{
Promotions: []*types.AccessRequestAllowedPromotion{
{AccessListName: "name1"},
{AccessListName: "name2"},
},
},
expectedReviewers: []string{"rev1", "rev2", "owner1", "owner2", "owner3"},
},
{
name: "with ownership through nested list",
req: mustRequest("rev1"),
accessLists: []*accesslist.AccessList{
mustAccessList("nested1", "owner1"),
mustAccessListWithMembershipKind(
"nested",
testAccessListOwner{"owner1", accesslist.MembershipKindUser},
testAccessListOwner{"nested1", accesslist.MembershipKindList},
),
mustAccessListWithMembershipKind(
"root",
testAccessListOwner{"owner1", accesslist.MembershipKindUser},
testAccessListOwner{"nested", accesslist.MembershipKindList},
),
},
accessListMembers: []struct {
Header header.Metadata
Spec accesslist.AccessListMemberSpec
}{
{
Header: header.Metadata{
Name: "nested",
},
Spec: accesslist.AccessListMemberSpec{
AccessList: "root",
Name: "nested",
Joined: clock.Now().UTC(),
Expires: clock.Now().UTC().Add(24 * time.Hour),
Reason: "because",
AddedBy: "owner1",
MembershipKind: accesslist.MembershipKindList,
},
},
{
Header: header.Metadata{
Name: "nested1",
},
Spec: accesslist.AccessListMemberSpec{
AccessList: "nested",
Name: "nested1",
Joined: clock.Now().UTC(),
Expires: clock.Now().UTC().Add(24 * time.Hour),
Reason: "because",
AddedBy: "owner1",
MembershipKind: accesslist.MembershipKindList,
},
},
{
Header: header.Metadata{
Name: "owner2",
},
Spec: accesslist.AccessListMemberSpec{
AccessList: "nested",
Name: "owner2",
Joined: clock.Now().UTC(),
Expires: clock.Now().UTC().Add(24 * time.Hour),
Reason: "because",
AddedBy: "owner1",
MembershipKind: accesslist.MembershipKindUser,
},
},
{
Header: header.Metadata{
Name: "owner3",
},
Spec: accesslist.AccessListMemberSpec{
AccessList: "nested1",
Name: "owner3",
Joined: clock.Now().UTC(),
Expires: clock.Now().UTC().Add(24 * time.Hour),
Reason: "because",
AddedBy: "owner1",
MembershipKind: accesslist.MembershipKindUser,
},
},
},
promotions: &types.AccessRequestAllowedPromotions{
Promotions: []*types.AccessRequestAllowedPromotion{
{AccessListName: "root"},
{AccessListName: "nested"},
},
},
// owner1 is owner of 'root', should be included
// owner2 is member of 'nested', which is owner of 'root', should be included via inheritance
// owner3 is member of 'nested1', which is member of 'nested', which is owner of 'root', should be included via two levels of inheritance
expectedReviewers: []string{"rev1", "owner1", "owner2", "owner3"},
},
{
name: "no promotions",
req: mustRequest("rev1", "rev2"),
accessLists: []*accesslist.AccessList{
mustAccessList("name1", "owner1", "owner2"),
mustAccessList("name2", "owner1", "owner3"),
mustAccessList("name3", "owner4", "owner5"),
},
promotions: &types.AccessRequestAllowedPromotions{
Promotions: []*types.AccessRequestAllowedPromotion{},
},
expectedReviewers: []string{"rev1", "rev2"},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
mem, err := memory.New(memory.Config{})
require.NoError(t, err)
accessLists, err := local.NewAccessListService(mem, clock)
require.NoError(t, err)
ctx := context.Background()
for _, accessList := range test.accessLists {
_, err = accessLists.UpsertAccessList(ctx, accessList)
require.NoError(t, err)
}
if test.accessListMembers != nil {
for _, memberData := range test.accessListMembers {
member, err := accesslist.NewAccessListMember(memberData.Header, memberData.Spec)
require.NoError(t, err)
_, err = accessLists.UpsertAccessListMember(ctx, member)
require.NoError(t, err)
}
}
req := test.req.Copy()
updateAccessRequestWithAdditionalReviewers(ctx, req, accessLists, test.promotions)
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 ...interface{}) {
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 ...interface{}) {
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 {
tc := tc
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 ...interface{}) {
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 ...interface{}) {
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 {
tc := tc
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 ...interface{}) {
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 ...interface{}) {
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 {
tc := tc
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()
modules.SetTestModules(t, &modules.TestModules{TestBuildType: modules.BuildEnterprise})
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
testPack := newAccessRequestTestPack(ctx, t)
const requesterUserName = "requester"
requester := 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,
}
}