Files
teleport/lib/auth/join_test.go
T
rosstimothy 0486c0935a Update backend.Key to be its own distinct type (#46701)
Converts the backend.Key from a slice of bytes to a concrete struct.
The main motivation behind this change is to be able to better
distinguish individual components of a key. The textual representation
of a backend key is constructed by joining all components of the key
with a /. By representing the entire key as a single textual object
it prevented resources containing a / in their name from being
properly identified. There was a lot of code that interpolated keys
in various manners and expected only a specific number of
subcomponents for a particular resource. This lead to most,
if not all, of the RPCs used to create a resource to permit a name
containing /, (i.e. a user named test/llama/1), but any RPCs used
to retrieve the resources would fail to retrieve them from the
backend.

Every backend.Key now carries a slice of all the individual
components in addition to the textual representation. This permits
more fine grained inspection per component, while also not having
to construct the textual representation for a group of components
more than once.

The backend.Sanitizer was updated to only validate keys for backend
writes. Retrieving and deleting invalid of malformed keys is now
permitted. This will allow any existing resources that were created
with / in one of the components to become retrievable and deletable.
However, any new resources with  / in their name are explicitly
prevented.

Closes #6088
Closes #9107
Closes #10576
Closes #42823
2024-10-04 14:02:00 +00:00

482 lines
14 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 (
"context"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/client/proto"
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"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/join"
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
"github.com/gravitational/teleport/lib/auth/state"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
)
func TestAuth_RegisterUsingToken(t *testing.T) {
ctx := context.Background()
p, err := newTestPack(ctx, t.TempDir())
require.NoError(t, err)
// create a static token
staticToken := types.ProvisionTokenV1{
Roles: []types.SystemRole{types.RoleNode},
Token: "static_token",
}
staticTokens, err := types.NewStaticTokens(types.StaticTokensSpecV2{
StaticTokens: []types.ProvisionTokenV1{staticToken},
})
require.NoError(t, err)
err = p.a.SetStaticTokens(staticTokens)
require.NoError(t, err)
// create a valid dynamic token
dynamicToken := generateTestToken(
ctx,
t,
types.SystemRoles{types.RoleNode},
p.a.GetClock().Now().Add(time.Minute*30),
p.a,
)
// create an expired dynamic token
expiredDynamicToken := generateTestToken(
ctx,
t,
types.SystemRoles{types.RoleNode},
p.a.GetClock().Now().Add(-time.Minute*30),
p.a,
)
sshPrivateKey, sshPublicKey, err := testauthority.New().GenerateKeyPair()
require.NoError(t, err)
tlsPublicKey, err := PrivateKeyToPublicKeyTLS(sshPrivateKey)
require.NoError(t, err)
testcases := []struct {
desc string
req *types.RegisterUsingTokenRequest
certsAssertion func(*proto.Certs)
errorAssertion func(error) bool
waitTokenDeleted bool // Expired tokens are deleted in background, might need slight delay in relevant test
}{
{
desc: "reject empty",
req: &types.RegisterUsingTokenRequest{},
errorAssertion: trace.IsBadParameter,
},
{
desc: "reject no token",
req: &types.RegisterUsingTokenRequest{
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleNode,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
},
errorAssertion: trace.IsBadParameter,
},
{
desc: "reject no HostID",
req: &types.RegisterUsingTokenRequest{
Token: staticToken.Token,
NodeName: "node-name",
Role: types.RoleNode,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
},
errorAssertion: trace.IsBadParameter,
},
{
desc: "allow no NodeName",
req: &types.RegisterUsingTokenRequest{
Token: staticToken.Token,
HostID: "localhost",
Role: types.RoleNode,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
},
},
{
desc: "reject no SSH pub",
req: &types.RegisterUsingTokenRequest{
Token: staticToken.Token,
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleNode,
PublicTLSKey: tlsPublicKey,
},
errorAssertion: trace.IsBadParameter,
},
{
desc: "reject no TLS pub",
req: &types.RegisterUsingTokenRequest{
Token: staticToken.Token,
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleNode,
PublicSSHKey: sshPublicKey,
},
errorAssertion: trace.IsBadParameter,
},
{
desc: "reject bad token",
req: &types.RegisterUsingTokenRequest{
Token: "not a token",
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleNode,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
},
errorAssertion: trace.IsAccessDenied,
},
{
desc: "allow static token",
req: &types.RegisterUsingTokenRequest{
Token: staticToken.Token,
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleNode,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
},
},
{
desc: "reject wrong role static",
req: &types.RegisterUsingTokenRequest{
Token: staticToken.Token,
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleProxy,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
},
errorAssertion: trace.IsBadParameter,
},
{
desc: "allow dynamic token",
req: &types.RegisterUsingTokenRequest{
Token: dynamicToken,
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleNode,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
},
},
{
desc: "reject wrong role dynamic",
req: &types.RegisterUsingTokenRequest{
Token: dynamicToken,
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleProxy,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
},
errorAssertion: trace.IsBadParameter,
},
{
desc: "check additional principals",
req: &types.RegisterUsingTokenRequest{
Token: dynamicToken,
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleNode,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
AdditionalPrincipals: []string{"example.com"},
},
certsAssertion: func(certs *proto.Certs) {
hostCert, err := sshutils.ParseCertificate(certs.SSH)
require.NoError(t, err)
require.Contains(t, hostCert.ValidPrincipals, "example.com")
},
},
{
desc: "reject expired dynamic token",
req: &types.RegisterUsingTokenRequest{
Token: expiredDynamicToken,
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleNode,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
},
waitTokenDeleted: true,
errorAssertion: trace.IsAccessDenied,
},
{
// relies on token being deleted during previous testcase
desc: "expired token should be gone",
req: &types.RegisterUsingTokenRequest{
Token: expiredDynamicToken,
HostID: "localhost",
NodeName: "node-name",
Role: types.RoleNode,
PublicSSHKey: sshPublicKey,
PublicTLSKey: tlsPublicKey,
},
errorAssertion: trace.IsAccessDenied,
},
}
for _, tc := range testcases {
t.Run(tc.desc, func(t *testing.T) {
certs, err := p.a.RegisterUsingToken(ctx, tc.req)
if tc.errorAssertion != nil {
require.True(t, tc.errorAssertion(err))
if tc.waitTokenDeleted {
require.Eventually(t, func() bool {
_, err := p.a.ValidateToken(ctx, tc.req.Token)
return err != nil && strings.Contains(err.Error(), TokenExpiredOrNotFound)
}, time.Millisecond*100, time.Millisecond*10)
}
return
}
require.NoError(t, err)
if tc.certsAssertion != nil {
tc.certsAssertion(certs)
}
// Check audit log event is emitted
evt := p.mockEmitter.LastEvent()
require.NotNil(t, evt)
joinEvent, ok := evt.(*apievents.InstanceJoin)
require.True(t, ok)
require.Equal(t, events.InstanceJoinEvent, joinEvent.Type)
require.Equal(t, events.InstanceJoinCode, joinEvent.Code)
require.Equal(t, tc.req.NodeName, joinEvent.NodeName)
require.Equal(t, tc.req.HostID, joinEvent.HostID)
require.EqualValues(t, tc.req.Role, joinEvent.Role)
require.EqualValues(t, types.JoinMethodToken, joinEvent.Method)
})
}
}
func newBotToken(t *testing.T, tokenName, botName string, role types.SystemRole, expiry time.Time) types.ProvisionToken {
t.Helper()
token, err := types.NewProvisionTokenFromSpec(tokenName, expiry, types.ProvisionTokenSpecV2{
Roles: []types.SystemRole{role},
BotName: botName,
})
require.NoError(t, err, "could not create bot token")
return token
}
// TestRegister_Bot tests that a provision token can be used to generate
// renewable certificates for a non-interactive user.
func TestRegister_Bot(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := newTestTLSServer(t)
bot, err := machineidv1.UpsertBot(ctx, srv.Auth(), &machineidv1pb.Bot{
Metadata: &headerv1.Metadata{
Name: "test",
},
Spec: &machineidv1pb.BotSpec{
Roles: []string{},
},
}, srv.Clock().Now(), "")
require.NoError(t, err)
later := srv.Clock().Now().Add(4 * time.Hour)
goodToken := newBotToken(t, "good-token", bot.Metadata.Name, types.RoleBot, later)
expiredToken := newBotToken(t, "expired", bot.Metadata.Name, types.RoleBot, srv.Clock().Now().Add(-1*time.Hour))
wrongKind := newBotToken(t, "wrong-kind", "", types.RoleNode, later)
wrongUser := newBotToken(t, "wrong-user", "llama", types.RoleBot, later)
invalidToken := newBotToken(t, "this-token-does-not-exist", bot.Metadata.Name, types.RoleBot, later)
err = srv.Auth().UpsertToken(ctx, goodToken)
require.NoError(t, err)
err = srv.Auth().UpsertToken(ctx, expiredToken)
require.NoError(t, err)
err = srv.Auth().UpsertToken(ctx, wrongKind)
require.NoError(t, err)
err = srv.Auth().UpsertToken(ctx, wrongUser)
require.NoError(t, err)
for _, test := range []struct {
desc string
token types.ProvisionToken
assertErr require.ErrorAssertionFunc
}{
{
desc: "OK good token",
token: goodToken,
assertErr: require.NoError,
},
{
desc: "NOK expired token",
token: expiredToken,
assertErr: require.Error,
},
{
desc: "NOK wrong token kind",
token: wrongKind,
assertErr: require.Error,
},
{
desc: "NOK token for wrong user",
token: wrongUser,
assertErr: require.Error,
},
{
desc: "NOK invalid token",
token: invalidToken,
assertErr: require.Error,
},
} {
t.Run(test.desc, func(t *testing.T) {
start := srv.Clock().Now()
result, err := join.Register(ctx, join.RegisterParams{
Token: test.token.GetName(),
ID: state.IdentityID{
Role: types.RoleBot,
},
AuthServers: []utils.NetAddr{*utils.MustParseAddr(srv.Addr().String())},
})
test.assertErr(t, err)
if err == nil {
require.NotEmpty(t, result.Certs.SSH)
require.NotEmpty(t, result.Certs.TLS)
// ensure token was removed
_, err = srv.Auth().GetToken(ctx, test.token.GetName())
require.True(t, trace.IsNotFound(err), "expected not found error, got %v", err)
// ensure cert is renewable
x509, err := tlsca.ParseCertificatePEM(result.Certs.TLS)
require.NoError(t, err)
id, err := tlsca.FromSubject(x509.Subject, later)
require.NoError(t, err)
require.True(t, id.Renewable)
// Check audit event
evts, _, err := srv.Auth().SearchEvents(ctx, events.SearchEventsRequest{
From: start,
To: srv.Clock().Now(),
EventTypes: []string{events.BotJoinEvent},
Limit: 1,
Order: types.EventOrderDescending,
})
require.NoError(t, err)
require.Len(t, evts, 1)
evt, ok := evts[0].(*apievents.BotJoin)
require.True(t, ok)
require.Equal(t, events.BotJoinEvent, evt.Type)
require.Equal(t, events.BotJoinCode, evt.Code)
require.EqualValues(t, types.JoinMethodToken, evt.Method)
}
})
}
}
// TestRegister_Bot_Expiry checks that bot certificate expiry can be set, and
// does not exceed the limit.
func TestRegister_Bot_Expiry(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := newTestTLSServer(t)
validExpires := srv.Clock().Now().Add(time.Hour * 6)
tooGreatExpires := srv.Clock().Now().Add(time.Hour * 24 * 365)
tests := []struct {
name string
requestExpires *time.Time
expectTTL time.Duration
}{
{
name: "unspecified defaults",
requestExpires: nil,
expectTTL: defaults.DefaultRenewableCertTTL,
},
{
name: "valid value specified",
requestExpires: &validExpires,
expectTTL: time.Hour * 6,
},
{
name: "value exceeding limit specified",
requestExpires: &tooGreatExpires,
// MaxSessionTTL set in createBotRole is 12 hours, so this cap will
// apply instead of the defaults.MaxRenewableCertTTL specified
// in generateInitialBotCerts.
expectTTL: 12 * time.Hour,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
botName := uuid.NewString()
_, err := machineidv1.UpsertBot(ctx, srv.Auth(), &machineidv1pb.Bot{
Metadata: &headerv1.Metadata{
Name: botName,
},
Spec: &machineidv1pb.BotSpec{
Roles: []string{},
Traits: []*machineidv1pb.Trait{},
},
}, srv.Clock().Now(), "")
require.NoError(t, err)
tok := newBotToken(t, uuid.NewString(), botName, types.RoleBot, srv.Clock().Now().Add(time.Hour))
require.NoError(t, srv.Auth().UpsertToken(ctx, tok))
result, err := join.Register(ctx, join.RegisterParams{
Token: tok.GetName(),
ID: state.IdentityID{
Role: types.RoleBot,
},
AuthServers: []utils.NetAddr{*utils.MustParseAddr(srv.Addr().String())},
Expires: tt.requestExpires,
})
require.NoError(t, err)
x509, err := tlsca.ParseCertificatePEM(result.Certs.TLS)
require.NoError(t, err)
id, err := tlsca.FromSubject(x509.Subject, x509.NotAfter)
require.NoError(t, err)
ttl := id.Expires.Sub(srv.Clock().Now())
require.Equal(t, tt.expectTTL, ttl)
})
}
}