Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add V4 Roles #7118

Merged
merged 21 commits into from
Jun 10, 2021
Merged
Show file tree
Hide file tree
Changes from 12 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
4 changes: 4 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/gravitational/teleport/api/client/webclient"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/metadata"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/utils"
Expand Down Expand Up @@ -277,6 +278,9 @@ func (c *Client) dialGRPC(ctx context.Context, addr string) error {

dialOpts := append([]grpc.DialOption{}, c.c.DialOpts...)
dialOpts = append(dialOpts, grpc.WithContextDialer(c.grpcDialer()))
dialOpts = append(dialOpts,
grpc.WithUnaryInterceptor(metadata.UnaryClientInterceptor),
grpc.WithStreamInterceptor(metadata.StreamClientInterceptor))
// Only set transportCredentials if tlsConfig is set. This makes it possible
// to explicitly provide gprc.WithInsecure in the client's dial options.
if c.tlsConfig != nil {
Expand Down
5 changes: 5 additions & 0 deletions api/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,8 @@ const (
// requests a connection to the remote auth server.
RemoteAuthServer = "@remote-auth-server"
)

const (
// TODO(Joerger): change this to generated value
Version = "7.0.0-dev"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have version in version.go file. Is this in addition to that? Does this mean we'd need to bump version in 3 places now when doing releases (Makefile, version.go and here)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

teleport/api/ doesn't depend on teleport/ which is why I can't really use the value from version.go. It sounds like @Joerger is working on something so that api/ will have access to the generated client version, the suggestion was to copy this here for now and let him fill in the generated value when his changes come in. I'm not sure how quickly those changes are coming or if we should put in something to get the generated value for this PR @Joerger ? fyi I think we want this going out in the 6.2 train

Copy link
Contributor

@Joerger Joerger Jun 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the PR for the client version generation - #7157
If this PR is merged before that one, I'll update it to use the new generated version in #7157

)
86 changes: 86 additions & 0 deletions api/metadata/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
Copyright 2021 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package metadata

import (
"context"

"github.com/gravitational/teleport/api/constants"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

const (
VersionKey = "version"
)

// defaultMetadata returns the default metadata which will be added to all outgoing calls.
func defaultMetadata() map[string]string {
return map[string]string{
VersionKey: constants.Version,
}
}

// AddMetadataToContext returns a new context copied from ctx with the given
// raw metadata added. Metadata already set on the given context for any key
// will not be overridden, but new key/value pairs will always be added.
func AddMetadataToContext(ctx context.Context, raw map[string]string) context.Context {
md := metadata.New(raw)
if existingMd, ok := metadata.FromOutgoingContext(ctx); ok {
for key, vals := range existingMd {
md.Set(key, vals...)
}
}
return metadata.NewOutgoingContext(ctx, md)
}

// DisableInterceptors can be set on the client context with context.WithValue(ctx, DisableInterceptors{}, struct{}{})
// to stop the client interceptors from adding any metadata to the context (useful for testing).
type DisableInterceptors struct{}

// StreamClientInterceptor intercepts a GRPC client stream call and adds
// default metadata to the context.
func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
if disable := ctx.Value(DisableInterceptors{}); disable == nil {
ctx = AddMetadataToContext(ctx, defaultMetadata())
}
return streamer(ctx, desc, cc, method, opts...)
}

// UnaryClientInterceptor intercepts a GRPC client unary call and adds default
// metadata to the context.
func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
if disable := ctx.Value(DisableInterceptors{}); disable == nil {
ctx = AddMetadataToContext(ctx, defaultMetadata())
}
return invoker(ctx, method, req, reply, cc, opts...)
}

// ClientVersionFromContext can be called from a GRPC server method to return
// the client version that was added to the GRPC metadata by
// StreamClientInterceptor or UnaryClientInterceptor on the client.
func ClientVersionFromContext(ctx context.Context) (string, bool) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", false
}
versionList := md.Get(VersionKey)
if len(versionList) != 1 {
return "", false
}
return versionList[0], true
}
3 changes: 3 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ const (
// KindBilling represents access to cloud billing features
KindBilling = "billing"

// V4 is the fourth version of resources.
V4 = "v4"

// V3 is the third version of resources.
V3 = "v3"

Expand Down
38 changes: 23 additions & 15 deletions api/types/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,25 +521,33 @@ func (r *RoleV3) CheckAndSetDefaults() error {
if r.Spec.Allow.Namespaces == nil {
r.Spec.Allow.Namespaces = []string{defaults.Namespace}
}
if r.Spec.Allow.NodeLabels == nil {
if len(r.Spec.Allow.Logins) == 0 {
// no logins implies no node access
r.Spec.Allow.NodeLabels = Labels{}
} else {
r.Spec.Allow.NodeLabels = Labels{Wildcard: []string{Wildcard}}

switch r.Version {
case V3:
if r.Spec.Allow.NodeLabels == nil {
if len(r.Spec.Allow.Logins) == 0 {
// no logins implies no node access
r.Spec.Allow.NodeLabels = Labels{}
} else {
r.Spec.Allow.NodeLabels = Labels{Wildcard: []string{Wildcard}}
}
}
}

if r.Spec.Allow.AppLabels == nil {
r.Spec.Allow.AppLabels = Labels{Wildcard: []string{Wildcard}}
}
if r.Spec.Allow.AppLabels == nil {
r.Spec.Allow.AppLabels = Labels{Wildcard: []string{Wildcard}}
}

if r.Spec.Allow.KubernetesLabels == nil {
r.Spec.Allow.KubernetesLabels = Labels{Wildcard: []string{Wildcard}}
}
if r.Spec.Allow.KubernetesLabels == nil {
r.Spec.Allow.KubernetesLabels = Labels{Wildcard: []string{Wildcard}}
}

if r.Spec.Allow.DatabaseLabels == nil {
r.Spec.Allow.DatabaseLabels = Labels{Wildcard: []string{Wildcard}}
if r.Spec.Allow.DatabaseLabels == nil {
r.Spec.Allow.DatabaseLabels = Labels{Wildcard: []string{Wildcard}}
}
case V4:
// Labels default to nil/empty for v4 roles
default:
return trace.BadParameter("unrecognized role version: %v", r.Version)
}

if r.Spec.Deny.Namespaces == nil {
Expand Down
37 changes: 35 additions & 2 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/metadata"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/auth/u2f"
Expand All @@ -35,6 +36,7 @@ import (
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/utils"

"github.com/coreos/go-semver/semver"
"github.com/golang/protobuf/ptypes/empty"
"github.com/gravitational/trace"
"github.com/gravitational/trace/trail"
Expand Down Expand Up @@ -293,7 +295,7 @@ func (g *GRPCServer) WatchEvents(watch *proto.Watch, stream proto.AuthService_Wa
case <-watcher.Done():
return trail.ToGRPC(watcher.Error())
case event := <-watcher.Events():
out, err := eventToGRPC(event)
out, err := eventToGRPC(stream.Context(), event)
if err != nil {
return trail.ToGRPC(err)
}
Expand All @@ -305,7 +307,7 @@ func (g *GRPCServer) WatchEvents(watch *proto.Watch, stream proto.AuthService_Wa
}

// eventToGRPC converts a types.Event to an proto.Event
func eventToGRPC(in types.Event) (*proto.Event, error) {
func eventToGRPC(ctx context.Context, in types.Event) (*proto.Event, error) {
eventType, err := eventTypeToGRPC(in.Type)
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -346,6 +348,9 @@ func eventToGRPC(in types.Event) (*proto.Event, error) {
User: r,
}
case *types.RoleV3:
if err = downgradeRole(ctx, r); err != nil {
return nil, trace.Wrap(err)
}
out.Resource = &proto.Event_Role{
Role: r,
}
Expand Down Expand Up @@ -1368,6 +1373,28 @@ func (g *GRPCServer) DeleteAllKubeServices(ctx context.Context, req *proto.Delet
return &empty.Empty{}, nil
}

// downgradeRole tests the client version passed through the GRPC metadata, and
// downgrades the given role to V3 in-place if V4 roles are not known to be
// supported (client version is unknown or < 6.3).
func downgradeRole(ctx context.Context, role *types.RoleV3) error {
var clientVersion *semver.Version
clientVersionString, ok := metadata.ClientVersionFromContext(ctx)
if ok {
var err error
clientVersion, err = semver.NewVersion(clientVersionString)
if err != nil {
return trace.BadParameter("unrecognized client version: %s is not a valid semver", clientVersionString)
}
}

minSupportedVersionForV4Roles := semver.New("6.3.0-aa") // "aa" is included so that this compares before v6.3.0-alpha
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will need to update this to the first version which will support V4 roles before merging

if clientVersion == nil || clientVersion.LessThan(*minSupportedVersionForV4Roles) {
log.Debugf(`Client version "%s" is unknown or less than 6.3.0, converting role to v3`, clientVersionString)
return trace.Wrap(services.DowngradeRoleToV3(role))
}
return nil
}

// GetRole retrieves a role by name.
func (g *GRPCServer) GetRole(ctx context.Context, req *proto.GetRoleRequest) (*types.RoleV3, error) {
auth, err := g.authenticate(ctx)
Expand All @@ -1382,6 +1409,9 @@ func (g *GRPCServer) GetRole(ctx context.Context, req *proto.GetRoleRequest) (*t
if !ok {
return nil, trail.ToGRPC(trace.Errorf("encountered unexpected role type"))
}
if err = downgradeRole(ctx, roleV3); err != nil {
return nil, trail.ToGRPC(err)
}
return roleV3, nil
}

Expand All @@ -1401,6 +1431,9 @@ func (g *GRPCServer) GetRoles(ctx context.Context, _ *empty.Empty) (*proto.GetRo
if !ok {
return nil, trail.ToGRPC(trace.BadParameter("unexpected type %T", r))
}
if err = downgradeRole(ctx, role); err != nil {
return nil, trail.ToGRPC(err)
}
rolesV3 = append(rolesV3, role)
}
return &proto.GetRolesResponse{
Expand Down
112 changes: 112 additions & 0 deletions lib/auth/grpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/metadata"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/mocku2f"
Expand Down Expand Up @@ -1042,6 +1043,117 @@ func testOriginDynamicStored(t *testing.T, setWithOrigin func(*Client, string) e
}
}

// TestRoleVersions tests that downgraded V3 roles are returned to older
// clients, and V4 roles are returned to newer clients.
func TestRoleVersions(t *testing.T) {
srv := newTestTLSServer(t)

role := &types.RoleV3{
Kind: types.KindRole,
Version: types.V4,
Metadata: types.Metadata{
Name: "test_role",
},
Spec: types.RoleSpecV3{
Allow: types.RoleConditions{
Rules: []types.Rule{
types.NewRule(types.KindRole, services.RO()),
types.NewRule(types.KindEvent, services.RW()),
},
},
},
}
user, err := CreateUser(srv.Auth(), "test_user", role)
require.NoError(t, err)

client, err := srv.NewClient(TestUser(user.GetName()))
require.NoError(t, err)

testCases := []struct {
desc string
clientVersion string
disableMetadata bool
expectedRoleVersion string
assertErr require.ErrorAssertionFunc
}{
{
desc: "old",
clientVersion: "6.2.1",
expectedRoleVersion: "v3",
assertErr: require.NoError,
},
{
desc: "new",
clientVersion: "6.3.0",
expectedRoleVersion: "v4",
assertErr: require.NoError,
},
{
desc: "alpha",
clientVersion: "6.3.0-alpha.0",
expectedRoleVersion: "v4",
assertErr: require.NoError,
},
{
desc: "greater than 10",
clientVersion: "10.0.0-beta",
expectedRoleVersion: "v4",
assertErr: require.NoError,
},
{
desc: "empty version",
clientVersion: "",
assertErr: require.Error,
},
{
desc: "invalid version",
clientVersion: "foo",
assertErr: require.Error,
},
{
desc: "no version metadata",
disableMetadata: true,
expectedRoleVersion: "v3",
assertErr: require.NoError,
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
// setup client metadata
ctx := context.Background()
if tc.disableMetadata {
ctx = context.WithValue(ctx, metadata.DisableInterceptors{}, struct{}{})
} else {
ctx = metadata.AddMetadataToContext(ctx, map[string]string{
metadata.VersionKey: tc.clientVersion,
})
}

// test GetRole
gotRole, err := client.GetRole(ctx, role.GetName())
tc.assertErr(t, err)
if err == nil {
require.Equal(t, tc.expectedRoleVersion, gotRole.GetVersion())
}

// test GetRoles
gotRoles, err := client.GetRoles(ctx)
tc.assertErr(t, err)
if err == nil {
foundTestRole := false
for _, gotRole := range gotRoles {
if gotRole.GetName() == role.GetName() {
require.Equal(t, tc.expectedRoleVersion, gotRole.GetVersion())
foundTestRole = true
}
}
require.True(t, foundTestRole)
}
})
}
}

func TestAuthPreferenceOriginDynamic(t *testing.T) {
t.Parallel()

Expand Down
Loading