From 13a921a9fac6d4f41fc8c94ba9de65060e1d2377 Mon Sep 17 00:00:00 2001 From: Mihaly Gyongyosi Date: Wed, 14 Jan 2026 16:38:56 +0100 Subject: [PATCH] Add custom authorizer, update noop search --- pkg/registry/apis/iam/authorizer.go | 19 ++- .../iam/authorizer/team_binding_authorizer.go | 4 +- .../iam/externalgroupmapping/search_noop.go | 40 ++++-- .../iam.grafana.app-v0alpha1.json | 133 ++++++++++++++++++ 4 files changed, 183 insertions(+), 13 deletions(-) diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index 83e4357c4e6..29a7e66566a 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -8,6 +8,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" iamauthorizer "github.com/grafana/grafana/pkg/registry/apis/iam/authorizer" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" @@ -40,6 +41,22 @@ func newIAMAuthorizer( return authorizer.DecisionAllow, "", nil }) + serviceIdentityAuthorizer := authorizer.AuthorizerFunc(func( + ctx context.Context, attr authorizer.Attributes, + ) (authorized authorizer.Decision, reason string, err error) { + if identity.IsServiceIdentity(ctx) { + // A Grafana sub-system should have full access. We trust them to make wise decisions. + return authorizer.DecisionAllow, "", nil + } + + req, err := identity.GetRequester(ctx) + if err == nil && req != nil && req.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + + return authorizer.DecisionDeny, "", nil + }) + // Identity specific resources legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient) resourceAuthorizer["display"] = legacyAuthorizer @@ -58,7 +75,7 @@ func newIAMAuthorizer( resourceAuthorizer["searchUsers"] = serviceAuthorizer resourceAuthorizer["searchTeams"] = serviceAuthorizer // TODO: Implement fine-grained authorization for external group mapping search on the search level - resourceAuthorizer["searchExternalGroupMappings"] = allowAuthorizer + resourceAuthorizer["searchExternalGroupMappings"] = serviceIdentityAuthorizer return &iamAuthorizer{resourceAuthorizer: resourceAuthorizer} } diff --git a/pkg/registry/apis/iam/authorizer/team_binding_authorizer.go b/pkg/registry/apis/iam/authorizer/team_binding_authorizer.go index 2a4f5ae5e51..4f0d1bd12f3 100644 --- a/pkg/registry/apis/iam/authorizer/team_binding_authorizer.go +++ b/pkg/registry/apis/iam/authorizer/team_binding_authorizer.go @@ -97,8 +97,8 @@ func (r *TeamBindingAuthorizer) beforeWrite(ctx context.Context, obj runtime.Obj teamName := concreteObj.Spec.TeamRef.Name checkReq := types.CheckRequest{ Namespace: authInfo.GetNamespace(), - Group: iamv0.GROUP, - Resource: iamv0.TeamResourceInfo.GetName(), + Group: iamv0.TeamResourceInfo.GroupResource().Group, + Resource: iamv0.TeamResourceInfo.GroupResource().Resource, Verb: utils.VerbSetPermissions, Name: teamName, } diff --git a/pkg/registry/apis/iam/externalgroupmapping/search_noop.go b/pkg/registry/apis/iam/externalgroupmapping/search_noop.go index c52ba75ab40..7253c706a9d 100644 --- a/pkg/registry/apis/iam/externalgroupmapping/search_noop.go +++ b/pkg/registry/apis/iam/externalgroupmapping/search_noop.go @@ -28,11 +28,40 @@ func (n *NoopSearchREST) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) { Path: "searchExternalGroupMappings", Spec: &spec3.PathProps{ - Get: &spec3.Operation{ + Post: &spec3.Operation{ OperationProps: spec3.OperationProps{ Description: "External Group Mapping search", Tags: []string{"Search"}, OperationId: "searchExternalGroupMappings", + RequestBody: &spec3.RequestBody{ + RequestBodyProps: spec3.RequestBodyProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "externalGroups": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Parameters: []*spec3.Parameter{ { ParameterProps: spec3.ParameterProps{ @@ -44,15 +73,6 @@ func (n *NoopSearchREST) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) Schema: spec.StringProperty(), }, }, - { - ParameterProps: spec3.ParameterProps{ - Name: "externalGroup", - In: "query", - Required: false, - Description: "External group name", - Schema: spec.StringProperty(), - }, - }, { ParameterProps: spec3.ParameterProps{ Name: "teamName", diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index f03b3c3369b..fad48e139aa 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -912,6 +912,139 @@ } ] }, + "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/searchExternalGroupMappings": { + "post": { + "tags": [ + "Search" + ], + "description": "External Group Mapping search", + "operationId": "searchExternalGroupMappings", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + }, + { + "name": "teamName", + "in": "query", + "description": "Team name", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "number of results to return", + "schema": { + "type": "integer", + "format": "int64" + }, + "example": 30 + }, + { + "name": "page", + "in": "query", + "description": "page number (starting from 1)", + "schema": { + "type": "integer", + "format": "int64" + }, + "example": 1 + }, + { + "name": "offset", + "in": "query", + "description": "number of results to skip", + "schema": { + "type": "integer", + "format": "int64" + }, + "example": 0 + }, + { + "name": "sort", + "in": "query", + "description": "sortable field", + "schema": { + "type": "string" + }, + "examples": { + "": { + "summary": "default sorting", + "value": "externalGroup" + }, + "-externalGroup": { + "summary": "externalGroup descending", + "value": "-externalGroup" + }, + "externalGroup": { + "summary": "externalGroup ascending", + "value": "externalGroup" + } + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "externalGroups": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "responses": { + "default": { + "description": "Default OK response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + } + } + } + } + } + } + }, "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/searchTeams": { "get": { "tags": [