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

resource: enforce lowercase resource names #19218

Merged
merged 2 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion agent/grpc-external/services/resource/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"time"

"github.com/oklog/ulid/v2"
Expand Down Expand Up @@ -175,5 +176,5 @@ func (s *Server) validateDeleteRequest(req *pbresource.DeleteRequest) (*resource
// name by embedding the resources's Uid in the name.
func tombstoneName(deleteId *pbresource.ID) string {
// deleteId.Name is just included for easier identification
return fmt.Sprintf("tombstone-%v-%v", deleteId.Name, deleteId.Uid)
return fmt.Sprintf("tombstone-%v-%v", deleteId.Name, strings.ToLower(deleteId.Uid))
}
94 changes: 77 additions & 17 deletions agent/grpc-external/services/resource/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package resource

import (
"context"
"strings"
"testing"

"github.com/stretchr/testify/mock"
Expand All @@ -22,39 +23,98 @@ import (
func TestDelete_InputValidation(t *testing.T) {
server := testServer(t)
client := testClient(t, server)

demo.RegisterTypes(server.Registry)

testCases := map[string]func(artistId, recordLabelId *pbresource.ID) *pbresource.ID{
"no id": func(artistId, recordLabelId *pbresource.ID) *pbresource.ID {
return nil
type testCase struct {
modFn func(artistId, recordLabelId *pbresource.ID) *pbresource.ID
errContains string
}

testCases := map[string]testCase{
"no id": {
modFn: func(_, _ *pbresource.ID) *pbresource.ID {
return nil
},
errContains: "id is required",
},
"no type": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Type = nil
return artistId
},
errContains: "id.type is required",
},
"no name": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = ""
return artistId
},
errContains: "id.name invalid",
},
"mixed case name": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = "DepecheMode"
return artistId
},
errContains: "id.name invalid",
},
"name too long": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = strings.Repeat("n", resource.MaxNameLength+1)
return artistId
},
errContains: "id.name invalid",
},
"partition mixed case": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy.Partition = "Default"
return artistId
},
errContains: "id.tenancy.partition invalid",
},
"no type": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Type = nil
return artistId
"partition name too long": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy.Partition = strings.Repeat("p", resource.MaxNameLength+1)
return artistId
},
errContains: "id.tenancy.partition invalid",
},
"no name": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = ""
return artistId
"namespace mixed case": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy.Namespace = "Default"
return artistId
},
errContains: "id.tenancy.namespace invalid",
},
"partition scoped resource with namespace": func(_, recordLabelId *pbresource.ID) *pbresource.ID {
recordLabelId.Tenancy.Namespace = "ishouldnothaveanamespace"
return recordLabelId
"namespace name too long": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy.Namespace = strings.Repeat("n", resource.MaxNameLength+1)
return artistId
},
errContains: "id.tenancy.namespace invalid",
},
"partition scoped resource with namespace": {
modFn: func(_, recordLabelId *pbresource.ID) *pbresource.ID {
recordLabelId.Tenancy.Namespace = "ishouldnothaveanamespace"
return recordLabelId
},
errContains: "cannot have a namespace",
},
}
for desc, modFn := range testCases {
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes")
require.NoError(t, err)

artist, err := demo.GenerateV2Artist()
require.NoError(t, err)

req := &pbresource.DeleteRequest{Id: modFn(artist.Id, recordLabel.Id), Version: ""}
req := &pbresource.DeleteRequest{Id: tc.modFn(artist.Id, recordLabel.Id), Version: ""}

_, err = client.Delete(testContext(t), req)
require.Error(t, err)
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String())
require.ErrorContains(t, err, tc.errContains)
})
}
}
Expand Down Expand Up @@ -129,7 +189,7 @@ func TestDelete_Success(t *testing.T) {
server, client, ctx := testDeps(t)
demo.RegisterTypes(server.Registry)

recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes")
require.NoError(t, err)
writeRsp, err := client.Write(ctx, &pbresource.WriteRequest{Resource: recordLabel})
require.NoError(t, err)
Expand Down
5 changes: 3 additions & 2 deletions agent/grpc-external/services/resource/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ func (s *Server) validateListRequest(req *pbresource.ListRequest) (*resource.Reg
return nil, err
}

// Lowercase
resource.Normalize(req.Tenancy)
if err := validateWildcardTenancy(req.Tenancy, req.NamePrefix); err != nil {
return nil, err
}

// Error when partition scoped and namespace not empty.
if reg.Scope == resource.ScopePartition && req.Tenancy.Namespace != "" {
Expand Down
3 changes: 0 additions & 3 deletions agent/grpc-external/services/resource/list_by_owner.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,6 @@ func (s *Server) validateListByOwnerRequest(req *pbresource.ListByOwnerRequest)
return nil, err
}

// Lowercase
resource.Normalize(req.Owner.Tenancy)

// Error when partition scoped and namespace not empty.
if reg.Scope == resource.ScopePartition && req.Owner.Tenancy.Namespace != "" {
return nil, status.Errorf(
Expand Down
158 changes: 118 additions & 40 deletions agent/grpc-external/services/resource/list_by_owner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ package resource
import (
"context"
"fmt"
"strings"
"testing"

"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/demo"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/proto/private/prototest"
"github.com/oklog/ulid/v2"

"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
Expand All @@ -26,41 +28,104 @@ func TestListByOwner_InputValidation(t *testing.T) {
client := testClient(t, server)
demo.RegisterTypes(server.Registry)

testCases := map[string]func(artistId, recordlabelId *pbresource.ID) *pbresource.ID{
"no owner": func(artistId, recordLabelId *pbresource.ID) *pbresource.ID {
return nil
type testCase struct {
modFn func(artistId, recordlabelId *pbresource.ID) *pbresource.ID
errContains string
}
testCases := map[string]testCase{
"no owner": {
modFn: func(artistId, recordLabelId *pbresource.ID) *pbresource.ID {
return nil
},
errContains: "owner is required",
},
"no type": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Type = nil
return artistId
},
errContains: "owner.type is required",
},
"no name": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = ""
return artistId
},
errContains: "owner.name invalid",
},
"name mixed case": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = "U2"
return artistId
},
errContains: "owner.name invalid",
},
"no type": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Type = nil
return artistId
"name too long": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = strings.Repeat("n", resource.MaxNameLength+1)
return artistId
},
errContains: "owner.name invalid",
},
"no name": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = ""
return artistId
"partition mixed case": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy.Partition = "Default"
return artistId
},
errContains: "owner.tenancy.partition invalid",
},
"no uid": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Uid = ""
return artistId
"partition too long": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy.Partition = strings.Repeat("p", resource.MaxNameLength+1)
return artistId
},
errContains: "owner.tenancy.partition invalid",
},
"partition scope with non-empty namespace": func(_, recordLabelId *pbresource.ID) *pbresource.ID {
recordLabelId.Tenancy.Namespace = "ishouldnothaveanamespace"
return recordLabelId
"namespace mixed case": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy.Namespace = "Default"
return artistId
},
errContains: "owner.tenancy.namespace invalid",
},
"namespace too long": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy.Namespace = strings.Repeat("n", resource.MaxNameLength+1)
return artistId
},
errContains: "owner.tenancy.namespace invalid",
},
"no uid": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Uid = ""
return artistId
},
errContains: "owner uid is required",
},
"partition scope with non-empty namespace": {
modFn: func(_, recordLabelId *pbresource.ID) *pbresource.ID {
recordLabelId.Uid = ulid.Make().String()
recordLabelId.Tenancy.Namespace = "ishouldnothaveanamespace"
return recordLabelId
},
errContains: "cannot have a namespace",
},
}
for desc, modFn := range testCases {
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
artist, err := demo.GenerateV2Artist()
require.NoError(t, err)

recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes")
require.NoError(t, err)

// Each test case picks which resource to use based on the resource type's scope.
req := &pbresource.ListByOwnerRequest{Owner: modFn(artist.Id, recordLabel.Id)}
req := &pbresource.ListByOwnerRequest{Owner: tc.modFn(artist.Id, recordLabel.Id)}

_, err = client.ListByOwner(testContext(t), req)
require.Error(t, err)
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String())
require.ErrorContains(t, err, tc.errContains)
})
}
}
Expand Down Expand Up @@ -131,33 +196,46 @@ func TestListByOwner_Many(t *testing.T) {
}

func TestListByOwner_OwnerTenancyDoesNotExist(t *testing.T) {
tenancyCases := map[string]func(artistId, recordlabelId *pbresource.ID) *pbresource.ID{
"partition not found when namespace scoped": func(artistId, _ *pbresource.ID) *pbresource.ID {
id := clone(artistId)
id.Uid = "doesnotmatter"
id.Tenancy.Partition = "boguspartition"
return id
type testCase struct {
modFn func(artistId, recordlabelId *pbresource.ID) *pbresource.ID
errContains string
}
tenancyCases := map[string]testCase{
"partition not found when namespace scoped": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
id := clone(artistId)
id.Uid = "doesnotmatter"
id.Tenancy.Partition = "boguspartition"
return id
},
errContains: "partition not found",
},
"namespace not found when namespace scoped": func(artistId, _ *pbresource.ID) *pbresource.ID {
id := clone(artistId)
id.Uid = "doesnotmatter"
id.Tenancy.Namespace = "bogusnamespace"
return id
"namespace not found when namespace scoped": {
modFn: func(artistId, _ *pbresource.ID) *pbresource.ID {
id := clone(artistId)
id.Uid = "doesnotmatter"
id.Tenancy.Namespace = "bogusnamespace"
return id
},
errContains: "namespace not found",
},
"partition not found when partition scoped": func(_, recordLabelId *pbresource.ID) *pbresource.ID {
id := clone(recordLabelId)
id.Uid = "doesnotmatter"
id.Tenancy.Partition = "boguspartition"
return id
"partition not found when partition scoped": {
modFn: func(_, recordLabelId *pbresource.ID) *pbresource.ID {
id := clone(recordLabelId)
id.Uid = "doesnotmatter"
id.Tenancy.Partition = "boguspartition"
return id
},
errContains: "partition not found",
},
}
for desc, modFn := range tenancyCases {
for desc, tc := range tenancyCases {
t.Run(desc, func(t *testing.T) {
server := testServer(t)
demo.RegisterTypes(server.Registry)
client := testClient(t, server)

recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes")
require.NoError(t, err)
recordLabel, err = server.Backend.WriteCAS(testContext(t), recordLabel)
require.NoError(t, err)
Expand All @@ -167,11 +245,11 @@ func TestListByOwner_OwnerTenancyDoesNotExist(t *testing.T) {
artist, err = server.Backend.WriteCAS(testContext(t), artist)
require.NoError(t, err)

// Verify non-existant tenancy units in owner err with not found.
_, err = client.ListByOwner(testContext(t), &pbresource.ListByOwnerRequest{Owner: modFn(artist.Id, recordLabel.Id)})
// Verify non-existant tenancy units in owner err with invalid arg.
_, err = client.ListByOwner(testContext(t), &pbresource.ListByOwnerRequest{Owner: tc.modFn(artist.Id, recordLabel.Id)})
require.Error(t, err)
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String())
require.Contains(t, err.Error(), "resource not found")
require.ErrorContains(t, err, tc.errContains)
})
}
}
Expand All @@ -184,7 +262,7 @@ func TestListByOwner_Tenancy_Defaults_And_Normalization(t *testing.T) {
client := testClient(t, server)

// Create partition scoped recordLabel.
recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes")
require.NoError(t, err)
rsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: recordLabel})
require.NoError(t, err)
Expand Down
Loading
Loading