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
32 changes: 26 additions & 6 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,9 @@ func (c *Client) GetCurrentUserRoles(ctx context.Context) ([]types.Role, error)
if err != nil {
return nil, trail.FromGRPC(err)
}
// An old server would send RequireSessionMFA instead of RequireMFAType
// DELETE IN 13.0.0
role.CheckSetRequireSessionMFA()
roles = append(roles, role)
}
return roles, nil
Expand Down Expand Up @@ -1595,11 +1598,14 @@ func (c *Client) GetRole(ctx context.Context, name string) (types.Role, error) {
if name == "" {
return nil, trace.BadParameter("missing name")
}
resp, err := c.grpc.GetRole(ctx, &proto.GetRoleRequest{Name: name}, c.callOpts...)
role, err := c.grpc.GetRole(ctx, &proto.GetRoleRequest{Name: name}, c.callOpts...)
if err != nil {
return nil, trail.FromGRPC(err)
}
return resp, nil
// An old server would send RequireSessionMFA instead of RequireMFAType
// DELETE IN 13.0.0
role.CheckSetRequireSessionMFA()
return role, nil
}

// GetRoles returns a list of roles
Expand All @@ -1610,18 +1616,26 @@ func (c *Client) GetRoles(ctx context.Context) ([]types.Role, error) {
}
roles := make([]types.Role, 0, len(resp.GetRoles()))
for _, role := range resp.GetRoles() {
// An old server would send RequireSessionMFA instead of RequireMFAType
// DELETE IN 13.0.0
role.CheckSetRequireSessionMFA()
roles = append(roles, role)
}
return roles, nil
}

// UpsertRole creates or updates role
func (c *Client) UpsertRole(ctx context.Context, role types.Role) error {
roleV4, ok := role.(*types.RoleV5)
r, ok := role.(*types.RoleV5)
if !ok {
return trace.BadParameter("invalid type %T", role)
}
_, err := c.grpc.UpsertRole(ctx, roleV4, c.callOpts...)

// An old server would expect RequireSessionMFA instead of RequireMFAType
// DELETE IN 13.0.0
r.CheckSetRequireSessionMFA()

_, err := c.grpc.UpsertRole(ctx, r, c.callOpts...)
return trail.FromGRPC(err)
}

Expand Down Expand Up @@ -2241,11 +2255,14 @@ func (c *Client) ResetSessionRecordingConfig(ctx context.Context) error {

// GetAuthPreference gets cluster auth preference.
func (c *Client) GetAuthPreference(ctx context.Context) (types.AuthPreference, error) {
resp, err := c.grpc.GetAuthPreference(ctx, &empty.Empty{}, c.callOpts...)
pref, err := c.grpc.GetAuthPreference(ctx, &empty.Empty{}, c.callOpts...)
if err != nil {
return nil, trail.FromGRPC(err)
}
return resp, nil
// An old server would send RequireSessionMFA instead of RequireMFAType
// DELETE IN 13.0.0
pref.CheckSetRequireSessionMFA()
return pref, nil
}

// SetAuthPreference sets cluster auth preference.
Expand All @@ -2254,6 +2271,9 @@ func (c *Client) SetAuthPreference(ctx context.Context, authPref types.AuthPrefe
if !ok {
return trace.BadParameter("invalid type %T", authPref)
}
// An old server would send RequireSessionMFA instead of RequireMFAType
// DELETE IN 13.0.0
authPrefV2.CheckSetRequireSessionMFA()
_, err := c.grpc.SetAuthPreference(ctx, authPrefV2, c.callOpts...)
return trail.FromGRPC(err)
}
Expand Down
200 changes: 200 additions & 0 deletions api/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -778,3 +778,203 @@ func TestAccessRequestDowngrade(t *testing.T) {
m.grpc.Stop()
require.NoError(t, <-remoteErr)
}

type mockRoleServer struct {
*mockServer
roles map[string]*types.RoleV5
}

func newMockRoleServer() *mockRoleServer {
m := &mockRoleServer{
&mockServer{
grpc: grpc.NewServer(),
UnimplementedAuthServiceServer: &proto.UnimplementedAuthServiceServer{},
},
make(map[string]*types.RoleV5),
}
proto.RegisterAuthServiceServer(m.grpc, m)
return m
}

func startMockRoleServer(t *testing.T) string {
l, err := net.Listen("tcp", "")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, l.Close()) })
go newMockRoleServer().grpc.Serve(l)
return l.Addr().String()
}

func (m *mockRoleServer) GetRole(ctx context.Context, req *proto.GetRoleRequest) (*types.RoleV5, error) {
conn, ok := m.roles[req.Name]
if !ok {
return nil, trace.NotFound("not found")
}
return conn, nil
}

func (m *mockRoleServer) GetRoles(ctx context.Context, _ *empty.Empty) (*proto.GetRolesResponse, error) {
var connectors []*types.RoleV5
for _, conn := range m.roles {
connectors = append(connectors, conn)
}
return &proto.GetRolesResponse{
Roles: connectors,
}, nil
}

func (m *mockRoleServer) UpsertRole(ctx context.Context, role *types.RoleV5) (*empty.Empty, error) {
m.roles[role.Metadata.Name] = role
return &empty.Empty{}, nil
}

func (m *mockRoleServer) GetCurrentUserRoles(_ *empty.Empty, stream proto.AuthService_GetCurrentUserRolesServer) error {
for _, role := range m.roles {
if err := stream.Send(role); err != nil {
return trace.Wrap(err)
}
}

return nil
}

// Test that client will perform properly with an old server
// DELETE IN 13.0.0
func TestSetRoleRequireSessionMFABackwardsCompatibility(t *testing.T) {
ctx := context.Background()
addr := startMockRoleServer(t)

// Create client
clt, err := New(ctx, Config{
Addrs: []string{addr},
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
})
require.NoError(t, err)

role := &types.RoleV5{
Metadata: types.Metadata{
Name: "one",
},
}

t.Run("UpsertRole", func(t *testing.T) {
// UpsertRole should set "RequireSessionMFA" on the provided role if "RequireMFAType" is set
role.Spec.Options.RequireMFAType = types.RequireMFAType_SESSION
role.Spec.Options.RequireSessionMFA = false
err = clt.UpsertRole(ctx, role)
require.NoError(t, err)
require.True(t, role.GetOptions().RequireSessionMFA)
})

t.Run("GetRole", func(t *testing.T) {
// GetRole should set "RequireMFAType" on the received role if empty
role.Spec.Options.RequireMFAType = 0
role.Spec.Options.RequireSessionMFA = true
roleResp, err := clt.GetRole(ctx, role.GetName())
require.NoError(t, err)
require.Equal(t, types.RequireMFAType_SESSION, roleResp.GetOptions().RequireMFAType)
})

t.Run("GetRoles", func(t *testing.T) {
// GetRoles should set "RequireMFAType" on the received roles if empty
role.Spec.Options.RequireMFAType = 0
role.Spec.Options.RequireSessionMFA = true
rolesResp, err := clt.GetRoles(ctx)
require.NoError(t, err)
require.Len(t, rolesResp, 1)
require.Equal(t, types.RequireMFAType_SESSION, rolesResp[0].GetOptions().RequireMFAType)
})

t.Run("GetCurrentUserRoles", func(t *testing.T) {
// GetCurrentUserRoles should set "RequireMFAType" on the received roles if empty
role.Spec.Options.RequireMFAType = 0
role.Spec.Options.RequireSessionMFA = true
rolesResp, err := clt.GetCurrentUserRoles(ctx)
require.NoError(t, err)
require.Len(t, rolesResp, 1)
require.Equal(t, types.RequireMFAType_SESSION, rolesResp[0].GetOptions().RequireMFAType)
})
}

type mockAuthPreferenceServer struct {
*mockServer
pref *types.AuthPreferenceV2
}

func newMockAuthPreferenceServer() *mockAuthPreferenceServer {
m := &mockAuthPreferenceServer{
mockServer: &mockServer{
grpc: grpc.NewServer(),
UnimplementedAuthServiceServer: &proto.UnimplementedAuthServiceServer{},
},
}
proto.RegisterAuthServiceServer(m.grpc, m)
return m
}

func startMockAuthPreferenceServer(t *testing.T) string {
l, err := net.Listen("tcp", "")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, l.Close()) })
go newMockAuthPreferenceServer().grpc.Serve(l)
return l.Addr().String()
}

func (m *mockAuthPreferenceServer) GetAuthPreference(ctx context.Context, _ *empty.Empty) (*types.AuthPreferenceV2, error) {
if m.pref == nil {
return nil, trace.NotFound("not found")
}
return m.pref, nil
}

func (m *mockAuthPreferenceServer) SetAuthPreference(ctx context.Context, pref *types.AuthPreferenceV2) (*empty.Empty, error) {
m.pref = pref
return &empty.Empty{}, nil
}

// Test that client will perform properly with an old server
// DELETE IN 13.0.0
func TestSetAuthPreferenceRequireSessionMFABackwardsCompatibility(t *testing.T) {
ctx := context.Background()
addr := startMockAuthPreferenceServer(t)

// Create client
clt, err := New(ctx, Config{
Addrs: []string{addr},
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
})
require.NoError(t, err)

pref := &types.AuthPreferenceV2{
Metadata: types.Metadata{
Name: "one",
},
}

t.Run("SetAuthPreference", func(t *testing.T) {
// SetAuthPreference should set "RequireSessionMFA" on the provided auth pref if "RequireMFAType" is set
pref.Spec.RequireMFAType = types.RequireMFAType_SESSION
pref.Spec.RequireSessionMFA = false
err = clt.SetAuthPreference(ctx, pref)
require.NoError(t, err)
require.True(t, pref.Spec.RequireSessionMFA)
})

t.Run("GetAuthPreference", func(t *testing.T) {
// GetAuthPreference should set "RequireMFAType" on the received auth pref if empty
pref.Spec.RequireMFAType = 0
pref.Spec.RequireSessionMFA = true
prefResp, err := clt.GetAuthPreference(ctx)
require.NoError(t, err)
require.Equal(t, types.RequireMFAType_SESSION, prefResp.GetRequireMFAType())
})
}
31 changes: 29 additions & 2 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1330,7 +1330,9 @@ message AuthPreferenceSpecV2 {

// RequireSessionMFA causes all sessions in this cluster to require MFA
// checks.
bool RequireSessionMFA = 5 [(gogoproto.jsontag) = "require_session_mfa,omitempty"];
//
// DELETE IN 13.0.0 in favor of RequireMFAType
bool RequireSessionMFA = 5 [(gogoproto.jsontag) = "-"];

// DisconnectExpiredCert provides disconnect expired certificate setting -
// if true, connections with expired client certificates will get disconnected
Expand Down Expand Up @@ -1367,6 +1369,9 @@ message AuthPreferenceSpecV2 {
(gogoproto.jsontag) = "allow_passwordless,omitempty",
(gogoproto.customtype) = "BoolOption"
];

// RequireMFAType is the type of MFA requirement enforced for this cluster.
RequireMFAType RequireMFAType = 12 [(gogoproto.jsontag) = "require_session_mfa,omitempty"];
}

// U2F defines settings for U2F device.
Expand Down Expand Up @@ -1961,7 +1966,9 @@ message RoleOptions {

// RequireSessionMFA specifies whether a user is required to do an MFA
// check for every session.
bool RequireSessionMFA = 13 [(gogoproto.jsontag) = "require_session_mfa,omitempty"];
//
// DELETE IN 13.0.0 in favor of RequireMFAType
bool RequireSessionMFA = 13 [(gogoproto.jsontag) = "-"];

// Lock specifies the locking mode (strict|best_effort) to be applied with
// the role.
Expand Down Expand Up @@ -2019,6 +2026,9 @@ message RoleOptions {
(gogoproto.jsontag) = "ssh_file_copy",
(gogoproto.customtype) = "BoolOption"
];

// RequireMFAType is the type of MFA requirement enforced for this user.
RequireMFAType RequireMFAType = 23 [(gogoproto.jsontag) = "require_session_mfa,omitempty"];
}

message RecordSession {
Expand Down Expand Up @@ -4235,3 +4245,20 @@ message GetClusterAlertsRequest {
// Labels is an optional label selector.
map<string, string> Labels = 3;
}

// RequireMFAType is a type of MFA requirement enforced outside of login,
// such as per-session MFA or per-request PIV touch.
enum RequireMFAType {
// OFF means additional MFA enforcement is not enabled.
OFF = 0;
// SESSION means MFA is required to begin server sessions.
SESSION = 1;
// SESSION_AND_HARDWARE_KEY means MFA is required to begin server sessions,
// and login sessions must use a private key backed by a hardware key.
SESSION_AND_HARDWARE_KEY = 2;
// HARDWARE_KEY_TOUCH means login sessions must use a hardware private key that
// requires touch to be used. This touch requirement applies to all API requests
// rather than only session requests. This touch is different from MFA, so to prevent
// requiring double touch on session requests, normal Session MFA is disabled.
HARDWARE_KEY_TOUCH = 3;
}
Loading