diff --git a/.betterer.results b/.betterer.results index 07d8e8267e8..cbb8cc5c8c0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -7332,9 +7332,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], - "public/app/types/teams.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`TeamDTO\`)", "0"] - ], "public/app/types/unified-alerting-dto.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/kinds/team/team_kind.cue b/kinds/team/team_kind.cue deleted file mode 100644 index 08237211a29..00000000000 --- a/kinds/team/team_kind.cue +++ /dev/null @@ -1,17 +0,0 @@ -package kind - -name: "Team" -maturity: "merged" -description: "A team is a named grouping of Grafana users to which access control rules may be assigned." - -lineage: schemas: [{ - version: [0, 0] - schema: { - spec: { - // Name of the team. - name: string - // Email of the team. - email?: string - } @cuetsy(kind="interface") - } -}] diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index 215c19e1098..2e02d765559 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -145,6 +145,3 @@ export type { BuiltinRoleRef, RoleBindingSubject } from './raw/rolebinding/x/rolebinding_types.gen'; - -// Raw generated types from Team kind. -export type { Team } from './raw/team/x/team_types.gen'; diff --git a/packages/grafana-schema/src/raw/team/x/team_types.gen.ts b/packages/grafana-schema/src/raw/team/x/team_types.gen.ts deleted file mode 100644 index 4f136372c37..00000000000 --- a/packages/grafana-schema/src/raw/team/x/team_types.gen.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// TSTypesJenny -// LatestMajorsOrXJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -export interface Team { - /** - * Email of the team. - */ - email?: string; - /** - * Name of the team. - */ - name: string; -} diff --git a/pkg/apimachinery/apis/identity/v0alpha1/doc.go b/pkg/apimachinery/apis/identity/v0alpha1/doc.go new file mode 100644 index 00000000000..5aaa55b2731 --- /dev/null +++ b/pkg/apimachinery/apis/identity/v0alpha1/doc.go @@ -0,0 +1,6 @@ +// +k8s:deepcopy-gen=package +// +k8s:openapi-gen=true +// +k8s:defaulter-gen=TypeMeta +// +groupName=identity.grafana.app + +package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/identity/v0alpha1" diff --git a/pkg/apimachinery/apis/identity/v0alpha1/register.go b/pkg/apimachinery/apis/identity/v0alpha1/register.go new file mode 100644 index 00000000000..f8aafa213b4 --- /dev/null +++ b/pkg/apimachinery/apis/identity/v0alpha1/register.go @@ -0,0 +1,68 @@ +package v0alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +const ( + GROUP = "identity.grafana.app" + VERSION = "v0alpha1" + APIVERSION = GROUP + "/" + VERSION +) + +var UserResourceInfo = common.NewResourceInfo(GROUP, VERSION, + "users", "user", "User", + func() runtime.Object { return &User{} }, + func() runtime.Object { return &UserList{} }, +) + +var TeamResourceInfo = common.NewResourceInfo(GROUP, VERSION, + "teams", "team", "Team", + func() runtime.Object { return &Team{} }, + func() runtime.Object { return &TeamList{} }, +) + +var ServiceAccountResourceInfo = common.NewResourceInfo(GROUP, VERSION, + "serviceaccounts", "serviceaccount", "ServiceAccount", + func() runtime.Object { return &ServiceAccount{} }, + func() runtime.Object { return &ServiceAccountList{} }, +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} + + // SchemaBuilder is used by standard codegen + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + localSchemeBuilder.Register(func(s *runtime.Scheme) error { + return AddKnownTypes(s, VERSION) + }) +} + +// Adds the list of known types to the given scheme. +func AddKnownTypes(scheme *runtime.Scheme, version string) error { + scheme.AddKnownTypes(schema.GroupVersion{Group: GROUP, Version: version}, + &User{}, + &UserList{}, + &ServiceAccount{}, + &ServiceAccountList{}, + &Team{}, + &TeamList{}, + &IdentityDisplayList{}, + ) + // metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/apimachinery/apis/identity/v0alpha1/types.go b/pkg/apimachinery/apis/identity/v0alpha1/types.go new file mode 100644 index 00000000000..75fd33e9b73 --- /dev/null +++ b/pkg/apimachinery/apis/identity/v0alpha1/types.go @@ -0,0 +1,91 @@ +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type User struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec UserSpec `json:"spec,omitempty"` +} + +type UserSpec struct { + Name string `json:"name,omitempty"` + Login string `json:"login,omitempty"` + Email string `json:"email,omitempty"` + EmailVerified bool `json:"emailVerified,omitempty"` + Disabled bool `json:"disabled,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type UserList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []User `json:"items,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type Team struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TeamSpec `json:"spec,omitempty"` +} + +type TeamSpec struct { + Title string `json:"name,omitempty"` + Email string `json:"email,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type TeamList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []Team `json:"items,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ServiceAccount struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServiceAccountSpec `json:"spec,omitempty"` +} + +type ServiceAccountSpec struct { + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + EmailVerified bool `json:"emailVerified,omitempty"` + Disabled bool `json:"disabled,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ServiceAccountList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []ServiceAccount `json:"items,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type IdentityDisplayList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []IdentityDisplay `json:"items,omitempty"` +} + +type IdentityDisplay struct { + IdentityType string `json:"type"` // The namespaced UID, eg `user|api-key|...` + UID string `json:"uid"` // The namespaced UID, eg `xyz` + Display string `json:"display"` + AvatarURL string `json:"avatarURL,omitempty"` + + // Legacy internal ID -- usage of this value should be phased out + LegacyID int64 `json:"legacyId,omitempty"` +} diff --git a/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.deepcopy.go b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..70fbea9caa6 --- /dev/null +++ b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.deepcopy.go @@ -0,0 +1,287 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IdentityDisplay) DeepCopyInto(out *IdentityDisplay) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IdentityDisplay. +func (in *IdentityDisplay) DeepCopy() *IdentityDisplay { + if in == nil { + return nil + } + out := new(IdentityDisplay) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IdentityDisplayList) DeepCopyInto(out *IdentityDisplayList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IdentityDisplay, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IdentityDisplayList. +func (in *IdentityDisplayList) DeepCopy() *IdentityDisplayList { + if in == nil { + return nil + } + out := new(IdentityDisplayList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IdentityDisplayList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccount) DeepCopyInto(out *ServiceAccount) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccount. +func (in *ServiceAccount) DeepCopy() *ServiceAccount { + if in == nil { + return nil + } + out := new(ServiceAccount) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceAccount) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountList) DeepCopyInto(out *ServiceAccountList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServiceAccount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountList. +func (in *ServiceAccountList) DeepCopy() *ServiceAccountList { + if in == nil { + return nil + } + out := new(ServiceAccountList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceAccountList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountSpec) DeepCopyInto(out *ServiceAccountSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountSpec. +func (in *ServiceAccountSpec) DeepCopy() *ServiceAccountSpec { + if in == nil { + return nil + } + out := new(ServiceAccountSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Team) DeepCopyInto(out *Team) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Team. +func (in *Team) DeepCopy() *Team { + if in == nil { + return nil + } + out := new(Team) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Team) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TeamList) DeepCopyInto(out *TeamList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Team, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamList. +func (in *TeamList) DeepCopy() *TeamList { + if in == nil { + return nil + } + out := new(TeamList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TeamList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TeamSpec) DeepCopyInto(out *TeamSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamSpec. +func (in *TeamSpec) DeepCopy() *TeamSpec { + if in == nil { + return nil + } + out := new(TeamSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *User) DeepCopyInto(out *User) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User. +func (in *User) DeepCopy() *User { + if in == nil { + return nil + } + out := new(User) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *User) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserList) DeepCopyInto(out *UserList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]User, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserList. +func (in *UserList) DeepCopy() *UserList { + if in == nil { + return nil + } + out := new(UserList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *UserList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserSpec) DeepCopyInto(out *UserSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserSpec. +func (in *UserSpec) DeepCopy() *UserSpec { + if in == nil { + return nil + } + out := new(UserSpec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.defaults.go b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.defaults.go new file mode 100644 index 00000000000..238fc2f4edc --- /dev/null +++ b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.defaults.go @@ -0,0 +1,19 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by defaulter-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + return nil +} diff --git a/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi.go b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi.go new file mode 100644 index 00000000000..ac83849d03b --- /dev/null +++ b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi.go @@ -0,0 +1,490 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by openapi-gen. DO NOT EDIT. + +// This file was autogenerated by openapi-gen. Do not edit it manually! + +package v0alpha1 + +import ( + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.IdentityDisplay": schema_apimachinery_apis_identity_v0alpha1_IdentityDisplay(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.IdentityDisplayList": schema_apimachinery_apis_identity_v0alpha1_IdentityDisplayList(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccount": schema_apimachinery_apis_identity_v0alpha1_ServiceAccount(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccountList": schema_apimachinery_apis_identity_v0alpha1_ServiceAccountList(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccountSpec": schema_apimachinery_apis_identity_v0alpha1_ServiceAccountSpec(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.Team": schema_apimachinery_apis_identity_v0alpha1_Team(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.TeamList": schema_apimachinery_apis_identity_v0alpha1_TeamList(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.TeamSpec": schema_apimachinery_apis_identity_v0alpha1_TeamSpec(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.User": schema_apimachinery_apis_identity_v0alpha1_User(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.UserList": schema_apimachinery_apis_identity_v0alpha1_UserList(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.UserSpec": schema_apimachinery_apis_identity_v0alpha1_UserSpec(ref), + } +} + +func schema_apimachinery_apis_identity_v0alpha1_IdentityDisplay(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "The namespaced UID, eg `user|api-key|...`", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "display": { + SchemaProps: spec.SchemaProps{ + Description: "The namespaced UID, eg `xyz`", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "avatarURL": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "legacyId": { + SchemaProps: spec.SchemaProps{ + Description: "Legacy internal ID -- usage of this value should be phased out", + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + Required: []string{"type", "uid", "display"}, + }, + }, + } +} + +func schema_apimachinery_apis_identity_v0alpha1_IdentityDisplayList(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: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "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/pkg/apimachinery/apis/identity/v0alpha1.IdentityDisplay"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.IdentityDisplay", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_apimachinery_apis_identity_v0alpha1_ServiceAccount(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: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccountSpec"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccountSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_apimachinery_apis_identity_v0alpha1_ServiceAccountList(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: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "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/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccount"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccount", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_apimachinery_apis_identity_v0alpha1_ServiceAccountSpec(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{ + Type: []string{"string"}, + Format: "", + }, + }, + "email": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "emailVerified": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "disabled": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_apimachinery_apis_identity_v0alpha1_Team(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: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.TeamSpec"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.TeamSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_apimachinery_apis_identity_v0alpha1_TeamList(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: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "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/pkg/apimachinery/apis/identity/v0alpha1.Team"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.Team", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_apimachinery_apis_identity_v0alpha1_TeamSpec(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{ + Type: []string{"string"}, + Format: "", + }, + }, + "email": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_apimachinery_apis_identity_v0alpha1_User(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: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.UserSpec"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.UserSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_apimachinery_apis_identity_v0alpha1_UserList(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: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "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/pkg/apimachinery/apis/identity/v0alpha1.User"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.User", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_apimachinery_apis_identity_v0alpha1_UserSpec(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{ + Type: []string{"string"}, + Format: "", + }, + }, + "login": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "email": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "emailVerified": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "disabled": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + } +} diff --git a/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list new file mode 100644 index 00000000000..bab555666f5 --- /dev/null +++ b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -0,0 +1,3 @@ +API rule violation: names_match,github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1,IdentityDisplay,IdentityType +API rule violation: names_match,github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1,IdentityDisplay,LegacyID +API rule violation: names_match,github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1,TeamSpec,Title diff --git a/pkg/kinds/team/team_gen.go b/pkg/kinds/team/team_gen.go deleted file mode 100644 index 155f91d0a54..00000000000 --- a/pkg/kinds/team/team_gen.go +++ /dev/null @@ -1,43 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package team - -import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/kinds" -) - -// Resource is the kubernetes style representation of Team. (TODO be better) -type K8sResource = kinds.GrafanaResource[Spec, Status] - -// NewResource creates a new instance of the resource with a given name (UID) -func NewK8sResource(name string, s *Spec) K8sResource { - return K8sResource{ - TypeMeta: v1.TypeMeta{ - Kind: "Team", - APIVersion: "v0-0-alpha", - }, - ObjectMeta: v1.ObjectMeta{ - Name: name, - Annotations: make(map[string]string), - Labels: make(map[string]string), - }, - Spec: s, - } -} - -// Resource is the wire representation of Team. -// It currently will soon be merged into the k8s flavor (TODO be better) -type Resource struct { - Metadata Metadata `json:"metadata"` - Spec Spec `json:"spec"` - Status Status `json:"status"` -} diff --git a/pkg/kinds/team/team_metadata_gen.go b/pkg/kinds/team/team_metadata_gen.go deleted file mode 100644 index 2709fc43743..00000000000 --- a/pkg/kinds/team/team_metadata_gen.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package team - -import ( - "time" -) - -// Metadata defines model for Metadata. -type Metadata struct { - CreatedBy string `json:"createdBy"` - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - - // extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - ExtraFields map[string]any `json:"extraFields"` - Finalizers []string `json:"finalizers"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` - UpdateTimestamp time.Time `json:"updateTimestamp"` - UpdatedBy string `json:"updatedBy"` -} - -// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -// It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -// As it is also intended to be generic enough to function with any API Server. -type KubeObjectMetadata struct { - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - Finalizers []string `json:"finalizers"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` -} diff --git a/pkg/kinds/team/team_spec_gen.go b/pkg/kinds/team/team_spec_gen.go deleted file mode 100644 index cfb7e648321..00000000000 --- a/pkg/kinds/team/team_spec_gen.go +++ /dev/null @@ -1,19 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// GoResourceTypes -// -// Run 'make gen-cue' from repository root to regenerate. - -package team - -// Spec defines model for Spec. -type Spec struct { - // Email of the team. - Email *string `json:"email,omitempty"` - - // Name of the team. - Name string `json:"name"` -} diff --git a/pkg/kinds/team/team_status_gen.go b/pkg/kinds/team/team_status_gen.go deleted file mode 100644 index d9e9c6535fd..00000000000 --- a/pkg/kinds/team/team_status_gen.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package team - -// Defines values for OperatorStateState. -const ( - OperatorStateStateFailed OperatorStateState = "failed" - OperatorStateStateInProgress OperatorStateState = "in_progress" - OperatorStateStateSuccess OperatorStateState = "success" -) - -// Defines values for StatusOperatorStateState. -const ( - StatusOperatorStateStateFailed StatusOperatorStateState = "failed" - StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" - StatusOperatorStateStateSuccess StatusOperatorStateState = "success" -) - -// OperatorState defines model for OperatorState. -type OperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]any `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State OperatorStateState `json:"state"` -} - -// OperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -type OperatorStateState string - -// Status defines model for Status. -type Status struct { - // additionalFields is reserved for future use - AdditionalFields map[string]any `json:"additionalFields,omitempty"` - - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"` -} - -// StatusOperatorState defines model for status.#OperatorState. -type StatusOperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]any `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State StatusOperatorStateState `json:"state"` -} - -// StatusOperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -type StatusOperatorStateState string diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index a44271582af..9a50a7a844c 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/folders" + "github.com/grafana/grafana/pkg/registry/apis/identity" "github.com/grafana/grafana/pkg/registry/apis/peakq" "github.com/grafana/grafana/pkg/registry/apis/playlist" "github.com/grafana/grafana/pkg/registry/apis/query" @@ -32,6 +33,7 @@ func ProvideRegistryServiceSink( _ *datasource.DataSourceAPIBuilder, _ *folders.FolderAPIBuilder, _ *peakq.PeakQAPIBuilder, + _ *identity.IdentityAPIBuilder, _ *scope.ScopeAPIBuilder, _ *query.QueryAPIBuilder, _ *notifications.NotificationsAPIBuilder, diff --git a/pkg/registry/apis/identity/legacy_sa.go b/pkg/registry/apis/identity/legacy_sa.go new file mode 100644 index 00000000000..bec879c586b --- /dev/null +++ b/pkg/registry/apis/identity/legacy_sa.go @@ -0,0 +1,130 @@ +package identity + +import ( + "context" + "fmt" + "strconv" + + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/user" +) + +var ( + _ rest.Scoper = (*legacyServiceAccountStorage)(nil) + _ rest.SingularNameProvider = (*legacyServiceAccountStorage)(nil) + _ rest.Getter = (*legacyServiceAccountStorage)(nil) + _ rest.Lister = (*legacyServiceAccountStorage)(nil) + _ rest.Storage = (*legacyServiceAccountStorage)(nil) +) + +type legacyServiceAccountStorage struct { + service user.Service + tableConverter rest.TableConvertor + resourceInfo common.ResourceInfo +} + +func (s *legacyServiceAccountStorage) New() runtime.Object { + return s.resourceInfo.NewFunc() +} + +func (s *legacyServiceAccountStorage) Destroy() {} + +func (s *legacyServiceAccountStorage) NamespaceScoped() bool { + return true // namespace == org +} + +func (s *legacyServiceAccountStorage) GetSingularName() string { + return s.resourceInfo.GetSingularName() +} + +func (s *legacyServiceAccountStorage) NewList() runtime.Object { + return s.resourceInfo.NewListFunc() +} + +func (s *legacyServiceAccountStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +func (s *legacyServiceAccountStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + query := &user.ListUsersCommand{ + OrgID: ns.OrgID, + Limit: options.Limit, + IsServiceAccount: true, + } + if options.Continue != "" { + query.ContinueID, err = strconv.ParseInt(options.Continue, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid continue token") + } + } + + found, err := s.service.List(ctx, query) + if err != nil { + return nil, err + } + list := &identity.ServiceAccountList{} + for _, item := range found.Users { + list.Items = append(list.Items, *toSAItem(item, ns.Value)) + } + if found.ContinueID > 0 { + list.ListMeta.Continue = strconv.FormatInt(found.ContinueID, 10) + } + if found.RV > 0 { + list.ListMeta.ResourceVersion = strconv.FormatInt(found.RV, 10) + } + return list, err +} + +func toSAItem(u *user.User, ns string) *identity.ServiceAccount { + item := &identity.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: u.UID, + Namespace: ns, + ResourceVersion: fmt.Sprintf("%d", u.Updated.UnixMilli()), + CreationTimestamp: metav1.NewTime(u.Created), + }, + Spec: identity.ServiceAccountSpec{ + Name: u.Name, + Email: u.Email, + EmailVerified: u.EmailVerified, + Disabled: u.IsDisabled, + }, + } + obj, _ := utils.MetaAccessor(item) + obj.SetUpdatedTimestamp(&u.Updated) + obj.SetOriginInfo(&utils.ResourceOriginInfo{ + Name: "SQL", + Path: strconv.FormatInt(u.ID, 10), + }) + return item +} + +func (s *legacyServiceAccountStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + found, err := s.service.GetByUID(ctx, &user.GetUserByUIDQuery{ + OrgID: ns.OrgID, + UID: name, + }) + if found == nil || err != nil { + return nil, s.resourceInfo.NewNotFound(name) + } + if !found.IsServiceAccount { + return nil, s.resourceInfo.NewNotFound(name) // looking up the wrong type + } + return toUserItem(found, ns.Value), nil +} diff --git a/pkg/registry/apis/identity/legacy_teams.go b/pkg/registry/apis/identity/legacy_teams.go new file mode 100644 index 00000000000..b69ae7621d7 --- /dev/null +++ b/pkg/registry/apis/identity/legacy_teams.go @@ -0,0 +1,119 @@ +package identity + +import ( + "context" + "strconv" + + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/team" +) + +var ( + _ rest.Scoper = (*legacyTeamStorage)(nil) + _ rest.SingularNameProvider = (*legacyTeamStorage)(nil) + _ rest.Getter = (*legacyTeamStorage)(nil) + _ rest.Lister = (*legacyTeamStorage)(nil) + _ rest.Storage = (*legacyTeamStorage)(nil) +) + +type legacyTeamStorage struct { + service team.Service + tableConverter rest.TableConvertor + resourceInfo common.ResourceInfo +} + +func (s *legacyTeamStorage) New() runtime.Object { + return s.resourceInfo.NewFunc() +} + +func (s *legacyTeamStorage) Destroy() {} + +func (s *legacyTeamStorage) NamespaceScoped() bool { + return true // namespace == org +} + +func (s *legacyTeamStorage) GetSingularName() string { + return s.resourceInfo.GetSingularName() +} + +func (s *legacyTeamStorage) NewList() runtime.Object { + return s.resourceInfo.NewListFunc() +} + +func (s *legacyTeamStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +func (s *legacyTeamStorage) doList(ctx context.Context, ns string, query *team.ListTeamsCommand) (*identity.TeamList, error) { + if query.Limit < 1 { + query.Limit = 100 + } + teams, err := s.service.ListTeams(ctx, query) + if err != nil { + return nil, err + } + list := &identity.TeamList{} + for _, team := range teams { + item := identity.Team{ + ObjectMeta: metav1.ObjectMeta{ + Name: team.UID, + Namespace: ns, + CreationTimestamp: metav1.NewTime(team.Created), + ResourceVersion: strconv.FormatInt(team.Updated.UnixMilli(), 10), + }, + Spec: identity.TeamSpec{ + Title: team.Name, + Email: team.Email, + }, + } + meta, err := utils.MetaAccessor(&item) + if err != nil { + return nil, err + } + meta.SetUpdatedTimestamp(&team.Updated) + meta.SetOriginInfo(&utils.ResourceOriginInfo{ + Name: "SQL", + Path: strconv.FormatInt(team.ID, 10), + }) + list.Items = append(list.Items, item) + } + return list, nil +} + +func (s *legacyTeamStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + return s.doList(ctx, ns.Value, &team.ListTeamsCommand{ + Limit: int(options.Limit), + OrgID: ns.OrgID, + }) +} + +func (s *legacyTeamStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + rsp, err := s.doList(ctx, ns.Value, &team.ListTeamsCommand{ + Limit: 1, + OrgID: ns.OrgID, + UID: name, + }) + if err != nil { + return nil, err + } + if len(rsp.Items) > 0 { + return &rsp.Items[0], nil + } + return nil, s.resourceInfo.NewNotFound(name) +} diff --git a/pkg/registry/apis/identity/legacy_users.go b/pkg/registry/apis/identity/legacy_users.go new file mode 100644 index 00000000000..b7a66e1cc81 --- /dev/null +++ b/pkg/registry/apis/identity/legacy_users.go @@ -0,0 +1,130 @@ +package identity + +import ( + "context" + "fmt" + "strconv" + + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/user" +) + +var ( + _ rest.Scoper = (*legacyUserStorage)(nil) + _ rest.SingularNameProvider = (*legacyUserStorage)(nil) + _ rest.Getter = (*legacyUserStorage)(nil) + _ rest.Lister = (*legacyUserStorage)(nil) + _ rest.Storage = (*legacyUserStorage)(nil) +) + +type legacyUserStorage struct { + service user.Service + tableConverter rest.TableConvertor + resourceInfo common.ResourceInfo +} + +func (s *legacyUserStorage) New() runtime.Object { + return s.resourceInfo.NewFunc() +} + +func (s *legacyUserStorage) Destroy() {} + +func (s *legacyUserStorage) NamespaceScoped() bool { + return true // namespace == org +} + +func (s *legacyUserStorage) GetSingularName() string { + return s.resourceInfo.GetSingularName() +} + +func (s *legacyUserStorage) NewList() runtime.Object { + return s.resourceInfo.NewListFunc() +} + +func (s *legacyUserStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +func (s *legacyUserStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + query := &user.ListUsersCommand{ + OrgID: ns.OrgID, + Limit: options.Limit, + } + if options.Continue != "" { + query.ContinueID, err = strconv.ParseInt(options.Continue, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid continue token") + } + } + + found, err := s.service.List(ctx, query) + if err != nil { + return nil, err + } + list := &identity.UserList{} + for _, item := range found.Users { + list.Items = append(list.Items, *toUserItem(item, ns.Value)) + } + if found.ContinueID > 0 { + list.ListMeta.Continue = strconv.FormatInt(found.ContinueID, 10) + } + if found.RV > 0 { + list.ListMeta.ResourceVersion = strconv.FormatInt(found.RV, 10) + } + return list, err +} + +func toUserItem(u *user.User, ns string) *identity.User { + item := &identity.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: u.UID, + Namespace: ns, + ResourceVersion: fmt.Sprintf("%d", u.Updated.UnixMilli()), + CreationTimestamp: metav1.NewTime(u.Created), + }, + Spec: identity.UserSpec{ + Name: u.Name, + Login: u.Login, + Email: u.Email, + EmailVerified: u.EmailVerified, + Disabled: u.IsDisabled, + }, + } + obj, _ := utils.MetaAccessor(item) + obj.SetUpdatedTimestamp(&u.Updated) + obj.SetOriginInfo(&utils.ResourceOriginInfo{ + Name: "SQL", + Path: strconv.FormatInt(u.ID, 10), + }) + return item +} + +func (s *legacyUserStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + found, err := s.service.GetByUID(ctx, &user.GetUserByUIDQuery{ + OrgID: ns.OrgID, + UID: name, + }) + if found == nil || err != nil { + return nil, s.resourceInfo.NewNotFound(name) + } + if found.IsServiceAccount { + return nil, s.resourceInfo.NewNotFound(name) // looking up the wrong type + } + return toUserItem(found, ns.Value), nil +} diff --git a/pkg/registry/apis/identity/register.go b/pkg/registry/apis/identity/register.go new file mode 100644 index 00000000000..3dbf11374ab --- /dev/null +++ b/pkg/registry/apis/identity/register.go @@ -0,0 +1,196 @@ +package identity + +import ( + "context" + "fmt" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/rest" + genericapiserver "k8s.io/apiserver/pkg/server" + common "k8s.io/kube-openapi/pkg/common" + + identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" + identityapi "github.com/grafana/grafana/pkg/apimachinery/identity" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/team" + "github.com/grafana/grafana/pkg/services/user" +) + +var _ builder.APIGroupBuilder = (*IdentityAPIBuilder)(nil) + +// This is used just so wire has something unique to return +type IdentityAPIBuilder struct { + svcTeam team.Service + svcUser user.Service +} + +func RegisterAPIService( + features featuremgmt.FeatureToggles, + apiregistration builder.APIRegistrar, + svcTeam team.Service, + svcUser user.Service, + +) *IdentityAPIBuilder { + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + return nil // skip registration unless opting into experimental apis + } + + builder := &IdentityAPIBuilder{ + svcTeam: svcTeam, + svcUser: svcUser, + } + apiregistration.RegisterAPI(builder) + return builder +} + +func (b *IdentityAPIBuilder) GetGroupVersion() schema.GroupVersion { + return identity.SchemeGroupVersion +} + +func (b *IdentityAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { + if err := identity.AddKnownTypes(scheme, identity.VERSION); err != nil { + return err + } + + // Link this version to the internal representation. + // This is used for server-side-apply (PATCH), and avoids the error: + // "no kind is registered for the type" + if err := identity.AddKnownTypes(scheme, runtime.APIVersionInternal); err != nil { + return err + } + + // If multiple versions exist, then register conversions from zz_generated.conversion.go + // if err := playlist.RegisterConversions(scheme); err != nil { + // return err + // } + metav1.AddToGroupVersion(scheme, identity.SchemeGroupVersion) + return scheme.SetVersionPriority(identity.SchemeGroupVersion) +} + +func (b *IdentityAPIBuilder) GetAPIGroupInfo( + scheme *runtime.Scheme, + codecs serializer.CodecFactory, // pointer? + optsGetter generic.RESTOptionsGetter, + dualWriteBuilder grafanarest.DualWriteBuilder, +) (*genericapiserver.APIGroupInfo, error) { + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(identity.GROUP, scheme, metav1.ParameterCodec, codecs) + storage := map[string]rest.Storage{} + + team := identity.TeamResourceInfo + teamStore := &legacyTeamStorage{ + service: b.svcTeam, + resourceInfo: team, + tableConverter: gapiutil.NewTableConverter( + team.GroupResource(), + []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string", Format: "string", Description: "The team name"}, + {Name: "Email", Type: "string", Format: "string", Description: "team email"}, + {Name: "Created At", Type: "date"}, + }, + func(obj any) ([]interface{}, error) { + m, ok := obj.(*identity.Team) + if !ok { + return nil, fmt.Errorf("expected playlist") + } + return []interface{}{ + m.Name, + m.Spec.Title, + m.Spec.Email, + m.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + }, + ), + } + storage[team.StoragePath()] = teamStore + + user := identity.UserResourceInfo + userStore := &legacyUserStorage{ + service: b.svcUser, + resourceInfo: user, + tableConverter: gapiutil.NewTableConverter( + user.GroupResource(), + []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Login", Type: "string", Format: "string", Description: "The user login"}, + {Name: "Email", Type: "string", Format: "string", Description: "The user email"}, + {Name: "Created At", Type: "date"}, + }, + func(obj any) ([]interface{}, error) { + u, ok := obj.(*identity.User) + if ok { + return []interface{}{ + u.Name, + u.Spec.Login, + u.Spec.Email, + u.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + return nil, fmt.Errorf("expected user") + }, + ), + } + storage[user.StoragePath()] = userStore + + sa := identity.ServiceAccountResourceInfo + saStore := &legacyServiceAccountStorage{ + service: b.svcUser, + resourceInfo: sa, + tableConverter: gapiutil.NewTableConverter( + user.GroupResource(), + []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Account", Type: "string", Format: "string", Description: "The service account email"}, + {Name: "Email", Type: "string", Format: "string", Description: "The user email"}, + {Name: "Created At", Type: "date"}, + }, + func(obj any) ([]interface{}, error) { + u, ok := obj.(*identity.ServiceAccount) + if ok { + return []interface{}{ + u.Name, + u.Spec.Name, + u.Spec.Email, + u.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + return nil, fmt.Errorf("expected user") + }, + ), + } + storage[sa.StoragePath()] = saStore + + apiGroupInfo.VersionedResourcesStorageMap[identity.VERSION] = storage + return &apiGroupInfo, nil +} + +func (b *IdentityAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return identity.GetOpenAPIDefinitions +} + +func (b *IdentityAPIBuilder) GetAPIRoutes() *builder.APIRoutes { + return nil // no custom API routes +} + +func (b *IdentityAPIBuilder) GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc( + func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + user, err := identityapi.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "no identity found", err + } + if user.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "only grafana admins have access for now", nil + }) +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 2ce96e4af0d..f7eb37f3064 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/folders" + "github.com/grafana/grafana/pkg/registry/apis/identity" "github.com/grafana/grafana/pkg/registry/apis/peakq" "github.com/grafana/grafana/pkg/registry/apis/playlist" "github.com/grafana/grafana/pkg/registry/apis/query" @@ -32,6 +33,7 @@ var WireSet = wire.NewSet( featuretoggle.RegisterAPIService, datasource.RegisterAPIService, folders.RegisterAPIService, + identity.RegisterAPIService, peakq.RegisterAPIService, service.RegisterAPIService, query.RegisterAPIService, diff --git a/pkg/registry/schemas/core_kind.go b/pkg/registry/schemas/core_kind.go index 596eeedf043..1827acefff8 100644 --- a/pkg/registry/schemas/core_kind.go +++ b/pkg/registry/schemas/core_kind.go @@ -93,15 +93,6 @@ func GetCoreKinds() ([]CoreKind, error) { CueFile: rolebindingCue, }) - teamCue, err := loadCueFile(ctx, filepath.Join(root, "./kinds/team/team_kind.cue")) - if err != nil { - return nil, err - } - kinds = append(kinds, CoreKind{ - Name: "team", - CueFile: teamCue, - }) - return kinds, nil } diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index 8bd371de36a..a609202dc68 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -90,6 +90,13 @@ type SearchTeamsQuery struct { HiddenUsers map[string]struct{} } +type ListTeamsCommand struct { + Limit int + Start int + OrgID int64 + UID string +} + type TeamDTO struct { ID int64 `json:"id" xorm:"id"` UID string `json:"uid" xorm:"uid"` diff --git a/pkg/services/team/team.go b/pkg/services/team/team.go index 3a44a8883fa..5ae7abb2857 100644 --- a/pkg/services/team/team.go +++ b/pkg/services/team/team.go @@ -8,6 +8,7 @@ type Service interface { CreateTeam(ctx context.Context, name, email string, orgID int64) (Team, error) UpdateTeam(ctx context.Context, cmd *UpdateTeamCommand) error DeleteTeam(ctx context.Context, cmd *DeleteTeamCommand) error + ListTeams(ctx context.Context, query *ListTeamsCommand) ([]*Team, error) SearchTeams(ctx context.Context, query *SearchTeamsQuery) (SearchTeamQueryResult, error) GetTeamByID(ctx context.Context, query *GetTeamByIDQuery) (*TeamDTO, error) GetTeamsByUser(ctx context.Context, query *GetTeamsByUserQuery) ([]*TeamDTO, error) diff --git a/pkg/services/team/teamimpl/store.go b/pkg/services/team/teamimpl/store.go index 8f3db689fd3..4cd5777937f 100644 --- a/pkg/services/team/teamimpl/store.go +++ b/pkg/services/team/teamimpl/store.go @@ -20,6 +20,7 @@ type store interface { Create(name, email string, orgID int64) (team.Team, error) Update(ctx context.Context, cmd *team.UpdateTeamCommand) error Delete(ctx context.Context, cmd *team.DeleteTeamCommand) error + ListTeams(ctx context.Context, query *team.ListTeamsCommand) ([]*team.Team, error) Search(ctx context.Context, query *team.SearchTeamsQuery) (team.SearchTeamQueryResult, error) GetByID(ctx context.Context, query *team.GetTeamByIDQuery) (*team.TeamDTO, error) GetByUser(ctx context.Context, query *team.GetTeamsByUserQuery) ([]*team.TeamDTO, error) @@ -267,6 +268,21 @@ func (ss *xormStore) Search(ctx context.Context, query *team.SearchTeamsQuery) ( return queryResult, nil } +func (ss *xormStore) ListTeams(ctx context.Context, query *team.ListTeamsCommand) ([]*team.Team, error) { + results := make([]*team.Team, 0) + err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { + q := sess.Table("team") + q.Where("team.org_id=?", query.OrgID) + if query.UID != "" { + q.Where("team.uid=?", query.UID) + } + q.Limit(query.Limit, query.Start) + + return q.Find(&results) + }) + return results, err +} + func (ss *xormStore) GetByID(ctx context.Context, query *team.GetTeamByIDQuery) (*team.TeamDTO, error) { var queryResult *team.TeamDTO err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { diff --git a/pkg/services/team/teamimpl/team.go b/pkg/services/team/teamimpl/team.go index e7c9537b751..627d4f75acd 100644 --- a/pkg/services/team/teamimpl/team.go +++ b/pkg/services/team/teamimpl/team.go @@ -51,6 +51,14 @@ func (s *Service) DeleteTeam(ctx context.Context, cmd *team.DeleteTeamCommand) e return s.store.Delete(ctx, cmd) } +func (s *Service) ListTeams(ctx context.Context, query *team.ListTeamsCommand) ([]*team.Team, error) { + ctx, span := s.tracer.Start(ctx, "team.ListTeams", trace.WithAttributes( + attribute.Int64("orgID", query.OrgID), + )) + defer span.End() + return s.store.ListTeams(ctx, query) +} + func (s *Service) SearchTeams(ctx context.Context, query *team.SearchTeamsQuery) (team.SearchTeamQueryResult, error) { ctx, span := s.tracer.Start(ctx, "team.SearchTeams", trace.WithAttributes( attribute.Int64("orgID", query.OrgID), diff --git a/pkg/services/team/teamtest/team.go b/pkg/services/team/teamtest/team.go index a561b7c4772..5a2e510c5d1 100644 --- a/pkg/services/team/teamtest/team.go +++ b/pkg/services/team/teamtest/team.go @@ -36,6 +36,10 @@ func (s *FakeService) SearchTeams(ctx context.Context, query *team.SearchTeamsQu return team.SearchTeamQueryResult{}, s.ExpectedError } +func (s *FakeService) ListTeams(ctx context.Context, query *team.ListTeamsCommand) ([]*team.Team, error) { + return nil, s.ExpectedError +} + func (s *FakeService) GetTeamByID(ctx context.Context, query *team.GetTeamByIDQuery) (*team.TeamDTO, error) { return s.ExpectedTeamDTO, s.ExpectedError } diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go index 12e992e5081..8a4db77f29c 100644 --- a/pkg/services/user/model.go +++ b/pkg/services/user/model.go @@ -98,6 +98,19 @@ type UpdateUserLastSeenAtCommand struct { OrgID int64 } +type ListUsersCommand struct { + OrgID int64 + Limit int64 + ContinueID int64 + IsServiceAccount bool +} + +type ListUserResult struct { + Users []*User + ContinueID int64 + RV int64 +} + type SearchUsersQuery struct { SignedInUser identity.Requester OrgID int64 `xorm:"org_id"` @@ -120,7 +133,7 @@ type SearchUserQueryResult struct { type UserSearchHitDTO struct { ID int64 `json:"id" xorm:"id"` - UID string `json:"uid" xorm:"id"` + UID string `json:"uid" xorm:"uid"` Name string `json:"name"` Login string `json:"login"` Email string `json:"email"` @@ -206,6 +219,11 @@ type GetUserByIDQuery struct { ID int64 } +type GetUserByUIDQuery struct { + OrgID int64 + UID string +} + type StartVerifyEmailCommand struct { User User Email string diff --git a/pkg/services/user/user.go b/pkg/services/user/user.go index bc6861c6552..56f6b819523 100644 --- a/pkg/services/user/user.go +++ b/pkg/services/user/user.go @@ -13,8 +13,10 @@ type Service interface { CreateServiceAccount(context.Context, *CreateUserCommand) (*User, error) Delete(context.Context, *DeleteUserCommand) error GetByID(context.Context, *GetUserByIDQuery) (*User, error) + GetByUID(context.Context, *GetUserByUIDQuery) (*User, error) GetByLogin(context.Context, *GetUserByLoginQuery) (*User, error) GetByEmail(context.Context, *GetUserByEmailQuery) (*User, error) + List(context.Context, *ListUsersCommand) (*ListUserResult, error) Update(context.Context, *UpdateUserCommand) error UpdateLastSeenAt(context.Context, *UpdateUserLastSeenAtCommand) error GetSignedInUser(context.Context, *GetSignedInUserQuery) (*SignedInUser, error) diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index 1a722e6c92b..fe38e8c9ad7 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -20,8 +20,10 @@ import ( type store interface { Insert(context.Context, *user.User) (int64, error) GetByID(context.Context, int64) (*user.User, error) + GetByUID(ctx context.Context, orgId int64, uid string) (*user.User, error) GetByLogin(context.Context, *user.GetUserByLoginQuery) (*user.User, error) GetByEmail(context.Context, *user.GetUserByEmailQuery) (*user.User, error) + List(context.Context, *user.ListUsersCommand) (*user.ListUserResult, error) Delete(context.Context, int64) error LoginConflict(ctx context.Context, login, email string) error Update(context.Context, *user.UpdateUserCommand) error @@ -107,6 +109,24 @@ func (ss *sqlStore) GetByID(ctx context.Context, userID int64) (*user.User, erro return &usr, err } +func (ss *sqlStore) GetByUID(ctx context.Context, orgId int64, uid string) (*user.User, error) { + var usr user.User + + err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { + has, err := sess.Table("user"). + Where("org_id = ? AND uid = ?", orgId, uid). + Get(&usr) + + if err != nil { + return err + } else if !has { + return user.ErrUserNotFound + } + return nil + }) + return &usr, err +} + func (ss *sqlStore) notServiceAccountFilter() string { return fmt.Sprintf("%s.is_service_account = %s", ss.dialect.Quote("user"), @@ -506,7 +526,7 @@ func (ss *sqlStore) Search(ctx context.Context, query *user.SearchUsersQuery) (* sess.Limit(query.Limit, offset) } - sess.Cols("u.id", "u.email", "u.name", "u.login", "u.is_admin", "u.is_disabled", "u.last_seen_at", "user_auth.auth_module") + sess.Cols("u.id", "u.uid", "u.email", "u.name", "u.login", "u.is_admin", "u.is_disabled", "u.last_seen_at", "user_auth.auth_module") if len(query.SortOpts) > 0 { for i := range query.SortOpts { @@ -559,6 +579,40 @@ func (ss *sqlStore) Search(ctx context.Context, query *user.SearchUsersQuery) (* return &result, err } +func (ss *sqlStore) List(ctx context.Context, query *user.ListUsersCommand) (*user.ListUserResult, error) { + limit := int(query.Limit) + if limit <= 0 { + limit = 25 + } + result := &user.ListUserResult{ + Users: make([]*user.User, 0), + } + max := "" + err := ss.db.WithDbSession(ctx, func(dbSess *db.Session) error { + sess := dbSess.Table("user") + sess.Where("id >= ? AND is_service_account = ?", query.ContinueID, query.IsServiceAccount) + err := sess.OrderBy("id asc").Limit(limit + 1).Find(&result.Users) + if err != nil { + return err + } + + // Set the revision version + _, err = dbSess.Table("user").Select("MAX(updated)").Get(&max) + return err + }) + if max != "" { + t, err := time.Parse(time.DateTime, max) + if err == nil { + result.RV = t.UnixMilli() + } + } + if len(result.Users) > limit { + result.ContinueID = result.Users[limit].ID + result.Users = result.Users[:limit] + } + return result, err +} + func setOptional[T any](v *T, add func(v T)) { if v != nil { add(*v) diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index db911d4c408..64db9fdc323 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -212,6 +212,16 @@ func (s *Service) GetByID(ctx context.Context, query *user.GetUserByIDQuery) (*u return s.store.GetByID(ctx, query.ID) } +func (s *Service) GetByUID(ctx context.Context, query *user.GetUserByUIDQuery) (*user.User, error) { + ctx, span := s.tracer.Start(ctx, "user.GetByUID", trace.WithAttributes( + attribute.Int64("orgID", query.OrgID), + attribute.String("userUID", query.UID), + )) + defer span.End() + + return s.store.GetByUID(ctx, query.OrgID, query.UID) +} + func (s *Service) GetByLogin(ctx context.Context, query *user.GetUserByLoginQuery) (*user.User, error) { ctx, span := s.tracer.Start(ctx, "user.GetByLogin") defer span.End() @@ -368,6 +378,15 @@ func (s *Service) getSignedInUser(ctx context.Context, query *user.GetSignedInUs return usr, err } +func (s *Service) List(ctx context.Context, query *user.ListUsersCommand) (*user.ListUserResult, error) { + ctx, span := s.tracer.Start(ctx, "user.List", trace.WithAttributes( + attribute.Int64("orgID", query.OrgID), + )) + defer span.End() + + return s.store.List(ctx, query) +} + func (s *Service) Search(ctx context.Context, query *user.SearchUsersQuery) (*user.SearchUserQueryResult, error) { ctx, span := s.tracer.Start(ctx, "user.Search", trace.WithAttributes( attribute.Int64("orgID", query.OrgID), diff --git a/pkg/services/user/userimpl/user_test.go b/pkg/services/user/userimpl/user_test.go index da30c586564..1b82d8c3f5e 100644 --- a/pkg/services/user/userimpl/user_test.go +++ b/pkg/services/user/userimpl/user_test.go @@ -291,6 +291,10 @@ func (f *FakeUserStore) GetByID(context.Context, int64) (*user.User, error) { return f.ExpectedUser, f.ExpectedError } +func (f *FakeUserStore) GetByUID(context.Context, int64, string) (*user.User, error) { + return f.ExpectedUser, f.ExpectedError +} + func (f *FakeUserStore) LoginConflict(context.Context, string, string) error { return f.ExpectedError } @@ -327,6 +331,10 @@ func (f *FakeUserStore) Search(ctx context.Context, query *user.SearchUsersQuery return f.ExpectedSearchUserQueryResult, f.ExpectedError } +func (f *FakeUserStore) List(ctx context.Context, query *user.ListUsersCommand) (*user.ListUserResult, error) { + return nil, f.ExpectedError +} + func (f *FakeUserStore) Count(ctx context.Context) (int64, error) { return 0, nil } diff --git a/pkg/services/user/usertest/fake.go b/pkg/services/user/usertest/fake.go index 6d59314e385..4dc6bee11ca 100644 --- a/pkg/services/user/usertest/fake.go +++ b/pkg/services/user/usertest/fake.go @@ -12,6 +12,7 @@ type FakeUserService struct { ExpectedError error ExpectedSetUsingOrgError error ExpectedSearchUsers user.SearchUserQueryResult + ExpectedListUsers user.ListUserResult ExpectedUserProfileDTO *user.UserProfileDTO ExpectedUserProfileDTOs []*user.UserProfileDTO ExpectedUsageStats map[string]any @@ -53,6 +54,10 @@ func (f *FakeUserService) GetByID(ctx context.Context, query *user.GetUserByIDQu return f.ExpectedUser, f.ExpectedError } +func (f *FakeUserService) GetByUID(ctx context.Context, query *user.GetUserByUIDQuery) (*user.User, error) { + return f.ExpectedUser, f.ExpectedError +} + func (f *FakeUserService) GetByLogin(ctx context.Context, query *user.GetUserByLoginQuery) (*user.User, error) { return f.ExpectedUser, f.ExpectedError } @@ -93,6 +98,10 @@ func (f *FakeUserService) Search(ctx context.Context, query *user.SearchUsersQue return &f.ExpectedSearchUsers, f.ExpectedError } +func (f *FakeUserService) List(ctx context.Context, query *user.ListUsersCommand) (*user.ListUserResult, error) { + return &f.ExpectedListUsers, f.ExpectedError +} + func (f *FakeUserService) BatchDisableUsers(ctx context.Context, cmd *user.BatchDisableUsersCommand) error { if f.BatchDisableUsersFn != nil { return f.BatchDisableUsersFn(ctx, cmd) diff --git a/pkg/services/user/usertest/mock.go b/pkg/services/user/usertest/mock.go index 01cfea90965..504f03c1aed 100644 --- a/pkg/services/user/usertest/mock.go +++ b/pkg/services/user/usertest/mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. +// Code generated by mockery v2.43.2. DO NOT EDIT. package usertest @@ -200,6 +200,36 @@ func (_m *MockService) GetByLogin(_a0 context.Context, _a1 *user.GetUserByLoginQ return r0, r1 } +// GetByUID provides a mock function with given fields: _a0, _a1 +func (_m *MockService) GetByUID(_a0 context.Context, _a1 *user.GetUserByUIDQuery) (*user.User, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for GetByUID") + } + + var r0 *user.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *user.GetUserByUIDQuery) (*user.User, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *user.GetUserByUIDQuery) *user.User); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*user.User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *user.GetUserByUIDQuery) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetProfile provides a mock function with given fields: _a0, _a1 func (_m *MockService) GetProfile(_a0 context.Context, _a1 *user.GetUserProfileQuery) (*user.UserProfileDTO, error) { ret := _m.Called(_a0, _a1) @@ -260,36 +290,6 @@ func (_m *MockService) GetSignedInUser(_a0 context.Context, _a1 *user.GetSignedI return r0, r1 } -// GetSignedInUserWithCacheCtx provides a mock function with given fields: _a0, _a1 -func (_m *MockService) GetSignedInUserWithCacheCtx(_a0 context.Context, _a1 *user.GetSignedInUserQuery) (*user.SignedInUser, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for GetSignedInUserWithCacheCtx") - } - - var r0 *user.SignedInUser - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *user.GetSignedInUserQuery) (*user.SignedInUser, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, *user.GetSignedInUserQuery) *user.SignedInUser); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*user.SignedInUser) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *user.GetSignedInUserQuery) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetUsageStats provides a mock function with given fields: ctx func (_m *MockService) GetUsageStats(ctx context.Context) map[string]interface{} { ret := _m.Called(ctx) @@ -310,6 +310,36 @@ func (_m *MockService) GetUsageStats(ctx context.Context) map[string]interface{} return r0 } +// List provides a mock function with given fields: _a0, _a1 +func (_m *MockService) List(_a0 context.Context, _a1 *user.ListUsersCommand) (*user.ListUserResult, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 *user.ListUserResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *user.ListUsersCommand) (*user.ListUserResult, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *user.ListUsersCommand) *user.ListUserResult); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*user.ListUserResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *user.ListUsersCommand) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Search provides a mock function with given fields: _a0, _a1 func (_m *MockService) Search(_a0 context.Context, _a1 *user.SearchUsersQuery) (*user.SearchUserQueryResult, error) { ret := _m.Called(_a0, _a1) diff --git a/pkg/tests/apis/peakq/peakq_test.go b/pkg/tests/apis/peakq/peakq_test.go index cda9024231a..6b95b6a98d6 100644 --- a/pkg/tests/apis/peakq/peakq_test.go +++ b/pkg/tests/apis/peakq/peakq_test.go @@ -1,4 +1,4 @@ -package playlist +package peakq import ( "encoding/json" @@ -16,7 +16,7 @@ func TestMain(m *testing.M) { testsuite.Run(m) } -func TestIntegrationFoldersApp(t *testing.T) { +func TestIntegrationPeakQ(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index 38f5abcf01a..325bb908b0d 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -1,10 +1,16 @@ -import { Team as TeamDTO } from '@grafana/schema/src/raw/team/x/team_types.gen'; - import { Role } from './accessControl'; import { TeamPermissionLevel } from './acl'; -// The team resource -export { TeamDTO }; +export interface TeamDTO { + /** + * Email of the team. + */ + email?: string; + /** + * Name of the team. + */ + name: string; +} // This is the team resource with permissions and metadata expanded export interface Team {