Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,963 changes: 985 additions & 978 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions api/client/proto/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,8 @@ func (h DownstreamInventoryHello) sealedDownstreamInventoryMessage() {}
func (p DownstreamInventoryPing) sealedDownstreamInventoryMessage() {}

func (u DownstreamInventoryUpdateLabels) sealedDownstreamInventoryMessage() {}

// AllowsMFAReuse returns true if the MFA response provided allows reuse.
func (r *UserCertsRequest) AllowsMFAReuse() bool {
return r.RequesterName == UserCertsRequest_TSH_DB_EXEC
}
6 changes: 6 additions & 0 deletions api/mfa/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ var (
// support MFA ceremonies, or the server does not support MFA ceremonies for
// the client user.
ErrMFANotSupported = trace.BadParameterError{Message: "re-authentication with MFA is not supported for this client"}

// ErrExpiredReusableMFAResponse is returned by Auth APIs like
// GenerateUserCerts when an expired reusable MFA response is provided.
ErrExpiredReusableMFAResponse = trace.AccessDeniedError{
Message: "Reusable MFA response validation failed and possibly expired",
}
)

// WithCredentials can be called on a GRPC client request to attach
Expand Down
4 changes: 4 additions & 0 deletions api/proto/teleport/legacy/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ message UserCertsRequest {
TSH_KUBE_LOCAL_PROXY_HEADLESS = 3;
// TSH_APP_LOCAL_PROXY is set when the request was sent by a tsh app local proxy.
TSH_APP_LOCAL_PROXY = 4;
// TSH_DB_EXEC is set when the request was sent by a tsh db exec local proxy
// tunnel. Requests from this requester allows reuse of the MFA session
// response but TTL is limited to single use TTL.
TSH_DB_EXEC = 5;
}
// RequesterName identifies who sent the request.
Requester RequesterName = 17 [(gogoproto.jsontag) = "requester_name"];
Expand Down
8 changes: 8 additions & 0 deletions api/types/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package types

import (
"iter"
"regexp"
"slices"
"sort"
Expand All @@ -30,6 +31,7 @@ import (
"github.com/gravitational/teleport/api/types/common"
"github.com/gravitational/teleport/api/types/compare"
"github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/iterutils"
)

var (
Expand Down Expand Up @@ -84,6 +86,12 @@ func GetName[R Resource](r R) string {
return r.GetName()
}

// ResourceNames creates an iterator that loops through the provided slice of
// resources and return their names.
func ResourceNames[R Resource, S ~[]R](s S) iter.Seq[string] {
return iterutils.Map(GetName, slices.Values(s))
}

// ResourceDetails includes details about the resource
type ResourceDetails struct {
Hostname string
Expand Down
19 changes: 19 additions & 0 deletions api/types/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package types

import (
"fmt"
"slices"
"testing"
"time"

Expand Down Expand Up @@ -820,3 +822,20 @@ func TestResourceHeaderIsEqual(t *testing.T) {
})
}
}

func TestResourceNames(t *testing.T) {
var apps Apps
var expectedNames []string
for i := 0; i < 10; i++ {
app, err := NewAppV3(Metadata{
Name: fmt.Sprintf("app-%d", i),
}, AppSpecV3{
URI: "tcp://localhost:1111",
})
require.NoError(t, err)
apps = append(apps, app)
expectedNames = append(expectedNames, app.GetName())
}

require.Equal(t, expectedNames, slices.Collect(ResourceNames(apps)))
}
37 changes: 37 additions & 0 deletions api/utils/iterutils/iter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Teleport
* Copyright (C) 2025 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 iterutils

import (
"iter"
)

// Map returns an iterator over f applied to seq.
//
// Copied from https://github.com/golang/go/issues/61898. We should switch to an
// official package once it is available.
func Map[In, Out any](f func(In) Out, seq iter.Seq[In]) iter.Seq[Out] {
return func(yield func(Out) bool) {
for in := range seq {
if !yield(f(in)) {
return
}
}
}
}
39 changes: 39 additions & 0 deletions api/utils/iterutils/iter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Teleport
* Copyright (C) 2025 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 iterutils

import (
"fmt"
"slices"
"strings"
)

func ExampleMap() {
inputs := []string{
"hello world",
"foo",
}

for mapped := range Map(strings.ToUpper, slices.Values(inputs)) {
fmt.Println(mapped)
}
// Output:
// HELLO WORLD
// FOO
}
12 changes: 12 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import (
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/metadata"
mfa "github.com/gravitational/teleport/api/mfa"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
apievents "github.com/gravitational/teleport/api/types/events"
Expand Down Expand Up @@ -7561,6 +7562,17 @@ func (a *Server) validateMFAAuthResponseInternal(
loginData, err = webLogin.Finish(ctx, user, wantypes.CredentialAssertionResponseFromProto(res.Webauthn), requiredExtensions)
}
if err != nil {
if requiredExtensions.AllowReuse == mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES &&
trace.IsNotFound(err) {
// Do not add extra user messages to
// mfa.ErrExpiredReusableMFAResponse. Doing so will prevent
// client-side code from using errors.Is to reliably identify
// this specific error after it goes through gRPC. The original
// error isn't particularly useful to the user anyway, so just
// log it at debug level.
a.logger.DebugContext(ctx, "Reusable MFA response validation failed and possibly expired", "error", err)
return nil, trace.Wrap(&mfa.ErrExpiredReusableMFAResponse)
}
return nil, trace.AccessDenied("MFA response validation failed: %v", err)
}

Expand Down
8 changes: 7 additions & 1 deletion lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -3231,7 +3231,13 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC

var verifiedMFADeviceID string
if req.MFAResponse != nil {
requiredExt := &mfav1.ChallengeExtensions{Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION}
requiredExt := &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION,
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO,
}
if req.AllowsMFAReuse() {
requiredExt.AllowReuse = mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES
}
mfaData, err := a.authServer.ValidateMFAAuthResponse(ctx, req.GetMFAResponse(), req.Username, requiredExt)
if err != nil {
return nil, trace.Wrap(err)
Expand Down
9 changes: 9 additions & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,15 @@ func validateUserCertsRequest(actx *grpcContext, req *authpb.UserCertsRequest) e
return trace.BadParameter("unknown certificate Usage %q", req.Usage)
}

if req.RequesterName == authpb.UserCertsRequest_TSH_DB_EXEC {
if req.Usage != authpb.UserCertsRequest_Database {
return trace.BadParameter("requester %s can only request database certificates", req.RequesterName)
}
if req.MFAResponse != nil && req.Purpose != authpb.UserCertsRequest_CERT_PURPOSE_SINGLE_USE_CERTS {
return trace.BadParameter("requester %q can only request single use certificates", req.RequesterName)
}
}

if req.Purpose != authpb.UserCertsRequest_CERT_PURPOSE_SINGLE_USE_CERTS {
return nil
}
Expand Down
Loading
Loading