package iam import ( "encoding/json" "fmt" "net/http" "net/url" "strconv" "go.opentelemetry.io/otel/trace" common "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" 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/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" teamsearch "github.com/grafana/grafana/pkg/services/team/search" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/search/builders" "github.com/grafana/grafana/pkg/util/errhttp" ) type TeamSearchHandler struct { log log.Logger client resourcepb.ResourceIndexClient tracer trace.Tracer features featuremgmt.FeatureToggles } func NewTeamSearchHandler(tracer trace.Tracer, dual dualwrite.Service, legacyTeamSearcher resourcepb.ResourceIndexClient, resourceClient resource.ResourceClient, features featuremgmt.FeatureToggles) *TeamSearchHandler { searchClient := resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0alpha1.TeamResourceInfo.GroupResource(), resourceClient, legacyTeamSearcher, features) return &TeamSearchHandler{ client: searchClient, log: log.New("grafana-apiserver.teams.search"), tracer: tracer, features: features, } } func (s *TeamSearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *builder.APIRoutes { searchResults := defs["github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeams"].Schema return &builder.APIRoutes{ Namespace: []builder.APIRouteHandler{ { Path: "searchTeams", Spec: &spec3.PathProps{ Get: &spec3.Operation{ OperationProps: spec3.OperationProps{ Tags: []string{"Search"}, Description: "Team search", Parameters: []*spec3.Parameter{ { ParameterProps: spec3.ParameterProps{ Name: "namespace", In: "path", Required: true, Example: "default", Description: "workspace", Schema: spec.StringProperty(), }, }, { ParameterProps: spec3.ParameterProps{ Name: "query", In: "query", Description: "team name query string", Required: false, Schema: spec.StringProperty(), }, }, { ParameterProps: spec3.ParameterProps{ Name: "limit", In: "query", Description: "limit the number of results", Required: false, Schema: spec.Int64Property(), }, }, { ParameterProps: spec3.ParameterProps{ Name: "offset", In: "query", Description: "start the query at the given offset", Required: false, Schema: spec.Int64Property(), }, }, { ParameterProps: spec3.ParameterProps{ Name: "page", In: "query", Description: "page number to start from", Required: false, Schema: spec.Int64Property(), }, }, }, Responses: &spec3.Responses{ ResponsesProps: spec3.ResponsesProps{ StatusCodeResponses: map[int]*spec3.Response{ 200: { ResponseProps: spec3.ResponseProps{ Content: map[string]*spec3.MediaType{ "application/json": { MediaTypeProps: spec3.MediaTypeProps{ Schema: &searchResults, }, }, }, }, }, }, }, }, }, }, }, Handler: s.DoTeamSearch, }, }, } } func (s *TeamSearchHandler) DoTeamSearch(w http.ResponseWriter, r *http.Request) { ctx, span := s.tracer.Start(r.Context(), "team.search") defer span.End() queryParams, err := url.ParseQuery(r.URL.RawQuery) if err != nil { errhttp.Write(ctx, err, w) return } requester, err := identity.GetRequester(ctx) if err != nil { errhttp.Write(ctx, fmt.Errorf("no identity found for request: %w", err), w) return } 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.TeamResourceInfo.GroupResource().Group, Resource: iamv0alpha1.TeamResourceInfo.GroupResource().Resource, Namespace: requester.GetNamespace(), }, }, Query: queryParams.Get("query"), Limit: int64(limit), Offset: int64(offset), Page: int64(page), Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false", Fields: []string{ resource.SEARCH_FIELD_TITLE, resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_EMAIL, resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_PROVISIONED, resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_EXTERNAL_UID, }, } result, err := s.client.Search(ctx, searchRequest) if err != nil { errhttp.Write(ctx, err, w) return } searchResults, err := teamsearch.ParseResults(result, searchRequest.Offset) if err != nil { errhttp.Write(ctx, err, w) return } if err := s.write(w, searchResults); err != nil { s.log.Error("failed to write team search results", "error", err) errhttp.Write(ctx, err, w) return } } func (s *TeamSearchHandler) write(w http.ResponseWriter, obj any) error { w.Header().Set("Content-Type", "application/json") return json.NewEncoder(w).Encode(obj) }