From cd635baf32a19af510e146d22fd89e520e00e06c Mon Sep 17 00:00:00 2001 From: Marcus Gartner Date: Tue, 25 Jul 2023 15:35:42 -0700 Subject: [PATCH] sql: fix EXECUTE privileges for UDFs EXECUTE privileges are now correctly checked for each user-defined function referenced in a query. Informs #103842 Release note (bug fix): A bug has been fixed that allowed users to execute a user-defined function without having EXECUTE privileges on the function. This bug has been present since version 22.2 when UDFs were introduced. --- .../settings/settings-for-tenants.txt | 2 +- docs/generated/settings/settings.html | 2 +- pkg/clusterversion/cockroach_versions.go | 8 +++ .../mixed_version_udf_execute_privileges | 70 +++++++++++++++++++ .../testdata/logic_test/udf_privileges | 54 ++++++++++++++ .../BUILD.bazel | 2 +- .../generated_test.go | 7 ++ pkg/sql/opt/cat/catalog.go | 5 ++ pkg/sql/opt/memo/BUILD.bazel | 1 + pkg/sql/opt/memo/memo_test.go | 30 +++++++- pkg/sql/opt/metadata.go | 8 +++ pkg/sql/opt/optbuilder/scalar.go | 12 ++++ pkg/sql/opt/testutils/build.go | 10 +-- pkg/sql/opt/testutils/testcat/function.go | 23 ++++++ pkg/sql/opt/testutils/testcat/test_catalog.go | 14 +++- pkg/sql/opt_catalog.go | 16 +++++ pkg/sql/schema_resolver.go | 12 ++++ 17 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 pkg/sql/logictest/testdata/logic_test/mixed_version_udf_execute_privileges diff --git a/docs/generated/settings/settings-for-tenants.txt b/docs/generated/settings/settings-for-tenants.txt index f282ca60d0ea..34655bc34f31 100644 --- a/docs/generated/settings/settings-for-tenants.txt +++ b/docs/generated/settings/settings-for-tenants.txt @@ -314,4 +314,4 @@ trace.opentelemetry.collector string address of an OpenTelemetry trace collecto trace.snapshot.rate duration 0s if non-zero, interval at which background trace snapshots are captured tenant-rw trace.span_registry.enabled boolean true if set, ongoing traces can be seen at https:///#/debug/tracez tenant-rw trace.zipkin.collector string the address of a Zipkin instance to receive traces, as :. If no port is specified, 9411 will be used. tenant-rw -version version 1000023.1-24 set the active cluster version in the format '.' tenant-rw +version version 1000023.1-26 set the active cluster version in the format '.' tenant-rw diff --git a/docs/generated/settings/settings.html b/docs/generated/settings/settings.html index 7f852826c3b9..b562f15bbad7 100644 --- a/docs/generated/settings/settings.html +++ b/docs/generated/settings/settings.html @@ -267,6 +267,6 @@
trace.span_registry.enabled
booleantrueif set, ongoing traces can be seen at https://<ui>/#/debug/tracezServerless/Dedicated/Self-Hosted
trace.zipkin.collector
stringthe address of a Zipkin instance to receive traces, as <host>:<port>. If no port is specified, 9411 will be used.Serverless/Dedicated/Self-Hosted
ui.display_timezone
enumerationetc/utcthe timezone used to format timestamps in the ui [etc/utc = 0, america/new_york = 1]Dedicated/Self-Hosted -
version
version1000023.1-24set the active cluster version in the format '<major>.<minor>'Serverless/Dedicated/Self-Hosted +
version
version1000023.1-26set the active cluster version in the format '<major>.<minor>'Serverless/Dedicated/Self-Hosted diff --git a/pkg/clusterversion/cockroach_versions.go b/pkg/clusterversion/cockroach_versions.go index 57d3f7a4403d..9d5e316453bc 100644 --- a/pkg/clusterversion/cockroach_versions.go +++ b/pkg/clusterversion/cockroach_versions.go @@ -570,6 +570,10 @@ const ( // options lagging_ranges_threshold and lagging_ranges_polling_interval. V23_2_ChangefeedLaggingRangesOpts + // V23_2_GrantExecuteToPublic grants the EXECUTE privilege to the public + // role for all existing functions. + V23_2_GrantExecuteToPublic + // ************************************************* // Step (1) Add new versions here. // Do not add new versions to a patch release. @@ -995,6 +999,10 @@ var rawVersionsSingleton = keyedVersions{ Key: V23_2_ChangefeedLaggingRangesOpts, Version: roachpb.Version{Major: 23, Minor: 1, Internal: 24}, }, + { + Key: V23_2_GrantExecuteToPublic, + Version: roachpb.Version{Major: 23, Minor: 1, Internal: 26}, + }, // ************************************************* // Step (2): Add new versions here. diff --git a/pkg/sql/logictest/testdata/logic_test/mixed_version_udf_execute_privileges b/pkg/sql/logictest/testdata/logic_test/mixed_version_udf_execute_privileges new file mode 100644 index 000000000000..f5f28ea2b986 --- /dev/null +++ b/pkg/sql/logictest/testdata/logic_test/mixed_version_udf_execute_privileges @@ -0,0 +1,70 @@ +# LogicTest: cockroach-go-testserver-upgrade-to-master + +# Verify that all nodes are running previous version binaries. + +query T nodeidx=0 +SELECT crdb_internal.node_executable_version() +---- +23.1 + +query T nodeidx=1 +SELECT crdb_internal.node_executable_version() +---- +23.1 + +query T nodeidx=2 +SELECT crdb_internal.node_executable_version() +---- +23.1 + +# Create test user. + +statement ok +CREATE USER testuser1 + +# Create a user-defined function. + +statement ok +CREATE FUNCTION f() RETURNS INT LANGUAGE SQL AS 'SELECT 1' + +user testuser1 + +query I +SELECT f() +---- +1 + +# Upgrade node 0 and verify that the user can use the function in mixed version +# mode. + +upgrade 0 + +user testuser1 nodeidx=0 + +query I +SELECT f() +---- +1 + +# Upgrade all nodes. + +upgrade 1 + +upgrade 2 + +# Verify that all nodes are now running 23.2 binaries. + +query B nodeidx=0 +SELECT crdb_internal.node_executable_version() SIMILAR TO '23.1-%' +---- +true + +query B nodeidx=1 +SELECT crdb_internal.node_executable_version() SIMILAR TO '23.1-%' +---- +true + +query B nodeidx=2 +SELECT crdb_internal.node_executable_version() SIMILAR TO '23.1-%' +---- +true diff --git a/pkg/sql/logictest/testdata/logic_test/udf_privileges b/pkg/sql/logictest/testdata/logic_test/udf_privileges index 002f1aa37feb..90095cf5a0ca 100644 --- a/pkg/sql/logictest/testdata/logic_test/udf_privileges +++ b/pkg/sql/logictest/testdata/logic_test/udf_privileges @@ -676,3 +676,57 @@ statement ok DROP USER u_test_owner; subtest end + +statement ok +CREATE USER tester + +statement ok +CREATE SCHEMA test; + +statement ok +GRANT USAGE ON SCHEMA test TO tester; + +statement ok +CREATE FUNCTION test.my_add(a INT, b INT) RETURNS INT IMMUTABLE LEAKPROOF LANGUAGE SQL AS 'SELECT a + b'; + +statement ok +SET ROLE tester + +# The tester role receives execute privileges to functions via the public role. +statement ok +SELECT test.my_add(1,2) + +statement ok +SET ROLE root + +# Revoke execute privilege from the public role. +statement ok +REVOKE EXECUTE ON FUNCTION test.my_add FROM public + +# The root role can still execute the function. +statement ok +SELECT test.my_add(1,2) + +statement ok +SET ROLE tester + +skipif config local-mixed-22.2-23.1 +statement error pgcode 42501 user tester does not have EXECUTE privilege on function my_add +SELECT test.my_add(1,2) + +skipif config local-mixed-22.2-23.1 +statement error pgcode 42501 user tester does not have EXECUTE privilege on function my_add +SELECT * FROM (VALUES (1), (2)) AS v(i) WHERE i = test.my_add(1,2) + +statement ok +SET ROLE root + +# Re-grant execut privilege to the public role. +statement ok +GRANT EXECUTE ON FUNCTION test.my_add TO public + +statement ok +SET ROLE tester + +statement ok +SELECT test.my_add(1,2) diff --git a/pkg/sql/logictest/tests/cockroach-go-testserver-upgrade-to-master/BUILD.bazel b/pkg/sql/logictest/tests/cockroach-go-testserver-upgrade-to-master/BUILD.bazel index f097c40fa4ab..e162ec11c334 100644 --- a/pkg/sql/logictest/tests/cockroach-go-testserver-upgrade-to-master/BUILD.bazel +++ b/pkg/sql/logictest/tests/cockroach-go-testserver-upgrade-to-master/BUILD.bazel @@ -17,7 +17,7 @@ go_test( "dockerNetwork": "standard", "Pool": "large", }, - shard_count = 10, + shard_count = 11, tags = [ "cpu:2", ], diff --git a/pkg/sql/logictest/tests/cockroach-go-testserver-upgrade-to-master/generated_test.go b/pkg/sql/logictest/tests/cockroach-go-testserver-upgrade-to-master/generated_test.go index 4f2530235153..8e13fd4eca21 100644 --- a/pkg/sql/logictest/tests/cockroach-go-testserver-upgrade-to-master/generated_test.go +++ b/pkg/sql/logictest/tests/cockroach-go-testserver-upgrade-to-master/generated_test.go @@ -141,6 +141,13 @@ func TestLogic_mixed_version_system_privileges_user_id( runLogicTest(t, "mixed_version_system_privileges_user_id") } +func TestLogic_mixed_version_udf_execute_privileges( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "mixed_version_udf_execute_privileges") +} + func TestLogic_mixed_version_upgrade_repair_descriptors( t *testing.T, ) { diff --git a/pkg/sql/opt/cat/catalog.go b/pkg/sql/opt/cat/catalog.go index 7867072b567b..1353f17e5ccc 100644 --- a/pkg/sql/opt/cat/catalog.go +++ b/pkg/sql/opt/cat/catalog.go @@ -173,6 +173,11 @@ type Catalog interface { // the given catalog object. If not, then CheckAnyPrivilege returns an error. CheckAnyPrivilege(ctx context.Context, o Object) error + // CheckExecutionPrivilege verifies that the current user has execution + // privileges for the UDF with the given OID. If not, then CheckPrivilege + // returns an error. + CheckExecutionPrivilege(ctx context.Context, oid oid.Oid) error + // HasAdminRole checks that the current user has admin privileges. If yes, // returns true. Returns an error if query on the `system.users` table failed HasAdminRole(ctx context.Context) (bool, error) diff --git a/pkg/sql/opt/memo/BUILD.bazel b/pkg/sql/opt/memo/BUILD.bazel index 20b525656569..9203a1654c52 100644 --- a/pkg/sql/opt/memo/BUILD.bazel +++ b/pkg/sql/opt/memo/BUILD.bazel @@ -102,6 +102,7 @@ go_test( "//pkg/sql/sem/eval", "//pkg/sql/sem/tree", "//pkg/sql/sem/tree/treewindow", + "//pkg/sql/sessiondata", "//pkg/sql/types", "//pkg/testutils", "//pkg/testutils/datapathutils", diff --git a/pkg/sql/opt/memo/memo_test.go b/pkg/sql/opt/memo/memo_test.go index 82fe097d87ac..2b95b7fffd61 100644 --- a/pkg/sql/opt/memo/memo_test.go +++ b/pkg/sql/opt/memo/memo_test.go @@ -25,9 +25,11 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/opt/testutils/testcat" "github.com/cockroachdb/cockroach/pkg/sql/opt/xform" "github.com/cockroachdb/cockroach/pkg/sql/parser" + _ "github.com/cockroachdb/cockroach/pkg/sql/sem/builtins" "github.com/cockroachdb/cockroach/pkg/sql/sem/catconstants" "github.com/cockroachdb/cockroach/pkg/sql/sem/eval" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/sql/sessiondata" "github.com/cockroachdb/cockroach/pkg/testutils" "github.com/cockroachdb/cockroach/pkg/testutils/datapathutils" "github.com/cockroachdb/cockroach/pkg/util/duration" @@ -142,6 +144,10 @@ func TestMemoIsStale(t *testing.T) { if err != nil { t.Fatal(err) } + _, err = catalog.ExecuteDDL("CREATE FUNCTION one() RETURNS INT LANGUAGE SQL AS $$ SELECT 1 $$") + if err != nil { + t.Fatal(err) + } // Revoke access to the underlying table. The user should retain indirect // access via the view. @@ -150,6 +156,7 @@ func TestMemoIsStale(t *testing.T) { // Initialize context with starting values. evalCtx := eval.MakeTestingEvalContext(cluster.MakeTestingClusterSettings()) evalCtx.SessionData().Database = "t" + evalCtx.SessionData().SearchPath = sessiondata.MakeSearchPath([]string{"public"}) // MakeTestingEvalContext created a fake planner that can only provide the // memory monitor and will encounter a nil-pointer error when other methods // are accessed. In this test, GetDatabaseSurvivalGoal method will be called @@ -159,7 +166,7 @@ func TestMemoIsStale(t *testing.T) { evalCtx.StreamManagerFactory = nil var o xform.Optimizer - opttestutils.BuildQuery(t, &o, catalog, &evalCtx, "SELECT a, b+1 FROM abcview WHERE c='foo'") + opttestutils.BuildQuery(t, &o, catalog, &evalCtx, "SELECT a, b+one() FROM abcview WHERE c='foo'") o.Memo().Metadata().AddSchema(catalog.Schema()) ctx := context.Background() @@ -175,7 +182,7 @@ func TestMemoIsStale(t *testing.T) { // tests as written still pass if the default value is 0. To detect this, we // create a new memo with the changed setting and verify it's not stale. var o2 xform.Optimizer - opttestutils.BuildQuery(t, &o2, catalog, &evalCtx, "SELECT a, b+1 FROM abcview WHERE c='foo'") + opttestutils.BuildQuery(t, &o2, catalog, &evalCtx, "SELECT a, b+one() FROM abcview WHERE c='foo'") if isStale, err := o2.Memo().IsStale(ctx, &evalCtx, catalog); err != nil { t.Fatal(err) @@ -408,6 +415,15 @@ func TestMemoIsStale(t *testing.T) { catalog.View(tree.NewTableNameWithSchema("t", catconstants.PublicSchemaName, "abcview")).Revoked = false notStale() + // User no longer has execution privilege on a UDF. + catalog.RevokeExecution(catalog.Function("one").Oid) + _, err = o.Memo().IsStale(ctx, &evalCtx, catalog) + if exp := "user does not have privilege to execute function"; !testutils.IsError(err, exp) { + t.Fatalf("expected %q error, but got %+v", exp, err) + } + catalog.GrantExecution(catalog.Function("one").Oid) + notStale() + // Stale data sources and schema. Create new catalog so that data sources are // recreated and can be modified independently. catalog = testcat.New() @@ -419,6 +435,10 @@ func TestMemoIsStale(t *testing.T) { if err != nil { t.Fatal(err) } + _, err = catalog.ExecuteDDL("CREATE FUNCTION one() RETURNS INT LANGUAGE SQL AS $$ SELECT 1 $$") + if err != nil { + t.Fatal(err) + } // Table ID changes. catalog.Table(tree.NewTableNameWithSchema("t", catconstants.PublicSchemaName, "abc")).TabID = 1 @@ -431,6 +451,12 @@ func TestMemoIsStale(t *testing.T) { stale() catalog.Table(tree.NewTableNameWithSchema("t", catconstants.PublicSchemaName, "abc")).TabVersion = 0 notStale() + + // Function Version changes. + catalog.Function("one").Version = 1 + stale() + catalog.Function("one").Version = 0 + notStale() } // TestStatsAvailable tests that the statisticsBuilder correctly identifies diff --git a/pkg/sql/opt/metadata.go b/pkg/sql/opt/metadata.go index 1df541de7fe0..a33797e03f5a 100644 --- a/pkg/sql/opt/metadata.go +++ b/pkg/sql/opt/metadata.go @@ -442,6 +442,14 @@ func (md *Metadata) CheckDependencies( } } + // Check that the role still has execution privilege on the user defined + // functions. + for _, overload := range md.udfDeps { + if err := optCatalog.CheckExecutionPrivilege(ctx, overload.Oid); err != nil { + return false, err + } + } + // Check that any references to builtin functions do not now resolve to a UDF // with the same signature (e.g. after changes to the search path). for name := range md.builtinRefsByName { diff --git a/pkg/sql/opt/optbuilder/scalar.go b/pkg/sql/opt/optbuilder/scalar.go index 98591a8643a9..33a070a9de6b 100644 --- a/pkg/sql/opt/optbuilder/scalar.go +++ b/pkg/sql/opt/optbuilder/scalar.go @@ -616,6 +616,9 @@ func (b *Builder) buildFunction( // buildUDF builds a set of memo groups that represents a user-defined function // invocation. +// +// TODO(mgartner): This function also builds built-in functions defined with a +// SQL body. Consider renaming it to make it more clear. func (b *Builder) buildUDF( f *tree.FuncExpr, def *tree.ResolvedFunctionDefinition, @@ -624,6 +627,15 @@ func (b *Builder) buildUDF( colRefs *opt.ColSet, ) (out opt.ScalarExpr) { o := f.ResolvedOverload() + + // Check for execution privileges for user-defined overloads. Built-in + // overloads do not need to be checked. + if o.IsUDF { + if err := b.catalog.CheckExecutionPrivilege(b.ctx, o.Oid); err != nil { + panic(err) + } + } + b.factory.Metadata().AddUserDefinedFunction(o, f.Func.ReferenceByName) if o.Type == tree.ProcedureRoutine { diff --git a/pkg/sql/opt/testutils/build.go b/pkg/sql/opt/testutils/build.go index a844c205b77f..181cbd15044e 100644 --- a/pkg/sql/opt/testutils/build.go +++ b/pkg/sql/opt/testutils/build.go @@ -31,19 +31,21 @@ func BuildQuery( ) { stmt, err := parser.ParseOne(sql) if err != nil { - t.Fatal(err) + t.Fatalf("%+v", err) } ctx := context.Background() semaCtx := tree.MakeSemaContext() + semaCtx.FunctionResolver = catalog + semaCtx.SearchPath = &evalCtx.SessionData().SearchPath if err := semaCtx.Placeholders.Init(stmt.NumPlaceholders, nil /* typeHints */); err != nil { - t.Fatal(err) + t.Fatalf("%+v", err) } semaCtx.Annotations = tree.MakeAnnotations(stmt.NumAnnotations) o.Init(ctx, evalCtx, catalog) err = optbuilder.New(ctx, &semaCtx, evalCtx, catalog, o.Factory(), stmt.AST).Build() if err != nil { - t.Fatal(err) + t.Fatalf("%+v", err) } } @@ -53,7 +55,7 @@ func BuildScalar( ) opt.ScalarExpr { expr, err := parser.ParseExpr(input) if err != nil { - t.Fatal(err) + t.Fatalf("%+v", err) } b := optbuilder.NewScalar(context.Background(), semaCtx, evalCtx, f) diff --git a/pkg/sql/opt/testutils/testcat/function.go b/pkg/sql/opt/testutils/testcat/function.go index de8c50b2a931..7142f9d7ecc6 100644 --- a/pkg/sql/opt/testutils/testcat/function.go +++ b/pkg/sql/opt/testutils/testcat/function.go @@ -102,7 +102,9 @@ func (tc *Catalog) CreateRoutine(c *tree.CreateRoutine) { if c.IsProcedure { routineType = tree.ProcedureRoutine } + tc.currUDFOid++ overload := &tree.Overload{ + Oid: tc.currUDFOid, Types: paramTypes, ReturnType: tree.FixedReturnType(retType), Body: body, @@ -124,6 +126,27 @@ func (tc *Catalog) CreateRoutine(c *tree.CreateRoutine) { tc.udfs[name] = def } +// RevokedExecution revokes execution of the function with the given OID. +func (tc *Catalog) RevokeExecution(oid oid.Oid) { + tc.revokedUDFOids.Add(int(oid)) +} + +// GrantExecution grants execution of the function with the given OID. +func (tc *Catalog) GrantExecution(oid oid.Oid) { + tc.revokedUDFOids.Remove(int(oid)) +} + +// Function returns the overload of the function with the given name. It returns +// nil if the function does not exist. +func (tc *Catalog) Function(name string) *tree.Overload { + for _, def := range tc.udfs { + if def.Name == name { + return def.Overloads[0].Overload + } + } + return nil +} + func collectFuncOptions( o tree.RoutineOptions, ) (body string, v volatility.V, calledOnNullInput bool, language tree.RoutineLanguage) { diff --git a/pkg/sql/opt/testutils/testcat/test_catalog.go b/pkg/sql/opt/testutils/testcat/test_catalog.go index b97da14a7734..7d407dab2955 100644 --- a/pkg/sql/opt/testutils/testcat/test_catalog.go +++ b/pkg/sql/opt/testutils/testcat/test_catalog.go @@ -37,6 +37,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/sqlerrors" "github.com/cockroachdb/cockroach/pkg/sql/stats" "github.com/cockroachdb/cockroach/pkg/sql/types" + "github.com/cockroachdb/cockroach/pkg/util/intsets" "github.com/cockroachdb/cockroach/pkg/util/treeprinter" "github.com/cockroachdb/errors" "github.com/lib/pq/oid" @@ -52,7 +53,10 @@ type Catalog struct { testSchema Schema counter int enumTypes map[string]*types.T - udfs map[string]*tree.ResolvedFunctionDefinition + + udfs map[string]*tree.ResolvedFunctionDefinition + currUDFOid oid.Oid + revokedUDFOids intsets.Fast } type dataSource interface { @@ -289,6 +293,14 @@ func (tc *Catalog) CheckAnyPrivilege(ctx context.Context, o cat.Object) error { return nil } +// CheckExecutionPrivilege is part of the cat.Catalog interface. +func (tc *Catalog) CheckExecutionPrivilege(ctx context.Context, oid oid.Oid) error { + if tc.revokedUDFOids.Contains(int(oid)) { + return pgerror.Newf(pgcode.InsufficientPrivilege, "user does not have privilege to execute function with OID %d", oid) + } + return nil +} + // HasAdminRole is part of the cat.Catalog interface. func (tc *Catalog) HasAdminRole(ctx context.Context) (bool, error) { return true, nil diff --git a/pkg/sql/opt_catalog.go b/pkg/sql/opt_catalog.go index 186406ef4546..d8629455d7c4 100644 --- a/pkg/sql/opt_catalog.go +++ b/pkg/sql/opt_catalog.go @@ -15,6 +15,7 @@ import ( "math" "time" + "github.com/cockroachdb/cockroach/pkg/clusterversion" "github.com/cockroachdb/cockroach/pkg/config" "github.com/cockroachdb/cockroach/pkg/geo/geoindex" "github.com/cockroachdb/cockroach/pkg/keys" @@ -422,6 +423,21 @@ func (oc *optCatalog) CheckAnyPrivilege(ctx context.Context, o cat.Object) error return oc.planner.CheckAnyPrivilege(ctx, desc) } +// CheckExecutionPrivilege is part of the cat.Catalog interface. +func (oc *optCatalog) CheckExecutionPrivilege(ctx context.Context, oid oid.Oid) error { + // If the required cluster version is not active, revert to pre-23.2 + // behavior without any privilege checks. + activeVersion := oc.planner.ExecCfg().Settings.Version.ActiveVersion(ctx) + if !activeVersion.IsActive(clusterversion.V23_2_GrantExecuteToPublic) { + return nil + } + desc, err := oc.planner.FunctionDesc(ctx, oid) + if err != nil { + return errors.WithAssertionFailure(err) + } + return oc.planner.CheckPrivilege(ctx, desc, privilege.EXECUTE) +} + // HasAdminRole is part of the cat.Catalog interface. func (oc *optCatalog) HasAdminRole(ctx context.Context) (bool, error) { return oc.planner.HasAdminRole(ctx) diff --git a/pkg/sql/schema_resolver.go b/pkg/sql/schema_resolver.go index 21c1d64c1f11..602330094409 100644 --- a/pkg/sql/schema_resolver.go +++ b/pkg/sql/schema_resolver.go @@ -598,6 +598,18 @@ func (sr *schemaResolver) ResolveFunctionByOID( return fnName, ret, nil } +// FunctionDesc returns the descriptor for the function with the given OID. +func (sr *schemaResolver) FunctionDesc( + ctx context.Context, oid oid.Oid, +) (catalog.FunctionDescriptor, error) { + if !funcdesc.IsOIDUserDefinedFunc(oid) { + return nil, errors.Wrapf(tree.ErrFunctionUndefined, "function %d not user-defined", oid) + } + g := sr.byIDGetterBuilder().WithoutNonPublic().WithoutOtherParent(sr.typeResolutionDbID).Get() + descID := funcdesc.UserDefinedFunctionOIDToID(oid) + return g.Function(ctx, descID) +} + // NewSkippingCacheSchemaResolver constructs a schemaResolver which always skip // descriptor cache. func NewSkippingCacheSchemaResolver(