implement the user teams handler

This commit is contained in:
Mihai Doarna
2026-01-14 12:17:51 +02:00
parent 748ce7ae26
commit c34242642f
6 changed files with 172 additions and 58 deletions
+1 -1
View File
@@ -38,9 +38,9 @@ userv0alpha1: userKind & {
"GET": {
response: {
#UserTeam: {
title: string
teamRef: v0alpha1.TeamRef
permission: v0alpha1.TeamPermission
external: bool
}
items: [...#UserTeam]
}
@@ -4,9 +4,9 @@ package v0alpha1
// +k8s:openapi-gen=true
type VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam struct {
Title string `json:"title"`
TeamRef TeamRef `json:"teamRef"`
Permission TeamPermission `json:"permission"`
External bool `json:"external"`
}
// NewVersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam creates a new VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam object.
+8 -8
View File
@@ -3219,13 +3219,6 @@ func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds6RoutesTeamsGETResponseUs
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"title": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"teamRef": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
@@ -3239,8 +3232,15 @@ func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds6RoutesTeamsGETResponseUs
Format: "",
},
},
"external": {
SchemaProps: spec.SchemaProps{
Default: false,
Type: []string{"boolean"},
Format: "",
},
},
},
Required: []string{"title", "teamRef", "permission"},
Required: []string{"teamRef", "permission", "external"},
},
},
Dependencies: []string{
+11 -1
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/logging"
@@ -16,6 +17,9 @@ import (
"github.com/grafana/grafana/apps/iam/pkg/reconcilers"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/authz"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
res "github.com/grafana/grafana/pkg/storage/unified/resource"
)
var appManifestData = app.ManifestData{
@@ -28,11 +32,15 @@ type InformerConfig struct {
}
type AppConfig struct {
Tracer trace.Tracer
ZanzanaClientCfg authz.ZanzanaClientConfig
InformerConfig InformerConfig
Namespace string
MetricsRegisterer prometheus.Registerer
LegacyStore legacy.LegacyIdentityStore
Dual dualwrite.Service
Features featuremgmt.FeatureToggles
Unified res.ResourceClient
}
func Provider(appCfg app.SpecificConfig) app.Provider {
@@ -89,6 +97,8 @@ func New(cfg app.Config) (app.App, error) {
logging.DefaultLogger.Info("FolderReconciler created")
userTeamsHandler := NewGetTeamsHandler(appSpecificConfig.Tracer, appSpecificConfig.Dual, nil, appSpecificConfig.Unified, appSpecificConfig.Features)
config := simple.AppConfig{
Name: cfg.ManifestData.AppName,
KubeConfig: cfg.KubeConfig,
@@ -111,7 +121,7 @@ func New(cfg app.Config) (app.App, error) {
{
Path: "teams",
Method: "GET",
}: GetTeamsHandler(appSpecificConfig.LegacyStore),
}: userTeamsHandler.Handle,
},
},
},
-47
View File
@@ -1,47 +0,0 @@
package app
import (
"context"
"encoding/json"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
)
// GetTeamsHandler handles requests for the GET /teams subresource route on User
func GetTeamsHandler(store legacy.LegacyIdentityStore) func(ctx context.Context, writer app.CustomRouteResponseWriter, req *app.CustomRouteRequest) error {
return func(ctx context.Context, writer app.CustomRouteResponseWriter, req *app.CustomRouteRequest) error {
userUID := req.ResourceIdentifier.Name
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return err
}
result, err := store.ListUserTeams(ctx, ns, legacy.ListUserTeamsQuery{
UserUID: userUID,
Pagination: common.PaginationFromListQuery(req.URL.Query()),
})
if err != nil {
return err
}
items := make([]v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam, len(result.Items))
for i, team := range result.Items {
items[i] = v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam{
Title: team.Name,
TeamRef: v0alpha1.TeamRef{Name: team.UID},
Permission: v0alpha1.TeamPermission(team.Permission.String()),
}
}
return json.NewEncoder(writer).Encode(v0alpha1.GetTeams{
GetTeamsBody: v0alpha1.GetTeamsBody{
Items: items,
},
})
}
}
+151
View File
@@ -0,0 +1,151 @@
package app
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strconv"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana-app-sdk/app"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
type GetTeamsHandler struct {
log log.Logger
client resourcepb.ResourceIndexClient
tracer trace.Tracer
features featuremgmt.FeatureToggles
}
func NewGetTeamsHandler(tracer trace.Tracer, dual dualwrite.Service, legacyTeamSearcher resourcepb.ResourceIndexClient, resourceClient resource.ResourceClient, features featuremgmt.FeatureToggles) *GetTeamsHandler {
searchClient := resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0alpha1.TeamBindingResourceInfo.GroupResource(), resourceClient, legacyTeamSearcher, features)
return &GetTeamsHandler{
client: searchClient,
log: log.New("grafana-apiserver.teams.search"),
tracer: tracer,
features: features,
}
}
func (h *GetTeamsHandler) Handle(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error {
ctx, span := h.tracer.Start(ctx, "user.teams")
defer span.End()
queryParams, err := url.ParseQuery(request.URL.RawQuery)
if err != nil {
return err
}
requester, err := identity.GetRequester(ctx)
if err != nil {
return fmt.Errorf("no identity found for request: %w", err)
}
limit := 50
offset := 0
page := 1
if queryParams.Has("limit") {
limit, _ = strconv.Atoi(queryParams.Get("limit"))
}
if queryParams.Has("offset") {
offset, _ = strconv.Atoi(queryParams.Get("offset"))
if offset > 0 {
page = (offset / limit) + 1
}
} else if queryParams.Has("page") {
page, _ = strconv.Atoi(queryParams.Get("page"))
offset = (page - 1) * limit
}
searchRequest := &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Group: iamv0alpha1.TeamBindingResourceInfo.GroupResource().Group,
Resource: iamv0alpha1.TeamBindingResourceInfo.GroupResource().Resource,
Namespace: requester.GetNamespace(),
},
},
Limit: int64(limit),
Offset: int64(offset),
Page: int64(page),
Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false",
Fields: []string{
resource.SEARCH_FIELD_PREFIX + "teamRef.name",
resource.SEARCH_FIELD_PREFIX + "permission",
resource.SEARCH_FIELD_PREFIX + "external",
},
}
result, err := h.client.Search(ctx, searchRequest)
if err != nil {
return err
}
searchResults, err := h.parseResults(result, searchRequest.Offset)
if err != nil {
return err
}
if err := json.NewEncoder(writer).Encode(searchResults); err != nil {
return err
}
return nil
}
func (h *GetTeamsHandler) parseResults(result *resourcepb.ResourceSearchResponse, offset int64) (iamv0alpha1.GetTeamsBody, error) {
if result == nil {
return iamv0alpha1.GetTeamsBody{}, nil
} else if result.Error != nil {
return iamv0alpha1.GetTeamsBody{}, fmt.Errorf("%d error searching: %s: %s", result.Error.Code, result.Error.Message, result.Error.Details)
} else if result.Results == nil {
return iamv0alpha1.GetTeamsBody{}, nil
}
teamRefIDX := -1
permissionIDX := -1
externalIDX := -1
for i, v := range result.Results.Columns {
if v == nil {
continue
}
switch v.Name {
case "teamRef.name":
teamRefIDX = i
case "permission":
permissionIDX = i
case "external":
externalIDX = i
}
}
body := iamv0alpha1.GetTeamsBody{
Items: make([]iamv0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam, len(result.Results.Rows)),
}
for i, row := range result.Results.Rows {
if len(row.Cells) != len(result.Results.Columns) {
return iamv0alpha1.GetTeamsBody{}, fmt.Errorf("error parsing team binding response: mismatch number of columns and cells")
}
body.Items[i] = iamv0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam{
TeamRef: iamv0alpha1.TeamRef{Name: string(row.Cells[teamRefIDX])},
Permission: iamv0alpha1.TeamPermission(string(row.Cells[permissionIDX])),
External: string(row.Cells[externalIDX]) == "true",
}
}
return body, nil
}