Compare commits

...

5 Commits

Author SHA1 Message Date
Mihai Doarna
c34242642f implement the user teams handler 2026-01-14 12:17:51 +02:00
Mihai Doarna
748ce7ae26 go mod tidy 2026-01-12 17:26:08 +02:00
Mihai Doarna
108cb3d3b9 Merge branch 'main' into dmihai/user-teams-endpoint 2026-01-12 15:49:38 +02:00
Mihai Doarna
9d15337a06 implement GetTeamsHandler() for legacy store 2026-01-12 15:39:11 +02:00
Mihai Doarna
8dbfd105dd define the /users/[id]/teams endpoint 2026-01-09 15:38:17 +02:00
11 changed files with 570 additions and 25 deletions

View File

@@ -48,6 +48,8 @@ replace github.com/grafana/grafana/apps/collections => ../collections
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
replace github.com/grafana/grafana/pkg/semconv => ../../pkg/semconv
require (
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
github.com/grafana/grafana-app-sdk v0.48.7
@@ -94,7 +96,7 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect
github.com/Yiling-J/theine-go v0.6.2 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
@@ -235,8 +237,9 @@ require (
github.com/grafana/grafana/apps/secret v0.0.0 // indirect
github.com/grafana/grafana/pkg/aggregator v0.0.0 // indirect
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
github.com/grafana/grafana/pkg/plugins v0.0.0-20260109132829-9cd811b9e64c // indirect
github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
github.com/grafana/grafana/pkg/semconv v0.0.0 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect

View File

@@ -122,8 +122,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+
github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw=
@@ -186,8 +186,8 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=
github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
@@ -280,8 +280,6 @@ github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 h1:Pwbxovp
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6/go.mod h1:Z4xLt5mXspLKjBV92i165wAJ/3T6TIv4n7RtIS8pWV0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
@@ -871,10 +869,10 @@ github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6B
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k=
github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDPMdshj3QMvpXn+wK4T8awF9n2sd8i4YRiGqX2xTvg=
github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo=
github.com/grafana/grafana/pkg/plugins v0.0.0-20260109132829-9cd811b9e64c h1:BBEquSCiTGt5AAOomTQBMTCCZBnhrdfzS2pUtfhhtWE=
github.com/grafana/grafana/pkg/plugins v0.0.0-20260109132829-9cd811b9e64c/go.mod h1:b9WxBFbMdf6pDxy90WRFHMyBl1/o8xY86SqnLLLN/yQ=
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:2HRzUK/xQEYc+8d5If/XSusMcaYq9IptnBSHACiQcOQ=
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 h1:NznuPwItog+rwdVg8hAuGKP29ndRSzJAwhxKldkP8oQ=
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32/go.mod h1:796sq+UcONnSlzA3RtlBZ+b/hrerkZXiEmO8oMjyRwY=
github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0=

View File

@@ -33,19 +33,18 @@ userv0alpha1: userKind & {
lastSeenAt: int64 | 0
}
}
// TODO: Uncomment when the custom routes implementation is done
// routes: {
// "/teams": {
// "GET": {
// response: {
// #UserTeam: {
// title: string
// teamRef: v0alpha1.TeamRef
// permission: v0alpha1.TeamPermission
// }
// items: [...#UserTeam]
// }
// }
// }
// }
routes: {
"/teams": {
"GET": {
response: {
#UserTeam: {
teamRef: v0alpha1.TeamRef
permission: v0alpha1.TeamPermission
external: bool
}
items: [...#UserTeam]
}
}
}
}
}

View File

@@ -2,6 +2,9 @@ package v0alpha1
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -97,3 +100,24 @@ func (c *UserClient) UpdateStatus(ctx context.Context, identifier resource.Ident
func (c *UserClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
return c.client.Delete(ctx, identifier, opts)
}
type GetTeamsRequest struct {
Headers http.Header
}
func (c *UserClient) GetTeams(ctx context.Context, identifier resource.Identifier, request GetTeamsRequest) (*GetTeams, error) {
resp, err := c.client.SubresourceRequest(ctx, identifier, resource.CustomRouteRequestOptions{
Path: "/teams",
Verb: "GET",
Headers: request.Headers,
})
if err != nil {
return nil, err
}
cast := GetTeams{}
err = json.Unmarshal(resp, &cast)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal response bytes into GetTeams: %w", err)
}
return &cast, nil
}

View File

@@ -0,0 +1,48 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
// +k8s:openapi-gen=true
type VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam struct {
TeamRef TeamRef `json:"teamRef"`
Permission TeamPermission `json:"permission"`
External bool `json:"external"`
}
// NewVersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam creates a new VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam object.
func NewVersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam() *VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam {
return &VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam{
TeamRef: *NewTeamRef(),
}
}
// +k8s:openapi-gen=true
type TeamRef struct {
// Name is the unique identifier for a team.
Name string `json:"name"`
}
// NewTeamRef creates a new TeamRef object.
func NewTeamRef() *TeamRef {
return &TeamRef{}
}
// +k8s:openapi-gen=true
type TeamPermission string
const (
TeamPermissionAdmin TeamPermission = "admin"
TeamPermissionMember TeamPermission = "member"
)
// +k8s:openapi-gen=true
type GetTeamsBody struct {
Items []VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam `json:"items"`
}
// NewGetTeamsBody creates a new GetTeamsBody object.
func NewGetTeamsBody() *GetTeamsBody {
return &GetTeamsBody{
Items: []VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam{},
}
}

View File

@@ -0,0 +1,37 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// +k8s:openapi-gen=true
type GetTeams struct {
metav1.TypeMeta `json:",inline"`
GetTeamsBody `json:",inline"`
}
func NewGetTeams() *GetTeams {
return &GetTeams{}
}
func (t *GetTeamsBody) DeepCopyInto(dst *GetTeamsBody) {
_ = resource.CopyObjectInto(dst, t)
}
func (o *GetTeams) DeepCopyObject() runtime.Object {
dst := NewGetTeams()
o.DeepCopyInto(dst)
return dst
}
func (o *GetTeams) DeepCopyInto(dst *GetTeams) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.GetTeamsBody.DeepCopyInto(&dst.GetTeamsBody)
}
var _ runtime.Object = NewGetTeams()

View File

@@ -22,6 +22,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeams": schema_pkg_apis_iam_v0alpha1_GetSearchTeams(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeamsBody": schema_pkg_apis_iam_v0alpha1_GetSearchTeamsBody(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchUsers": schema_pkg_apis_iam_v0alpha1_GetSearchUsers(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetTeams": schema_pkg_apis_iam_v0alpha1_GetTeams(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetTeamsBody": schema_pkg_apis_iam_v0alpha1_GetTeamsBody(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": schema_pkg_apis_iam_v0alpha1_GlobalRole(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBinding": schema_pkg_apis_iam_v0alpha1_GlobalRoleBinding(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingList": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingList(ref),
@@ -69,6 +71,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject": schema_pkg_apis_iam_v0alpha1_TeamBindingspecSubject(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamBindingstatusOperatorState(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamList": schema_pkg_apis_iam_v0alpha1_TeamList(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef": schema_pkg_apis_iam_v0alpha1_TeamRef(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": schema_pkg_apis_iam_v0alpha1_TeamSpec(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus": schema_pkg_apis_iam_v0alpha1_TeamStatus(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamstatusOperatorState(ref),
@@ -77,6 +80,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserList": schema_pkg_apis_iam_v0alpha1_UserList(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec": schema_pkg_apis_iam_v0alpha1_UserSpec(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": schema_pkg_apis_iam_v0alpha1_UserStatus(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit(ref),
}
@@ -745,6 +749,76 @@ func schema_pkg_apis_iam_v0alpha1_GetSearchUsers(ref common.ReferenceCallback) c
}
}
func schema_pkg_apis_iam_v0alpha1_GetTeams(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
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{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
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{"string"},
Format: "",
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam"},
}
}
func schema_pkg_apis_iam_v0alpha1_GetTeamsBody(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam"},
}
}
func schema_pkg_apis_iam_v0alpha1_GlobalRole(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -2721,6 +2795,27 @@ func schema_pkg_apis_iam_v0alpha1_TeamList(ref common.ReferenceCallback) common.
}
}
func schema_pkg_apis_iam_v0alpha1_TeamRef(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Description: "Name is the unique identifier for a team.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"name"},
},
},
}
}
func schema_pkg_apis_iam_v0alpha1_TeamSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -3118,6 +3213,41 @@ func schema_pkg_apis_iam_v0alpha1_UserStatus(ref common.ReferenceCallback) commo
}
}
func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"teamRef": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef"),
},
},
"permission": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"external": {
SchemaProps: spec.SchemaProps{
Default: false,
Type: []string{"boolean"},
Format: "",
},
},
},
Required: []string{"teamRef", "permission", "external"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef"},
}
}
func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

View File

@@ -74,6 +74,65 @@ var appManifestData = app.ManifestData{
Plural: "Users",
Scope: "Namespaced",
Conversion: false,
Routes: map[string]spec3.PathProps{
"/teams": {
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
OperationId: "getTeams",
Responses: &spec3.Responses{
ResponsesProps: spec3.ResponsesProps{
Default: &spec3.Response{
ResponseProps: spec3.ResponseProps{
Description: "Default OK response",
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"apiVersion": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
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",
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/components/schemas/getTeamsUserTeam"),
}},
},
},
},
"kind": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
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",
},
},
},
Required: []string{
"items",
"apiVersion",
"kind",
},
}},
}},
},
},
},
}},
},
},
},
},
},
{
@@ -544,6 +603,8 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist
}
var customRouteToGoResponseType = map[string]any{
"v0alpha1|User|teams|GET": v0alpha1.GetTeams{},
"v0alpha1|Team|groups|GET": v0alpha1.GetGroups{},
"v0alpha1||<namespace>/searchTeams|GET": v0alpha1.GetSearchTeams{},

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"
@@ -12,8 +13,13 @@ import (
"github.com/grafana/grafana-app-sdk/resource"
"github.com/grafana/grafana-app-sdk/simple"
foldersKind "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"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{
@@ -26,10 +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 {
@@ -86,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,
@@ -101,6 +114,17 @@ func New(cfg app.Config) (app.App, error) {
},
},
},
ManagedKinds: []simple.AppManagedKind{
{
Kind: v0alpha1.UserKind(),
CustomRoutes: simple.AppCustomRouteHandlers{
{
Path: "teams",
Method: "GET",
}: userTeamsHandler.Handle,
},
},
},
UnmanagedKinds: []simple.AppUnmanagedKind{
{
Kind: foldersKind.FolderKind(),

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
}

View File

@@ -6776,6 +6776,42 @@
}
}
},
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetTeams": {
"type": "object",
"required": [
"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"
}
}
},
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetTeamsBody": {
"type": "object",
"required": [
"items"
],
"properties": {
"items": {
"type": "array",
"items": {
"default": {}
}
}
}
},
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": {
"type": "object",
"required": [
@@ -7890,6 +7926,19 @@
}
}
},
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"description": "Name is the unique identifier for a team.",
"type": "string",
"default": ""
}
}
},
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": {
"type": "object",
"required": [
@@ -8134,6 +8183,27 @@
}
}
},
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam": {
"type": "object",
"required": [
"title",
"teamRef",
"permission"
],
"properties": {
"permission": {
"type": "string",
"default": ""
},
"teamRef": {
"default": {}
},
"title": {
"type": "string",
"default": ""
}
}
},
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": {
"type": "object",
"required": [