From b3980eeec8214055116e99f078646f7a56b509ad Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Tue, 9 Dec 2025 21:46:18 +0200 Subject: [PATCH] IAM: Add tracing for legacy stores (#114974) --- pkg/registry/apis/iam/register.go | 12 +++++------ pkg/registry/apis/iam/serviceaccount/store.go | 18 ++++++++++++++-- pkg/registry/apis/iam/sso/store.go | 18 ++++++++++++++-- pkg/registry/apis/iam/team/store.go | 21 +++++++++++++++++-- pkg/registry/apis/iam/teambinding/store.go | 21 +++++++++++++++++-- pkg/registry/apis/iam/user/legacy_search.go | 8 ++++++- .../apis/iam/user/legacy_search_test.go | 7 ++++--- pkg/registry/apis/iam/user/store.go | 21 +++++++++++++++++-- 8 files changed, 106 insertions(+), 20 deletions(-) diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index b9da9e6f2c5..85d8bc4e4f1 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -90,11 +90,11 @@ func RegisterAPIService( builder := &IdentityAccessManagementAPIBuilder{ store: store, - userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation), - saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation), - legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation), - teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation), - ssoLegacyStore: sso.NewLegacyStore(ssoService), + userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing), + saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing), + legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation, tracing), + teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation, tracing), + ssoLegacyStore: sso.NewLegacyStore(ssoService, tracing), coreRolesStorage: coreRolesStorage, rolesStorage: rolesStorage, resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), @@ -114,7 +114,7 @@ func RegisterAPIService( dual: dual, unified: unified, userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), - unified, user.NewUserLegacySearchClient(userService), features), + unified, user.NewUserLegacySearchClient(userService, tracing), features), teamSearch: NewTeamSearchHandler(tracing, dual, team.NewLegacyTeamSearchClient(teamService), unified, features), } apiregistration.RegisterAPI(builder) diff --git a/pkg/registry/apis/iam/serviceaccount/store.go b/pkg/registry/apis/iam/serviceaccount/store.go index 2980322cb27..db4709a4e2e 100644 --- a/pkg/registry/apis/iam/serviceaccount/store.go +++ b/pkg/registry/apis/iam/serviceaccount/store.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,14 +36,15 @@ var ( var resource = iamv0alpha1.ServiceAccountResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } // DeleteCollection implements rest.CollectionDeleter. @@ -52,6 +54,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -95,6 +100,9 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda // Create implements rest.Creater. func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") } @@ -165,6 +173,9 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.ServiceAccount], error) { @@ -228,6 +239,9 @@ func extractPluginNameFromTitle(title string) string { } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/sso/store.go b/pkg/registry/apis/iam/sso/store.go index c9965adc152..9ab09fa6ef4 100644 --- a/pkg/registry/apis/iam/sso/store.go +++ b/pkg/registry/apis/iam/sso/store.go @@ -7,6 +7,7 @@ import ( commonv1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,12 +33,13 @@ var ( var resource = iamv0.SSOSettingResourceInfo -func NewLegacyStore(service ssosettings.Service) *LegacyStore { - return &LegacyStore{service} +func NewLegacyStore(service ssosettings.Service, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{service, tracer} } type LegacyStore struct { service ssosettings.Service + tracer trace.Tracer } // Destroy implements rest.Storage. @@ -71,6 +73,9 @@ func (s *LegacyStore) NewList() runtime.Object { // List implements rest.Lister. func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "sso.List") + defer span.End() + ns, _ := request.NamespaceInfoFrom(ctx, false) settings, err := s.service.List(ctx) @@ -88,6 +93,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt // Get implements rest.Getter. func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "sso.Get") + defer span.End() + ns, _ := request.NamespaceInfoFrom(ctx, false) setting, err := s.service.GetForProviderWithRedactedSecrets(ctx, name) @@ -112,6 +120,9 @@ func (s *LegacyStore) Update( _ bool, _ *metav1.UpdateOptions, ) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "sso.Update") + defer span.End() + const created = false ident, err := identity.GetRequester(ctx) if err != nil { @@ -148,6 +159,9 @@ func (s *LegacyStore) Delete( _ rest.ValidateObjectFunc, options *metav1.DeleteOptions, ) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "sso.Delete") + defer span.End() + obj, err := s.Get(ctx, name, nil) if err != nil { return obj, false, err diff --git a/pkg/registry/apis/iam/team/store.go b/pkg/registry/apis/iam/team/store.go index e048fdb7a6d..c667c8ec374 100644 --- a/pkg/registry/apis/iam/team/store.go +++ b/pkg/registry/apis/iam/team/store.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" + "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -35,14 +36,15 @@ var ( var resource = iamv0alpha1.TeamResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } func (s *LegacyStore) New() runtime.Object { @@ -74,6 +76,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "team.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -112,6 +117,9 @@ func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation // Update implements rest.Updater. func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "team.Update") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update") } @@ -161,6 +169,9 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.Team], error) { @@ -197,6 +208,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -219,6 +233,9 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO } func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") } diff --git a/pkg/registry/apis/iam/teambinding/store.go b/pkg/registry/apis/iam/teambinding/store.go index eaaa4766e39..6f6fd4ce162 100644 --- a/pkg/registry/apis/iam/teambinding/store.go +++ b/pkg/registry/apis/iam/teambinding/store.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,13 +36,14 @@ var ( _ rest.CollectionDeleter = (*LegacyBindingStore)(nil) ) -func NewLegacyBindingStore(store legacy.LegacyIdentityStore, enableAuthnMutation bool) *LegacyBindingStore { - return &LegacyBindingStore{store, enableAuthnMutation} +func NewLegacyBindingStore(store legacy.LegacyIdentityStore, enableAuthnMutation bool, tracer trace.Tracer) *LegacyBindingStore { + return &LegacyBindingStore{store, enableAuthnMutation, tracer} } type LegacyBindingStore struct { store legacy.LegacyIdentityStore enableAuthnMutation bool + tracer trace.Tracer } // Destroy implements rest.Storage. @@ -73,6 +75,9 @@ func (l *LegacyBindingStore) ConvertToTable(ctx context.Context, object runtime. } func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Update") + defer span.End() + if !l.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update") } @@ -125,6 +130,9 @@ func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo re } func (l *LegacyBindingStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Delete") + defer span.End() + if !l.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete") } @@ -160,6 +168,9 @@ func (l *LegacyBindingStore) DeleteCollection(ctx context.Context, deleteValidat } func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Create") + defer span.End() + if !l.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "create") } @@ -230,6 +241,9 @@ func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, cre // Get implements rest.Getter. func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -254,6 +268,9 @@ func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *meta // List implements rest.Lister. func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.List") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/user/legacy_search.go b/pkg/registry/apis/iam/user/legacy_search.go index 534a4f016eb..7fb6c13f7a6 100644 --- a/pkg/registry/apis/iam/user/legacy_search.go +++ b/pkg/registry/apis/iam/user/legacy_search.go @@ -6,6 +6,7 @@ import ( "log/slog" "math" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -27,13 +28,15 @@ type UserLegacySearchClient struct { resourcepb.ResourceIndexClient userService user.Service log *slog.Logger + tracer trace.Tracer } // NewUserLegacySearchClient creates a new UserLegacySearchClient. -func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient { +func NewUserLegacySearchClient(userService user.Service, tracer trace.Tracer) *UserLegacySearchClient { return &UserLegacySearchClient{ userService: userService, log: slog.Default().With("logger", "legacy-user-search-client"), + tracer: tracer, } } @@ -41,6 +44,9 @@ func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient // It only supports exact matching for title, login, or email. // FIXME: This implementation only supports a single field query and will be extended in the future. func (c *UserLegacySearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, _ ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) { + ctx, span := c.tracer.Start(ctx, "user.Search") + defer span.End() + signedInUser, err := identity.GetRequester(ctx) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/user/legacy_search_test.go b/pkg/registry/apis/iam/user/legacy_search_test.go index b8a754f34ce..6bd49185786 100644 --- a/pkg/registry/apis/iam/user/legacy_search_test.go +++ b/pkg/registry/apis/iam/user/legacy_search_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" res "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -17,7 +18,7 @@ import ( func TestUserLegacySearchClient_Search(t *testing.T) { t.Run("should return error if no query fields are provided", func(t *testing.T) { mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService()) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ @@ -66,7 +67,7 @@ func TestUserLegacySearchClient_Search(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService()) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Limit: 10, @@ -125,7 +126,7 @@ func TestUserLegacySearchClient_Search(t *testing.T) { t.Run("title should have precedence over login and email", func(t *testing.T) { mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService()) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ diff --git a/pkg/registry/apis/iam/user/store.go b/pkg/registry/apis/iam/user/store.go index 803b2972b85..97daee3af18 100644 --- a/pkg/registry/apis/iam/user/store.go +++ b/pkg/registry/apis/iam/user/store.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -36,18 +37,22 @@ var ( var resource = iamv0alpha1.UserResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } // Update implements rest.Updater. func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "user.Update") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update") } @@ -105,6 +110,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "user.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -171,6 +179,9 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.User], error) { @@ -206,6 +217,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -229,6 +243,9 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO // Create implements rest.Creater. func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") }