diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 0a0e0bd2a50..7e99af69887 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -338,7 +338,7 @@ require ( github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect - github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect + github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect @@ -433,7 +433,7 @@ require ( gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect - gopkg.in/telebot.v3 v3.2.1 // indirect + gopkg.in/telebot.v3 v3.3.8 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.1 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 8b7f9fa06c2..2f6549ac9c1 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -1217,8 +1217,8 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvqWEUH6SjNiu7VhSjuVFTFiTcphaLU= -github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 h1:OfRzdxCzDhp+rsKWXuOO2I/quKMJ/+TQwVbIP/gltZg= +github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92/go.mod h1:7/OT02F6S6I7v6WXb+IjhMuZEYfH/RJ5RwEWnEo5BMg= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -2058,8 +2058,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= -gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= -gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= +gopkg.in/telebot.v3 v3.3.8 h1:uVDGjak9l824FN9YARWUHMsiNZnlohAVwUycw21k6t8= +gopkg.in/telebot.v3 v3.3.8/go.mod h1:1mlbqcLTVSfK9dx7fdp+Nb5HZsy4LLPtpZTKmwhwtzM= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/apps/iam/kinds/v0alpha1/userspec.cue b/apps/iam/kinds/v0alpha1/userspec.cue index 8ddcf57151d..35fffd208a4 100644 --- a/apps/iam/kinds/v0alpha1/userspec.cue +++ b/apps/iam/kinds/v0alpha1/userspec.cue @@ -8,6 +8,5 @@ UserSpec: { login: string name: string provisioned: bool - - // What to do with salt, rands and password? + role: string } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/corerole_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/corerole_client_gen.go new file mode 100644 index 00000000000..ae553035b02 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/corerole_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type CoreRoleClient struct { + client *resource.TypedClient[*CoreRole, *CoreRoleList] +} + +func NewCoreRoleClient(client resource.Client) *CoreRoleClient { + return &CoreRoleClient{ + client: resource.NewTypedClient[*CoreRole, *CoreRoleList](client, CoreRoleKind()), + } +} + +func NewCoreRoleClientFromGenerator(generator resource.ClientGenerator) (*CoreRoleClient, error) { + c, err := generator.ClientFor(CoreRoleKind()) + if err != nil { + return nil, err + } + return NewCoreRoleClient(c), nil +} + +func (c *CoreRoleClient) Get(ctx context.Context, identifier resource.Identifier) (*CoreRole, error) { + return c.client.Get(ctx, identifier) +} + +func (c *CoreRoleClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*CoreRoleList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *CoreRoleClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*CoreRoleList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *CoreRoleClient) Create(ctx context.Context, obj *CoreRole, opts resource.CreateOptions) (*CoreRole, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = CoreRoleKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *CoreRoleClient) Update(ctx context.Context, obj *CoreRole, opts resource.UpdateOptions) (*CoreRole, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *CoreRoleClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*CoreRole, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *CoreRoleClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus CoreRoleStatus, opts resource.UpdateOptions) (*CoreRole, error) { + return c.client.Update(ctx, &CoreRole{ + TypeMeta: metav1.TypeMeta{ + Kind: CoreRoleKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *CoreRoleClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_client_gen.go new file mode 100644 index 00000000000..71db46f40fa --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GlobalRoleClient struct { + client *resource.TypedClient[*GlobalRole, *GlobalRoleList] +} + +func NewGlobalRoleClient(client resource.Client) *GlobalRoleClient { + return &GlobalRoleClient{ + client: resource.NewTypedClient[*GlobalRole, *GlobalRoleList](client, GlobalRoleKind()), + } +} + +func NewGlobalRoleClientFromGenerator(generator resource.ClientGenerator) (*GlobalRoleClient, error) { + c, err := generator.ClientFor(GlobalRoleKind()) + if err != nil { + return nil, err + } + return NewGlobalRoleClient(c), nil +} + +func (c *GlobalRoleClient) Get(ctx context.Context, identifier resource.Identifier) (*GlobalRole, error) { + return c.client.Get(ctx, identifier) +} + +func (c *GlobalRoleClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*GlobalRoleList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *GlobalRoleClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*GlobalRoleList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *GlobalRoleClient) Create(ctx context.Context, obj *GlobalRole, opts resource.CreateOptions) (*GlobalRole, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = GlobalRoleKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *GlobalRoleClient) Update(ctx context.Context, obj *GlobalRole, opts resource.UpdateOptions) (*GlobalRole, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *GlobalRoleClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*GlobalRole, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *GlobalRoleClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus GlobalRoleStatus, opts resource.UpdateOptions) (*GlobalRole, error) { + return c.client.Update(ctx, &GlobalRole{ + TypeMeta: metav1.TypeMeta{ + Kind: GlobalRoleKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *GlobalRoleClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_client_gen.go new file mode 100644 index 00000000000..66ea08e4d69 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GlobalRoleBindingClient struct { + client *resource.TypedClient[*GlobalRoleBinding, *GlobalRoleBindingList] +} + +func NewGlobalRoleBindingClient(client resource.Client) *GlobalRoleBindingClient { + return &GlobalRoleBindingClient{ + client: resource.NewTypedClient[*GlobalRoleBinding, *GlobalRoleBindingList](client, GlobalRoleBindingKind()), + } +} + +func NewGlobalRoleBindingClientFromGenerator(generator resource.ClientGenerator) (*GlobalRoleBindingClient, error) { + c, err := generator.ClientFor(GlobalRoleBindingKind()) + if err != nil { + return nil, err + } + return NewGlobalRoleBindingClient(c), nil +} + +func (c *GlobalRoleBindingClient) Get(ctx context.Context, identifier resource.Identifier) (*GlobalRoleBinding, error) { + return c.client.Get(ctx, identifier) +} + +func (c *GlobalRoleBindingClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*GlobalRoleBindingList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *GlobalRoleBindingClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*GlobalRoleBindingList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *GlobalRoleBindingClient) Create(ctx context.Context, obj *GlobalRoleBinding, opts resource.CreateOptions) (*GlobalRoleBinding, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = GlobalRoleBindingKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *GlobalRoleBindingClient) Update(ctx context.Context, obj *GlobalRoleBinding, opts resource.UpdateOptions) (*GlobalRoleBinding, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *GlobalRoleBindingClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*GlobalRoleBinding, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *GlobalRoleBindingClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus GlobalRoleBindingStatus, opts resource.UpdateOptions) (*GlobalRoleBinding, error) { + return c.client.Update(ctx, &GlobalRoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: GlobalRoleBindingKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *GlobalRoleBindingClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_client_gen.go new file mode 100644 index 00000000000..05c70845e26 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ResourcePermissionClient struct { + client *resource.TypedClient[*ResourcePermission, *ResourcePermissionList] +} + +func NewResourcePermissionClient(client resource.Client) *ResourcePermissionClient { + return &ResourcePermissionClient{ + client: resource.NewTypedClient[*ResourcePermission, *ResourcePermissionList](client, ResourcePermissionKind()), + } +} + +func NewResourcePermissionClientFromGenerator(generator resource.ClientGenerator) (*ResourcePermissionClient, error) { + c, err := generator.ClientFor(ResourcePermissionKind()) + if err != nil { + return nil, err + } + return NewResourcePermissionClient(c), nil +} + +func (c *ResourcePermissionClient) Get(ctx context.Context, identifier resource.Identifier) (*ResourcePermission, error) { + return c.client.Get(ctx, identifier) +} + +func (c *ResourcePermissionClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*ResourcePermissionList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *ResourcePermissionClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*ResourcePermissionList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *ResourcePermissionClient) Create(ctx context.Context, obj *ResourcePermission, opts resource.CreateOptions) (*ResourcePermission, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = ResourcePermissionKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *ResourcePermissionClient) Update(ctx context.Context, obj *ResourcePermission, opts resource.UpdateOptions) (*ResourcePermission, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *ResourcePermissionClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*ResourcePermission, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *ResourcePermissionClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus ResourcePermissionStatus, opts resource.UpdateOptions) (*ResourcePermission, error) { + return c.client.Update(ctx, &ResourcePermission{ + TypeMeta: metav1.TypeMeta{ + Kind: ResourcePermissionKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *ResourcePermissionClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/role_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/role_client_gen.go new file mode 100644 index 00000000000..e61d3fc83dd --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/role_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type RoleClient struct { + client *resource.TypedClient[*Role, *RoleList] +} + +func NewRoleClient(client resource.Client) *RoleClient { + return &RoleClient{ + client: resource.NewTypedClient[*Role, *RoleList](client, RoleKind()), + } +} + +func NewRoleClientFromGenerator(generator resource.ClientGenerator) (*RoleClient, error) { + c, err := generator.ClientFor(RoleKind()) + if err != nil { + return nil, err + } + return NewRoleClient(c), nil +} + +func (c *RoleClient) Get(ctx context.Context, identifier resource.Identifier) (*Role, error) { + return c.client.Get(ctx, identifier) +} + +func (c *RoleClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*RoleList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *RoleClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*RoleList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *RoleClient) Create(ctx context.Context, obj *Role, opts resource.CreateOptions) (*Role, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = RoleKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *RoleClient) Update(ctx context.Context, obj *Role, opts resource.UpdateOptions) (*Role, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *RoleClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Role, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *RoleClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus RoleStatus, opts resource.UpdateOptions) (*Role, error) { + return c.client.Update(ctx, &Role{ + TypeMeta: metav1.TypeMeta{ + Kind: RoleKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *RoleClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_client_gen.go new file mode 100644 index 00000000000..13e78c70adf --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type RoleBindingClient struct { + client *resource.TypedClient[*RoleBinding, *RoleBindingList] +} + +func NewRoleBindingClient(client resource.Client) *RoleBindingClient { + return &RoleBindingClient{ + client: resource.NewTypedClient[*RoleBinding, *RoleBindingList](client, RoleBindingKind()), + } +} + +func NewRoleBindingClientFromGenerator(generator resource.ClientGenerator) (*RoleBindingClient, error) { + c, err := generator.ClientFor(RoleBindingKind()) + if err != nil { + return nil, err + } + return NewRoleBindingClient(c), nil +} + +func (c *RoleBindingClient) Get(ctx context.Context, identifier resource.Identifier) (*RoleBinding, error) { + return c.client.Get(ctx, identifier) +} + +func (c *RoleBindingClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*RoleBindingList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *RoleBindingClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*RoleBindingList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *RoleBindingClient) Create(ctx context.Context, obj *RoleBinding, opts resource.CreateOptions) (*RoleBinding, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = RoleBindingKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *RoleBindingClient) Update(ctx context.Context, obj *RoleBinding, opts resource.UpdateOptions) (*RoleBinding, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *RoleBindingClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*RoleBinding, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *RoleBindingClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus RoleBindingStatus, opts resource.UpdateOptions) (*RoleBinding, error) { + return c.client.Update(ctx, &RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: RoleBindingKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *RoleBindingClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_client_gen.go new file mode 100644 index 00000000000..3b922e8b313 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ServiceAccountClient struct { + client *resource.TypedClient[*ServiceAccount, *ServiceAccountList] +} + +func NewServiceAccountClient(client resource.Client) *ServiceAccountClient { + return &ServiceAccountClient{ + client: resource.NewTypedClient[*ServiceAccount, *ServiceAccountList](client, ServiceAccountKind()), + } +} + +func NewServiceAccountClientFromGenerator(generator resource.ClientGenerator) (*ServiceAccountClient, error) { + c, err := generator.ClientFor(ServiceAccountKind()) + if err != nil { + return nil, err + } + return NewServiceAccountClient(c), nil +} + +func (c *ServiceAccountClient) Get(ctx context.Context, identifier resource.Identifier) (*ServiceAccount, error) { + return c.client.Get(ctx, identifier) +} + +func (c *ServiceAccountClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*ServiceAccountList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *ServiceAccountClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*ServiceAccountList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *ServiceAccountClient) Create(ctx context.Context, obj *ServiceAccount, opts resource.CreateOptions) (*ServiceAccount, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = ServiceAccountKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *ServiceAccountClient) Update(ctx context.Context, obj *ServiceAccount, opts resource.UpdateOptions) (*ServiceAccount, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *ServiceAccountClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*ServiceAccount, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *ServiceAccountClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus ServiceAccountStatus, opts resource.UpdateOptions) (*ServiceAccount, error) { + return c.client.Update(ctx, &ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: ServiceAccountKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *ServiceAccountClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go new file mode 100644 index 00000000000..c166c6c2881 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type TeamClient struct { + client *resource.TypedClient[*Team, *TeamList] +} + +func NewTeamClient(client resource.Client) *TeamClient { + return &TeamClient{ + client: resource.NewTypedClient[*Team, *TeamList](client, TeamKind()), + } +} + +func NewTeamClientFromGenerator(generator resource.ClientGenerator) (*TeamClient, error) { + c, err := generator.ClientFor(TeamKind()) + if err != nil { + return nil, err + } + return NewTeamClient(c), nil +} + +func (c *TeamClient) Get(ctx context.Context, identifier resource.Identifier) (*Team, error) { + return c.client.Get(ctx, identifier) +} + +func (c *TeamClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*TeamList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *TeamClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*TeamList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *TeamClient) Create(ctx context.Context, obj *Team, opts resource.CreateOptions) (*Team, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = TeamKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *TeamClient) Update(ctx context.Context, obj *Team, opts resource.UpdateOptions) (*Team, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *TeamClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Team, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *TeamClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus TeamStatus, opts resource.UpdateOptions) (*Team, error) { + return c.client.Update(ctx, &Team{ + TypeMeta: metav1.TypeMeta{ + Kind: TeamKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *TeamClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_client_gen.go new file mode 100644 index 00000000000..39729e1ece1 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type TeamBindingClient struct { + client *resource.TypedClient[*TeamBinding, *TeamBindingList] +} + +func NewTeamBindingClient(client resource.Client) *TeamBindingClient { + return &TeamBindingClient{ + client: resource.NewTypedClient[*TeamBinding, *TeamBindingList](client, TeamBindingKind()), + } +} + +func NewTeamBindingClientFromGenerator(generator resource.ClientGenerator) (*TeamBindingClient, error) { + c, err := generator.ClientFor(TeamBindingKind()) + if err != nil { + return nil, err + } + return NewTeamBindingClient(c), nil +} + +func (c *TeamBindingClient) Get(ctx context.Context, identifier resource.Identifier) (*TeamBinding, error) { + return c.client.Get(ctx, identifier) +} + +func (c *TeamBindingClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*TeamBindingList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *TeamBindingClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*TeamBindingList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *TeamBindingClient) Create(ctx context.Context, obj *TeamBinding, opts resource.CreateOptions) (*TeamBinding, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = TeamBindingKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *TeamBindingClient) Update(ctx context.Context, obj *TeamBinding, opts resource.UpdateOptions) (*TeamBinding, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *TeamBindingClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*TeamBinding, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *TeamBindingClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus TeamBindingStatus, opts resource.UpdateOptions) (*TeamBinding, error) { + return c.client.Update(ctx, &TeamBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: TeamBindingKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *TeamBindingClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go new file mode 100644 index 00000000000..bd7af9b3361 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type UserClient struct { + client *resource.TypedClient[*User, *UserList] +} + +func NewUserClient(client resource.Client) *UserClient { + return &UserClient{ + client: resource.NewTypedClient[*User, *UserList](client, UserKind()), + } +} + +func NewUserClientFromGenerator(generator resource.ClientGenerator) (*UserClient, error) { + c, err := generator.ClientFor(UserKind()) + if err != nil { + return nil, err + } + return NewUserClient(c), nil +} + +func (c *UserClient) Get(ctx context.Context, identifier resource.Identifier) (*User, error) { + return c.client.Get(ctx, identifier) +} + +func (c *UserClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*UserList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *UserClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*UserList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *UserClient) Create(ctx context.Context, obj *User, opts resource.CreateOptions) (*User, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = UserKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *UserClient) Update(ctx context.Context, obj *User, opts resource.UpdateOptions) (*User, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *UserClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*User, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *UserClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus UserStatus, opts resource.UpdateOptions) (*User, error) { + return c.client.Update(ctx, &User{ + TypeMeta: metav1.TypeMeta{ + Kind: UserKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *UserClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_spec_gen.go index 8e83f1d8b5d..74e1ba681dd 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/user_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_spec_gen.go @@ -10,8 +10,8 @@ type UserSpec struct { GrafanaAdmin bool `json:"grafanaAdmin"` Login string `json:"login"` Name string `json:"name"` - // What to do with salt, rands and password? - Provisioned bool `json:"provisioned"` + Provisioned bool `json:"provisioned"` + Role string `json:"role"` } // NewUserSpec creates a new UserSpec object. diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index 845e27bd7ee..ac63422d531 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -2660,14 +2660,20 @@ func schema_pkg_apis_iam_v0alpha1_UserSpec(ref common.ReferenceCallback) common. }, "provisioned": { SchemaProps: spec.SchemaProps{ - Description: "What to do with salt, rands and password?", - Default: false, - Type: []string{"boolean"}, - Format: "", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "role": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", }, }, }, - Required: []string{"disabled", "email", "emailVerified", "grafanaAdmin", "login", "name", "provisioned"}, + Required: []string{"disabled", "email", "emailVerified", "grafanaAdmin", "login", "name", "provisioned", "role"}, }, }, } diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index c534546ff64..fba76fbd7df 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -11,6 +11,8 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" ) @@ -93,6 +95,10 @@ var appManifestData = app.ManifestData{ Conversion: false, }, }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + }, }, }, } @@ -130,6 +136,7 @@ var customRouteToGoResponseType = map[string]any{} // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. // kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. // If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { if len(path) > 0 && path[0] == '/' { path = path[1:] @@ -137,3 +144,42 @@ func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (g goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] return goType, exists } + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/go.mod b/go.mod index 4939f4b35eb..125078c7571 100644 --- a/go.mod +++ b/go.mod @@ -567,7 +567,7 @@ require ( github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect - github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect + github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smarty/assertions v1.15.0 // indirect github.com/sony/gobreaker v0.5.0 // indirect @@ -639,7 +639,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect - gopkg.in/telebot.v3 v3.2.1 // indirect + gopkg.in/telebot.v3 v3.3.8 // indirect k8s.io/apiextensions-apiserver v0.34.1 // indirect k8s.io/kms v0.34.1 // indirect modernc.org/libc v1.66.3 // indirect diff --git a/go.sum b/go.sum index 4068b931179..ace71828555 100644 --- a/go.sum +++ b/go.sum @@ -2353,8 +2353,9 @@ github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+Yg github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= -github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvqWEUH6SjNiu7VhSjuVFTFiTcphaLU= github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 h1:OfRzdxCzDhp+rsKWXuOO2I/quKMJ/+TQwVbIP/gltZg= +github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92/go.mod h1:7/OT02F6S6I7v6WXb+IjhMuZEYfH/RJ5RwEWnEo5BMg= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -3563,8 +3564,9 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= -gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= +gopkg.in/telebot.v3 v3.3.8 h1:uVDGjak9l824FN9YARWUHMsiNZnlohAVwUycw21k6t8= +gopkg.in/telebot.v3 v3.3.8/go.mod h1:1mlbqcLTVSfK9dx7fdp+Nb5HZsy4LLPtpZTKmwhwtzM= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/pkg/registry/apis/iam/common/models.go b/pkg/registry/apis/iam/common/models.go new file mode 100644 index 00000000000..4b80b1cf473 --- /dev/null +++ b/pkg/registry/apis/iam/common/models.go @@ -0,0 +1,8 @@ +package common + +import "github.com/grafana/grafana/pkg/services/user" + +type UserWithRole struct { + user.User + Role string +} diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_1.sql index 399aee41f46..7458d020039 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_1.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_2.sql index 63b9fc610ab..1a586f00f3e 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_2.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_uid.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_uid.sql index bf2a9b7ccb5..672ed7946cd 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_uid.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_uid.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_1.sql index 4e771e18f26..0e53e82a4d6 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_1.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_2.sql index f6e2d6dab11..05751ec3d63 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_2.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_uid.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_uid.sql index a43df88f8a8..5123e9193c2 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_uid.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_uid.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_1.sql index 4e771e18f26..0e53e82a4d6 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_1.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_2.sql index f6e2d6dab11..05751ec3d63 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_2.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_uid.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_uid.sql index a43df88f8a8..5123e9193c2 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_uid.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_uid.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/user.go b/pkg/registry/apis/iam/legacy/user.go index 733230b3266..d437879c3c6 100644 --- a/pkg/registry/apis/iam/legacy/user.go +++ b/pkg/registry/apis/iam/legacy/user.go @@ -98,7 +98,7 @@ type ListUserQuery struct { } type ListUserResult struct { - Users []user.User + Users []common.UserWithRole Continue int64 RV int64 } @@ -166,10 +166,10 @@ func (s *legacySQLStore) queryUsers(ctx context.Context, sql *legacysql.LegacyDa if err == nil { var lastID int64 for rows.Next() { - u := user.User{} + u := common.UserWithRole{} err = rows.Scan(&u.OrgID, &u.ID, &u.UID, &u.Login, &u.Email, &u.Name, &u.Created, &u.Updated, &u.IsServiceAccount, &u.IsDisabled, &u.IsAdmin, &u.EmailVerified, - &u.IsProvisioned, &u.LastSeenAt, + &u.IsProvisioned, &u.LastSeenAt, &u.Role, ) if err != nil { return res, err @@ -310,7 +310,7 @@ type CreateUserCommand struct { } type CreateUserResult struct { - User user.User + User common.UserWithRole } type CreateOrgUserCommand struct { @@ -398,7 +398,7 @@ func (s *legacySQLStore) CreateUser(ctx context.Context, ns claims.NamespaceInfo req := newCreateUser(sql, &cmd) - var createdUser user.User + var createdUser common.UserWithRole err = sql.DB.GetSqlxSession().WithTransaction(ctx, func(st *session.SessionTx) error { userQuery, err := sqltemplate.Execute(sqlCreateUserTemplate, req) if err != nil { @@ -429,23 +429,26 @@ func (s *legacySQLStore) CreateUser(ctx context.Context, ns claims.NamespaceInfo return fmt.Errorf("failed to create org_user relationship: %w", err) } - createdUser = user.User{ - ID: userID, - UID: cmd.UID, - Login: cmd.Login, - Email: cmd.Email, - Name: cmd.Name, - OrgID: cmd.OrgID, - IsAdmin: cmd.IsAdmin, - IsDisabled: cmd.IsDisabled, - EmailVerified: cmd.EmailVerified, - IsProvisioned: cmd.IsProvisioned, - Salt: cmd.Salt, - Rands: cmd.Rands, - Created: cmd.Created.Time, - Updated: cmd.Updated.Time, - LastSeenAt: cmd.LastSeenAt.Time, - IsServiceAccount: false, + createdUser = common.UserWithRole{ + User: user.User{ + ID: userID, + UID: cmd.UID, + Login: cmd.Login, + Email: cmd.Email, + Name: cmd.Name, + OrgID: cmd.OrgID, + IsAdmin: cmd.IsAdmin, + IsDisabled: cmd.IsDisabled, + EmailVerified: cmd.EmailVerified, + IsProvisioned: cmd.IsProvisioned, + Salt: cmd.Salt, + Rands: cmd.Rands, + Created: cmd.Created.Time, + Updated: cmd.Updated.Time, + LastSeenAt: cmd.LastSeenAt.Time, + IsServiceAccount: false, + }, + Role: cmd.Role, } return nil diff --git a/pkg/registry/apis/iam/legacy/users_query.sql b/pkg/registry/apis/iam/legacy/users_query.sql index 5c66dbd9c54..59b7b722465 100644 --- a/pkg/registry/apis/iam/legacy/users_query.sql +++ b/pkg/registry/apis/iam/legacy/users_query.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM {{ .Ident .UserTable }} as u JOIN {{ .Ident .OrgUserTable }} as o ON u.id = o.user_id WHERE o.org_id = {{ .Arg .Query.OrgID }} AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 7bce600c92e..228fc98b636 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -2,12 +2,10 @@ package iam import ( "context" - "fmt" "maps" "strings" "github.com/prometheus/client_golang/prometheus" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -325,7 +323,7 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm case admission.Create: switch typedObj := a.GetObject().(type) { case *iamv0.User: - return b.validateCreateUser(ctx, a, o) + return user.ValidateOnCreate(ctx, typedObj) case *iamv0.ServiceAccount: return serviceaccount.ValidateOnCreate(ctx, typedObj) case *iamv0.Team: @@ -343,31 +341,6 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm return nil } -func (b *IdentityAccessManagementAPIBuilder) validateCreateUser(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { - userObj, ok := a.GetObject().(*iamv0.User) - if !ok { - return nil - } - - requester, err := identity.GetRequester(ctx) - if err != nil { - return apierrors.NewUnauthorized("no identity found") - } - - // Temporary validation that the user is not trying to create a Grafana Admin without being a Grafana Admin. - if userObj.Spec.GrafanaAdmin && !requester.GetIsGrafanaAdmin() { - return apierrors.NewForbidden(iamv0.UserResourceInfo.GroupResource(), - userObj.Name, - fmt.Errorf("only grafana admins can create grafana admins")) - } - - if userObj.Spec.Login == "" && userObj.Spec.Email == "" { - return apierrors.NewBadRequest("user must have either login or email") - } - - return nil -} - // Mutate implements builder.APIGroupMutation. // TODO: Move this to the MutateFunc of the user resource after moving the APIs to use the app-platofrm-sdk. // TODO: https://github.com/grafana/grafana/blob/main/apps/playlist/pkg/app/app.go#L62 diff --git a/pkg/registry/apis/iam/user/mutate.go b/pkg/registry/apis/iam/user/mutate.go index 75c55d3d029..ca7a2558123 100644 --- a/pkg/registry/apis/iam/user/mutate.go +++ b/pkg/registry/apis/iam/user/mutate.go @@ -14,9 +14,6 @@ func MutateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error { if obj.Spec.Login == "" { obj.Spec.Login = obj.Spec.Email } - if obj.Spec.Email == "" { - obj.Spec.Email = obj.Spec.Login - } return nil } diff --git a/pkg/registry/apis/iam/user/mutate_test.go b/pkg/registry/apis/iam/user/mutate_test.go index 0429e554799..0ee742902c1 100644 --- a/pkg/registry/apis/iam/user/mutate_test.go +++ b/pkg/registry/apis/iam/user/mutate_test.go @@ -44,7 +44,6 @@ func TestMutateOnCreate_LoginEmail(t *testing.T) { }, }, expectedLogin: "only.login", - expectedEmail: "only.login", }, { name: "login and email already lowercase", diff --git a/pkg/registry/apis/iam/user/store.go b/pkg/registry/apis/iam/user/store.go index 1c1bf9eae67..893fa456c60 100644 --- a/pkg/registry/apis/iam/user/store.go +++ b/pkg/registry/apis/iam/user/store.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/common" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/util" apierrors "k8s.io/apimachinery/pkg/api/errors" ) @@ -196,16 +196,17 @@ func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createVali return nil, fmt.Errorf("expected User object, got %T", obj) } + if userObj.GenerateName != "" { + userObj.Name = userObj.GenerateName + util.GenerateShortUID() + userObj.GenerateName = "" + } + if createValidation != nil { if err := createValidation(ctx, obj); err != nil { return nil, err } } - if userObj.Spec.Login == "" && userObj.Spec.Email == "" { - return nil, fmt.Errorf("user must have either login or email") - } - createCmd := legacy.CreateUserCommand{ UID: userObj.Name, Login: userObj.Spec.Login, @@ -215,6 +216,7 @@ func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createVali IsDisabled: userObj.Spec.Disabled, EmailVerified: userObj.Spec.EmailVerified, IsProvisioned: userObj.Spec.Provisioned, + Role: userObj.Spec.Role, } result, err := s.store.CreateUser(ctx, ns, createCmd) @@ -226,7 +228,7 @@ func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createVali return &iamUser, nil } -func toUserItem(u *user.User, ns string) iamv0alpha1.User { +func toUserItem(u *common.UserWithRole, ns string) iamv0alpha1.User { item := &iamv0alpha1.User{ ObjectMeta: metav1.ObjectMeta{ Name: u.UID, @@ -242,6 +244,7 @@ func toUserItem(u *user.User, ns string) iamv0alpha1.User { Disabled: u.IsDisabled, GrafanaAdmin: u.IsAdmin, Provisioned: u.IsProvisioned, + Role: u.Role, }, } obj, _ := utils.MetaAccessor(item) diff --git a/pkg/registry/apis/iam/user/validate.go b/pkg/registry/apis/iam/user/validate.go new file mode 100644 index 00000000000..444015cc8f9 --- /dev/null +++ b/pkg/registry/apis/iam/user/validate.go @@ -0,0 +1,31 @@ +package user + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error { + requester, err := identity.GetRequester(ctx) + if err != nil { + return apierrors.NewUnauthorized("no identity found") + } + + // Temporary validation that the user is not trying to create a Grafana Admin without being a Grafana Admin. + if obj.Spec.GrafanaAdmin && !requester.GetIsGrafanaAdmin() { + return apierrors.NewForbidden(iamv0alpha1.UserResourceInfo.GroupResource(), + obj.Name, + fmt.Errorf("only grafana admins can create grafana admins")) + } + + if obj.Spec.Login == "" && obj.Spec.Email == "" { + return apierrors.NewBadRequest("user must have either login or email") + } + + return nil +} diff --git a/pkg/registry/apis/iam/user/validate_test.go b/pkg/registry/apis/iam/user/validate_test.go new file mode 100644 index 00000000000..675b62255f1 --- /dev/null +++ b/pkg/registry/apis/iam/user/validate_test.go @@ -0,0 +1,123 @@ +package user + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/authlib/types" + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +func TestValidateOnCreate(t *testing.T) { + tests := []struct { + name string + user *iamv0alpha1.User + requester *identity.StaticRequester + expectError bool + errorContains string + }{ + { + name: "valid user creation by grafana admin", + user: &iamv0alpha1.User{ + Spec: iamv0alpha1.UserSpec{ + Login: "testuser", + }, + }, + requester: &identity.StaticRequester{ + Type: types.TypeUser, + IsGrafanaAdmin: true, + }, + expectError: false, + }, + { + name: "grafana admin creating another grafana admin", + user: &iamv0alpha1.User{ + Spec: iamv0alpha1.UserSpec{ + Login: "newadmin", + GrafanaAdmin: true, + }, + }, + requester: &identity.StaticRequester{ + Type: types.TypeUser, + IsGrafanaAdmin: true, + }, + expectError: false, + }, + { + name: "non-admin trying to create a grafana admin", + user: &iamv0alpha1.User{ + Spec: iamv0alpha1.UserSpec{ + Login: "newadmin", + GrafanaAdmin: true, + }, + }, + requester: &identity.StaticRequester{ + Type: types.TypeUser, + IsGrafanaAdmin: false, + }, + expectError: true, + errorContains: "only grafana admins can create grafana admins", + }, + { + name: "user with empty login and email", + user: &iamv0alpha1.User{ + Spec: iamv0alpha1.UserSpec{}, + }, + requester: &identity.StaticRequester{ + Type: types.TypeUser, + IsGrafanaAdmin: false, + }, + expectError: true, + errorContains: "user must have either login or email", + }, + { + name: "user with only login", + user: &iamv0alpha1.User{ + Spec: iamv0alpha1.UserSpec{ + Login: "testuser", + }, + }, + requester: &identity.StaticRequester{ + Type: types.TypeUser, + IsGrafanaAdmin: false, + }, + expectError: false, + }, + { + name: "user with only email", + user: &iamv0alpha1.User{ + Spec: iamv0alpha1.UserSpec{ + Email: "test@test.com", + }, + }, + requester: &identity.StaticRequester{ + Type: types.TypeUser, + IsGrafanaAdmin: false, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := identity.WithRequester( + context.Background(), + tt.requester, + ) + + err := ValidateOnCreate(ctx, tt.user) + + if tt.expectError { + require.Error(t, err) + if tt.errorContains != "" { + require.Contains(t, err.Error(), tt.errorContains) + } + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/tests/apis/iam/iam_test.go b/pkg/tests/apis/iam/iam_test.go index b7a6c3284ac..5ad2eccd8f4 100644 --- a/pkg/tests/apis/iam/iam_test.go +++ b/pkg/tests/apis/iam/iam_test.go @@ -2,18 +2,13 @@ package identity import ( "context" - "fmt" "testing" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" @@ -98,7 +93,8 @@ func TestIntegrationIdentity(t *testing.T) { "grafanaAdmin": true, "login": "admin", "name": "", - "provisioned": false + "provisioned": false, + "role": "Admin" }, { "disabled": false, @@ -107,7 +103,8 @@ func TestIntegrationIdentity(t *testing.T) { "grafanaAdmin": true, "login": "grafana-admin", "name": "admin2", - "provisioned": false + "provisioned": false, + "role": "Admin" }, { "disabled": false, @@ -116,7 +113,8 @@ func TestIntegrationIdentity(t *testing.T) { "grafanaAdmin": false, "login": "editor", "name": "editor", - "provisioned": false + "provisioned": false, + "role": "Editor" }, { "disabled": false, @@ -125,7 +123,8 @@ func TestIntegrationIdentity(t *testing.T) { "grafanaAdmin": false, "login": "viewer", "name": "viewer", - "provisioned": false + "provisioned": false, + "role": "Viewer" }, { "disabled": false, @@ -134,7 +133,8 @@ func TestIntegrationIdentity(t *testing.T) { "grafanaAdmin": false, "login": "none", "name": "none", - "provisioned": false + "provisioned": false, + "role": "None" } ]`, found) @@ -157,7 +157,8 @@ func TestIntegrationIdentity(t *testing.T) { "grafanaAdmin": true, "login": "grafana-admin", "name": "admin2", - "provisioned": false + "provisioned": false, + "role": "Admin" }, { "disabled": false, @@ -166,7 +167,8 @@ func TestIntegrationIdentity(t *testing.T) { "grafanaAdmin": false, "login": "admin2-org-2", "name": "admin2", - "provisioned": false + "provisioned": false, + "role": "Admin" }, { "disabled": false, @@ -175,7 +177,8 @@ func TestIntegrationIdentity(t *testing.T) { "grafanaAdmin": false, "login": "editor-org-2", "name": "editor", - "provisioned": false + "provisioned": false, + "role": "Editor" }, { "disabled": false, @@ -184,7 +187,8 @@ func TestIntegrationIdentity(t *testing.T) { "grafanaAdmin": false, "login": "viewer-org-2", "name": "viewer", - "provisioned": false + "provisioned": false, + "role": "Viewer" }, { "disabled": false, @@ -193,174 +197,9 @@ func TestIntegrationIdentity(t *testing.T) { "grafanaAdmin": false, "login": "none-org-2", "name": "none", - "provisioned": false + "provisioned": false, + "role": "None" } ] `, found) }) } - -func TestIntegrationUsers(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - // TODO: Figure out why rest.Mode4 is failing - modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3} - for _, mode := range modes { - t.Run(fmt.Sprintf("User CRUD operations with dual writer mode %d", mode), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "users.iam.grafana.app": { - DualWriterMode: mode, - }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, - featuremgmt.FlagKubernetesAuthnMutation, - }, - }) - doUserCRUDTestsUsingTheNewAPIs(t, helper) - - if mode < 3 { - doUserCRUDTestsUsingTheLegacyAPIs(t, helper) - } - }) - } -} - -func doUserCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { - t.Run("should create user and delete it using the new APIs as a GrafanaAdmin", func(t *testing.T) { - ctx := context.Background() - - userClient := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), - GVR: gvrUsers, - }) - - // Create the user - created, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml"), metav1.CreateOptions{}) - require.NoError(t, err) - require.NotNil(t, created) - - // Verify creation response - createdSpec := created.Object["spec"].(map[string]interface{}) - require.Equal(t, "testuser1@example123.com", createdSpec["email"]) - require.Equal(t, "testuser1", createdSpec["login"]) - require.Equal(t, "Test User 1", createdSpec["name"]) - require.Equal(t, false, createdSpec["provisioned"]) - - // Get the UID from created user for fetching - createdUID := created.GetName() - require.NotEmpty(t, createdUID) - - _, err = userClient.Resource.List(ctx, metav1.ListOptions{}) - require.NoError(t, err) - - fetched, err := userClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) - require.NoError(t, err) - require.NotNil(t, fetched) - - // Verify fetched user matches created user - fetchedSpec := fetched.Object["spec"].(map[string]interface{}) - require.Equal(t, "testuser1@example123.com", fetchedSpec["email"]) - require.Equal(t, "testuser1", fetchedSpec["login"]) - require.Equal(t, "Test User 1", fetchedSpec["name"]) - require.Equal(t, false, fetchedSpec["provisioned"]) - - // Verify metadata - require.Equal(t, createdUID, fetched.GetName()) - require.Equal(t, "default", fetched.GetNamespace()) - - // TODO: Uncomment when we know how to handle global scope (global.users:) - // err = userClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{}) - // require.NoError(t, err) - - // Verify deletion - // _, err = userClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) - // require.Error(t, err) - // require.Contains(t, err.Error(), "not found") - }) - - t.Run("should not be able to create user when using a user with insufficient permissions", func(t *testing.T) { - for _, user := range []apis.User{ - helper.OrgB.Admin, // Not a Grafana Admin - helper.Org1.Editor, - helper.Org1.Viewer, - } { - t.Run(fmt.Sprintf("with basic role_%s", user.Identity.GetOrgRole()), func(t *testing.T) { - ctx := context.Background() - userClient := helper.GetResourceClient(apis.ResourceClientArgs{ - User: user, - Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), - GVR: gvrUsers, - }) - - // Create the user - _, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml"), metav1.CreateOptions{}) - require.Error(t, err) - var statusErr *errors.StatusError - require.ErrorAs(t, err, &statusErr) - require.Equal(t, int32(403), statusErr.ErrStatus.Code) - }) - } - }) -} - -func doUserCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) { - t.Run("should create user using legacy APIs and delete it using the new APIs", func(t *testing.T) { - ctx := context.Background() - userClient := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvrUsers, - }) - - legacyUserPayload := `{ - "name": "Test User 2", - "email": "testuser2@example.com", - "login": "testuser2", - "password": "password123" - }` - - rsp := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: "POST", - Path: "/api/admin/users", - Body: []byte(legacyUserPayload), - }, &user.User{}) - - require.NotNil(t, rsp) - require.Equal(t, 200, rsp.Response.StatusCode) - require.NotEmpty(t, rsp.Result.UID) - - // Now try to fetch the user via the new API - user, err := userClient.Resource.Get(context.Background(), rsp.Result.UID, metav1.GetOptions{}) - require.NoError(t, err) - require.NotNil(t, user) - - // Verify fetched user matches created user - userSpec := user.Object["spec"].(map[string]interface{}) - require.Equal(t, "testuser2@example.com", userSpec["email"]) - require.Equal(t, "testuser2", userSpec["login"]) - require.Equal(t, "Test User 2", userSpec["name"]) - require.Equal(t, false, userSpec["provisioned"]) - - // Verify metadata - require.Equal(t, rsp.Result.UID, user.GetName()) - require.Equal(t, "default", user.GetNamespace()) - - // Now delete the user using the legacy API - deleteRsp := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: "DELETE", - Path: fmt.Sprintf("/api/admin/users/%d", rsp.Result.ID), - }, &apis.AnyResource{}) - require.Equal(t, 200, deleteRsp.Response.StatusCode) - - // Verify deletion - _, err = userClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{}) - require.Error(t, err) - require.Contains(t, err.Error(), "not found") - }) -} diff --git a/pkg/tests/apis/iam/user_integration_test.go b/pkg/tests/apis/iam/user_integration_test.go new file mode 100644 index 00000000000..d24ac919b5d --- /dev/null +++ b/pkg/tests/apis/iam/user_integration_test.go @@ -0,0 +1,184 @@ +package identity + +import ( + "context" + "fmt" + "testing" + + "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/util/testutil" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestIntegrationUsers(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + // TODO: Figure out why rest.Mode4 is failing + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3} + for _, mode := range modes { + t.Run(fmt.Sprintf("User CRUD operations with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "users.iam.grafana.app": { + DualWriterMode: mode, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, + featuremgmt.FlagKubernetesAuthnMutation, + }, + }) + doUserCRUDTestsUsingTheNewAPIs(t, helper) + + if mode < 3 { + doUserCRUDTestsUsingTheLegacyAPIs(t, helper) + } + }) + } +} + +func doUserCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { + t.Run("should create user and delete it using the new APIs as a GrafanaAdmin", func(t *testing.T) { + ctx := context.Background() + + userClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrUsers, + }) + + // Create the user + created, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml"), metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, created) + + // Verify creation response + createdSpec := created.Object["spec"].(map[string]interface{}) + require.Equal(t, "testuser1@example123.com", createdSpec["email"]) + require.Equal(t, "testuser1", createdSpec["login"]) + require.Equal(t, "Test User 1", createdSpec["name"]) + require.Equal(t, false, createdSpec["provisioned"]) + + // Get the UID from created user for fetching + createdUID := created.GetName() + require.NotEmpty(t, createdUID) + + _, err = userClient.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + + fetched, err := userClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, fetched) + + // Verify fetched user matches created user + fetchedSpec := fetched.Object["spec"].(map[string]interface{}) + require.Equal(t, "testuser1@example123.com", fetchedSpec["email"]) + require.Equal(t, "testuser1", fetchedSpec["login"]) + require.Equal(t, "Test User 1", fetchedSpec["name"]) + require.Equal(t, false, fetchedSpec["provisioned"]) + + // Verify metadata + require.Equal(t, createdUID, fetched.GetName()) + require.Equal(t, "default", fetched.GetNamespace()) + + // TODO: Uncomment when we know how to handle global scope (global.users:) + // err = userClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{}) + // require.NoError(t, err) + + // Verify deletion + // _, err = userClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) + // require.Error(t, err) + // require.Contains(t, err.Error(), "not found") + }) + + t.Run("should not be able to create user when using a user with insufficient permissions", func(t *testing.T) { + for _, user := range []apis.User{ + helper.OrgB.Admin, // Not a Grafana Admin + helper.Org1.Editor, + helper.Org1.Viewer, + } { + t.Run(fmt.Sprintf("with basic role_%s", user.Identity.GetOrgRole()), func(t *testing.T) { + ctx := context.Background() + userClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: user, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrUsers, + }) + + // Create the user + _, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml"), metav1.CreateOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(403), statusErr.ErrStatus.Code) + }) + } + }) +} + +func doUserCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) { + t.Run("should create user using legacy APIs and delete it using the new APIs", func(t *testing.T) { + ctx := context.Background() + userClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvrUsers, + }) + + legacyUserPayload := `{ + "name": "Test User 2", + "email": "testuser2@example.com", + "login": "testuser2", + "password": "password123" + }` + + rsp := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "POST", + Path: "/api/admin/users", + Body: []byte(legacyUserPayload), + }, &user.User{}) + + require.NotNil(t, rsp) + require.Equal(t, 200, rsp.Response.StatusCode) + require.NotEmpty(t, rsp.Result.UID) + + // Now try to fetch the user via the new API + user, err := userClient.Resource.Get(context.Background(), rsp.Result.UID, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, user) + + // Verify fetched user matches created user + userSpec := user.Object["spec"].(map[string]interface{}) + require.Equal(t, "testuser2@example.com", userSpec["email"]) + require.Equal(t, "testuser2", userSpec["login"]) + require.Equal(t, "Test User 2", userSpec["name"]) + require.Equal(t, false, userSpec["provisioned"]) + + // Verify metadata + require.Equal(t, rsp.Result.UID, user.GetName()) + require.Equal(t, "default", user.GetNamespace()) + + // Now delete the user using the legacy API + deleteRsp := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "DELETE", + Path: fmt.Sprintf("/api/admin/users/%d", rsp.Result.ID), + }, &apis.AnyResource{}) + require.Equal(t, 200, deleteRsp.Response.StatusCode) + + // Verify deletion + _, err = userClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{}) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + }) +} diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index 4a741ca3cf8..1004a318bc6 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -4155,7 +4155,8 @@ "grafanaAdmin", "login", "name", - "provisioned" + "provisioned", + "role" ], "properties": { "disabled": { @@ -4183,9 +4184,12 @@ "default": "" }, "provisioned": { - "description": "What to do with salt, rands and password?", "type": "boolean", "default": false + }, + "role": { + "type": "string", + "default": "" } } }, @@ -6139,7 +6143,8 @@ "grafanaAdmin", "login", "name", - "provisioned" + "provisioned", + "role" ], "properties": { "disabled": { @@ -6167,9 +6172,12 @@ "default": "" }, "provisioned": { - "description": "What to do with salt, rands and password?", "type": "boolean", "default": false + }, + "role": { + "type": "string", + "default": "" } } },