-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
533 changed files
with
40,025 additions
and
4,036 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:improvement | ||
docs: add Link API documentation | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:bug | ||
logging: add /api prefix to v2 resource endpoint logs | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:feature | ||
ui: adds V2CatalogEnabled to config that is passed to the ui | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
139 changes: 139 additions & 0 deletions
139
agent/grpc-external/services/resource/mutate_and_validate.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package resource | ||
|
||
import ( | ||
"context" | ||
"strings" | ||
|
||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
|
||
"github.com/hashicorp/consul/acl" | ||
"github.com/hashicorp/consul/internal/resource" | ||
"github.com/hashicorp/consul/proto-public/pbresource" | ||
) | ||
|
||
func (s *Server) MutateAndValidate(ctx context.Context, req *pbresource.MutateAndValidateRequest) (*pbresource.MutateAndValidateResponse, error) { | ||
tenancyMarkedForDeletion, err := s.mutateAndValidate(ctx, req.Resource) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if tenancyMarkedForDeletion { | ||
return nil, status.Errorf( | ||
codes.InvalidArgument, | ||
"tenancy marked for deletion: %s/%s", | ||
req.Resource.Id.Tenancy.Partition, | ||
req.Resource.Id.Tenancy.Namespace, | ||
) | ||
} | ||
return &pbresource.MutateAndValidateResponse{Resource: req.Resource}, nil | ||
} | ||
|
||
// private DRY impl that is used by both the Write and MutateAndValidate RPCs. | ||
func (s *Server) mutateAndValidate(ctx context.Context, res *pbresource.Resource) (tenancyMarkedForDeletion bool, err error) { | ||
reg, err := s.ensureResourceValid(res) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
v1EntMeta := v2TenancyToV1EntMeta(res.Id.Tenancy) | ||
authz, authzContext, err := s.getAuthorizer(tokenFromContext(ctx), v1EntMeta) | ||
if err != nil { | ||
return false, err | ||
} | ||
v1EntMetaToV2Tenancy(reg, v1EntMeta, res.Id.Tenancy) | ||
|
||
// Check the user sent the correct type of data. | ||
if res.Data != nil && !res.Data.MessageIs(reg.Proto) { | ||
got := strings.TrimPrefix(res.Data.TypeUrl, "type.googleapis.com/") | ||
|
||
return false, status.Errorf( | ||
codes.InvalidArgument, | ||
"resource.data is of wrong type (expected=%q, got=%q)", | ||
reg.Proto.ProtoReflect().Descriptor().FullName(), | ||
got, | ||
) | ||
} | ||
|
||
if err = reg.Mutate(res); err != nil { | ||
return false, status.Errorf(codes.Internal, "failed mutate hook: %v", err.Error()) | ||
} | ||
|
||
if err = reg.Validate(res); err != nil { | ||
return false, status.Error(codes.InvalidArgument, err.Error()) | ||
} | ||
|
||
// ACL check comes before tenancy existence checks to not leak tenancy "existence". | ||
err = reg.ACLs.Write(authz, authzContext, res) | ||
switch { | ||
case acl.IsErrPermissionDenied(err): | ||
return false, status.Error(codes.PermissionDenied, err.Error()) | ||
case err != nil: | ||
return false, status.Errorf(codes.Internal, "failed write acl: %v", err) | ||
} | ||
|
||
// Check tenancy exists for the V2 resource | ||
if err = tenancyExists(reg, s.TenancyBridge, res.Id.Tenancy, codes.InvalidArgument); err != nil { | ||
return false, err | ||
} | ||
|
||
// This is used later in the "create" and "update" paths to block non-delete related writes | ||
// when a tenancy unit has been marked for deletion. | ||
tenancyMarkedForDeletion, err = isTenancyMarkedForDeletion(reg, s.TenancyBridge, res.Id.Tenancy) | ||
if err != nil { | ||
return false, status.Errorf(codes.Internal, "failed tenancy marked for deletion check: %v", err) | ||
} | ||
if tenancyMarkedForDeletion { | ||
return true, nil | ||
} | ||
return false, nil | ||
} | ||
|
||
func (s *Server) ensureResourceValid(res *pbresource.Resource) (*resource.Registration, error) { | ||
var field string | ||
switch { | ||
case res == nil: | ||
field = "resource" | ||
case res.Id == nil: | ||
field = "resource.id" | ||
} | ||
|
||
if field != "" { | ||
return nil, status.Errorf(codes.InvalidArgument, "%s is required", field) | ||
} | ||
|
||
if err := validateId(res.Id, "resource.id"); err != nil { | ||
return nil, err | ||
} | ||
|
||
if res.Owner != nil { | ||
if err := validateId(res.Owner, "resource.owner"); err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
// Check type exists. | ||
reg, err := s.resolveType(res.Id.Type) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err = checkV2Tenancy(s.UseV2Tenancy, res.Id.Type); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Check scope | ||
if reg.Scope == resource.ScopePartition && res.Id.Tenancy.Namespace != "" { | ||
return nil, status.Errorf( | ||
codes.InvalidArgument, | ||
"partition scoped resource %s cannot have a namespace. got: %s", | ||
resource.ToGVK(res.Id.Type), | ||
res.Id.Tenancy.Namespace, | ||
) | ||
} | ||
|
||
return reg, nil | ||
} |
212 changes: 212 additions & 0 deletions
212
agent/grpc-external/services/resource/mutate_and_validate_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package resource_test | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
|
||
svc "github.com/hashicorp/consul/agent/grpc-external/services/resource" | ||
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing" | ||
"github.com/hashicorp/consul/internal/resource/demo" | ||
"github.com/hashicorp/consul/proto-public/pbresource" | ||
pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2" | ||
"github.com/hashicorp/consul/proto/private/prototest" | ||
) | ||
|
||
func TestMutateAndValidate_InputValidation(t *testing.T) { | ||
run := func(t *testing.T, client pbresource.ResourceServiceClient, tc resourceValidTestCase) { | ||
artist, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
|
||
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes") | ||
require.NoError(t, err) | ||
|
||
req := &pbresource.MutateAndValidateRequest{Resource: tc.modFn(artist, recordLabel)} | ||
_, err = client.MutateAndValidate(testContext(t), req) | ||
require.Error(t, err) | ||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) | ||
require.ErrorContains(t, err, tc.errContains) | ||
} | ||
|
||
for _, v2tenancy := range []bool{false, true} { | ||
t.Run(fmt.Sprintf("v2tenancy %v", v2tenancy), func(t *testing.T) { | ||
client := svctest.NewResourceServiceBuilder(). | ||
WithRegisterFns(demo.RegisterTypes). | ||
WithV2Tenancy(v2tenancy). | ||
Run(t) | ||
|
||
for desc, tc := range resourceValidTestCases(t) { | ||
t.Run(desc, func(t *testing.T) { | ||
run(t, client, tc) | ||
}) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestMutateAndValidate_OwnerValidation(t *testing.T) { | ||
run := func(t *testing.T, client pbresource.ResourceServiceClient, tc ownerValidTestCase) { | ||
artist, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
|
||
album, err := demo.GenerateV2Album(artist.Id) | ||
require.NoError(t, err) | ||
|
||
tc.modFn(album) | ||
|
||
_, err = client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: album}) | ||
require.Error(t, err) | ||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) | ||
require.ErrorContains(t, err, tc.errorContains) | ||
} | ||
|
||
for _, v2tenancy := range []bool{false, true} { | ||
t.Run(fmt.Sprintf("v2tenancy %v", v2tenancy), func(t *testing.T) { | ||
client := svctest.NewResourceServiceBuilder(). | ||
WithRegisterFns(demo.RegisterTypes). | ||
WithV2Tenancy(v2tenancy). | ||
Run(t) | ||
|
||
for desc, tc := range ownerValidationTestCases(t) { | ||
t.Run(desc, func(t *testing.T) { | ||
run(t, client, tc) | ||
}) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestMutateAndValidate_TypeNotFound(t *testing.T) { | ||
run := func(t *testing.T, client pbresource.ResourceServiceClient) { | ||
res, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
|
||
_, err = client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: res}) | ||
require.Error(t, err) | ||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) | ||
require.Contains(t, err.Error(), "resource type demo.v2.Artist not registered") | ||
} | ||
|
||
for _, v2tenancy := range []bool{false, true} { | ||
t.Run(fmt.Sprintf("v2tenancy %v", v2tenancy), func(t *testing.T) { | ||
client := svctest.NewResourceServiceBuilder().WithV2Tenancy(v2tenancy).Run(t) | ||
run(t, client) | ||
}) | ||
} | ||
} | ||
|
||
func TestMutateAndValidate_Success(t *testing.T) { | ||
run := func(t *testing.T, client pbresource.ResourceServiceClient, tc mavOrWriteSuccessTestCase) { | ||
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes") | ||
require.NoError(t, err) | ||
|
||
artist, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
|
||
rsp, err := client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: tc.modFn(artist, recordLabel)}) | ||
require.NoError(t, err) | ||
prototest.AssertDeepEqual(t, tc.expectedTenancy, rsp.Resource.Id.Tenancy) | ||
} | ||
|
||
for _, v2tenancy := range []bool{false, true} { | ||
t.Run(fmt.Sprintf("v2tenancy %v", v2tenancy), func(t *testing.T) { | ||
client := svctest.NewResourceServiceBuilder(). | ||
WithRegisterFns(demo.RegisterTypes). | ||
WithV2Tenancy(v2tenancy). | ||
Run(t) | ||
|
||
for desc, tc := range mavOrWriteSuccessTestCases(t) { | ||
t.Run(desc, func(t *testing.T) { | ||
run(t, client, tc) | ||
}) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestMutateAndValidate_Mutate(t *testing.T) { | ||
for _, v2tenancy := range []bool{false, true} { | ||
t.Run(fmt.Sprintf("v2tenancy %v", v2tenancy), func(t *testing.T) { | ||
client := svctest.NewResourceServiceBuilder(). | ||
WithRegisterFns(demo.RegisterTypes). | ||
WithV2Tenancy(v2tenancy). | ||
Run(t) | ||
|
||
artist, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
|
||
artistData := &pbdemov2.Artist{} | ||
artist.Data.UnmarshalTo(artistData) | ||
require.NoError(t, err) | ||
|
||
// mutate hook sets genre to disco when unspecified | ||
artistData.Genre = pbdemov2.Genre_GENRE_UNSPECIFIED | ||
artist.Data.MarshalFrom(artistData) | ||
require.NoError(t, err) | ||
|
||
rsp, err := client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: artist}) | ||
require.NoError(t, err) | ||
|
||
// verify mutate hook set genre to disco | ||
require.NoError(t, rsp.Resource.Data.UnmarshalTo(artistData)) | ||
require.Equal(t, pbdemov2.Genre_GENRE_DISCO, artistData.Genre) | ||
}) | ||
} | ||
} | ||
|
||
func TestMutateAndValidate_Tenancy_NotFound(t *testing.T) { | ||
for desc, tc := range mavOrWriteTenancyNotFoundTestCases(t) { | ||
t.Run(desc, func(t *testing.T) { | ||
client := svctest.NewResourceServiceBuilder(). | ||
WithV2Tenancy(true). | ||
WithRegisterFns(demo.RegisterTypes). | ||
Run(t) | ||
|
||
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes") | ||
require.NoError(t, err) | ||
|
||
artist, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
|
||
_, err = client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: tc.modFn(artist, recordLabel)}) | ||
require.Error(t, err) | ||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) | ||
require.Contains(t, err.Error(), tc.errContains) | ||
}) | ||
} | ||
} | ||
|
||
func TestMutateAndValidate_TenancyMarkedForDeletion_Fails(t *testing.T) { | ||
for desc, tc := range mavOrWriteTenancyMarkedForDeletionTestCases(t) { | ||
t.Run(desc, func(t *testing.T) { | ||
server := testServer(t) | ||
client := testClient(t, server) | ||
demo.RegisterTypes(server.Registry) | ||
|
||
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes") | ||
require.NoError(t, err) | ||
recordLabel.Id.Tenancy.Partition = "ap1" | ||
|
||
artist, err := demo.GenerateV2Artist() | ||
require.NoError(t, err) | ||
artist.Id.Tenancy.Partition = "ap1" | ||
artist.Id.Tenancy.Namespace = "ns1" | ||
|
||
mockTenancyBridge := &svc.MockTenancyBridge{} | ||
mockTenancyBridge.On("PartitionExists", "ap1").Return(true, nil) | ||
mockTenancyBridge.On("NamespaceExists", "ap1", "ns1").Return(true, nil) | ||
server.TenancyBridge = mockTenancyBridge | ||
|
||
_, err = client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: tc.modFn(artist, recordLabel, mockTenancyBridge)}) | ||
require.Error(t, err) | ||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) | ||
require.Contains(t, err.Error(), tc.errContains) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.