K8s/IAM: Use raw handler for display (not rest.Connector) (#99898)

This commit is contained in:
Ryan McKinley
2025-02-03 14:24:35 +03:00
committed by GitHub
parent 2aa78139c4
commit b636b81b16
7 changed files with 270 additions and 262 deletions
+132 -101
View File
@@ -1,123 +1,154 @@
package user
import (
"context"
"encoding/json"
"net/http"
"strconv"
"strings"
errorsK8s "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
claims "github.com/grafana/authlib/types"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/api/dtos"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
iam "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errhttp"
)
type LegacyDisplayREST struct {
store legacy.LegacyIdentityStore
}
var (
_ rest.Storage = (*LegacyDisplayREST)(nil)
_ rest.SingularNameProvider = (*LegacyDisplayREST)(nil)
_ rest.Connecter = (*LegacyDisplayREST)(nil)
_ rest.Scoper = (*LegacyDisplayREST)(nil)
_ rest.StorageMetadata = (*LegacyDisplayREST)(nil)
)
func NewLegacyDisplayREST(store legacy.LegacyIdentityStore) *LegacyDisplayREST {
return &LegacyDisplayREST{store}
}
func (r *LegacyDisplayREST) New() runtime.Object {
return &iamv0.DisplayList{}
}
func (r *LegacyDisplayREST) Destroy() {}
func (r *LegacyDisplayREST) NamespaceScoped() bool {
return true
}
func (r *LegacyDisplayREST) GetSingularName() string {
return "display"
}
func (r *LegacyDisplayREST) ProducesMIMETypes(verb string) []string {
return []string{"application/json"}
}
func (r *LegacyDisplayREST) ProducesObject(verb string) any {
return &iamv0.DisplayList{}
}
func (r *LegacyDisplayREST) ConnectMethods() []string {
return []string{http.MethodGet}
}
func (r *LegacyDisplayREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, "" // true means you can use the trailing path as a variable
func (r *LegacyDisplayREST) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *builder.APIRoutes {
listSchema := defs["github.com/grafana/grafana/pkg/apis/iam/v0alpha1.DisplayList"].Schema
displaySchema := defs["github.com/grafana/grafana/pkg/apis/iam/v0alpha1.Display"].Schema
identitySchema := defs["github.com/grafana/grafana/pkg/apis/iam/v0alpha1.IdentityRef"].Schema
listSchema.Properties["display"].Items.Schema = &displaySchema // not sure why this is lost
displaySchema.Properties["identity"] = identitySchema // not sure why this is lost
return &builder.APIRoutes{
Namespace: []builder.APIRouteHandler{
{
Path: "display",
Spec: &spec3.PathProps{
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
OperationId: "getDisplayMapping", // This is used by RTK client generator
Tags: []string{"Display"},
Description: "Show user display information",
Parameters: []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "namespace",
In: "path",
Required: true,
Example: "default",
Description: "workspace",
Schema: spec.StringProperty(),
},
},
{
ParameterProps: spec3.ParameterProps{
Name: "key",
In: "query",
Description: "Display keys",
Required: true,
Example: "user:u000000001",
Schema: spec.ArrayProperty(spec.StringProperty()),
// Style: "form",
Explode: true,
},
},
},
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: &listSchema,
},
},
},
},
},
},
},
},
},
},
},
Handler: r.handleDisplay,
},
},
}
}
// This will always have an empty app url
var fakeCfgForGravatar = &setting.Cfg{}
func (r *LegacyDisplayREST) Connect(ctx context.Context, name string, _ runtime.Object, responder rest.Responder) (http.Handler, error) {
// See: /pkg/services/apiserver/builder/helper.go#L34
// The name is set with a rewriter hack
if name != "name" {
return nil, errorsK8s.NewNotFound(schema.GroupResource{}, name)
func (r *LegacyDisplayREST) handleDisplay(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
user, ok := authlib.AuthInfoFrom(ctx)
if !ok {
errhttp.Write(ctx, apierrors.NewUnauthorized("missing auth info"), w)
return
}
ns, err := request.NamespaceInfoFrom(ctx, true)
ns, err := authlib.ParseNamespace(user.GetNamespace())
if err != nil {
return nil, err
errhttp.Write(ctx, err, w)
return
}
keys := parseKeys(req.URL.Query()["key"])
users, err := r.store.ListDisplay(ctx, ns, legacy.ListDisplayQuery{
OrgID: ns.OrgID,
UIDs: keys.uids,
IDs: keys.ids,
})
if err != nil {
errhttp.Write(ctx, err, w)
return
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
keys := parseKeys(req.URL.Query()["key"])
users, err := r.store.ListDisplay(ctx, ns, legacy.ListDisplayQuery{
OrgID: ns.OrgID,
UIDs: keys.uids,
IDs: keys.ids,
})
if err != nil {
responder.Error(err)
return
rsp := &iam.DisplayList{
Keys: keys.keys,
InvalidKeys: keys.invalid,
Items: make([]iam.Display, 0, len(users.Users)+len(keys.disp)+1),
}
for _, user := range users.Users {
disp := iam.Display{
Identity: iam.IdentityRef{
Type: authlib.TypeUser,
Name: user.UID,
},
DisplayName: user.NameOrFallback(),
InternalID: user.ID, // nolint:staticcheck
}
if user.IsServiceAccount {
disp.Identity.Type = authlib.TypeServiceAccount
}
disp.AvatarURL = dtos.GetGravatarUrlWithDefault(fakeCfgForGravatar, user.Email, disp.DisplayName)
rsp.Items = append(rsp.Items, disp)
}
rsp := &iamv0.DisplayList{
Keys: keys.keys,
InvalidKeys: keys.invalid,
Items: make([]iamv0.Display, 0, len(users.Users)+len(keys.disp)+1),
}
for _, user := range users.Users {
disp := iamv0.Display{
Identity: iamv0.IdentityRef{
Type: claims.TypeUser,
Name: user.UID,
},
DisplayName: user.NameOrFallback(),
InternalID: user.ID,
}
if user.IsServiceAccount {
disp.Identity.Type = claims.TypeServiceAccount
}
disp.AvatarURL = dtos.GetGravatarUrlWithDefault(fakeCfgForGravatar, user.Email, disp.DisplayName)
rsp.Items = append(rsp.Items, disp)
}
// Append the constants here
if len(keys.disp) > 0 {
rsp.Items = append(rsp.Items, keys.disp...)
}
// Append the constants here
if len(keys.disp) > 0 {
rsp.Items = append(rsp.Items, keys.disp...)
}
responder.Object(200, rsp)
}), nil
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(rsp)
}
type dispKeys struct {
@@ -127,7 +158,7 @@ type dispKeys struct {
invalid []string
// For terminal keys, this is a constant
disp []iamv0.Display
disp []iam.Display
}
func parseKeys(req []string) dispKeys {
@@ -139,7 +170,7 @@ func parseKeys(req []string) dispKeys {
for _, key := range req {
idx := strings.Index(key, ":")
if idx > 0 {
t, err := claims.ParseType(key[0:idx])
t, err := authlib.ParseType(key[0:idx])
if err != nil {
keys.invalid = append(keys.invalid, key)
continue
@@ -147,18 +178,18 @@ func parseKeys(req []string) dispKeys {
key = key[idx+1:]
switch t {
case claims.TypeAnonymous:
keys.disp = append(keys.disp, iamv0.Display{
Identity: iamv0.IdentityRef{
case authlib.TypeAnonymous:
keys.disp = append(keys.disp, iam.Display{
Identity: iam.IdentityRef{
Type: t,
},
DisplayName: "Anonymous",
AvatarURL: dtos.GetGravatarUrl(fakeCfgForGravatar, string(t)),
})
continue
case claims.TypeAPIKey:
keys.disp = append(keys.disp, iamv0.Display{
Identity: iamv0.IdentityRef{
case authlib.TypeAPIKey:
keys.disp = append(keys.disp, iam.Display{
Identity: iam.IdentityRef{
Type: t,
Name: key,
},
@@ -166,9 +197,9 @@ func parseKeys(req []string) dispKeys {
AvatarURL: dtos.GetGravatarUrl(fakeCfgForGravatar, string(t)),
})
continue
case claims.TypeProvisioning:
keys.disp = append(keys.disp, iamv0.Display{
Identity: iamv0.IdentityRef{
case authlib.TypeProvisioning:
keys.disp = append(keys.disp, iam.Display{
Identity: iam.IdentityRef{
Type: t,
},
DisplayName: "Provisioning",
@@ -184,9 +215,9 @@ func parseKeys(req []string) dispKeys {
id, err := strconv.ParseInt(key, 10, 64)
if err == nil {
if id == 0 {
keys.disp = append(keys.disp, iamv0.Display{
Identity: iamv0.IdentityRef{
Type: claims.TypeUser,
keys.disp = append(keys.disp, iam.Display{
Identity: iam.IdentityRef{
Type: authlib.TypeUser,
Name: key,
},
DisplayName: "System admin",
@@ -15,7 +15,6 @@ import (
var (
_ rest.Storage = (*LegacyUserTeamREST)(nil)
_ rest.Scoper = (*LegacyUserTeamREST)(nil)
_ rest.StorageMetadata = (*LegacyUserTeamREST)(nil)
_ rest.Connecter = (*LegacyUserTeamREST)(nil)
)
@@ -36,11 +35,6 @@ func (s *LegacyUserTeamREST) New() runtime.Object {
// Destroy implements rest.Storage.
func (s *LegacyUserTeamREST) Destroy() {}
// NamespaceScoped implements rest.Scoper.
func (s *LegacyUserTeamREST) NamespaceScoped() bool {
return true
}
// ProducesMIMETypes implements rest.StorageMetadata.
func (s *LegacyUserTeamREST) ProducesMIMETypes(verb string) []string {
return []string{"application/json"}