diff --git a/lib/auth/machineid/machineidv1/bot_service.go b/lib/auth/machineid/machineidv1/bot_service.go index ee898d6614cf..e9d24cb74b71 100644 --- a/lib/auth/machineid/machineidv1/bot_service.go +++ b/lib/auth/machineid/machineidv1/bot_service.go @@ -29,6 +29,7 @@ import ( "github.com/jonboulle/clockwork" "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/gravitational/teleport" headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" @@ -619,11 +620,14 @@ func botFromUserAndRole(user types.User, role types.Role) (*pb.Bot, error) { return nil, trace.BadParameter("user missing bot label") } + expiry := botExpiryFromUser(user) + b := &pb.Bot{ Kind: types.KindBot, Version: types.V1, Metadata: &headerv1.Metadata{ - Name: botName, + Name: botName, + Expires: expiry, }, Status: &pb.BotStatus{ UserName: user.GetName(), @@ -686,6 +690,7 @@ func botToUserAndRole(bot *pb.Bot, now time.Time, createdBy string) (types.User, roleMeta.Labels = map[string]string{ types.BotLabel: bot.Metadata.Name, } + roleMeta.Expires = userAndRoleExpiryFromBot(bot) role.SetMetadata(roleMeta) // Setup user @@ -707,7 +712,7 @@ func botToUserAndRole(bot *pb.Bot, now time.Time, createdBy string) (types.User, // We always set this to zero here - but in Upsert, we copy from the // previous user before writing if necessary userMeta.Labels[types.BotGenerationLabel] = "0" - + userMeta.Expires = userAndRoleExpiryFromBot(bot) user.SetMetadata(userMeta) traits := map[string][]string{} @@ -728,3 +733,24 @@ func botToUserAndRole(bot *pb.Bot, now time.Time, createdBy string) (types.User, return user, role, nil } + +func userAndRoleExpiryFromBot(bot *pb.Bot) *time.Time { + if bot.Metadata.GetExpires() == nil { + return nil + } + + expiry := bot.Metadata.GetExpires().AsTime() + if expiry.IsZero() || expiry.Unix() == 0 { + return nil + } + return &expiry +} + +func botExpiryFromUser(user types.User) *timestamppb.Timestamp { + userMeta := user.GetMetadata() + userExpiry := userMeta.Expiry() + if userExpiry.IsZero() || userExpiry.Unix() == 0 { + return nil + } + return timestamppb.New(userExpiry) +} diff --git a/lib/auth/machineid/machineidv1/machineidv1_test.go b/lib/auth/machineid/machineidv1/machineidv1_test.go index b95624faaf96..6e66704c674e 100644 --- a/lib/auth/machineid/machineidv1/machineidv1_test.go +++ b/lib/auth/machineid/machineidv1/machineidv1_test.go @@ -33,6 +33,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/fieldmaskpb" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/defaults" @@ -104,6 +105,7 @@ func TestCreateBot(t *testing.T) { }, ) require.NoError(t, err) + expiry := time.Now().Add(time.Hour) tests := []struct { name string @@ -223,6 +225,118 @@ func TestCreateBot(t *testing.T) { }, }, }, + { + name: "success with expiry", + user: botCreator.GetName(), + req: &machineidv1pb.CreateBotRequest{ + Bot: &machineidv1pb.Bot{ + Metadata: &headerv1.Metadata{ + Name: "success-with-expiry", + Labels: map[string]string{ + "my-label": "my-value", + "my-other-label": "my-other-value", + }, + Expires: timestamppb.New(expiry), + }, + Spec: &machineidv1pb.BotSpec{ + Roles: []string{testRole.GetName()}, + Traits: []*machineidv1pb.Trait{ + { + Name: constants.TraitLogins, + Values: []string{"root"}, + }, + { + Name: constants.TraitKubeUsers, + Values: []string{}, + }, + }, + }, + }, + }, + + assertError: require.NoError, + want: &machineidv1pb.Bot{ + Kind: types.KindBot, + Version: types.V1, + Metadata: &headerv1.Metadata{ + Name: "success-with-expiry", + Labels: map[string]string{ + "my-label": "my-value", + "my-other-label": "my-other-value", + }, + Expires: timestamppb.New(expiry), + }, + Spec: &machineidv1pb.BotSpec{ + Roles: []string{testRole.GetName()}, + Traits: []*machineidv1pb.Trait{ + { + Name: constants.TraitLogins, + Values: []string{"root"}, + }, + }, + }, + Status: &machineidv1pb.BotStatus{ + UserName: "bot-success-with-expiry", + RoleName: "bot-success-with-expiry", + }, + }, + wantUser: &types.UserV2{ + Kind: types.KindUser, + Version: types.V2, + Metadata: types.Metadata{ + Name: "bot-success-with-expiry", + Namespace: defaults.Namespace, + Labels: map[string]string{ + types.BotLabel: "success-with-expiry", + types.BotGenerationLabel: "0", + "my-label": "my-value", + "my-other-label": "my-other-value", + }, + Expires: &expiry, + }, + Spec: types.UserSpecV2{ + CreatedBy: types.CreatedBy{ + User: types.UserRef{Name: botCreator.GetName()}, + }, + Roles: []string{"bot-success-with-expiry"}, + Traits: map[string][]string{ + constants.TraitLogins: {"root"}, + }, + }, + Status: types.UserStatusV2{ + PasswordState: types.PasswordState_PASSWORD_STATE_UNSET, + }, + }, + wantRole: &types.RoleV6{ + Kind: types.KindRole, + Version: types.V7, + Metadata: types.Metadata{ + Name: "bot-success-with-expiry", + Namespace: defaults.Namespace, + Labels: map[string]string{ + types.BotLabel: "success-with-expiry", + }, + Description: "Automatically generated role for bot success-with-expiry", + Expires: &expiry, + }, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + MaxSessionTTL: types.Duration(12 * time.Hour), + }, + Allow: types.RoleConditions{ + Impersonate: &types.ImpersonateConditions{ + Roles: []string{testRole.GetName()}, + }, + Rules: []types.Rule{ + types.NewRule( + types.KindCertAuthority, + []string{types.VerbReadNoSecrets}, + ), + }, + }, + }, + }, + }, { name: "bot already exists", user: botCreator.GetName(), @@ -759,6 +873,7 @@ func TestUpsertBot(t *testing.T) { }, }) require.NoError(t, err) + expiry := time.Now().Add(time.Hour) // We find the user associated with the Bot and set the generation label. This allows us to ensure that the // generation label is preserved when UpsertBot is called. @@ -880,6 +995,108 @@ func TestUpsertBot(t *testing.T) { }, }, }, + { + name: "new with expiry", + user: botCreator.GetName(), + req: &machineidv1pb.UpsertBotRequest{ + Bot: &machineidv1pb.Bot{ + Metadata: &headerv1.Metadata{ + Name: "new-with-expiry", + Labels: map[string]string{ + "my-label": "my-value", + "my-other-label": "my-other-value", + }, + Expires: timestamppb.New(expiry), + }, + Spec: &machineidv1pb.BotSpec{ + Roles: []string{testRole.GetName()}, + Traits: []*machineidv1pb.Trait{ + { + Name: constants.TraitLogins, + Values: []string{"root"}, + }, + }, + }, + }, + }, + + assertError: require.NoError, + want: &machineidv1pb.Bot{ + Kind: types.KindBot, + Version: types.V1, + Metadata: &headerv1.Metadata{ + Name: "new-with-expiry", + Labels: map[string]string{ + "my-label": "my-value", + "my-other-label": "my-other-value", + }, + Expires: timestamppb.New(expiry), + }, + Spec: &machineidv1pb.BotSpec{ + Roles: []string{testRole.GetName()}, + Traits: []*machineidv1pb.Trait{ + { + Name: constants.TraitLogins, + Values: []string{"root"}, + }, + }, + }, + Status: &machineidv1pb.BotStatus{ + UserName: "bot-new-with-expiry", + RoleName: "bot-new-with-expiry", + }, + }, + wantUser: &types.UserV2{ + Kind: types.KindUser, + Version: types.V2, + Metadata: types.Metadata{ + Name: "bot-new-with-expiry", + Namespace: defaults.Namespace, + Labels: map[string]string{ + types.BotLabel: "new-with-expiry", + types.BotGenerationLabel: "0", + "my-label": "my-value", + "my-other-label": "my-other-value", + }, + Expires: &expiry, + }, + Spec: types.UserSpecV2{ + Roles: []string{"bot-new-with-expiry"}, + Traits: map[string][]string{ + constants.TraitLogins: {"root"}, + }, + CreatedBy: types.CreatedBy{ + User: types.UserRef{Name: botCreator.GetName()}, + }, + }, + }, + wantRole: &types.RoleV6{ + Kind: types.KindRole, + Version: types.V7, + Metadata: types.Metadata{ + Name: "bot-new-with-expiry", + Namespace: defaults.Namespace, + Labels: map[string]string{ + types.BotLabel: "new-with-expiry", + }, + Description: "Automatically generated role for bot new-with-expiry", + Expires: &expiry, + }, + Spec: types.RoleSpecV6{ + Options: types.RoleOptions{ + MaxSessionTTL: types.Duration(12 * time.Hour), + }, + Allow: types.RoleConditions{ + Impersonate: &types.ImpersonateConditions{ + Roles: []string{testRole.GetName()}, + }, + Rules: []types.Rule{ + types.NewRule(types.KindCertAuthority, []string{types.VerbReadNoSecrets}), + }, + }, + }, + }, + }, { name: "already exists", user: botCreator.GetName(),