Skip to content

Commit

Permalink
add v4 roles
Browse files Browse the repository at this point in the history
  • Loading branch information
nklaassen committed Jun 1, 2021
1 parent 722558d commit 77c704a
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 30 deletions.
1 change: 1 addition & 0 deletions api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/gogo/protobuf v1.3.1
github.com/golang/protobuf v1.4.2
github.com/google/go-cmp v0.5.4
github.com/google/uuid v1.2.0
github.com/gravitational/trace v1.1.15
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
Expand Down
2 changes: 2 additions & 0 deletions api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gravitational/trace v1.1.15 h1:dfaFcARt110nCX6RSvrcRUbvRawEYAasXyCqnhXo0Xg=
github.com/gravitational/trace v1.1.15/go.mod h1:RvdOUHE4SHqR3oXlFFKnGzms8a5dugHygGw1bqDstYI=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
Expand Down
18 changes: 15 additions & 3 deletions api/metadata/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,39 @@ import (

const versionKey = "version"

var versionValue = constants.Version
var (
versionValue = constants.Version
disabled = false
)

// SetVersion is used only for tests to set the version value that the client
// will send in the GRPC metadata.
func SetVersion(version string) {
versionValue = version
}

// DisabledIs disables (or enables) including metadata in GRPC requests. Used in tests.
func DisabledIs(metadataDisabled bool) {
disabled = metadataDisabled
}

func addMetadataToContext(ctx context.Context) context.Context {
return metadata.AppendToOutgoingContext(ctx, versionKey, versionValue)
}

// StreamClientInterceptor intercepts a GRPC client stream call and adds 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) {
ctx = addMetadataToContext(ctx)
if !disabled {
ctx = addMetadataToContext(ctx)
}
return streamer(ctx, desc, cc, method, opts...)
}

// UnaryClientInterceptor intercepts a GRPC client unary call and adds metadata to the context
func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx = addMetadataToContext(ctx)
if !disabled {
ctx = addMetadataToContext(ctx)
}
return invoker(ctx, method, req, reply, cc, opts...)
}

Expand Down
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
70 changes: 55 additions & 15 deletions api/types/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/gravitational/teleport/api/utils"

"github.com/gogo/protobuf/proto"
"github.com/google/uuid"
"github.com/gravitational/trace"
)

Expand Down Expand Up @@ -518,25 +519,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 Expand Up @@ -612,6 +621,37 @@ func (r *RoleV3) CheckAndSetDefaults() error {
return nil
}

// DowngradeToV3 converts a V4 role to V3 so that it will be compatible with older instances.
// DELETE IN 8.0.0
func (r *RoleV3) DowngradeToV3() error {
switch r.Version {
case V3:
case V4:
r.Version = V3

// V3 roles will set the default labels to wildcard allow if they are
// empty. To prevent this for roles which are created as V4 and
// downgraded, set a placeholder label
const labelKey = "__teleport_no_labels"
labelVal := uuid.NewString()
if r.Spec.Allow.NodeLabels == nil || len(r.Spec.Allow.NodeLabels) == 0 {
r.Spec.Allow.NodeLabels = Labels{labelKey: []string{labelVal}}
}
if r.Spec.Allow.AppLabels == nil || len(r.Spec.Allow.AppLabels) == 0 {
r.Spec.Allow.AppLabels = Labels{labelKey: []string{labelVal}}
}
if r.Spec.Allow.KubernetesLabels == nil || len(r.Spec.Allow.KubernetesLabels) == 0 {
r.Spec.Allow.KubernetesLabels = Labels{labelKey: []string{labelVal}}
}
if r.Spec.Allow.DatabaseLabels == nil || len(r.Spec.Allow.DatabaseLabels) == 0 {
r.Spec.Allow.DatabaseLabels = Labels{labelKey: []string{labelVal}}
}
default:
return trace.BadParameter("unrecognized role version %T", r.Version)
}
return nil
}

// String returns the human readable representation of a role.
func (r *RoleV3) String() string {
return fmt.Sprintf("Role(Name=%v,Options=%v,Allow=%+v,Deny=%+v)",
Expand Down
36 changes: 34 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 @@ -36,6 +37,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 @@ -294,7 +296,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 @@ -306,7 +308,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 @@ -347,6 +349,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 @@ -1374,6 +1379,27 @@ 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 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.VersionFromContext(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
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(role.DowngradeToV3())
}
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 @@ -1388,6 +1414,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 @@ -1407,6 +1436,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
87 changes: 87 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 @@ -1018,3 +1019,89 @@ func TestDeleteLastMFADevice(t *testing.T) {
checkErr: require.Error,
})
}

// TestRoleVersions tests that downgraded V3 roles are returned to older
// clients, and V4 roles are returned to newer clients.
func TestRoleVersions(t *testing.T) {
ctx := context.Background()
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
metadataDisabled bool
expectedRoleVersion string
}{
{
desc: "old",
clientVersion: "6.2.1",
expectedRoleVersion: "v3",
},
{
desc: "new",
clientVersion: "6.3.0",
expectedRoleVersion: "v4",
},
{
desc: "alpha",
clientVersion: "6.3.0-alpha.0",
expectedRoleVersion: "v4",
},
{
desc: "no metadata",
metadataDisabled: true,
expectedRoleVersion: "v3",
},
{
desc: "greater than 10",
clientVersion: "10.0.0-beta",
expectedRoleVersion: "v4",
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
// setup client metadata
metadata.SetVersion(tc.clientVersion)
metadata.DisabledIs(tc.metadataDisabled)

// test GetRole
gotRole, err := client.GetRole(ctx, role.GetName())
require.NoError(t, err)
require.Equal(t, tc.expectedRoleVersion, gotRole.GetVersion())

// test GetRoles
gotRoles, err := client.GetRoles(ctx)
require.NoError(t, err)
foundTestRole := false
for _, gotRole := range gotRoles {
if gotRole.GetName() == role.GetName() {
require.Equal(t, tc.expectedRoleVersion, gotRole.GetVersion())
foundTestRole = true
}
}
require.True(t, foundTestRole)
})
}
}
12 changes: 8 additions & 4 deletions lib/services/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func NewAdminRole() Role {
func NewImplicitRole() Role {
return &RoleV3{
Kind: KindRole,
Version: V3,
Version: types.V4,
Metadata: Metadata{
Name: teleport.DefaultImplicitRole,
Namespace: defaults.Namespace,
Expand All @@ -172,7 +172,7 @@ func NewImplicitRole() Role {
func RoleForUser(u User) Role {
return &RoleV3{
Kind: KindRole,
Version: V3,
Version: types.V4,
Metadata: Metadata{
Name: RoleNameForUser(u.GetName()),
Namespace: defaults.Namespace,
Expand Down Expand Up @@ -2334,7 +2334,10 @@ func UnmarshalRole(bytes []byte, opts ...MarshalOption) (Role, error) {
}

switch h.Version {
case V3:
case types.V4:
// V4 roles are identical to V3 except for their defaults
fallthrough
case types.V3:
var role RoleV3
if cfg.SkipValidation {
if err := utils.FastUnmarshal(bytes, &role); err != nil {
Expand Down Expand Up @@ -2371,7 +2374,8 @@ func MarshalRole(role Role, opts ...MarshalOption) ([]byte, error) {

switch role := role.(type) {
case *RoleV3:
if version := role.GetVersion(); version != V3 {
// V4 role version is compatible with V3
if version := role.GetVersion(); version != types.V3 && version != types.V4 {
return nil, trace.BadParameter("mismatched role version %v and type %T", version, role)
}
if !cfg.PreserveResourceID {
Expand Down
Loading

0 comments on commit 77c704a

Please sign in to comment.