Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs

This commit is contained in:
Ryan McKinley
2025-07-07 10:38:32 -07:00
787 changed files with 17267 additions and 5285 deletions
+5 -5
View File
@@ -10,11 +10,11 @@ require (
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/otel v1.36.0
k8s.io/api v0.33.1
k8s.io/apimachinery v0.33.1
k8s.io/apiserver v0.33.1
k8s.io/client-go v0.33.1
k8s.io/component-base v0.33.1
k8s.io/api v0.33.2
k8s.io/apimachinery v0.33.2
k8s.io/apiserver v0.33.2
k8s.io/client-go v0.33.2
k8s.io/component-base v0.33.2
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
sigs.k8s.io/structured-merge-diff/v4 v4.6.0
+10 -10
View File
@@ -520,16 +520,16 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo=
k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs=
k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI=
k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY=
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4=
k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M=
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0=
k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
+2 -2
View File
@@ -7,8 +7,8 @@ require (
github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team
github.com/stretchr/testify v1.10.0
k8s.io/apimachinery v0.33.1
k8s.io/apiserver v0.33.1
k8s.io/apimachinery v0.33.2
k8s.io/apiserver v0.33.2
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
)
+4 -4
View File
@@ -166,10 +166,10 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo=
k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4=
k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
+4 -3
View File
@@ -115,9 +115,10 @@ var serviceIdentityPermissions = getWildcardPermissions(
"datasources:delete",
"alert.provisioning:write",
"alert.provisioning.secrets:read",
"users:read", // accesscontrol.ActionUsersRead,
"org.users:read", // accesscontrol.ActionOrgUsersRead,
"teams:read", // accesscontrol.ActionTeamsRead,
"users:read", // accesscontrol.ActionUsersRead,
"org.users:read", // accesscontrol.ActionOrgUsersRead,
"teams:read", // accesscontrol.ActionTeamsRead,
"serviceaccounts:read", // serviceaccounts.ActionRead,
)
var serviceIdentityTokenPermissions = getTokenPermissions(
+36 -28
View File
@@ -2,8 +2,10 @@ package v0alpha1
import (
"fmt"
"strings"
"time"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -16,10 +18,11 @@ const (
APIVERSION = GROUP + "/" + VERSION
)
var UserResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
"users", "user", "User",
func() runtime.Object { return &User{} },
func() runtime.Object { return &UserList{} },
var userKind = iamv0alpha1.UserKind()
var UserResourceInfo = utils.NewResourceInfo(userKind.Group(), userKind.Version(),
userKind.GroupVersionResource().Resource, strings.ToLower(userKind.Kind()), userKind.Kind(),
func() runtime.Object { return userKind.ZeroValue() },
func() runtime.Object { return userKind.ZeroListValue() },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
@@ -28,7 +31,7 @@ var UserResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
u, ok := obj.(*User)
u, ok := obj.(*iamv0alpha1.User)
if ok {
return []interface{}{
u.Name,
@@ -42,10 +45,11 @@ var UserResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
},
)
var TeamResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
"teams", "team", "Team",
func() runtime.Object { return &Team{} },
func() runtime.Object { return &TeamList{} },
var teamKind = iamv0alpha1.TeamKind()
var TeamResourceInfo = utils.NewResourceInfo(teamKind.Group(), teamKind.Version(),
teamKind.GroupVersionResource().Resource, strings.ToLower(teamKind.Kind()), teamKind.Kind(),
func() runtime.Object { return teamKind.ZeroValue() },
func() runtime.Object { return teamKind.ZeroListValue() },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
@@ -54,7 +58,7 @@ var TeamResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*Team)
m, ok := obj.(*iamv0alpha1.Team)
if !ok {
return nil, fmt.Errorf("expected team")
}
@@ -68,10 +72,11 @@ var TeamResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
},
)
var ServiceAccountResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
"serviceaccounts", "serviceaccount", "ServiceAccount",
func() runtime.Object { return &ServiceAccount{} },
func() runtime.Object { return &ServiceAccountList{} },
var serviceAccountKind = iamv0alpha1.ServiceAccountKind()
var ServiceAccountResourceInfo = utils.NewResourceInfo(serviceAccountKind.Group(), serviceAccountKind.Version(),
serviceAccountKind.GroupVersionResource().Resource, strings.ToLower(serviceAccountKind.Kind()), serviceAccountKind.Kind(),
func() runtime.Object { return serviceAccountKind.ZeroValue() },
func() runtime.Object { return serviceAccountKind.ZeroListValue() },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
@@ -80,7 +85,7 @@ var ServiceAccountResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
sa, ok := obj.(*ServiceAccount)
sa, ok := obj.(*iamv0alpha1.ServiceAccount)
if ok {
return []interface{}{
sa.Name,
@@ -120,10 +125,13 @@ var SSOSettingResourceInfo = utils.NewResourceInfo(
},
)
var teamBindingKind = iamv0alpha1.TeamBindingKind()
var TeamBindingResourceInfo = utils.NewResourceInfo(
GROUP, VERSION, "teambindings", "teambinding", "TeamBinding",
func() runtime.Object { return &TeamBinding{} },
func() runtime.Object { return &TeamBindingList{} },
teamBindingKind.Group(), teamBindingKind.Version(),
teamBindingKind.GroupVersionResource().Resource,
strings.ToLower(teamBindingKind.Kind()), teamBindingKind.Kind(),
func() runtime.Object { return teamBindingKind.ZeroValue() },
func() runtime.Object { return teamBindingKind.ZeroListValue() },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
@@ -131,13 +139,13 @@ var TeamBindingResourceInfo = utils.NewResourceInfo(
{Name: "Created At", Type: "string", Format: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*TeamBinding)
m, ok := obj.(*iamv0alpha1.TeamBinding)
if !ok {
return nil, fmt.Errorf("expected team binding")
}
return []interface{}{
m.Name,
m.Spec.Team.Name,
m.Spec.TeamRef.Name,
m.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
},
@@ -158,19 +166,19 @@ var (
func AddKnownTypes(scheme *runtime.Scheme, version string) {
scheme.AddKnownTypes(
schema.GroupVersion{Group: GROUP, Version: version},
&User{},
&UserList{},
&iamv0alpha1.User{},
&iamv0alpha1.UserList{},
&UserTeamList{},
&ServiceAccount{},
&ServiceAccountList{},
&iamv0alpha1.ServiceAccount{},
&iamv0alpha1.ServiceAccountList{},
&ServiceAccountTokenList{},
&Team{},
&TeamList{},
&iamv0alpha1.Team{},
&iamv0alpha1.TeamList{},
&DisplayList{},
&SSOSetting{},
&SSOSettingList{},
&TeamBinding{},
&TeamBindingList{},
&iamv0alpha1.TeamBinding{},
&iamv0alpha1.TeamBindingList{},
&TeamMemberList{},
)
}
@@ -0,0 +1,19 @@
package v0alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type ServiceAccountTokenList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ServiceAccountToken `json:"items"`
}
type ServiceAccountToken struct {
Name string `json:"name,omitempty"`
Revoked bool `json:"revoked,omitempty"`
Expires *metav1.Time `json:"expires,omitempty"`
LastUsed *metav1.Time `json:"lastUsed,omitempty"`
Created metav1.Time `json:"created"`
}
@@ -1,50 +0,0 @@
package v0alpha1
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +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"`
}
func (s ServiceAccount) AuthID() string {
return fmt.Sprintf("%d", s.Spec.InternalID)
}
type ServiceAccountSpec struct {
Title string `json:"title,omitempty"`
Disabled bool `json:"disabled,omitempty"`
// This is currently used for authorization checks but we don't want to expose it
InternalID int64 `json:"-"`
}
// +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"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type ServiceAccountTokenList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ServiceAccountToken `json:"items"`
}
type ServiceAccountToken struct {
Name string `json:"name,omitempty"`
Revoked bool `json:"revoked,omitempty"`
Expires *metav1.Time `json:"expires,omitempty"`
LastUsed *metav1.Time `json:"lastUsed,omitempty"`
Created metav1.Time `json:"created"`
}
-51
View File
@@ -1,60 +1,9 @@
package v0alpha1
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +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:"title,omitempty"`
Email string `json:"email,omitempty"`
// This is currently used for authorization checks but we don't want to expose it
InternalID int64 `json:"-"`
}
func (t Team) AuthID() string {
return fmt.Sprintf("%d", t.Spec.InternalID)
}
// +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"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TeamBinding struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TeamBindingSpec `json:"spec,omitempty"`
}
type TeamBindingSpec struct {
Subjects []TeamSubject `json:"subjects,omitempty"`
Team TeamRef `json:"team,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TeamBindingList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TeamBinding `json:"items"`
}
type TeamSubject struct {
// Identity is a reference to the identity of this subject.
Identity IdentityRef `json:"identity"`
+1 -35
View File
@@ -1,40 +1,6 @@
package v0alpha1
import (
"fmt"
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"`
}
func (u User) AuthID() string {
return fmt.Sprintf("%d", u.Spec.InternalID)
}
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"`
// This is currently used for authorization checks but we don't want to expose it
InternalID int64 `json:"-"`
}
// +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"`
}
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type UserTeamList struct {
@@ -162,82 +162,6 @@ func (in *SSOSettingSpec) DeepCopy() *SSOSettingSpec {
return out
}
// 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 *ServiceAccountToken) DeepCopyInto(out *ServiceAccountToken) {
*out = *in
@@ -296,148 +220,6 @@ func (in *ServiceAccountTokenList) DeepCopyObject() runtime.Object {
return nil
}
// 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 *TeamBinding) DeepCopyInto(out *TeamBinding) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamBinding.
func (in *TeamBinding) DeepCopy() *TeamBinding {
if in == nil {
return nil
}
out := new(TeamBinding)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TeamBinding) 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 *TeamBindingList) DeepCopyInto(out *TeamBindingList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TeamBinding, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamBindingList.
func (in *TeamBindingList) DeepCopy() *TeamBindingList {
if in == nil {
return nil
}
out := new(TeamBindingList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TeamBindingList) 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 *TeamBindingSpec) DeepCopyInto(out *TeamBindingSpec) {
*out = *in
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]TeamSubject, len(*in))
copy(*out, *in)
}
out.Team = in.Team
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamBindingSpec.
func (in *TeamBindingSpec) DeepCopy() *TeamBindingSpec {
if in == nil {
return nil
}
out := new(TeamBindingSpec)
in.DeepCopyInto(out)
return out
}
// 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 *TeamMember) DeepCopyInto(out *TeamMember) {
*out = *in
@@ -502,22 +284,6 @@ func (in *TeamRef) DeepCopy() *TeamRef {
return out
}
// 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 *TeamSubject) DeepCopyInto(out *TeamSubject) {
*out = *in
@@ -535,82 +301,6 @@ func (in *TeamSubject) DeepCopy() *TeamSubject {
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
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UserTeam) DeepCopyInto(out *UserTeam) {
*out = *in
@@ -20,24 +20,12 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.SSOSetting": schema_pkg_apis_iam_v0alpha1_SSOSetting(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.SSOSettingList": schema_pkg_apis_iam_v0alpha1_SSOSettingList(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.SSOSettingSpec": schema_pkg_apis_iam_v0alpha1_SSOSettingSpec(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccount": schema_pkg_apis_iam_v0alpha1_ServiceAccount(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountList": schema_pkg_apis_iam_v0alpha1_ServiceAccountList(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountSpec": schema_pkg_apis_iam_v0alpha1_ServiceAccountSpec(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountToken": schema_pkg_apis_iam_v0alpha1_ServiceAccountToken(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountTokenList": schema_pkg_apis_iam_v0alpha1_ServiceAccountTokenList(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.Team": schema_pkg_apis_iam_v0alpha1_Team(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBinding": schema_pkg_apis_iam_v0alpha1_TeamBinding(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBindingList": schema_pkg_apis_iam_v0alpha1_TeamBindingList(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBindingSpec": schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamList": schema_pkg_apis_iam_v0alpha1_TeamList(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamMember": schema_pkg_apis_iam_v0alpha1_TeamMember(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamMemberList": schema_pkg_apis_iam_v0alpha1_TeamMemberList(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamRef": schema_pkg_apis_iam_v0alpha1_TeamRef(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamSpec": schema_pkg_apis_iam_v0alpha1_TeamSpec(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamSubject": schema_pkg_apis_iam_v0alpha1_TeamSubject(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.User": schema_pkg_apis_iam_v0alpha1_User(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserList": schema_pkg_apis_iam_v0alpha1_UserList(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserSpec": schema_pkg_apis_iam_v0alpha1_UserSpec(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserTeam": schema_pkg_apis_iam_v0alpha1_UserTeam(ref),
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserTeamList": schema_pkg_apis_iam_v0alpha1_UserTeamList(ref),
}
@@ -328,118 +316,6 @@ func schema_pkg_apis_iam_v0alpha1_SSOSettingSpec(ref common.ReferenceCallback) c
}
}
func schema_pkg_apis_iam_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/apis/iam/v0alpha1.ServiceAccountSpec"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_iam_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/apis/iam/v0alpha1.ServiceAccount"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccount", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_iam_v0alpha1_ServiceAccountSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"title": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"disabled": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
},
},
},
}
}
func schema_pkg_apis_iam_v0alpha1_ServiceAccountToken(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -530,215 +406,6 @@ func schema_pkg_apis_iam_v0alpha1_ServiceAccountTokenList(ref common.ReferenceCa
}
}
func schema_pkg_apis_iam_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/apis/iam/v0alpha1.TeamSpec"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_iam_v0alpha1_TeamBinding(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/apis/iam/v0alpha1.TeamBindingSpec"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBindingSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_iam_v0alpha1_TeamBindingList(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/apis/iam/v0alpha1.TeamBinding"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBinding", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"subjects": {
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/apis/iam/v0alpha1.TeamSubject"),
},
},
},
},
},
"team": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamRef"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamRef", "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamSubject"},
}
}
func schema_pkg_apis_iam_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/apis/iam/v0alpha1.Team"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.Team", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_iam_v0alpha1_TeamMember(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -864,30 +531,6 @@ func schema_pkg_apis_iam_v0alpha1_TeamRef(ref common.ReferenceCallback) common.O
}
}
func schema_pkg_apis_iam_v0alpha1_TeamSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"title": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"email": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
func schema_pkg_apis_iam_v0alpha1_TeamSubject(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -918,136 +561,6 @@ func schema_pkg_apis_iam_v0alpha1_TeamSubject(ref common.ReferenceCallback) comm
}
}
func schema_pkg_apis_iam_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/apis/iam/v0alpha1.UserSpec"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_iam_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/apis/iam/v0alpha1.User"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/iam/v0alpha1.User", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_iam_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: "",
},
},
},
},
},
}
}
func schema_pkg_apis_iam_v0alpha1_UserTeam(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -1,4 +1,3 @@
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/iam/v0alpha1,DisplayList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/iam/v0alpha1,TeamBindingSpec,Subjects
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/iam/v0alpha1,Display,InternalID
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/iam/v0alpha1,DisplayList,Items
+5 -5
View File
@@ -8,8 +8,8 @@ require (
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.33.1
k8s.io/apiserver v0.33.1
k8s.io/apimachinery v0.33.2
k8s.io/apiserver v0.33.2
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
)
@@ -86,9 +86,9 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/api v0.33.1 // indirect
k8s.io/client-go v0.33.1 // indirect
k8s.io/component-base v0.33.1 // indirect
k8s.io/api v0.33.2 // indirect
k8s.io/client-go v0.33.2 // indirect
k8s.io/component-base v0.33.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
+10 -10
View File
@@ -319,16 +319,16 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo=
k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs=
k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI=
k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY=
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4=
k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M=
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0=
k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
+6 -6
View File
@@ -5,16 +5,16 @@ go 1.24.4
require (
github.com/google/go-cmp v0.7.0
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d
github.com/grafana/grafana-app-sdk/logging v0.38.2
github.com/grafana/grafana-app-sdk/logging v0.39.1
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
github.com/prometheus/client_golang v1.22.0
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
k8s.io/apimachinery v0.33.1
k8s.io/apiserver v0.33.1
k8s.io/component-base v0.33.1
k8s.io/apimachinery v0.33.2
k8s.io/apiserver v0.33.2
k8s.io/component-base v0.33.2
k8s.io/klog/v2 v2.130.1
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/structured-merge-diff/v4 v4.6.0
@@ -98,8 +98,8 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.33.1 // indirect
k8s.io/client-go v0.33.1 // indirect
k8s.io/api v0.33.2 // indirect
k8s.io/client-go v0.33.2 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
+12 -12
View File
@@ -84,8 +84,8 @@ github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAh
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE=
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E=
github.com/grafana/grafana-app-sdk/logging v0.38.2 h1:EdQTRxbbH72zdqJ09Z76zcSjfALJXkpPLgvKEPPnloc=
github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU=
github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
@@ -364,16 +364,16 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo=
k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs=
k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI=
k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY=
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4=
k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M=
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0=
k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
+1 -1
View File
@@ -6,7 +6,7 @@ require (
cuelang.org/go v0.11.1
github.com/dave/dst v0.27.3
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d
github.com/grafana/cog v0.0.35
github.com/grafana/cog v0.0.36
github.com/grafana/cuetsy v0.1.11
github.com/matryer/is v1.4.1
golang.org/x/tools v0.34.0
+2 -2
View File
@@ -31,8 +31,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cog v0.0.35 h1:ZrGca+X17lczqhuXVCxHbLyGszP8VHGh1eAQLxbplkY=
github.com/grafana/cog v0.0.35/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc=
github.com/grafana/cog v0.0.36 h1:5kijtkHRzabhCG+ck4sCAEtYp2luaBK8v1CrUHcQ0Es=
github.com/grafana/cog v0.0.36/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc=
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ=
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws=
github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk=
@@ -302,6 +302,8 @@ func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client
data.Groups = userInfo.Groups
}
data.raw = rawJSON
s.log.Debug("Resolved user data", "data", fmt.Sprintf("%+v", data))
return &data, nil
}
@@ -37,6 +37,8 @@ const (
rootUserRespBody = `{"id":1,"username":"root","name":"Administrator","state":"active","email":"root@example.org", "confirmed_at":"2022-09-13T19:38:04.891Z","is_admin":true,"namespace_id":1}`
editorUserRespBody = `{"id":3,"username":"gitlab-editor","name":"Gitlab Editor","state":"active","email":"gitlab-editor@example.org", "confirmed_at":"2022-09-13T19:38:04.891Z","is_admin":false,"namespace_id":1}`
editorUserIDToken = `{"sub":"3","preferred_username":"gitlab-editor","name":"Gitlab Editor","email":"gitlab-editor@example.org","email_verified":true,"groups_direct":["editors", "viewers"]}` // #nosec G101 not a hardcoded credential
adminGroup = `{"id":4,"web_url":"http://grafana-gitlab.local/groups/admins","name":"Admins","path":"admins","project_creation_level":"developer","full_name":"Admins","full_path":"admins","created_at":"2022-09-13T19:38:04.891Z"}`
editorGroup = `{"id":5,"web_url":"http://grafana-gitlab.local/groups/editors","name":"Editors","path":"editors","project_creation_level":"developer","full_name":"Editors","full_path":"editors","created_at":"2022-09-13T19:38:15.074Z"}`
viewerGroup = `{"id":6,"web_url":"http://grafana-gitlab.local/groups/viewers","name":"Viewers","path":"viewers","project_creation_level":"developer","full_name":"Viewers","full_path":"viewers","created_at":"2022-09-13T19:38:25.777Z"}`
@@ -61,6 +63,7 @@ func TestSocialGitlab_UserInfo(t *testing.T) {
GroupsRespBody string
GroupHeaders map[string]string
RoleAttributePath string
IDToken string
ExpectedLogin string
ExpectedEmail string
ExpectedRoles map[int64]org.RoleType
@@ -180,6 +183,24 @@ func TestSocialGitlab_UserInfo(t *testing.T) {
ExpectedEmail: "gitlab-editor@example.org",
ExpectedRoles: map[int64]org.RoleType{4: "Editor", 5: "Viewer"},
},
{
Name: "Maps roles from ID token attributes if available",
RoleAttributePath: `email=='gitlab-editor@example.org' && 'Editor' || 'Viewer'`,
IDToken: editorUserIDToken,
ExpectedLogin: "gitlab-editor",
ExpectedEmail: "gitlab-editor@example.org",
ExpectedRoles: map[int64]org.RoleType{1: "Editor"},
ExpectedGrafanaAdmin: nilPointer,
},
{
Name: "Maps groups from ID token groups if available",
RoleAttributePath: gitlabAttrPath,
IDToken: editorUserIDToken,
ExpectedLogin: "gitlab-editor",
ExpectedEmail: "gitlab-editor@example.org",
ExpectedRoles: map[int64]org.RoleType{1: "Editor"},
ExpectedGrafanaAdmin: nilPointer,
},
{
Name: "Should return error when neither role attribute path nor org mapping evaluates to a role and role attribute strict is enabled",
Cfg: conf{RoleAttributeStrict: true, OrgMapping: []string{"other:Org4:Editor"}},
@@ -230,8 +251,17 @@ func TestSocialGitlab_UserInfo(t *testing.T) {
require.Fail(t, "unexpected request URI: "+r.RequestURI)
}
}))
token := &oauth2.Token{}
if tt.IDToken != "" {
emptyJWTHeader := base64.RawURLEncoding.EncodeToString([]byte("{}"))
JWTBody := base64.RawURLEncoding.EncodeToString([]byte(tt.IDToken))
idToken := fmt.Sprintf("%s.%s.signature", emptyJWTHeader, JWTBody)
token = token.WithExtra(map[string]any{"id_token": idToken})
}
provider.info.ApiUrl = ts.URL + apiURI
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), &oauth2.Token{})
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
if tt.ExpectedError != nil {
require.ErrorIs(t, err, tt.ExpectedError)
return
@@ -382,6 +412,9 @@ func TestSocialGitlab_extractFromToken(t *testing.T) {
}
for _, tc := range testCases {
if tc.wantUser != nil {
tc.wantUser.raw = []byte(tc.payload)
}
t.Run(tc.name, func(t *testing.T) {
// Create a test client with a dummy token
client := oauth2.NewClient(context.Background(), &tokenSource{accessToken: "dummy_access_token"})
+1 -1
View File
@@ -7,7 +7,7 @@ replace github.com/grafana/grafana/pkg/codegen => ../../codegen
require (
cuelang.org/go v0.11.1
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d
github.com/grafana/cog v0.0.35
github.com/grafana/cog v0.0.36
github.com/grafana/cuetsy v0.1.11
github.com/grafana/grafana/pkg/codegen v0.0.0-20250514132646-acbc7b54ed9e
)
+2 -2
View File
@@ -30,8 +30,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cog v0.0.35 h1:ZrGca+X17lczqhuXVCxHbLyGszP8VHGh1eAQLxbplkY=
github.com/grafana/cog v0.0.35/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc=
github.com/grafana/cog v0.0.36 h1:5kijtkHRzabhCG+ck4sCAEtYp2luaBK8v1CrUHcQ0Es=
github.com/grafana/cog v0.0.36/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc=
github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk=
github.com/grafana/cuetsy v0.1.11/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+2 -2
View File
@@ -13,7 +13,7 @@ require (
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
google.golang.org/protobuf v1.36.6
k8s.io/apimachinery v0.33.1
k8s.io/apimachinery v0.33.2
)
require (
@@ -128,7 +128,7 @@ require (
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/client-go v0.33.1 // indirect
k8s.io/client-go v0.33.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
+4 -4
View File
@@ -430,10 +430,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
+13 -4
View File
@@ -5,8 +5,9 @@ import (
"strconv"
authlib "github.com/grafana/authlib/types"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/team"
)
@@ -20,11 +21,19 @@ func OptionalFormatInt(num int64) string {
return ""
}
func MapTeamPermission(p team.PermissionType) iamv0.TeamPermission {
func MapTeamPermission(p team.PermissionType) iamv0alpha1.TeamBindingTeamPermission {
if p == team.PermissionTypeAdmin {
return iamv0.TeamPermissionAdmin
return iamv0alpha1.TeamBindingTeamPermissionAdmin
} else {
return iamv0.TeamPermissionMember
return iamv0alpha1.TeamBindingTeamPermissionMember
}
}
func MapUserTeamPermission(p team.PermissionType) legacyiamv0.TeamPermission {
if p == team.PermissionTypeAdmin {
return legacyiamv0.TeamPermissionAdmin
} else {
return legacyiamv0.TeamPermissionMember
}
}
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
FROM {{ .Ident .UserTable }} as u JOIN {{ .Ident .OrgUserTable }} as o ON u.id = o.user_id
WHERE o.org_id = {{ .Arg .Query.OrgID }} AND ( 1=2
{{ if .Query.UIDs }}
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id
WHERE o.org_id = 2 AND ( 1=2
OR u.id IN (1, 2)
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id
WHERE o.org_id = 2 AND ( 1=2
OR uid IN ('a', 'b')
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id
WHERE o.org_id = 2 AND ( 1=2
OR uid IN ('a', 'b')
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
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
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
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
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
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
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
WHERE o.org_id = 2 AND ( 1=2
OR u.id IN (1, 2)
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
WHERE o.org_id = 2 AND ( 1=2
OR uid IN ('a', 'b')
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
WHERE o.org_id = 2 AND ( 1=2
OR uid IN ('a', 'b')
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
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
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
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
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
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
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
WHERE o.org_id = 2 AND ( 1=2
OR u.id IN (1, 2)
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
WHERE o.org_id = 2 AND ( 1=2
OR uid IN ('a', 'b')
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
WHERE o.org_id = 2 AND ( 1=2
OR uid IN ('a', 'b')
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
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
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
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
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
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
+2 -1
View File
@@ -165,7 +165,8 @@ func (s *legacySQLStore) queryUsers(ctx context.Context, sql *legacysql.LegacyDa
for rows.Next() {
u := user.User{}
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.Created, &u.Updated, &u.IsServiceAccount, &u.IsDisabled, &u.IsAdmin, &u.EmailVerified,
&u.IsProvisioned, &u.LastSeenAt,
)
if err != nil {
return res, err
+2 -1
View File
@@ -1,5 +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.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin,
u.email_verified, u.is_provisioned, u.last_seen_at
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
+6 -11
View File
@@ -2,6 +2,7 @@ package iam
import (
"context"
"maps"
"strings"
"github.com/prometheus/client_golang/prometheus"
@@ -149,18 +150,12 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
}
func (b *IdentityAccessManagementAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
defs := legacyiamv0.GetOpenAPIDefinitions
if b.enableAuthZApis {
defs = func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
def1 := legacyiamv0.GetOpenAPIDefinitions(ref)
def2 := iamv0.GetOpenAPIDefinitions(ref)
for k, v := range def2 {
def1[k] = v
}
return def1
}
return func(rc common.ReferenceCallback) map[string]common.OpenAPIDefinition {
dst := legacyiamv0.GetOpenAPIDefinitions(rc)
maps.Copy(dst, iamv0.GetOpenAPIDefinitions(rc))
return dst
}
return defs
}
func (b *IdentityAccessManagementAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
@@ -9,7 +9,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
claims "github.com/grafana/authlib/types"
legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"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"
@@ -32,7 +33,7 @@ type LegacyTokenRest struct {
// New implements rest.Storage.
func (s *LegacyTokenRest) New() runtime.Object {
return &iamv0.UserTeamList{}
return &legacyiamv0.ServiceAccountTokenList{}
}
// Destroy implements rest.Storage.
@@ -70,10 +71,10 @@ func (s *LegacyTokenRest) Connect(ctx context.Context, name string, options runt
return
}
list := &iamv0.ServiceAccountTokenList{Items: make([]iamv0.ServiceAccountToken, 0, len(res.Items))}
list := &legacyiamv0.ServiceAccountTokenList{Items: make([]legacyiamv0.ServiceAccountToken, 0, len(res.Items))}
for _, t := range res.Items {
list.Items = append(list.Items, mapToToken(t))
list.Items = append(list.Items, mapToToken(t, ns))
}
list.Continue = common.OptionalFormatInt(res.Continue)
@@ -92,7 +93,7 @@ func (s *LegacyTokenRest) ConnectMethods() []string {
return []string{http.MethodGet}
}
func mapToToken(t legacy.ServiceAccountToken) iamv0.ServiceAccountToken {
func mapToToken(t legacy.ServiceAccountToken, ns claims.NamespaceInfo) legacyiamv0.ServiceAccountToken {
var expires, lastUsed *metav1.Time
if t.Expires != nil {
@@ -105,7 +106,7 @@ func mapToToken(t legacy.ServiceAccountToken) iamv0.ServiceAccountToken {
lastUsed = &ts
}
return iamv0.ServiceAccountToken{
return legacyiamv0.ServiceAccountToken{
Name: t.Name,
Expires: expires,
LastUsed: lastUsed,
@@ -10,6 +10,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
claims "github.com/grafana/authlib/types"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
@@ -61,7 +62,7 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object,
func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
res, err := common.List(
ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options),
func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.ServiceAccount], error) {
func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.ServiceAccount], error) {
found, err := s.store.ListServiceAccounts(ctx, ns, legacy.ListServiceAccountsQuery{
Pagination: p,
})
@@ -70,12 +71,12 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
return nil, err
}
items := make([]iamv0.ServiceAccount, 0, len(found.Items))
items := make([]iamv0alpha1.ServiceAccount, 0, len(found.Items))
for _, sa := range found.Items {
items = append(items, toSAItem(sa, ns.Value))
}
return &common.ListResponse[iamv0.ServiceAccount]{
return &common.ListResponse[iamv0alpha1.ServiceAccount]{
Items: items,
RV: found.RV,
Continue: found.Continue,
@@ -87,21 +88,21 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
return nil, err
}
obj := &iamv0.ServiceAccountList{Items: res.Items}
obj := &iamv0alpha1.ServiceAccountList{Items: res.Items}
obj.Continue = common.OptionalFormatInt(res.Continue)
obj.ResourceVersion = common.OptionalFormatInt(res.RV)
return obj, nil
}
func toSAItem(sa legacy.ServiceAccount, ns string) iamv0.ServiceAccount {
item := iamv0.ServiceAccount{
func toSAItem(sa legacy.ServiceAccount, ns string) iamv0alpha1.ServiceAccount {
item := iamv0alpha1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: sa.UID,
Namespace: ns,
ResourceVersion: fmt.Sprintf("%d", sa.Updated.UnixMilli()),
CreationTimestamp: metav1.NewTime(sa.Created),
},
Spec: iamv0.ServiceAccountSpec{
Spec: iamv0alpha1.ServiceAccountSpec{
Title: sa.Name,
Disabled: sa.Disabled,
},
+10 -10
View File
@@ -11,6 +11,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
claims "github.com/grafana/authlib/types"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
@@ -64,7 +65,7 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object,
func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
res, err := common.List(
ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options),
func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.Team], error) {
func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.Team], error) {
found, err := s.store.ListTeams(ctx, ns, legacy.ListTeamQuery{
Pagination: p,
})
@@ -73,12 +74,12 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
return nil, err
}
teams := make([]iamv0.Team, 0, len(found.Teams))
teams := make([]iamv0alpha1.Team, 0, len(found.Teams))
for _, t := range found.Teams {
teams = append(teams, toTeamObject(t, ns))
}
return &common.ListResponse[iamv0.Team]{
return &common.ListResponse[iamv0alpha1.Team]{
Items: teams,
RV: found.RV,
Continue: found.Continue,
@@ -90,7 +91,7 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
return nil, fmt.Errorf("failed to list teams: %w", err)
}
list := &iamv0.TeamList{Items: res.Items}
list := &iamv0alpha1.TeamList{Items: res.Items}
list.Continue = common.OptionalFormatInt(res.Continue)
list.ResourceVersion = common.OptionalFormatInt(res.RV)
@@ -119,18 +120,17 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO
return &obj, nil
}
func toTeamObject(t team.Team, ns claims.NamespaceInfo) iamv0.Team {
obj := iamv0.Team{
func toTeamObject(t team.Team, ns claims.NamespaceInfo) iamv0alpha1.Team {
obj := iamv0alpha1.Team{
ObjectMeta: metav1.ObjectMeta{
Name: t.UID,
Namespace: ns.Value,
CreationTimestamp: metav1.NewTime(t.Created),
ResourceVersion: strconv.FormatInt(t.Updated.UnixMilli(), 10),
},
Spec: iamv0.TeamSpec{
Title: t.Name,
Email: t.Email,
InternalID: t.ID,
Spec: iamv0alpha1.TeamSpec{
Title: t.Name,
Email: t.Email,
},
}
meta, _ := utils.MetaAccessor(&obj)
+11 -13
View File
@@ -11,6 +11,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
claims "github.com/grafana/authlib/types"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
@@ -102,8 +103,8 @@ func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion.
return nil, err
}
list := iamv0.TeamBindingList{
Items: make([]iamv0.TeamBinding, 0, len(res.Bindings)),
list := iamv0alpha1.TeamBindingList{
Items: make([]iamv0alpha1.TeamBinding, 0, len(res.Bindings)),
}
for _, b := range res.Bindings {
@@ -116,7 +117,7 @@ func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion.
return &list, nil
}
func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) iamv0.TeamBinding {
func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) iamv0alpha1.TeamBinding {
rv := time.Time{}
ct := time.Now()
@@ -129,15 +130,15 @@ func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) iamv0.Tea
}
}
return iamv0.TeamBinding{
return iamv0alpha1.TeamBinding{
ObjectMeta: metav1.ObjectMeta{
Name: b.TeamUID,
Namespace: ns.Value,
ResourceVersion: strconv.FormatInt(rv.UnixMilli(), 10),
CreationTimestamp: metav1.NewTime(ct),
},
Spec: iamv0.TeamBindingSpec{
Team: iamv0.TeamRef{
Spec: iamv0alpha1.TeamBindingSpec{
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: b.TeamUID,
},
Subjects: mapToSubjects(b.Members),
@@ -145,14 +146,11 @@ func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) iamv0.Tea
}
}
func mapToSubjects(members []legacy.TeamMember) []iamv0.TeamSubject {
out := make([]iamv0.TeamSubject, 0, len(members))
func mapToSubjects(members []legacy.TeamMember) []iamv0alpha1.TeamBindingspecSubject {
out := make([]iamv0alpha1.TeamBindingspecSubject, 0, len(members))
for _, m := range members {
out = append(out, iamv0.TeamSubject{
Identity: iamv0.IdentityRef{
Type: claims.TypeUser,
Name: m.UserUID,
},
out = append(out, iamv0alpha1.TeamBindingspecSubject{
Name: m.UserUID,
Permission: common.MapTeamPermission(m.Permission),
})
}
+7 -7
View File
@@ -7,7 +7,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"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"
@@ -29,7 +29,7 @@ type LegacyUserTeamREST struct {
// New implements rest.Storage.
func (s *LegacyUserTeamREST) New() runtime.Object {
return &iamv0.UserTeamList{}
return &legacyiamv0.UserTeamList{}
}
// Destroy implements rest.Storage.
@@ -62,7 +62,7 @@ func (s *LegacyUserTeamREST) Connect(ctx context.Context, name string, options r
return
}
list := &iamv0.UserTeamList{Items: make([]iamv0.UserTeam, 0, len(res.Items))}
list := &legacyiamv0.UserTeamList{Items: make([]legacyiamv0.UserTeam, 0, len(res.Items))}
for _, m := range res.Items {
list.Items = append(list.Items, mapToUserTeam(m))
@@ -84,12 +84,12 @@ func (s *LegacyUserTeamREST) ConnectMethods() []string {
return []string{http.MethodGet}
}
func mapToUserTeam(t legacy.UserTeam) iamv0.UserTeam {
return iamv0.UserTeam{
func mapToUserTeam(t legacy.UserTeam) legacyiamv0.UserTeam {
return legacyiamv0.UserTeam{
Title: t.Name,
TeamRef: iamv0.TeamRef{
TeamRef: legacyiamv0.TeamRef{
Name: t.UID,
},
Permission: common.MapTeamPermission(t.Permission),
Permission: common.MapUserTeamPermission(t.Permission),
}
}
+22 -8
View File
@@ -3,6 +3,7 @@ package user
import (
"context"
"fmt"
"time"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -10,6 +11,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
claims "github.com/grafana/authlib/types"
iamv0alpha "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
@@ -18,6 +20,8 @@ import (
"github.com/grafana/grafana/pkg/services/user"
)
const AnnoKeyLastSeenAt = "iam.grafana.app/lastSeenAt"
var (
_ rest.Scoper = (*LegacyStore)(nil)
_ rest.SingularNameProvider = (*LegacyStore)(nil)
@@ -62,7 +66,7 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object,
func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
res, err := common.List(
ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options),
func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.User], error) {
func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha.User], error) {
found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{
Pagination: p,
})
@@ -71,12 +75,12 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
return nil, err
}
users := make([]iamv0.User, 0, len(found.Users))
users := make([]iamv0alpha.User, 0, len(found.Users))
for _, u := range found.Users {
users = append(users, toUserItem(&u, ns.Value))
}
return &common.ListResponse[iamv0.User]{
return &common.ListResponse[iamv0alpha.User]{
Items: users,
RV: found.RV,
Continue: found.Continue,
@@ -88,7 +92,7 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
return nil, err
}
obj := &iamv0.UserList{Items: res.Items}
obj := &iamv0alpha.UserList{Items: res.Items}
obj.Continue = common.OptionalFormatInt(res.Continue)
obj.ResourceVersion = common.OptionalFormatInt(res.RV)
return obj, nil
@@ -116,25 +120,35 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO
return &obj, nil
}
func toUserItem(u *user.User, ns string) iamv0.User {
item := &iamv0.User{
func toUserItem(u *user.User, ns string) iamv0alpha.User {
item := &iamv0alpha.User{
ObjectMeta: metav1.ObjectMeta{
Name: u.UID,
Namespace: ns,
ResourceVersion: fmt.Sprintf("%d", u.Updated.UnixMilli()),
CreationTimestamp: metav1.NewTime(u.Created),
},
Spec: iamv0.UserSpec{
Spec: iamv0alpha.UserSpec{
Name: u.Name,
Login: u.Login,
Email: u.Email,
EmailVerified: u.EmailVerified,
Disabled: u.IsDisabled,
InternalID: u.ID,
GrafanaAdmin: u.IsAdmin,
Provisioned: u.IsProvisioned,
},
}
obj, _ := utils.MetaAccessor(item)
obj.SetUpdatedTimestamp(&u.Updated)
obj.SetAnnotation(AnnoKeyLastSeenAt, formatTime(&u.LastSeenAt))
obj.SetDeprecatedInternalID(u.ID) // nolint:staticcheck
return *item
}
func formatTime(v *time.Time) string {
txt := ""
if v != nil && v.Unix() != 0 {
txt = v.UTC().Format(time.RFC3339)
}
return txt
}
@@ -61,8 +61,10 @@ func (a *decryptAuthorizer) Authorize(ctx context.Context, secureValueName strin
// TEMPORARY: while we can't onboard every app into secrets, we can block them from decrypting
// securevalues preemptively here before even reaching out to the database.
// This check can be removed once we open the gates for any service to use secrets.
if _, exists := a.allowList[serviceIdentity]; !exists || serviceIdentity == "" {
return serviceIdentity, false
if len(a.allowList) > 0 {
if _, exists := a.allowList[serviceIdentity]; !exists || serviceIdentity == "" {
return serviceIdentity, false
}
}
// Checks whether the token has the permission to decrypt secure values.
@@ -108,6 +108,15 @@ func TestDecryptAuthorizer(t *testing.T) {
require.False(t, allowed)
})
t.Run("when the allow list is empty, it allows all identities", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:decrypt"})
authorizer := ProvideDecryptAuthorizer(tracer, nil)
identity, allowed := authorizer.Authorize(ctx, "", []string{"identity"})
require.NotEmpty(t, identity)
require.True(t, allowed)
})
t.Run("when the identity is not in the allow list, it returns false", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:decrypt"})
authorizer := ProvideDecryptAuthorizer(tracer, map[string]struct{}{"allowed1": {}})
@@ -0,0 +1,268 @@
package service
import (
"context"
"fmt"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/utils"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/tracectx"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
type SecureValueService struct {
tracer trace.Tracer
accessClient claims.AccessClient
database contracts.Database
secureValueMetadataStorage contracts.SecureValueMetadataStorage
outboxQueue contracts.OutboxQueue
encryptionManager contracts.EncryptionManager
}
func ProvideSecureValueService(
tracer trace.Tracer,
accessClient claims.AccessClient,
database contracts.Database,
secureValueMetadataStorage contracts.SecureValueMetadataStorage,
outboxQueue contracts.OutboxQueue,
encryptionManager contracts.EncryptionManager,
) *SecureValueService {
return &SecureValueService{
tracer: tracer,
accessClient: accessClient,
database: database,
secureValueMetadataStorage: secureValueMetadataStorage,
outboxQueue: outboxQueue,
encryptionManager: encryptionManager,
}
}
func (s *SecureValueService) Create(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) {
ctx, span := s.tracer.Start(ctx, "SecureValueService.Create", trace.WithAttributes(
attribute.String("name", sv.GetName()),
attribute.String("namespace", sv.GetNamespace()),
attribute.String("actor", actorUID),
))
defer span.End()
sv.Status = secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhasePending, Message: "Creating secure value"}
var out *secretv0alpha1.SecureValue
encryptedSecret, err := s.encryptionManager.Encrypt(ctx, sv.Namespace, []byte(sv.Spec.Value.DangerouslyExposeAndConsumeValue()))
if err != nil {
return nil, fmt.Errorf("encrypting secure value secret: %w", err)
}
// Specifically here so that the spans from the worker are not inside the transaction.
requestID := tracectx.HexEncodeTraceFromContext(ctx)
if err := s.database.Transaction(ctx, func(ctx context.Context) error {
createdSecureValue, err := s.secureValueMetadataStorage.Create(ctx, sv, actorUID)
if err != nil {
return fmt.Errorf("failed to create securevalue: %w", err)
}
out = createdSecureValue
if _, err := s.outboxQueue.Append(ctx, contracts.AppendOutboxMessage{
RequestID: requestID,
Type: contracts.CreateSecretOutboxMessage,
Name: sv.Name,
Namespace: sv.Namespace,
EncryptedSecret: string(encryptedSecret),
KeeperName: sv.Spec.Keeper,
}); err != nil {
return fmt.Errorf("failed to append message to create secure value to outbox queue: %w", err)
}
return nil
}); err != nil {
return out, err
}
return out, nil
}
func (s *SecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) {
ctx, span := s.tracer.Start(ctx, "SecureValueService.Read", trace.WithAttributes(
attribute.String("name", name),
attribute.String("namespace", namespace.String()),
))
defer span.End()
return s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: false})
}
func (s *SecureValueService) List(ctx context.Context, namespace xkube.Namespace) (*secretv0alpha1.SecureValueList, error) {
ctx, span := s.tracer.Start(ctx, "SecureValueService.List", trace.WithAttributes(
attribute.String("namespace", namespace.String()),
))
defer span.End()
user, ok := claims.AuthInfoFrom(ctx)
if !ok {
return nil, fmt.Errorf("missing auth info in context")
}
hasPermissionFor, err := s.accessClient.Compile(ctx, user, claims.ListRequest{
Group: secretv0alpha1.GROUP,
Resource: secretv0alpha1.SecureValuesResourceInfo.GetName(),
Namespace: namespace.String(),
Verb: utils.VerbGet, // Why not VerbList?
})
if err != nil {
return nil, fmt.Errorf("failed to compile checker: %w", err)
}
secureValuesMetadata, err := s.secureValueMetadataStorage.List(ctx, namespace)
if err != nil {
return nil, fmt.Errorf("fetching secure values from storage: %+w", err)
}
out := make([]secretv0alpha1.SecureValue, 0)
for _, metadata := range secureValuesMetadata {
// Check whether the user has permission to access this specific SecureValue in the namespace.
if !hasPermissionFor(metadata.Name, "") {
continue
}
out = append(out, metadata)
}
return &secretv0alpha1.SecureValueList{
Items: out,
}, nil
}
func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, bool, error) {
ctx, span := s.tracer.Start(ctx, "SecureValueService.Create", trace.WithAttributes(
attribute.String("name", newSecureValue.GetName()),
attribute.String("namespace", newSecureValue.GetNamespace()),
attribute.String("actor", actorUID),
))
defer span.End()
// True when the effects of an update can be seen immediately.
// Never true in this case since updating a secure value is async.
const updateIsSync = false
var (
out *secretv0alpha1.SecureValue
encryptedSecret string
)
if newSecureValue.Spec.Value != "" {
buffer, err := s.encryptionManager.Encrypt(ctx, newSecureValue.Namespace, []byte(newSecureValue.Spec.Value.DangerouslyExposeAndConsumeValue()))
if err != nil {
return nil, false, fmt.Errorf("encrypting secure value secret: %w", err)
}
encryptedSecret = string(buffer)
}
// Especifically here so that the spans from the worker are not inside the transaction.
requestID := tracectx.HexEncodeTraceFromContext(ctx)
if err := s.database.Transaction(ctx, func(ctx context.Context) error {
sv, err := s.secureValueMetadataStorage.Read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{ForUpdate: true})
if err != nil {
return fmt.Errorf("fetching secure value: %+w", err)
}
if sv.Status.Phase == secretv0alpha1.SecureValuePhasePending {
return contracts.ErrSecureValueOperationInProgress
}
// Succeed immediately if the value is not going to be updated
if encryptedSecret == "" {
newSecureValue.Status = secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseSucceeded}
} else {
newSecureValue.Status = secretv0alpha1.SecureValueStatus{
Message: "Updating secure value",
Phase: secretv0alpha1.SecureValuePhasePending,
}
}
// Current implementation replaces everything passed in the spec, so it is not a PATCH. Do we want/need to support that?
updatedSecureValue, err := s.secureValueMetadataStorage.Update(ctx, newSecureValue, actorUID)
if err != nil {
return fmt.Errorf("failed to update secure value: %w", err)
}
out = updatedSecureValue
// Only the value needs to be updated asynchronously by the outbox worker
if encryptedSecret != "" {
if _, err := s.outboxQueue.Append(ctx, contracts.AppendOutboxMessage{
RequestID: requestID,
Type: contracts.UpdateSecretOutboxMessage,
Name: newSecureValue.Name,
Namespace: newSecureValue.Namespace,
EncryptedSecret: encryptedSecret,
KeeperName: newSecureValue.Spec.Keeper,
ExternalID: &updatedSecureValue.Status.ExternalID,
}); err != nil {
return fmt.Errorf("failed to append message to update secure value to outbox queue: %w", err)
}
}
return nil
}); err != nil {
return out, updateIsSync, err
}
return out, updateIsSync, nil
}
func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) {
ctx, span := s.tracer.Start(ctx, "SecureValueService.Delete", trace.WithAttributes(
attribute.String("name", name),
attribute.String("namespace", namespace.String()),
))
defer span.End()
// Set inside of the transaction callback
var out *secretv0alpha1.SecureValue
// Especifically here so that the spans from the worker are not inside the transaction.
requestID := tracectx.HexEncodeTraceFromContext(ctx)
if err := s.database.Transaction(ctx, func(ctx context.Context) error {
sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true})
if err != nil {
return fmt.Errorf("fetching secure value: %+w", err)
}
if sv.Status.Phase == secretv0alpha1.SecureValuePhasePending {
return contracts.ErrSecureValueOperationInProgress
}
sv.Status = secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhasePending, Message: "Deleting secure value"}
if err := s.secureValueMetadataStorage.SetStatus(ctx, namespace, name, sv.Status); err != nil {
return fmt.Errorf("setting secure value status phase: %+w", err)
}
if _, err := s.outboxQueue.Append(ctx, contracts.AppendOutboxMessage{
RequestID: requestID,
Type: contracts.DeleteSecretOutboxMessage,
Name: name,
Namespace: namespace.String(),
KeeperName: sv.Spec.Keeper,
ExternalID: &sv.Status.ExternalID,
}); err != nil {
return fmt.Errorf("appending delete secure value message to outbox queue: %+w", err)
}
out = sv
return nil
}); err != nil {
return out, err
}
return out, nil
}
@@ -0,0 +1,202 @@
package testutils
import (
"context"
"testing"
"time"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption"
"go.opentelemetry.io/otel/trace/noop"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
"github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper"
"github.com/grafana/grafana/pkg/registry/apis/secret/service"
"github.com/grafana/grafana/pkg/registry/apis/secret/worker"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/secret/database"
"github.com/grafana/grafana/pkg/storage/secret/metadata"
"github.com/grafana/grafana/pkg/storage/secret/migrator"
"github.com/stretchr/testify/require"
)
type setupConfig struct {
workerCfg worker.Config
keeperService contracts.KeeperService
}
func defaultSetupCfg() setupConfig {
return setupConfig{
workerCfg: worker.Config{
BatchSize: 10,
ReceiveTimeout: 1 * time.Second,
PollingInterval: time.Millisecond,
MaxMessageProcessingAttempts: 5,
},
}
}
func WithWorkerConfig(cfg worker.Config) func(*setupConfig) {
return func(setupCfg *setupConfig) {
setupCfg.workerCfg = cfg
}
}
func WithKeeperService(keeperService contracts.KeeperService) func(*setupConfig) {
return func(setupCfg *setupConfig) {
setupCfg.keeperService = keeperService
}
}
func Setup(t *testing.T, opts ...func(*setupConfig)) Sut {
setupCfg := defaultSetupCfg()
for _, opt := range opts {
opt(&setupCfg)
}
tracer := noop.NewTracerProvider().Tracer("test")
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
database := database.ProvideDatabase(testDB, tracer)
outboxQueue := metadata.ProvideOutboxQueue(database, tracer, nil)
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(database, tracer, features, nil)
require.NoError(t, err)
secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(database, tracer, features, nil)
require.NoError(t, err)
// Initialize access client + access control
accessControl := &actest.FakeAccessControl{ExpectedEvaluate: true}
accessClient := accesscontrol.NewLegacyAccessClient(accessControl)
defaultKey := "SdlklWklckeLS"
cfg := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: defaultKey,
EncryptionProvider: "secretKey.v1",
},
}
store, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features, nil)
require.NoError(t, err)
usageStats := &usagestats.UsageStatsMock{T: t}
encryptionManager, err := manager.ProvideEncryptionManager(
tracer,
store,
cfg,
usageStats,
encryption.ProvideThirdPartyProviderMap(),
)
require.NoError(t, err)
// Initialize encrypted value storage with a fake db
encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features)
require.NoError(t, err)
sqlKeeper := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encValueStore, nil)
var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper)
if setupCfg.keeperService != nil {
keeperService = setupCfg.keeperService
}
secureValueService := service.ProvideSecureValueService(tracer, accessClient, database, secureValueMetadataStorage, outboxQueue, encryptionManager)
worker, err := worker.NewWorker(
setupCfg.workerCfg,
tracer,
database,
outboxQueue,
secureValueMetadataStorage,
keeperMetadataStorage,
keeperService,
encryptionManager,
features,
nil, // metrics
)
require.NoError(t, err)
return Sut{Worker: worker, SecureValueService: secureValueService, SecureValueMetadataStorage: secureValueMetadataStorage, OutboxQueue: outboxQueue, Database: database}
}
type Sut struct {
Worker *worker.Worker
SecureValueService *service.SecureValueService
SecureValueMetadataStorage contracts.SecureValueMetadataStorage
OutboxQueue contracts.OutboxQueue
Database *database.Database
}
type CreateSvConfig struct {
Sv *secretv0alpha1.SecureValue
}
func CreateSvWithSv(sv *secretv0alpha1.SecureValue) func(*CreateSvConfig) {
return func(cfg *CreateSvConfig) {
cfg.Sv = sv
}
}
func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*secretv0alpha1.SecureValue, error) {
cfg := CreateSvConfig{
Sv: &secretv0alpha1.SecureValue{
ObjectMeta: metav1.ObjectMeta{
Name: "sv1",
Namespace: "ns1",
},
Spec: secretv0alpha1.SecureValueSpec{
Description: "desc1",
Value: secretv0alpha1.NewExposedSecureValue("v1"),
},
Status: secretv0alpha1.SecureValueStatus{
Phase: secretv0alpha1.SecureValuePhasePending,
},
},
}
for _, opt := range opts {
opt(&cfg)
}
createdSv, err := s.SecureValueService.Create(ctx, cfg.Sv, "actor")
if err != nil {
return nil, err
}
return createdSv, nil
}
func (s *Sut) UpdateSv(ctx context.Context, sv *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error) {
newSv, _, err := s.SecureValueService.Update(ctx, sv, "actor")
return newSv, err
}
func (s *Sut) DeleteSv(ctx context.Context, namespace, name string) (*secretv0alpha1.SecureValue, error) {
sv, err := s.SecureValueService.Delete(ctx, xkube.Namespace(namespace), name)
return sv, err
}
type keeperServiceWrapper struct {
keeper contracts.Keeper
}
func newKeeperServiceWrapper(keeper contracts.Keeper) *keeperServiceWrapper {
return &keeperServiceWrapper{keeper: keeper}
}
func (wrapper *keeperServiceWrapper) KeeperForConfig(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error) {
return wrapper.keeper, nil
}
@@ -0,0 +1,57 @@
package tracectx
import (
"context"
"encoding/hex"
"fmt"
"strings"
"go.opentelemetry.io/otel/propagation"
)
const (
kvSeparator = "="
pairSeparator = "#"
)
func HexEncodeTraceFromContext(ctx context.Context) string {
carrier := propagation.MapCarrier(make(map[string]string))
propagation.TraceContext{}.Inject(ctx, carrier)
// no trace in context
if len(carrier) == 0 {
return ""
}
pairs := make([]string, 0, len(carrier))
for k, v := range carrier {
pairs = append(pairs, k+kvSeparator+v)
}
return hex.EncodeToString([]byte(strings.Join(pairs, pairSeparator)))
}
func HexDecodeTraceIntoContext(ctx context.Context, encoded string) (context.Context, error) {
if encoded == "" {
return ctx, nil
}
decoded, err := hex.DecodeString(encoded)
if err != nil {
return nil, err
}
pairs := strings.Split(string(decoded), pairSeparator)
carrier := make(propagation.MapCarrier, len(pairs))
for _, pair := range pairs {
kv := strings.SplitN(pair, kvSeparator, 2)
if len(kv) != 2 || kv[0] == "" || kv[1] == "" {
return nil, fmt.Errorf("invalid key-value pair: %s", pair)
}
carrier[kv[0]] = kv[1]
}
return propagation.TraceContext{}.Extract(ctx, carrier), nil
}
@@ -0,0 +1,88 @@
package tracectx
import (
"context"
"encoding/hex"
"testing"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
func TestHexEncodeTraceFromContext(t *testing.T) {
t.Run("when no trace is present in context, it returns empty string", func(t *testing.T) {
ctx := context.Background()
encoded := HexEncodeTraceFromContext(ctx)
require.Empty(t, encoded)
})
t.Run("when trace is present in context, it returns hex-encoded string", func(t *testing.T) {
carrier := propagation.MapCarrier{
"traceparent": "00-446e31681d64f9dcefd947c95ef321d0-009e2f3d8ded1892-01",
"tracestate": "first=abc1234,second=xyz7890",
}
ctx := propagation.TraceContext{}.Extract(context.Background(), carrier)
encoded := HexEncodeTraceFromContext(ctx)
require.NotEmpty(t, encoded)
traceCtx, err := HexDecodeTraceIntoContext(context.Background(), encoded)
require.NoError(t, err)
span := trace.SpanFromContext(traceCtx)
require.True(t, span.SpanContext().IsValid())
carrier = propagation.MapCarrier(make(map[string]string))
propagation.TraceContext{}.Inject(traceCtx, carrier)
require.Contains(t, carrier, "traceparent")
require.Contains(t, carrier, "tracestate")
})
}
func TestHexDecodeTraceIntoContext(t *testing.T) {
t.Run("when encoded string is empty, it returns original context", func(t *testing.T) {
ctx := context.Background()
result, err := HexDecodeTraceIntoContext(ctx, "")
require.NoError(t, err)
require.Equal(t, ctx, result)
})
t.Run("when encoded string is valid hex, it returns context with trace", func(t *testing.T) {
encoded := hex.EncodeToString([]byte("traceparent=00-446e31681d64f9dcefd947c95ef321d0-009e2f3d8ded1892-01#tracestate=first=abc1234,second=xyz7890"))
ctx, err := HexDecodeTraceIntoContext(context.Background(), encoded)
require.NoError(t, err)
span := trace.SpanFromContext(ctx)
require.True(t, span.SpanContext().IsValid())
})
t.Run("when encoded string has invalid hex encoding, it returns an error", func(t *testing.T) {
invalidHex := "invalid-hex-zzz"
result, err := HexDecodeTraceIntoContext(context.Background(), invalidHex)
require.Error(t, err)
require.Nil(t, result)
})
t.Run("when decoded string has invalid key-value pair format, it returns an error", func(t *testing.T) {
// missing key
encoded := hex.EncodeToString([]byte("00-446e31681d64f9dcefd947c95ef321d0-009e2f3d8ded1892-01"))
result, err := HexDecodeTraceIntoContext(context.Background(), encoded)
require.Error(t, err)
require.Nil(t, result)
})
t.Run("when decoded string has key without value, it returns error", func(t *testing.T) {
// missing value
encoded := hex.EncodeToString([]byte("traceparent="))
result, err := HexDecodeTraceIntoContext(context.Background(), encoded)
require.Error(t, err)
require.Nil(t, result)
})
}
@@ -0,0 +1,44 @@
package worker
import (
"github.com/prometheus/client_golang/prometheus"
)
const (
namespace = "grafana_secrets_manager"
subsystem = "outbox_worker"
)
// OutboxMetrics is a struct that contains all the metrics for an implementation of the secrets service.
type OutboxMetrics struct {
OutboxMessageProcessingDuration *prometheus.HistogramVec
}
func newOutboxMetrics() *OutboxMetrics {
return &OutboxMetrics{
OutboxMessageProcessingDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "message_processing_duration_seconds",
Help: "Duration of outbox message processing",
Buckets: prometheus.DefBuckets,
}, []string{"message_type", "keeper_type"}),
}
}
// NewOutboxMetrics creates a new SecretsMetrics struct containing registered metrics
func NewOutboxMetrics(reg prometheus.Registerer) *OutboxMetrics {
m := newOutboxMetrics()
if reg != nil {
reg.MustRegister(
m.OutboxMessageProcessingDuration,
)
}
return m
}
func NewTestMetrics() *OutboxMetrics {
return newOutboxMetrics()
}
+274
View File
@@ -0,0 +1,274 @@
package worker
import (
"context"
"errors"
"fmt"
"time"
"github.com/grafana/grafana-app-sdk/logging"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/tracectx"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// Consumes and processes messages from the secure value outbox queue
type Worker struct {
config Config
tracer trace.Tracer
database contracts.Database
outboxQueue contracts.OutboxQueue
secureValueMetadataStorage contracts.SecureValueMetadataStorage
keeperMetadataStorage contracts.KeeperMetadataStorage
keeperService contracts.KeeperService
encryptionManager contracts.EncryptionManager
metrics *OutboxMetrics
enabled bool
}
// DefaultConfig for the secure value outbox worker.
var DefaultConfig = Config{
BatchSize: 20,
ReceiveTimeout: 5 * time.Second,
PollingInterval: 100 * time.Millisecond,
MaxMessageProcessingAttempts: 10,
}
// ProvideWorkerConfig used for wire.
func ProvideWorkerConfig() Config {
return DefaultConfig
}
type Config struct {
// The max number of messages to fetch from the outbox queue in a batch
BatchSize uint
// How long to wait for a request to fetch messages from the outbox queue
ReceiveTimeout time.Duration
// How often to poll the outbox queue for new messages
PollingInterval time.Duration
// How many tries to try to process a message before marking the operation as failed
MaxMessageProcessingAttempts uint
}
func NewWorker(
config Config,
tracer trace.Tracer,
database contracts.Database,
outboxQueue contracts.OutboxQueue,
secureValueMetadataStorage contracts.SecureValueMetadataStorage,
keeperMetadataStorage contracts.KeeperMetadataStorage,
keeperService contracts.KeeperService,
encryptionManager contracts.EncryptionManager,
features featuremgmt.FeatureToggles,
reg prometheus.Registerer,
) (*Worker, error) {
if config.BatchSize == 0 {
return nil, fmt.Errorf("config.BatchSize is required")
}
if config.ReceiveTimeout == 0 {
return nil, fmt.Errorf("config.ReceiveTimeout is required")
}
if config.PollingInterval == 0 {
return nil, fmt.Errorf("config.PollingInterval is required")
}
if config.MaxMessageProcessingAttempts == 0 {
return nil, fmt.Errorf("config.MaxMessageProcessingAttempts is required")
}
// Require both features to be enabled for the worker to run.
enabled := features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) && features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform)
return &Worker{
config: config,
tracer: tracer,
database: database,
outboxQueue: outboxQueue,
secureValueMetadataStorage: secureValueMetadataStorage,
keeperMetadataStorage: keeperMetadataStorage,
keeperService: keeperService,
encryptionManager: encryptionManager,
metrics: NewOutboxMetrics(reg),
enabled: enabled,
}, nil
}
// Ensure that Worker implements the BackgroundService interface, so we can start it as a background service.
var _ registry.BackgroundService = (*Worker)(nil)
// Run is the main method to drive the worker
func (w *Worker) Run(ctx context.Context) error {
if !w.enabled {
return nil
}
logging.FromContext(ctx).Debug("starting worker control loop")
t := time.NewTicker(w.config.PollingInterval)
defer t.Stop()
for {
select {
// If the context was canceled
case <-ctx.Done():
// return the reason it was canceled
return ctx.Err()
// Otherwise try to receive messages
case <-t.C:
if ctx.Err() != nil {
return ctx.Err()
}
if err := w.ReceiveAndProcessMessages(ctx); err != nil {
logging.FromContext(ctx).Error("receiving outbox messages", "err", err.Error())
}
}
}
}
// TODO: don't rollback every message when a single error happens
func (w *Worker) ReceiveAndProcessMessages(ctx context.Context) error {
messageIDs := make([]int64, 0)
txErr := w.database.Transaction(ctx, func(ctx context.Context) error {
timeoutCtx, cancel := context.WithTimeout(ctx, w.config.ReceiveTimeout)
messages, err := w.outboxQueue.ReceiveN(timeoutCtx, w.config.BatchSize)
cancel()
if err != nil {
return err
}
for _, message := range messages {
messageIDs = append(messageIDs, message.MessageID)
if err := w.processMessage(ctx, message); err != nil {
return fmt.Errorf("processing message: %+v %w", message, err)
}
}
return nil
})
// This call is made outside the transaction to make sure the receive count is updated on rollbacks.
incrementErr := w.outboxQueue.IncrementReceiveCount(ctx, messageIDs)
if incrementErr != nil {
incrementErr = fmt.Errorf("incrementing receive count for outbox message: %w", incrementErr)
}
return errors.Join(txErr, incrementErr)
}
func (w *Worker) processMessage(ctx context.Context, message contracts.OutboxMessage) error {
start := time.Now()
keeperType := "unknown"
defer func() {
w.metrics.OutboxMessageProcessingDuration.WithLabelValues(string(message.Type), keeperType).Observe(time.Since(start).Seconds())
}()
logging.FromContext(ctx).Debug("processing message", "type", message.Type, "name", message.Name, "namespace", message.Namespace, "receiveCount", message.ReceiveCount)
opts := []trace.SpanStartOption{}
// If there's no request ID in the message, start a new root span and log an error.
ctx, err := tracectx.HexDecodeTraceIntoContext(ctx, message.RequestID)
if err != nil {
opts = append(opts, trace.WithNewRoot())
logging.FromContext(ctx).Error("decoding trace context from message", "err", err.Error(), "message.requestID", message.RequestID)
}
opts = append(opts, trace.WithAttributes(
attribute.String("message.requestID", message.RequestID),
attribute.Int64("message.id", message.MessageID),
attribute.String("message.type", string(message.Type)),
attribute.String("message.namespace", message.Namespace),
attribute.String("message.secureValue.name", message.Name),
attribute.Int("message.receive.count", message.ReceiveCount),
))
ctx, span := w.tracer.Start(ctx, "Worker.ProcessMessage", opts...)
defer span.End()
if message.ReceiveCount >= int(w.config.MaxMessageProcessingAttempts) {
if err := w.secureValueMetadataStorage.SetStatus(ctx, xkube.Namespace(message.Namespace), message.Name, secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseFailed, Message: fmt.Sprintf("Reached max number of attempts to complete operation: %s", message.Type)}); err != nil {
return fmt.Errorf("setting secret metadata status to Succeeded: message=%+v", message)
}
if err := w.outboxQueue.Delete(ctx, message.MessageID); err != nil {
return fmt.Errorf("deleting message from outbox queue: %w", err)
}
return nil
}
keeperCfg, err := w.keeperMetadataStorage.GetKeeperConfig(ctx, message.Namespace, message.KeeperName, contracts.ReadOpts{ForUpdate: true})
if err != nil {
return fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", message.Namespace, message.KeeperName, err)
}
keeperType = string(keeperCfg.Type())
keeper, err := w.keeperService.KeeperForConfig(keeperCfg)
if err != nil {
return fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", message.Namespace, message.KeeperName, err)
}
logging.FromContext(ctx).Debug("retrieved keeper", "namespace", message.Namespace, "keeperName", message.KeeperName, "type", keeperCfg.Type())
switch message.Type {
case contracts.CreateSecretOutboxMessage:
rawSecret, err := w.encryptionManager.Decrypt(ctx, message.Namespace, []byte(message.EncryptedSecret))
if err != nil {
return fmt.Errorf("decrypting secure value secret: %w", err)
}
externalID, err := keeper.Store(ctx, keeperCfg, message.Namespace, string(rawSecret))
if err != nil {
return fmt.Errorf("storing secret: message=%+v %w", message, err)
}
if err := w.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(message.Namespace), message.Name, externalID); err != nil {
return fmt.Errorf("setting secret metadata externalID: externalID=%+v message=%+v %w", externalID, message, err)
}
// Setting the status to Succeeded must be the last action
// since it acts as a fence to clients.
if err := w.secureValueMetadataStorage.SetStatus(ctx, xkube.Namespace(message.Namespace), message.Name, secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseSucceeded}); err != nil {
return fmt.Errorf("setting secret metadata status to Succeeded: message=%+v %w", message, err)
}
case contracts.UpdateSecretOutboxMessage:
rawSecret, err := w.encryptionManager.Decrypt(ctx, message.Namespace, []byte(message.EncryptedSecret))
if err != nil {
return fmt.Errorf("decrypting secure value secret: %w", err)
}
if err := keeper.Update(ctx, keeperCfg, message.Namespace, contracts.ExternalID(*message.ExternalID), string(rawSecret)); err != nil {
return fmt.Errorf("calling keeper to update secret: %w", err)
}
// Setting the status to Succeeded must be the last action
// since it acts as a fence to clients.
if err := w.secureValueMetadataStorage.SetStatus(ctx, xkube.Namespace(message.Namespace), message.Name, secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseSucceeded}); err != nil {
return fmt.Errorf("setting secret metadata status to Succeeded: message=%+v", message)
}
case contracts.DeleteSecretOutboxMessage:
if err := keeper.Delete(ctx, keeperCfg, message.Namespace, contracts.ExternalID(*message.ExternalID)); err != nil {
return fmt.Errorf("calling keeper to delete secret: %w", err)
}
if err := w.secureValueMetadataStorage.Delete(ctx, xkube.Namespace(message.Namespace), message.Name); err != nil {
return fmt.Errorf("deleting secure value metadata: %+w", err)
}
default:
return fmt.Errorf("unhandled message type: %s", message.Type)
}
// Delete the message from the queue after completing all operations because
// if the message is deleted first, the response may be lost,
// resulting in an error, but since the message was actually deleted
// the worker would never retry.
if err := w.outboxQueue.Delete(ctx, message.MessageID); err != nil {
return fmt.Errorf("deleting message from outbox queue: %w", err)
}
return nil
}
@@ -0,0 +1,246 @@
package worker_test
import (
"context"
"fmt"
"testing"
"time"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/registry/apis/secret/worker"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/stretchr/testify/require"
)
type fakeKeeperService struct {
keeperForConfigFunc func(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error)
}
func newFakeKeeperService(keeperForConfigFunc func(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error)) *fakeKeeperService {
return &fakeKeeperService{keeperForConfigFunc: keeperForConfigFunc}
}
func (s *fakeKeeperService) KeeperForConfig(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error) {
return s.keeperForConfigFunc(cfg)
}
func TestProcessMessage(t *testing.T) {
t.Parallel()
t.Run("secure value metadata status is set to Failed when processing a message fails too many times", func(t *testing.T) {
t.Parallel()
// Given a worker that will attempt to process a message N times
workerCfg := worker.Config{
BatchSize: 10,
ReceiveTimeout: 1 * time.Second,
PollingInterval: time.Millisecond,
MaxMessageProcessingAttempts: 2,
}
// And an error that keeps happening
keeperService := newFakeKeeperService(func(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error) {
return nil, fmt.Errorf("oops")
})
sut := testutils.Setup(t, testutils.WithWorkerConfig(workerCfg), testutils.WithKeeperService(keeperService))
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
for range workerCfg.MaxMessageProcessingAttempts + 1 {
// The secure value status should be Pending while the worker is trying to process the message
sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, sv.Status.Phase)
// Worker tries to process messages
_ = sut.Worker.ReceiveAndProcessMessages(ctx)
}
// After the worker fails to process a message too many times,
// the secure value status is changed to Failed
sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhaseFailed, sv.Status.Phase)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Empty(t, messages)
})
t.Run("create sv: secure value metadata status is set to Succeeded when message is processed successfully", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
// Worker receives and processes the message
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
// and sets the secure value status to Succeeded
sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, sv.Status.Phase)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Empty(t, messages)
})
t.Run("update sv: secure value metadata status is set to Succeeded when message is processed successfully", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
// Worker receives and processes the message
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
// and sets the secure value status to Succeeded
sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, sv.Status.Phase)
sv.Spec.Description = "desc2"
sv.Spec.Value = secretv0alpha1.NewExposedSecureValue("v2")
// Queue an update operation
sv, err = sut.UpdateSv(ctx, sv)
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, sv.Status.Phase)
// Worker receives and processes the message
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
updatedSv, err := sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, updatedSv.Status.Phase)
require.Equal(t, sv.Spec.Description, updatedSv.Spec.Description)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Empty(t, messages)
})
t.Run("delete sv: secure value metadata is deleted", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
// Worker receives and processes the message
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
// and sets the secure value status to Succeeded
sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, sv.Status.Phase)
// Queue a delete operation
updatedSv, err := sut.DeleteSv(ctx, sv.Namespace, sv.Name)
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, updatedSv.Status.Phase)
// Worker receives and processes the message
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
// The secure value has been deleted
_, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Empty(t, messages)
})
t.Run("when creating a secure value, the secret is encrypted before it is added to the outbox queue", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
var secret string
_, err := sut.CreateSv(ctx, func(cfg *testutils.CreateSvConfig) {
secret = string(cfg.Sv.Spec.Value)
})
require.NoError(t, err)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Equal(t, 1, len(messages))
encryptedSecret := messages[0].EncryptedSecret
require.NotEmpty(t, secret)
require.NotEmpty(t, encryptedSecret)
require.NotEqual(t, secret, encryptedSecret)
})
t.Run("when updating a secure value, the secret is encrypted before it is added to the outbox queue", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
sv.Spec.Value = secretv0alpha1.NewExposedSecureValue("v2")
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
newValue := "v2"
sv.Spec.Value = secretv0alpha1.NewExposedSecureValue(newValue)
// Queue an update secure value operation
_, err = sut.UpdateSv(ctx, sv)
require.NoError(t, err)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Equal(t, 1, len(messages))
encryptedSecret := messages[0].EncryptedSecret
require.NotEmpty(t, encryptedSecret)
require.NotEqual(t, newValue, encryptedSecret)
})
t.Run("when deleting a secure value, no value is added to the outbox message", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
sv.Spec.Value = secretv0alpha1.NewExposedSecureValue("v2")
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
// Queue a delete secure value operation
_, err = sut.DeleteSv(ctx, sv.Namespace, sv.Name)
require.NoError(t, err)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Equal(t, 1, len(messages))
require.Empty(t, messages[0].EncryptedSecret)
})
}
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
"github.com/grafana/grafana/pkg/registry"
apiregistry "github.com/grafana/grafana/pkg/registry/apis"
secretworker "github.com/grafana/grafana/pkg/registry/apis/secret/worker"
appregistry "github.com/grafana/grafana/pkg/registry/apps"
"github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite"
"github.com/grafana/grafana/pkg/services/anonymous/anonimpl"
@@ -70,6 +71,7 @@ func ProvideBackgroundServiceRegistry(
appRegistry *appregistry.Service,
pluginDashboardUpdater *plugindashboardsservice.DashboardUpdater,
dashboardServiceImpl *service.DashboardServiceImpl,
secretManagerWorker *secretworker.Worker,
// Need to make sure these are initialized, is there a better place to put them?
_ dashboardsnapshots.Service,
_ serviceaccounts.Service,
@@ -117,6 +119,7 @@ func ProvideBackgroundServiceRegistry(
appRegistry,
pluginDashboardUpdater,
dashboardServiceImpl,
secretManagerWorker,
)
}
+9 -3
View File
@@ -45,6 +45,8 @@ import (
secretdecrypt "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt"
gsmEncryption "github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
encryptionManager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
secretsecurevalueservice "github.com/grafana/grafana/pkg/registry/apis/secret/service"
secretworker "github.com/grafana/grafana/pkg/registry/apis/secret/worker"
appregistry "github.com/grafana/grafana/pkg/registry/apps"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
@@ -424,16 +426,20 @@ var wireBasicSet = wire.NewSet(
// Secrets Manager
secretmetadata.ProvideSecureValueMetadataStorage,
secretmetadata.ProvideKeeperMetadataStorage,
secretmetadata.ProvideOutboxQueue,
secretmetadata.ProvideDecryptStorage,
secretdecrypt.ProvideDecryptAuthorizer,
secretdecrypt.ProvideDecryptAllowList,
secretencryption.ProvideDataKeyStorage,
secretencryption.ProvideEncryptedValueStorage,
secretmetadata.ProvideOutboxQueue,
secretsecurevalueservice.ProvideSecureValueService,
secretmigrator.NewWithEngine,
secretdatabase.ProvideDatabase,
wire.Bind(new(secretcontracts.Database), new(*secretdatabase.Database)),
encryptionManager.ProvideEncryptionManager,
gsmEncryption.ProvideThirdPartyProviderMap,
secretdecrypt.ProvideDecryptAuthorizer,
secretdecrypt.ProvideDecryptAllowList,
secretworker.ProvideWorkerConfig,
secretworker.NewWorker,
// Unified storage
resource.ProvideStorageMetrics,
resource.ProvideIndexMetrics,
+73 -6
View File
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
@@ -132,6 +133,11 @@ func (b *appBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI
return err
}
apiGroupInfo.VersionedResourcesStorageMap[version][resourceInfo.StoragePath()] = store
if registryStore, ok := store.(*genericregistry.Store); ok {
for subPath := range kind.ZeroValue().GetSubresources() {
apiGroupInfo.VersionedResourcesStorageMap[version][resourceInfo.StoragePath(subPath)] = grafanaregistry.NewRegistryStatusStore(opts.Scheme, registryStore)
}
}
}
}
return nil
+7 -4
View File
@@ -22,10 +22,11 @@ import (
type StorageType string
const (
StorageTypeFile StorageType = "file"
StorageTypeEtcd StorageType = "etcd"
StorageTypeUnified StorageType = "unified"
StorageTypeUnifiedGrpc StorageType = "unified-grpc"
StorageTypeFile StorageType = "file"
StorageTypeEtcd StorageType = "etcd"
StorageTypeUnified StorageType = "unified"
StorageTypeUnifiedGrpc StorageType = "unified-grpc"
StorageTypeUnifiedKVGrpc StorageType = "unified-kv-grpc"
// Deprecated: legacy is a shim that is no longer necessary
StorageTypeLegacy StorageType = "legacy"
@@ -95,6 +96,8 @@ func (o *StorageOptions) Validate() []error {
// nolint:staticcheck
case StorageTypeLegacy:
// no-op
case StorageTypeUnifiedKVGrpc:
// no-op (enterprise only)
case StorageTypeFile, StorageTypeEtcd, StorageTypeUnified, StorageTypeUnifiedGrpc:
// no-op
default:
@@ -83,6 +83,7 @@ func (ss *SqlStore) getDataSource(_ context.Context, query *datasources.GetDataS
ss.logger.Error("Failed getting data source", "err", err, "uid", query.UID, "id", query.ID, "name", query.Name, "orgId", query.OrgID) // nolint:staticcheck
return nil, err
} else if !has {
ss.logger.Debug("Data source not found", "uid", query.UID, "id", query.ID, "name", query.Name, "orgId", query.OrgID) // nolint:staticcheck
return nil, datasources.ErrDataSourceNotFound
}
@@ -281,7 +281,10 @@ func TestIntegrationDataAccess(t *testing.T) {
t.Run("Can not delete datasource with wrong orgID", func(t *testing.T) {
db := db.InitTestDB(t)
ds := initDatasource(db)
ss := SqlStore{db: db}
ss := SqlStore{
db: db,
logger: log.NewNopLogger(),
}
err := ss.DeleteDataSource(context.Background(),
&datasources.DeleteDataSourceCommand{ID: ds.ID, OrgID: 123123})
@@ -322,7 +325,10 @@ func TestIntegrationDataAccess(t *testing.T) {
t.Run("does not fire an event when the datasource is not deleted", func(t *testing.T) {
db := db.InitTestDB(t)
ss := SqlStore{db: db}
ss := SqlStore{
db: db,
logger: log.NewNopLogger(),
}
var called bool
db.Bus().AddEventListener(func(ctx context.Context, e *events.DataSourceDeleted) error {
+2 -2
View File
@@ -1678,11 +1678,11 @@ var (
{
Name: "skipTokenRotationIfRecent",
Description: "Skip token rotation if it was already rotated less than 5 seconds ago",
Stage: FeatureStagePrivatePreview,
Stage: FeatureStageGeneralAvailability,
Owner: identityAccessTeam,
HideFromAdminPage: true,
HideFromDocs: true,
Expression: "false",
Expression: "true", // enabled by default
},
{
Name: "alertEnrichment",
+1 -1
View File
@@ -219,7 +219,7 @@ alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,
alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true
kubernetesAuthzApis,experimental,@grafana/identity-access-team,false,false,false
restoreDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false
skipTokenRotationIfRecent,privatePreview,@grafana/identity-access-team,false,false,false
skipTokenRotationIfRecent,GA,@grafana/identity-access-team,false,false,false
alertEnrichment,experimental,@grafana/alerting-squad,false,false,false
alertingImportAlertmanagerAPI,experimental,@grafana/alerting-squad,false,false,false
preferLibraryPanelTitle,privatePreview,@grafana/dashboards-squad,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
219 alertingBulkActionsInUI GA @grafana/alerting-squad false false true
220 kubernetesAuthzApis experimental @grafana/identity-access-team false false false
221 restoreDashboards experimental @grafana/grafana-frontend-platform false false false
222 skipTokenRotationIfRecent privatePreview GA @grafana/identity-access-team false false false
223 alertEnrichment experimental @grafana/alerting-squad false false false
224 alertingImportAlertmanagerAPI experimental @grafana/alerting-squad false false false
225 preferLibraryPanelTitle privatePreview @grafana/dashboards-squad false false false
+7 -4
View File
@@ -2805,16 +2805,19 @@
{
"metadata": {
"name": "skipTokenRotationIfRecent",
"resourceVersion": "1750434297879",
"creationTimestamp": "2025-06-03T06:59:40Z"
"resourceVersion": "1751872762065",
"creationTimestamp": "2025-06-03T06:59:40Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-07-07 07:19:22.065046 +0000 UTC"
}
},
"spec": {
"description": "Skip token rotation if it was already rotated less than 5 seconds ago",
"stage": "privatePreview",
"stage": "GA",
"codeowner": "@grafana/identity-access-team",
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
"expression": "true"
}
},
{
@@ -2034,7 +2034,7 @@ func createProvisioningSrvSut(t *testing.T) ProvisioningSrv {
func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) ProvisioningSrv {
t.Helper()
tracer := tracing.InitializeTracerForTest()
configStore := legacy_storage.NewAlertmanagerConfigStore(env.configs)
configStore := legacy_storage.NewAlertmanagerConfigStore(env.configs, notifier.NewExtraConfigsCrypto(env.secrets))
receiverSvc := notifier.NewReceiverService(
ac.NewReceiverAccess[*models.Receiver](env.ac, true),
configStore,
+1 -1
View File
@@ -412,7 +412,7 @@ func (ng *AlertNG) init() error {
ng.stateManager = stateManager
ng.schedule = scheduler
configStore := legacy_storage.NewAlertmanagerConfigStore(ng.store)
configStore := legacy_storage.NewAlertmanagerConfigStore(ng.store, notifier.NewExtraConfigsCrypto(ng.SecretsService))
receiverService := notifier.NewReceiverService(
ac.NewReceiverAccess[*models.Receiver](ng.accesscontrol, false),
configStore,
+40 -11
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -14,6 +15,16 @@ import (
"github.com/grafana/grafana/pkg/services/secrets"
)
const (
// encryptedContentPrefix is a marker that identifies encrypted Alertmanager configurations.
// When this prefix is present at the beginning of a configuration string:
// 1. During encryption: It indicates the content is already encrypted and should be skipped
// 2. During decryption: It indicates the content (minus this prefix) should be base64 decoded
// and then decrypted using the secrets service
// This prefix helps maintain idempotency in encryption/decryption operations.
cryptoPrefix = "crypto_"
)
// Crypto allows decryption of Alertmanager Configuration and encryption of arbitrary payloads.
type Crypto interface {
LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver) error
@@ -28,16 +39,16 @@ type Crypto interface {
// alertmanagerCrypto implements decryption of Alertmanager configuration and encryption of arbitrary payloads based on Grafana's encryptions.
type alertmanagerCrypto struct {
secrets secrets.Service
*ExtraConfigsCrypto
configs configurationStore
log log.Logger
}
func NewCrypto(secrets secrets.Service, configs configurationStore, log log.Logger) Crypto {
return &alertmanagerCrypto{
secrets: secrets,
configs: configs,
log: log,
ExtraConfigsCrypto: NewExtraConfigsCrypto(secrets),
configs: configs,
log: log,
}
}
@@ -241,27 +252,45 @@ func (c *alertmanagerCrypto) Decrypt(ctx context.Context, payload []byte) ([]byt
return c.secrets.Decrypt(ctx, payload)
}
func (c *alertmanagerCrypto) EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error {
type ExtraConfigsCrypto struct {
secrets secretService
}
func NewExtraConfigsCrypto(secrets secretService) *ExtraConfigsCrypto {
return &ExtraConfigsCrypto{
secrets: secrets,
}
}
func (c *ExtraConfigsCrypto) EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error {
for i := range config.ExtraConfigs {
// If it has prefix, consider it encrypted already
if strings.HasPrefix(config.ExtraConfigs[i].AlertmanagerConfig, cryptoPrefix) {
continue
}
encryptedValue, err := c.secrets.Encrypt(ctx, []byte(config.ExtraConfigs[i].AlertmanagerConfig), secrets.WithoutScope())
if err != nil {
return fmt.Errorf("failed to encrypt extra configuration: %w", err)
}
config.ExtraConfigs[i].AlertmanagerConfig = base64.StdEncoding.EncodeToString(encryptedValue)
config.ExtraConfigs[i].AlertmanagerConfig = cryptoPrefix + base64.StdEncoding.EncodeToString(encryptedValue)
}
return nil
}
func (c *alertmanagerCrypto) DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error {
func (c *ExtraConfigsCrypto) DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error {
for i := range config.ExtraConfigs {
// Check if the config is encrypted by trying to base64 decode it
encryptedValue, err := base64.StdEncoding.DecodeString(config.ExtraConfigs[i].AlertmanagerConfig)
if err != nil {
// If it can't be base64 decoded, assume it's already decrypted and skip
// If it does not have prefix, consider it decrypted already
if !strings.HasPrefix(config.ExtraConfigs[i].AlertmanagerConfig, cryptoPrefix) {
continue
}
// Check if the config is encrypted by trying to base64 decode it
encryptedValue, err := base64.StdEncoding.DecodeString(config.ExtraConfigs[i].AlertmanagerConfig[len(cryptoPrefix):])
if err != nil {
return fmt.Errorf("failed to decode extra configuration: %w", err)
}
decryptedValue, err := c.secrets.Decrypt(ctx, encryptedValue)
if err != nil {
@@ -0,0 +1,106 @@
package notifier
import (
"context"
"encoding/base64"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
)
func TestEncryptExtraConfigs(t *testing.T) {
config := "plain-text-config"
encryptedConfig := base64.StdEncoding.EncodeToString([]byte(config))
tests := []struct {
name string
inputConfig string
expectedConfig string
}{
{
name: "Encrypts unencrypted configs",
inputConfig: config,
expectedConfig: cryptoPrefix + encryptedConfig,
},
{
name: "Skips already encrypted configs",
inputConfig: cryptoPrefix + "very-encrypted-data",
expectedConfig: cryptoPrefix + "very-encrypted-data",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := fakes.NewFakeSecretsService()
c := &alertmanagerCrypto{
secrets: m,
}
cfg := &definitions.PostableUserConfig{
ExtraConfigs: []definitions.ExtraConfiguration{
{AlertmanagerConfig: tt.inputConfig},
},
}
err := c.EncryptExtraConfigs(context.Background(), cfg)
require.NoError(t, err)
require.Equal(t, tt.expectedConfig, cfg.ExtraConfigs[0].AlertmanagerConfig)
})
}
}
func TestDecryptExtraConfigs(t *testing.T) {
decryptedData := "derypted-data"
decryptedDataBase64 := base64.StdEncoding.EncodeToString([]byte(decryptedData))
tests := []struct {
name string
inputConfig string
expectedError string
expectedConfig string
}{
{
name: "Decrypts encrypted configs",
inputConfig: cryptoPrefix + decryptedDataBase64,
expectedConfig: decryptedData,
},
{
name: "Skips already encrypted configs",
inputConfig: "very-decrypted-data",
expectedConfig: "very-decrypted-data",
},
{
name: "Fails if not base64 encoded",
inputConfig: cryptoPrefix + "plain-text-config",
expectedError: "failed to decode extra configuration",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := fakes.NewFakeSecretsService()
c := &alertmanagerCrypto{
secrets: m,
}
cfg := &definitions.PostableUserConfig{
ExtraConfigs: []definitions.ExtraConfiguration{
{AlertmanagerConfig: tt.inputConfig},
},
}
err := c.DecryptExtraConfigs(context.Background(), cfg)
if tt.expectedError != "" {
require.ErrorContains(t, err, tt.expectedError)
return
}
require.NoError(t, err)
require.Equal(t, tt.expectedConfig, cfg.ExtraConfigs[0].AlertmanagerConfig)
})
}
}
@@ -9,6 +9,11 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
type crypto interface {
EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error
DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error
}
type amConfigStore interface {
GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*models.AlertConfiguration, error)
UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error
@@ -31,9 +36,17 @@ type ConfigRevision struct {
ConcurrencyToken string
Version string
}
type alertmanagerConfigStoreImpl struct {
store amConfigStore
crypto crypto
}
func getLastConfiguration(ctx context.Context, orgID int64, store amConfigStore) (*ConfigRevision, error) {
alertManagerConfig, err := store.GetLatestAlertmanagerConfiguration(ctx, orgID)
func NewAlertmanagerConfigStore(store amConfigStore, crypto crypto) *alertmanagerConfigStoreImpl {
return &alertmanagerConfigStoreImpl{store: store, crypto: crypto}
}
func (a alertmanagerConfigStoreImpl) Get(ctx context.Context, orgID int64) (*ConfigRevision, error) {
alertManagerConfig, err := a.store.GetLatestAlertmanagerConfiguration(ctx, orgID)
if err != nil {
return nil, err
}
@@ -48,6 +61,11 @@ func getLastConfiguration(ctx context.Context, orgID int64, store amConfigStore)
return nil, err
}
err = a.crypto.DecryptExtraConfigs(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("failed to decrypt extra configurations: %w", err)
}
return &ConfigRevision{
Config: cfg,
ConcurrencyToken: concurrencyToken,
@@ -55,38 +73,21 @@ func getLastConfiguration(ctx context.Context, orgID int64, store amConfigStore)
}, nil
}
type alertmanagerConfigStoreImpl struct {
store amConfigStore
}
func NewAlertmanagerConfigStore(store amConfigStore) *alertmanagerConfigStoreImpl {
return &alertmanagerConfigStoreImpl{store: store}
}
func (a alertmanagerConfigStoreImpl) Get(ctx context.Context, orgID int64) (*ConfigRevision, error) {
return getLastConfiguration(ctx, orgID, a.store)
}
func (a alertmanagerConfigStoreImpl) Save(ctx context.Context, revision *ConfigRevision, orgID int64) error {
err := a.crypto.EncryptExtraConfigs(ctx, revision.Config)
if err != nil {
return fmt.Errorf("failed to encrypt extra configurations: %w", err)
}
serialized, err := SerializeAlertmanagerConfig(*revision.Config)
if err != nil {
return err
}
cmd := models.SaveAlertmanagerConfigurationCmd{
return a.store.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{
AlertmanagerConfiguration: string(serialized),
ConfigurationVersion: revision.Version,
FetchedConfigurationHash: revision.ConcurrencyToken,
Default: false,
OrgID: orgID,
}
return a.PersistConfig(ctx, &cmd)
}
// PersistConfig validates to config before eventually persisting it if no error occurs
func (a alertmanagerConfigStoreImpl) PersistConfig(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error {
cfg := &definitions.PostableUserConfig{}
if err := json.Unmarshal([]byte(cmd.AlertmanagerConfiguration), cfg); err != nil {
return fmt.Errorf("change would result in an invalid configuration state: %w", err)
}
return a.store.UpdateAlertmanagerConfiguration(ctx, cmd)
})
}
@@ -22,8 +22,9 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) {
orgID := int64(1)
t.Run("should read the latest config for giving organization", func(t *testing.T) {
cryptoMock := newFakeCrypto()
storeMock := &MockAMConfigStore{}
store := &alertmanagerConfigStoreImpl{store: storeMock}
store := NewAlertmanagerConfigStore(storeMock, cryptoMock)
expected := models.AlertConfiguration{
ID: 1,
@@ -48,12 +49,18 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) {
require.Equal(t, expectedCfg, *revision.Config)
storeMock.AssertCalled(t, "GetLatestAlertmanagerConfiguration", mock.Anything, orgID)
t.Run("should decrypt extra configs ", func(t *testing.T) {
require.Len(t, cryptoMock.Calls, 1)
require.Equal(t, "DecryptExtraConfigs", cryptoMock.Calls[0].Method)
require.Equal(t, &expectedCfg, cryptoMock.Calls[0].Args[1])
})
})
t.Run("propagate errors", func(t *testing.T) {
t.Run("when underlying store fails", func(t *testing.T) {
storeMock := &MockAMConfigStore{}
store := &alertmanagerConfigStoreImpl{store: storeMock}
store := NewAlertmanagerConfigStore(storeMock, newFakeCrypto())
expectedErr := errors.New("test=err")
storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil, expectedErr)
@@ -63,7 +70,7 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) {
t.Run("return ErrNoAlertmanagerConfiguration config does not exist", func(t *testing.T) {
storeMock := &MockAMConfigStore{}
store := &alertmanagerConfigStoreImpl{store: storeMock}
store := NewAlertmanagerConfigStore(storeMock, newFakeCrypto())
storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil, nil)
_, err := store.Get(context.Background(), orgID)
@@ -72,7 +79,7 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) {
t.Run("when config cannot be unmarshalled", func(t *testing.T) {
storeMock := &MockAMConfigStore{}
store := &alertmanagerConfigStoreImpl{store: storeMock}
store := NewAlertmanagerConfigStore(storeMock, newFakeCrypto())
storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(&models.AlertConfiguration{
AlertmanagerConfiguration: "invalid-json",
}, nil)
@@ -80,6 +87,23 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) {
_, err := store.Get(context.Background(), orgID)
require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error())
})
t.Run("when decrypting extra configs fails", func(t *testing.T) {
cryptoMock := newFakeCrypto()
storeMock := &MockAMConfigStore{}
store := NewAlertmanagerConfigStore(storeMock, cryptoMock)
expectedErr := errors.New("test-err")
cryptoMock.DecryptExtraConfigsFunc = func(ctx context.Context, config *definitions.PostableUserConfig) error {
return expectedErr
}
storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(&models.AlertConfiguration{
AlertmanagerConfiguration: defaultConfig,
}, nil)
_, err := store.Get(context.Background(), orgID)
require.ErrorIs(t, err, expectedErr)
})
})
}
@@ -98,8 +122,9 @@ func TestAlertmanagerConfigStoreSave(t *testing.T) {
}
t.Run("should save the config to store", func(t *testing.T) {
cryptoMock := newFakeCrypto()
storeMock := &MockAMConfigStore{}
store := &alertmanagerConfigStoreImpl{store: storeMock}
store := NewAlertmanagerConfigStore(storeMock, cryptoMock)
storeMock.EXPECT().UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error {
assert.Equal(t, string(expectedCfg), cmd.AlertmanagerConfiguration)
@@ -114,17 +139,39 @@ func TestAlertmanagerConfigStoreSave(t *testing.T) {
require.NoError(t, err)
storeMock.AssertCalled(t, "UpdateAlertmanagerConfiguration", mock.Anything, mock.Anything)
t.Run("should encrypt extra configs ", func(t *testing.T) {
require.Len(t, cryptoMock.Calls, 1)
require.Equal(t, "EncryptExtraConfigs", cryptoMock.Calls[0].Method)
require.Equal(t, &cfg, cryptoMock.Calls[0].Args[1])
})
})
t.Run("propagates errors when underlying storage returns error", func(t *testing.T) {
storeMock := &MockAMConfigStore{}
store := &alertmanagerConfigStoreImpl{store: storeMock}
t.Run("propagates errors", func(t *testing.T) {
t.Run("when underlying storage returns error", func(t *testing.T) {
storeMock := &MockAMConfigStore{}
store := NewAlertmanagerConfigStore(storeMock, newFakeCrypto())
expectedErr := errors.New("test-err")
storeMock.EXPECT().UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(expectedErr)
expectedErr := errors.New("test-err")
storeMock.EXPECT().UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(expectedErr)
err := store.Save(context.Background(), &revision, orgID)
err := store.Save(context.Background(), &revision, orgID)
require.ErrorIs(t, err, expectedErr)
require.ErrorIs(t, err, expectedErr)
})
t.Run("when encrypting extra configs fails", func(t *testing.T) {
cryptoMock := newFakeCrypto()
storeMock := &MockAMConfigStore{}
store := NewAlertmanagerConfigStore(storeMock, cryptoMock)
expectedErr := errors.New("test-err")
cryptoMock.EncryptExtraConfigsFunc = func(ctx context.Context, config *definitions.PostableUserConfig) error {
return expectedErr
}
err := store.Save(context.Background(), &revision, orgID)
require.ErrorIs(t, err, expectedErr)
})
})
}
@@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
@@ -59,3 +60,39 @@ func (a *AlertmanagerConfigStoreFake) Save(ctx context.Context, revision *Config
}
return nil
}
type fakeCrypto struct {
Calls []methodCall
EncryptExtraConfigsFunc func(context.Context, *definitions.PostableUserConfig) error
DecryptExtraConfigsFunc func(context.Context, *definitions.PostableUserConfig) error
}
func newFakeCrypto() *fakeCrypto {
return &fakeCrypto{
Calls: []methodCall{},
}
}
func (f *fakeCrypto) EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error {
f.Calls = append(f.Calls, methodCall{
Method: "EncryptExtraConfigs",
Args: []interface{}{ctx, config},
})
if f.EncryptExtraConfigsFunc != nil {
return f.EncryptExtraConfigsFunc(ctx, config)
}
return nil
}
func (f *fakeCrypto) DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error {
f.Calls = append(f.Calls, methodCall{
Method: "DecryptExtraConfigs",
Args: []interface{}{ctx, config},
})
if f.DecryptExtraConfigsFunc != nil {
return f.DecryptExtraConfigsFunc(ctx, config)
}
return nil
}
@@ -275,7 +275,7 @@ func TestReceiverService_Delete(t *testing.T) {
deleteUID: baseReceiver.UID,
callerProvenance: definitions.Provenance(models.ProvenanceFile),
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceAPI))),
//expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceAPI, models.ProvenanceFile),
// expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceAPI, models.ProvenanceFile),
},
{
name: "delete receiver with optimistic version mismatch fails",
@@ -673,7 +673,7 @@ func TestReceiverService_Update(t *testing.T) {
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceAPI)),
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceFile))),
//expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceFile, models.ProvenanceAPI),
// expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceFile, models.ProvenanceAPI),
expectedUpdate: models.CopyReceiverWith(baseReceiver,
models.ReceiverMuts.WithProvenance(models.ProvenanceAPI),
rm.Encrypted(models.Base64Enrypt)),
@@ -1529,7 +1529,7 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secretService) *ReceiverS
return NewReceiverService(
ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), false),
legacy_storage.NewAlertmanagerConfigStore(store),
legacy_storage.NewAlertmanagerConfigStore(store, NewExtraConfigsCrypto(encryptSvc)),
provisioningStore,
&fakeAlertRuleNotificationStore{},
encryptSvc,
@@ -493,7 +493,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec
receiverService := notifier.NewReceiverService(
ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), true),
legacy_storage.NewAlertmanagerConfigStore(configStore),
legacy_storage.NewAlertmanagerConfigStore(configStore, notifier.NewExtraConfigsCrypto(secretService)),
provisioningStore,
&fakeAlertRuleNotificationStore{},
secretService,
@@ -504,7 +504,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec
)
return NewContactPointService(
legacy_storage.NewAlertmanagerConfigStore(configStore),
legacy_storage.NewAlertmanagerConfigStore(configStore, notifier.NewExtraConfigsCrypto(secretService)),
secretService,
provisioningStore,
xact,
@@ -37,6 +37,10 @@ var (
"Invalid format of the submitted route.",
errutil.WithPublic("Invalid format of the submitted route: {{.Public.Error}}. Correct the payload and try again."),
)
ErrRouteConflictingMatchers = errutil.BadRequest("alerting.notifications.routes.conflictingMatchers").MustTemplate("Routing tree conflicts with the external configuration",
errutil.WithPublic("Cannot add\\update route: matchers conflict with an external routing tree merging matchers {{ .Public.Matchers }}, making the added\\updated route unreachable."),
)
)
// MakeErrTimeIntervalInvalid creates an error with the ErrTimeIntervalInvalid template
@@ -109,6 +113,14 @@ func MakeErrRouteInvalidFormat(err error) error {
})
}
func MakeErrRouteConflictingMatchers(matchers string) error {
return ErrRouteConflictingMatchers.Build(errutil.TemplateData{
Public: map[string]any{
"Matchers": matchers,
},
})
}
func MakeErrContactPointUidExists(uid, name string) error {
return ErrContactPointUidExists.Build(errutil.TemplateData{
Public: map[string]any{
@@ -3,12 +3,14 @@ package provisioning
import (
"context"
"encoding/binary"
"errors"
"fmt"
"hash"
"hash/fnv"
"slices"
"unsafe"
"github.com/grafana/alerting/definition"
"github.com/prometheus/common/model"
"golang.org/x/exp/maps"
@@ -113,7 +115,11 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI
_, err = revision.Config.GetMergedAlertmanagerConfig()
if err != nil {
return definitions.Route{}, "", fmt.Errorf("new routing tree is not compatible with extra configuration: %w", err)
if errors.Is(err, definition.ErrSubtreeMatchersConflict) {
// TODO temporarily get the conflicting matchers
return definitions.Route{}, "", MakeErrRouteConflictingMatchers(fmt.Sprintf("%s", revision.Config.ExtraConfigs[0].MergeMatchers))
}
nps.log.Warn("Unable to validate the combined routing tree because of an error during merging. This could be a sign of broken external configuration. Skipping", "error", err)
}
err = nps.xact.InTransaction(ctx, func(ctx context.Context) error {
@@ -189,6 +189,93 @@ func TestUpdatePolicyTree(t *testing.T) {
assert.Equal(t, orgID, prov.Calls[0].Arguments[2].(int64))
})
t.Run("ErrRouteConflictingMatchers if the new route has conflicting matchers ", func(t *testing.T) {
rev := getDefaultConfigRevision()
rev.Config.ExtraConfigs = append(rev.Config.ExtraConfigs, definitions.ExtraConfiguration{
Identifier: "test",
MergeMatchers: config.Matchers{
{
Type: labels.MatchEqual,
Name: "imported",
Value: "true",
},
},
AlertmanagerConfig: `{"route":{"receiver":"mimir-receiver"},"receivers":[{"name":"mimir-receiver"}]}`,
})
route := definitions.Route{
Receiver: rev.Config.AlertmanagerConfig.Receivers[0].Name,
Routes: []*definitions.Route{
{
ObjectMatchers: definitions.ObjectMatchers{
{
Type: labels.MatchEqual,
Name: "imported",
Value: "true",
},
{
Type: labels.MatchEqual,
Name: "label",
Value: "value",
},
},
},
},
}
sut, store, _ := createNotificationPolicyServiceSut()
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return &rev, nil
}
_, _, err := sut.UpdatePolicyTree(context.Background(), orgID, route, models.ProvenanceAPI, defaultVersion)
require.ErrorIs(t, err, ErrRouteConflictingMatchers)
})
t.Run("should ignore extra config validation if it is invalid", func(t *testing.T) {
extra := definitions.ExtraConfiguration{
MergeMatchers: config.Matchers{
{
Type: labels.MatchEqual,
Name: "imported",
Value: "true",
},
},
}
rev := getDefaultConfigRevision()
rev.Config.ExtraConfigs = append(rev.Config.ExtraConfigs, extra)
route := definitions.Route{
Receiver: rev.Config.AlertmanagerConfig.Receivers[0].Name,
Routes: []*definitions.Route{
{
ObjectMatchers: definitions.ObjectMatchers{
{
Type: labels.MatchEqual,
Name: "imported",
Value: "true",
},
{
Type: labels.MatchEqual,
Name: "label",
Value: "value",
},
},
},
},
}
sut, store, _ := createNotificationPolicyServiceSut()
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return &rev, nil
}
result, version, err := sut.UpdatePolicyTree(context.Background(), orgID, route, models.ProvenanceAPI, defaultVersion)
require.NoError(t, err)
assert.Equal(t, route, result)
assert.Equal(t, calculateRouteFingerprint(route), version)
})
t.Run("updates Route and sets provenance in transaction if route is valid and version matches", func(t *testing.T) {
sut, store, prov := createNotificationPolicyServiceSut()
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
+2 -2
View File
@@ -294,7 +294,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
ps.alertingStore,
ps.alertingStore,
ps.folderService,
//ps.dashboardService,
// ps.dashboardService,
ps.quotaService,
ps.SQLStore,
int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
@@ -304,7 +304,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
notifier.NewCachedNotificationSettingsValidationService(ps.alertingStore),
alertingauthz.NewRuleService(ps.ac),
)
configStore := legacy_storage.NewAlertmanagerConfigStore(ps.alertingStore)
configStore := legacy_storage.NewAlertmanagerConfigStore(ps.alertingStore, notifier.NewExtraConfigsCrypto(ps.secretService))
receiverSvc := notifier.NewReceiverService(
alertingauthz.NewReceiverAccess[*ngmodels.Receiver](ps.ac, true),
configStore,
+2
View File
@@ -39,6 +39,7 @@ type DatabaseConfig struct {
WALEnabled bool
UrlQueryParams map[string][]string
SkipMigrations bool
EnsureDefaultOrgAndUser bool
MigrationLock bool
MigrationLockAttemptTimeout int
LogQueries bool
@@ -114,6 +115,7 @@ func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error {
dbCfg.CacheMode = sec.Key("cache_mode").MustString("private")
dbCfg.WALEnabled = sec.Key("wal").MustBool(false)
dbCfg.SkipMigrations = sec.Key("skip_migrations").MustBool()
dbCfg.EnsureDefaultOrgAndUser = sec.Key("ensure_default_org_and_user").MustBool(true)
dbCfg.MigrationLock = sec.Key("migration_locking").MustBool(true)
dbCfg.MigrationLockAttemptTimeout = sec.Key("locking_attempt_timeout_sec").MustInt()
+20 -31
View File
@@ -47,7 +47,6 @@ type SQLStore struct {
engine *xorm.Engine
log log.Logger
dialect migrator.Dialect
skipEnsureDefaultOrgAndUser bool
migrations registry.DatabaseMigrator
tracer tracing.Tracer
recursiveQueriesAreSupported *bool
@@ -62,7 +61,7 @@ func ProvideService(cfg *setting.Cfg,
// by that mimic the functionality of how it was functioning before
// xorm's changes above.
xorm.DefaultPostgresSchema = ""
s, err := newStore(cfg, nil, features, migrations, bus, tracer, false)
s, err := newStore(cfg, nil, features, migrations, bus, tracer)
if err != nil {
return nil, err
}
@@ -80,7 +79,7 @@ func ProvideService(cfg *setting.Cfg,
}
func ProvideServiceForTests(t sqlutil.ITestDB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, bus bus.Bus, migrations registry.DatabaseMigrator) (*SQLStore, error) {
return initTestDB(t, cfg, features, migrations, bus, InitTestDBOpt{EnsureDefaultOrgAndUser: true})
return initTestDB(t, cfg, features, migrations, bus, InitTestDBOpt{})
}
// NewSQLStoreWithoutSideEffects creates a new *SQLStore without side-effects such as
@@ -88,20 +87,20 @@ func ProvideServiceForTests(t sqlutil.ITestDB, cfg *setting.Cfg, features featur
func NewSQLStoreWithoutSideEffects(cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
bus bus.Bus, tracer tracing.Tracer) (*SQLStore, error) {
return newStore(cfg, nil, features, nil, bus, tracer, true)
cfgDBSection := cfg.Raw.Section("database")
cfgDBSection.Key("ensure_default_org_and_user").SetValue("false")
return newStore(cfg, nil, features, nil, bus, tracer)
}
func newStore(cfg *setting.Cfg, engine *xorm.Engine, features featuremgmt.FeatureToggles,
migrations registry.DatabaseMigrator, bus bus.Bus, tracer tracing.Tracer,
skipEnsureDefaultOrgAndUser bool) (*SQLStore, error) {
migrations registry.DatabaseMigrator, bus bus.Bus, tracer tracing.Tracer) (*SQLStore, error) {
ss := &SQLStore{
cfg: cfg,
log: log.New("sqlstore"),
skipEnsureDefaultOrgAndUser: skipEnsureDefaultOrgAndUser,
migrations: migrations,
bus: bus,
tracer: tracer,
features: features,
cfg: cfg,
log: log.New("sqlstore"),
migrations: migrations,
bus: bus,
tracer: tracer,
features: features,
}
if err := ss.initEngine(engine); err != nil {
@@ -148,11 +147,10 @@ func (ss *SQLStore) Migrate(isDatabaseLockingEnabled bool) error {
// Reset resets database state.
// If default org and user creation is enabled, it will be ensured they exist in the database.
func (ss *SQLStore) Reset() error {
if ss.skipEnsureDefaultOrgAndUser {
return nil
if ss.dbCfg.EnsureDefaultOrgAndUser {
return ss.ensureMainOrgAndAdminUser(false)
}
return ss.ensureMainOrgAndAdminUser(false)
return nil
}
// Quote quotes the value in the used SQL dialect
@@ -412,10 +410,8 @@ var testSQLStoreCleanup []func()
// InitTestDBOpt contains options for InitTestDB.
type InitTestDBOpt struct {
// EnsureDefaultOrgAndUser flags whether to ensure that default org and user exist.
EnsureDefaultOrgAndUser bool
FeatureFlags []string
Cfg *setting.Cfg
FeatureFlags []string
Cfg *setting.Cfg
}
// InitTestDBWithMigration initializes the test DB given custom migrations.
@@ -533,7 +529,8 @@ func TestMain(m *testing.M) {
}
if len(opts) == 0 {
opts = []InitTestDBOpt{{EnsureDefaultOrgAndUser: false, FeatureFlags: []string{}}}
cfgDBSec := testCfg.Raw.Section("database")
cfgDBSec.Key("ensure_default_org_and_user").SetValue("false")
}
if testSQLStore == nil {
@@ -566,16 +563,8 @@ func TestMain(m *testing.M) {
engine.DatabaseTZ = time.UTC
engine.TZLocation = time.UTC
skipEnsureDefaultOrgAndUser := false
for _, opt := range opts {
if !opt.EnsureDefaultOrgAndUser {
skipEnsureDefaultOrgAndUser = true
break
}
}
tracer := tracing.InitializeTracerForTest()
testSQLStore, err = newStore(testCfg, engine, features, migration, bus, tracer, skipEnsureDefaultOrgAndUser)
testSQLStore, err = newStore(testCfg, engine, features, migration, bus, tracer)
if err != nil {
return nil, err
}
+5 -1
View File
@@ -174,8 +174,12 @@ func NewTestStore(tb TestingTB, opts ...TestOption) *SQLStore {
engine.DatabaseTZ = time.UTC
engine.TZLocation = time.UTC
cfgDBSec := cfg.Raw.Section("database")
shouldEnsure := fmt.Sprintf("%t", !options.NoDefaultUserOrg && !options.Truncate)
cfgDBSec.Key("ensure_default_org_and_user").SetValue(shouldEnsure)
store, err := newStore(cfg, engine, features, options.MigratorFactory(features),
options.Bus, options.Tracer, options.NoDefaultUserOrg || options.Truncate)
options.Bus, options.Tracer)
if err != nil {
tb.Fatalf("failed to create a new SQLStore: %v", err)
panic("unreachable")
+2
View File
@@ -16,6 +16,8 @@ const (
HelpFlagGettingStartedPanelDismissed HelpFlags1 = 1 << iota
HelpFlagDashboardHelp1
HelpFlagEnterpriseAuth1
HelpFlagSyntheticMonitoring1
HelpFlagIRM1
)
type UpdateEmailActionType string
@@ -0,0 +1,123 @@
package metadata
import (
"context"
"fmt"
"strconv"
"time"
claims "github.com/grafana/authlib/types"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana-app-sdk/logging"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/storage/secret/metadata/metrics"
)
// TODO: this should be a "decrypt" service rather, so that other services can wire and call it.
func ProvideDecryptStorage(
features featuremgmt.FeatureToggles,
tracer trace.Tracer,
keeperService contracts.KeeperService,
keeperMetadataStorage contracts.KeeperMetadataStorage,
secureValueMetadataStorage contracts.SecureValueMetadataStorage,
decryptAuthorizer contracts.DecryptAuthorizer,
reg prometheus.Registerer,
) (contracts.DecryptStorage, error) {
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) ||
!features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) {
return &decryptStorage{}, nil
}
if decryptAuthorizer == nil {
return nil, fmt.Errorf("a decrypt authorizer is required")
}
return &decryptStorage{
tracer: tracer,
keeperMetadataStorage: keeperMetadataStorage,
keeperService: keeperService,
secureValueMetadataStorage: secureValueMetadataStorage,
decryptAuthorizer: decryptAuthorizer,
metrics: metrics.NewStorageMetrics(reg),
}, nil
}
// decryptStorage is the actual implementation of the decrypt storage.
type decryptStorage struct {
tracer trace.Tracer
keeperMetadataStorage contracts.KeeperMetadataStorage
keeperService contracts.KeeperService
secureValueMetadataStorage contracts.SecureValueMetadataStorage
decryptAuthorizer contracts.DecryptAuthorizer
metrics *metrics.StorageMetrics
}
// Decrypt decrypts a secure value from the keeper.
func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (_ secretv0alpha1.ExposedSecureValue, decryptErr error) {
ctx, span := s.tracer.Start(ctx, "DecryptStorage.Decrypt", trace.WithAttributes(
attribute.String("namespace", namespace.String()),
attribute.String("name", name),
))
defer span.End()
var decrypterIdentity string
start := time.Now()
// TEMPORARY: While we evaluate all of our auditing needs, provide one for decrypt operations.
defer func() {
span.SetAttributes(attribute.String("decrypter.identity", decrypterIdentity))
if decryptErr == nil {
logging.FromContext(ctx).Info("Audit log:", "operation", "decrypt_secret_success", "namespace", namespace, "secret_name", name, "decrypter_identity", decrypterIdentity)
} else {
span.SetStatus(codes.Error, "Decrypt failed")
span.RecordError(decryptErr)
logging.FromContext(ctx).Info("Audit log:", "operation", "decrypt_secret_error", "namespace", namespace, "secret_name", name, "decrypter_identity", decrypterIdentity, "error", decryptErr)
}
success := decryptErr == nil
s.metrics.DecryptDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
s.metrics.DecryptRequestCount.WithLabelValues(strconv.FormatBool(success)).Inc()
}()
// Basic authn check before reading a secure value metadata, it is here on purpose.
if _, ok := claims.AuthInfoFrom(ctx); !ok {
return "", contracts.ErrDecryptNotAuthorized
}
// The auth token will not necessarily have the permission to read the secure value metadata,
// but we still need to do it to inspect the `decrypters` field, hence the actual `authorize`
// function call happens after this.
sv, err := s.secureValueMetadataStorage.ReadForDecrypt(ctx, namespace, name)
if err != nil {
return "", contracts.ErrDecryptNotFound
}
decrypterIdentity, authorized := s.decryptAuthorizer.Authorize(ctx, name, sv.Decrypters)
if !authorized {
return "", contracts.ErrDecryptNotAuthorized
}
keeperConfig, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, namespace.String(), sv.Keeper, contracts.ReadOpts{})
if err != nil {
return "", contracts.ErrDecryptFailed
}
keeper, err := s.keeperService.KeeperForConfig(keeperConfig)
if err != nil {
return "", contracts.ErrDecryptFailed
}
exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace.String(), contracts.ExternalID(sv.ExternalID))
if err != nil {
return "", contracts.ErrDecryptFailed
}
return exposedValue, nil
}
@@ -0,0 +1,402 @@
package metadata
import (
"context"
"testing"
"github.com/grafana/authlib/authn"
"github.com/grafana/authlib/types"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"github.com/grafana/grafana/pkg/apimachinery/identity"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/decrypt"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
encryptionmanager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
"github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/secret/database"
encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption"
"github.com/grafana/grafana/pkg/storage/secret/migrator"
)
func TestIntegrationDecrypt(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Parallel()
t.Run("when no auth info is present, it returns an error", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
decryptSvc, _, _, _ := setupDecryptTestService(t, nil)
exposed, err := decryptSvc.Decrypt(ctx, "default", "name")
require.Error(t, err)
require.Empty(t, exposed)
})
t.Run("when secure value cannot be found, it returns an error", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
// Create auth context with proper permissions
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/group1:decrypt"}, "svc", types.TypeUser)
decryptSvc, _, _, _ := setupDecryptTestService(t, map[string]struct{}{"group1": {}})
exposed, err := decryptSvc.Decrypt(authCtx, "default", "non-existent-value")
require.ErrorIs(t, err, contracts.ErrDecryptNotFound)
require.Empty(t, exposed)
})
t.Run("when auth info is not in allowlist, it returns an unauthorized error", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
svName := "sv-test"
svcIdentity := "svc"
// Create auth context with identity that is not in allowlist
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/" + svName + ":decrypt"}, svcIdentity, types.TypeUser)
// Create an allowlist that doesn't include the permission
allowList := map[string]struct{}{"allowed-group": {}}
// Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, allowList)
// Create a secure value that is not in the allowlist
spec := secretv0alpha1.SecureValueSpec{
Description: "description",
Decrypters: []string{svcIdentity},
Value: secretv0alpha1.NewExposedSecureValue("value"),
}
sv := &secretv0alpha1.SecureValue{Spec: spec}
sv.Name = svName
sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid")
exposed, err := decryptSvc.Decrypt(authCtx, "default", svName)
require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized)
require.Empty(t, exposed)
})
t.Run("when happy path with valid auth and permissions, it returns decrypted value", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
svcIdentity := "svc"
// Create auth context with proper permissions that match the decrypters
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues:decrypt"}, svcIdentity, types.TypeUser)
// Include the group in allowlist
allowList := map[string]struct{}{svcIdentity: {}}
// Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, allowList)
// Create a secure value that is in the allowlist
spec := secretv0alpha1.SecureValueSpec{
Description: "description",
Decrypters: []string{svcIdentity},
Value: secretv0alpha1.NewExposedSecureValue("value"),
}
sv := &secretv0alpha1.SecureValue{Spec: spec}
sv.Name = "sv-test"
sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid")
exposed, err := decryptSvc.Decrypt(authCtx, "default", "sv-test")
require.NoError(t, err)
require.NotEmpty(t, exposed)
require.Equal(t, "value", exposed.DangerouslyExposeAndConsumeValue())
})
t.Run("with permissions for a specific secure value but trying to decrypt another one, it returns unauthorized error", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
svName := "sv-test"
svcIdentity := "svc"
// Create auth context with proper permissions that match the decrypters
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/sv-test2:decrypt"}, svcIdentity, types.TypeUser)
// Include the group in allowlist
allowList := map[string]struct{}{svcIdentity: {}}
// Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, allowList)
// Create a secure value that is in the allowlist
spec := secretv0alpha1.SecureValueSpec{
Description: "description",
Decrypters: []string{svcIdentity},
Value: secretv0alpha1.NewExposedSecureValue("value"),
}
sv := &secretv0alpha1.SecureValue{Spec: spec}
sv.Name = svName
sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid")
exposed, err := decryptSvc.Decrypt(authCtx, "default", svName)
require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized)
require.Empty(t, exposed)
})
t.Run("when permission format is malformed (no verb), it returns unauthorized error", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
svcIdentity := "svc"
// Create auth context with malformed permission (no verb)
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues"}, svcIdentity, types.TypeUser)
// Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}})
// Create a secure value
spec := secretv0alpha1.SecureValueSpec{
Description: "description",
Decrypters: []string{svcIdentity},
Value: secretv0alpha1.NewExposedSecureValue("value"),
}
sv := &secretv0alpha1.SecureValue{Spec: spec}
sv.Name = "sv-test"
sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid")
exposed, err := decryptSvc.Decrypt(authCtx, "default", "sv-test")
require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized)
require.Empty(t, exposed)
})
t.Run("when permission verb is not 'decrypt', it returns unauthorized error", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
svName := "sv-test"
svcIdentity := "svc"
// Create auth context with wrong verb
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/" + svName + ":read"}, svcIdentity, types.TypeUser)
// Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}})
// Create a secure value
spec := secretv0alpha1.SecureValueSpec{
Description: "description",
Decrypters: []string{svcIdentity},
Value: secretv0alpha1.NewExposedSecureValue("value"),
}
sv := &secretv0alpha1.SecureValue{Spec: spec}
sv.Name = svName
sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid")
exposed, err := decryptSvc.Decrypt(authCtx, "default", svName)
require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized)
require.Empty(t, exposed)
})
t.Run("when permission has incorrect number of parts, it returns unauthorized error", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
svcIdentity := "svc"
// Create auth context with incorrect number of parts
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/:decrypt"}, svcIdentity, types.TypeUser)
// Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}})
// Create a secure value
spec := secretv0alpha1.SecureValueSpec{
Description: "description",
Decrypters: []string{svcIdentity},
Value: secretv0alpha1.NewExposedSecureValue("value"),
}
sv := &secretv0alpha1.SecureValue{Spec: spec}
sv.Name = "sv-test"
sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid")
exposed, err := decryptSvc.Decrypt(authCtx, "default", "sv-test")
require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized)
require.Empty(t, exposed)
})
t.Run("when permission has incorrect group or resource, it returns unauthorized error", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
svName := "sv-test"
svcIdentity := "svc"
// Create auth context with incorrect group
authCtx := createAuthContext(ctx, "default", []string{"wrong.group/securevalues/" + svName + ":decrypt"}, svcIdentity, types.TypeUser)
// Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}})
// Create a secure value
spec := secretv0alpha1.SecureValueSpec{
Description: "description",
Decrypters: []string{svcIdentity},
Value: secretv0alpha1.NewExposedSecureValue("value"),
}
sv := &secretv0alpha1.SecureValue{Spec: spec}
sv.Name = svName
sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid")
exposed, err := decryptSvc.Decrypt(authCtx, "default", svName)
require.Error(t, err)
require.Equal(t, err.Error(), "not authorized")
require.Empty(t, exposed)
})
// TODO: add more tests for keeper failure scenarios, lets see how the async work will change this though.
}
func setupDecryptTestService(t *testing.T, allowList map[string]struct{}) (*decryptStorage, contracts.SecureValueMetadataStorage, *secretkeeper.OSSKeeperService, contracts.KeeperMetadataStorage) {
t.Helper()
// Initialize infra dependencies
cfg := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: "sdDkslslld",
EncryptionProvider: "secretKey.v1",
},
}
features := featuremgmt.WithFeatures(
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
featuremgmt.FlagSecretsManagementAppPlatform,
)
db := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
tracer := noop.NewTracerProvider().Tracer("test")
database := database.ProvideDatabase(db, tracer)
// Initialize encryption manager and storage
dataKeyStore, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features, nil)
require.NoError(t, err)
encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features)
require.NoError(t, err)
encryptionManager, err := encryptionmanager.ProvideEncryptionManager(
tracer,
dataKeyStore,
cfg,
&usagestats.UsageStatsMock{},
encryption.ProviderMap{},
)
require.NoError(t, err)
// Initialize the keeper service
keeperService, err := secretkeeper.ProvideService(tracer, encValueStore, encryptionManager, nil)
require.NoError(t, err)
keeperMetadataStorage, err := ProvideKeeperMetadataStorage(database, tracer, features, nil)
require.NoError(t, err)
// Initialize the secure value storage
secureValueMetadataStorage, err := ProvideSecureValueMetadataStorage(database, tracer, features, nil)
require.NoError(t, err)
decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, allowList)
// Initialize the decrypt storage
decryptSvc, err := ProvideDecryptStorage(features, tracer, keeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, nil)
require.NoError(t, err)
return decryptSvc.(*decryptStorage), secureValueMetadataStorage, keeperService, keeperMetadataStorage
}
func createAuthContext(ctx context.Context, namespace string, permissions []string, svc string, identityType types.IdentityType) context.Context {
requester := &identity.StaticRequester{
Type: identityType,
Namespace: namespace,
AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{
Rest: authn.AccessTokenClaims{
Permissions: permissions,
ServiceIdentity: svc,
},
},
}
if identityType == types.TypeUser {
requester.UserID = 1
}
return types.WithAuthInfo(ctx, requester)
}
// This helper will also delete the secureValue from the db when the test is done.
func newTestSecureValue(ctx context.Context, t *testing.T, db contracts.SecureValueMetadataStorage, keeperService *secretkeeper.OSSKeeperService, keeperMetadataStorage contracts.KeeperMetadataStorage, sv *secretv0alpha1.SecureValue, actorUID string) {
t.Helper()
_, err := db.Create(ctx, sv, actorUID)
require.NoError(t, err)
require.NoError(t, err)
// Since creating secrets is async, store the secret in the keeper synchronously to make testing easier
cfg, err := keeperMetadataStorage.GetKeeperConfig(ctx, sv.Namespace, sv.Spec.Keeper, contracts.ReadOpts{})
require.NoError(t, err)
keeper, err := keeperService.KeeperForConfig(cfg)
require.NoError(t, err)
externalID, err := keeper.Store(ctx, cfg, sv.Namespace, sv.Spec.Value.DangerouslyExposeAndConsumeValue())
require.NoError(t, err)
// Set external id for the secure value
err = db.SetExternalID(ctx, xkube.Namespace(sv.Namespace), sv.Name, externalID)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, keeper.Delete(ctx, cfg, sv.Namespace, externalID))
require.NoError(t, db.Delete(ctx, xkube.Namespace(sv.Namespace), sv.Name))
})
}
+2 -2
View File
@@ -52,7 +52,6 @@ type outboxMessageDB struct {
}
func (s *outboxStore) Append(ctx context.Context, input contracts.AppendOutboxMessage) (messageID int64, err error) {
start := time.Now()
ctx, span := s.tracer.Start(ctx, "outboxStore.Append", trace.WithAttributes(
attribute.String("name", input.Name),
attribute.String("namespace", input.Namespace),
@@ -74,6 +73,7 @@ func (s *outboxStore) Append(ctx context.Context, input contracts.AppendOutboxMe
assert.True(input.Type != "", "outboxStore.Append: outbox message type is required")
start := time.Now()
messageID, err = s.insertMessage(ctx, input)
if err != nil {
return messageID, fmt.Errorf("inserting message into outbox table: %+w", err)
@@ -156,7 +156,6 @@ func (s *outboxStore) insertMessage(ctx context.Context, input contracts.AppendO
}
func (s *outboxStore) ReceiveN(ctx context.Context, limit uint) ([]contracts.OutboxMessage, error) {
start := time.Now()
messageIDs, err := s.fetchMessageIdsInQueue(ctx, limit)
if err != nil {
return nil, fmt.Errorf("fetching message ids from queue: %w", err)
@@ -170,6 +169,7 @@ func (s *outboxStore) ReceiveN(ctx context.Context, limit uint) ([]contracts.Out
MessageIDs: messageIDs,
}
start := time.Now()
query, err := sqltemplate.Execute(sqlSecureValueOutboxReceiveN, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxReceiveN.Name(), err)
+15
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"iter"
"regexp"
"time"
badger "github.com/dgraph-io/badger/v4"
@@ -216,3 +217,17 @@ func PrefixRangeEnd(prefix string) string {
}
return string(end)
}
var (
// validKeyRegex validates keys used in the unified storage
// Keys can contain lowercase alphanumeric characters, '-', '.', '/', and '~'
// Any combination of these characters is allowed as long as the key is not empty
validKeyRegex = regexp.MustCompile(`^[a-z0-9./~-]+$`)
)
func IsValidKey(key string) bool {
if key == "" {
return false
}
return validKeyRegex.MatchString(key)
}
+40
View File
@@ -222,3 +222,43 @@ func TestBadgerKV_UnderlyingStorage(t *testing.T) {
}
})
}
func TestIsValidKey(t *testing.T) {
tests := []struct {
name string
key string
expected bool
}{
// Valid keys
{"simple key", "a", true},
{"key with numbers", "a123", true},
{"key with hyphens", "a-b-c", true},
{"key with dots", "a.b.c", true},
{"key with mixed", "a1-b2.c3", true},
{"composite key with slash", "ns/group", true},
{"composite key with tilde", "ns~action", true},
{"complex composite key", "ns/group/resource/name", true},
{"data key format", "ns/group/resource/name/123~created", true},
{"metadata key format", "group/resource/ns/name/123~created~folder", true},
{"metadata key format ending with a ~", "group/resource/ns/name/123~created~", true},
// invalid keys
{"empty key", "", false},
{"uppercase letters", "Invalid", false},
{"special characters", "a@b", false},
{"spaces", "a b", false},
{"leading space", " key", false},
{"trailing space", "key ", false},
{"tab character", "a\tb", false},
{"newline character", "a\nb", false},
{"underscores", "a_b", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsValidKey(tt.key)
require.Equal(t, tt.expected, result,
"IsValidKey(%q) = %v, expected %v", tt.key, result, tt.expected)
})
}
}
@@ -41,11 +41,8 @@ func ProvideSearchDistributorServer(cfg *setting.Cfg, features featuremgmt.Featu
grpcServer := grpcHandler.GetServer()
resourcepb.RegisterResourceStoreServer(grpcServer, distributorServer)
// resourcepb.RegisterBulkStoreServer(grpcServer, distributorServer)
resourcepb.RegisterResourceIndexServer(grpcServer, distributorServer)
resourcepb.RegisterManagedObjectIndexServer(grpcServer, distributorServer)
resourcepb.RegisterBlobStoreServer(grpcServer, distributorServer)
grpc_health_v1.RegisterHealthServer(grpcServer, healthService)
_, err = grpcserver.ProvideReflectionService(cfg, grpcHandler)
if err != nil {
@@ -106,86 +103,6 @@ func (ds *distributorServer) GetStats(ctx context.Context, r *resourcepb.Resourc
return client.GetStats(ctx, r)
}
func (ds *distributorServer) Read(ctx context.Context, r *resourcepb.ReadRequest) (*resourcepb.ReadResponse, error) {
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Key.Namespace, "Read")
if err != nil {
return nil, err
}
return client.Read(ctx, r)
}
func (ds *distributorServer) Create(ctx context.Context, r *resourcepb.CreateRequest) (*resourcepb.CreateResponse, error) {
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Key.Namespace, "Create")
if err != nil {
return nil, err
}
return client.Create(ctx, r)
}
func (ds *distributorServer) Update(ctx context.Context, r *resourcepb.UpdateRequest) (*resourcepb.UpdateResponse, error) {
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Key.Namespace, "Update")
if err != nil {
return nil, err
}
return client.Update(ctx, r)
}
func (ds *distributorServer) Delete(ctx context.Context, r *resourcepb.DeleteRequest) (*resourcepb.DeleteResponse, error) {
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Key.Namespace, "Delete")
if err != nil {
return nil, err
}
return client.Delete(ctx, r)
}
func (ds *distributorServer) List(ctx context.Context, r *resourcepb.ListRequest) (*resourcepb.ListResponse, error) {
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Options.Key.Namespace, "List")
if err != nil {
return nil, err
}
return client.List(ctx, r)
}
func (ds *distributorServer) Watch(r *resourcepb.WatchRequest, srv resourcepb.ResourceStore_WatchServer) error {
// r -> consumer watch request
// srv -> stream connection with consumer
ctx := srv.Context()
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Options.Key.Namespace, "Watch")
if err != nil {
return err
}
// watchClient -> stream connection with storage-api pod
watchClient, err := client.Watch(ctx, r)
if err != nil {
return err
}
// WARNING
// in Watch, all messages flow from the resource server (watchClient) to the consumer (srv)
// but since this is a streaming connection, in theory the consumer could also send a message to the server
// however for the sake of simplicity we are not handling it here
// but if we decide to handle bi-directional message passing in this method, we will need to update this
// we also never handle EOF err, as the server never closes the connection willingly
for {
msg, err := watchClient.Recv()
if err != nil {
return err
}
_ = srv.Send(msg)
}
}
// TODO implement this if we want to support it in cloud
// func (ds *DistributorServer) BulkProcess(srv BulkStore_BulkProcessServer) error {
// return nil
// }
func (ds *distributorServer) CountManagedObjects(ctx context.Context, r *resourcepb.CountManagedObjectsRequest) (*resourcepb.CountManagedObjectsResponse, error) {
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Namespace, "CountManagedObjects")
if err != nil {
@@ -204,24 +121,6 @@ func (ds *distributorServer) ListManagedObjects(ctx context.Context, r *resource
return client.ListManagedObjects(ctx, r)
}
func (ds *distributorServer) PutBlob(ctx context.Context, r *resourcepb.PutBlobRequest) (*resourcepb.PutBlobResponse, error) {
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Resource.Namespace, "PutBlob")
if err != nil {
return nil, err
}
return client.PutBlob(ctx, r)
}
func (ds *distributorServer) GetBlob(ctx context.Context, r *resourcepb.GetBlobRequest) (*resourcepb.GetBlobResponse, error) {
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Resource.Namespace, "GetBlob")
if err != nil {
return nil, err
}
return client.GetBlob(ctx, r)
}
func (ds *distributorServer) getClientToDistributeRequest(ctx context.Context, namespace string, methodName string) (context.Context, ResourceClient, error) {
ringHasher := fnv.New32a()
_, err := ringHasher.Write([]byte(namespace))
+31
View File
@@ -179,6 +179,24 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) {
err = obj.Value.Close()
require.NoError(t, err)
})
t.Run("save key with no data", func(t *testing.T) {
// Save a key with empty data
err := kv.Save(ctx, section, "empty-key", strings.NewReader(""))
require.NoError(t, err)
// Verify it was saved with empty data
obj, err := kv.Get(ctx, section, "empty-key")
require.NoError(t, err)
assert.Equal(t, "empty-key", obj.Key)
value, err := io.ReadAll(obj.Value)
require.NoError(t, err)
assert.Equal(t, "", string(value))
assert.Len(t, value, 0)
err = obj.Value.Close()
require.NoError(t, err)
})
}
func runTestKVDelete(t *testing.T, kv resource.KV, nsPrefix string) {
@@ -251,6 +269,19 @@ func runTestKVKeys(t *testing.T, kv resource.KV, nsPrefix string) {
assert.Contains(t, errors[0].Error(), "section is required")
assert.Empty(t, keys)
})
t.Run("list keys returns 0 keys", func(t *testing.T) {
// Use a different section with no keys
emptySection := nsPrefix + "-empty-keys"
var keys []string
for k, err := range kv.Keys(ctx, emptySection, resource.ListOptions{}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Empty(t, keys)
assert.Len(t, keys, 0)
})
}
func runTestKVKeysWithLimits(t *testing.T, kv resource.KV, nsPrefix string) {
@@ -663,3 +663,66 @@ func TestIntegrationDataConsistency(t *testing.T) {
assert.ElementsMatch(t, expected, tree.Spec.Routes[0].Matchers)
})
}
func TestIntegrationExtraConfigsConflicts(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"alertingImportAlertmanagerAPI"},
})
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
client := common.NewRoutingTreeClient(t, helper.Org1.Admin)
// Now upload a new extra config
testAlertmanagerConfigYAML := `
route:
receiver: default
receivers:
- name: default
webhook_configs:
- url: 'http://localhost/webhook'
`
headers := map[string]string{
"Content-Type": "application/yaml",
"X-Grafana-Alerting-Config-Identifier": "external-system",
"X-Grafana-Alerting-Merge-Matchers": "imported=true",
}
// Post the configuration to Grafana
response := legacyCli.ConvertPrometheusPostAlertmanagerConfig(t, definitions.AlertmanagerUserConfig{
AlertmanagerConfig: testAlertmanagerConfigYAML,
}, headers)
require.Equal(t, "success", response.Status)
current, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
require.NoError(t, err)
updated := current.Copy().(*v0alpha1.RoutingTree)
updated.Spec.Routes = append(updated.Spec.Routes, v0alpha1.RoutingTreeRoute{
Matchers: []v0alpha1.RoutingTreeMatcher{
{
Label: "imported",
Type: v0alpha1.RoutingTreeMatcherTypeEqual,
Value: "true",
},
},
})
_, err = client.Update(ctx, updated, v1.UpdateOptions{})
require.Error(t, err)
require.Truef(t, errors.IsBadRequest(err), "Should get BadRequest error but got: %s", err)
// Now delete extra config
legacyCli.ConvertPrometheusDeleteAlertmanagerConfig(t, headers)
// and try again
_, err = client.Update(ctx, updated, v1.UpdateOptions{})
require.NoError(t, err)
}
+50 -9
View File
@@ -67,7 +67,8 @@ func TestIntegrationIdentity(t *testing.T) {
"spec": {
"email": "staff@Org1",
"title": "staff"
}
},
"status": {}
}
]
}`, found)
@@ -84,20 +85,40 @@ func TestIntegrationIdentity(t *testing.T) {
found = teamClient.SpecJSON(rsp)
require.JSONEq(t, `[
{
"disabled": false,
"email": "admin@localhost",
"login": "admin"
"emailVerified": false,
"grafanaAdmin": true,
"login": "admin",
"name": "",
"provisioned": false
},
{
"disabled": false,
"email": "admin2-1",
"login": "admin2-1"
"emailVerified": false,
"grafanaAdmin": true,
"login": "admin2-1",
"name": "",
"provisioned": false
},
{
"disabled": false,
"email": "editor-1",
"login": "editor-1"
"emailVerified": false,
"grafanaAdmin": false,
"login": "editor-1",
"name": "",
"provisioned": false
},
{
"disabled": false,
"email": "viewer-1",
"login": "viewer-1"
"emailVerified": false,
"grafanaAdmin": false,
"login": "viewer-1",
"name": "",
"provisioned": false
}
]`, found)
@@ -114,20 +135,40 @@ func TestIntegrationIdentity(t *testing.T) {
found = teamClient.SpecJSON(rsp)
require.JSONEq(t, `[
{
"disabled": false,
"email": "admin2-1",
"login": "admin2-1"
"emailVerified": false,
"grafanaAdmin": true,
"login": "admin2-1",
"name": "",
"provisioned": false
},
{
"disabled": false,
"email": "admin2-2",
"login": "admin2-2"
"emailVerified": false,
"grafanaAdmin": false,
"login": "admin2-2",
"name": "",
"provisioned": false
},
{
"disabled": false,
"email": "editor-2",
"login": "editor-2"
"emailVerified": false,
"grafanaAdmin": false,
"login": "editor-2",
"name": "",
"provisioned": false
},
{
"disabled": false,
"email": "viewer-2",
"login": "viewer-2"
"emailVerified": false,
"grafanaAdmin": false,
"login": "viewer-2",
"name": "",
"provisioned": false
}
]`, found)
})
@@ -865,6 +865,296 @@
}
]
},
"/apis/advisor.grafana.app/v0alpha1/namespaces/{namespace}/checks/{name}/status": {
"get": {
"tags": [
"Check"
],
"description": "read status of the specified Check",
"operationId": "getCheckStatus",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
}
}
}
},
"x-kubernetes-action": "get",
"x-kubernetes-group-version-kind": {
"group": "advisor.grafana.app",
"version": "v0alpha1",
"kind": "Check"
}
},
"put": {
"tags": [
"Check"
],
"description": "replace status of the specified Check",
"operationId": "replaceCheckStatus",
"parameters": [
{
"name": "dryRun",
"in": "query",
"description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed",
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "fieldManager",
"in": "query",
"description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.",
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "fieldValidation",
"in": "query",
"description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.",
"schema": {
"type": "string",
"uniqueItems": true
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
}
}
},
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
}
}
}
},
"x-kubernetes-action": "put",
"x-kubernetes-group-version-kind": {
"group": "advisor.grafana.app",
"version": "v0alpha1",
"kind": "Check"
}
},
"patch": {
"tags": [
"Check"
],
"description": "partially update status of the specified Check",
"operationId": "updateCheckStatus",
"parameters": [
{
"name": "dryRun",
"in": "query",
"description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed",
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "fieldManager",
"in": "query",
"description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).",
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "fieldValidation",
"in": "query",
"description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.",
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "force",
"in": "query",
"description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.",
"schema": {
"type": "boolean",
"uniqueItems": true
}
}
],
"requestBody": {
"content": {
"application/apply-patch+yaml": {
"schema": {
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
}
},
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
}
},
"application/merge-patch+json": {
"schema": {
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
}
},
"application/strategic-merge-patch+json": {
"schema": {
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
}
}
},
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check"
}
}
}
}
},
"x-kubernetes-action": "patch",
"x-kubernetes-group-version-kind": {
"group": "advisor.grafana.app",
"version": "v0alpha1",
"kind": "Check"
}
},
"parameters": [
{
"name": "name",
"in": "path",
"description": "name of the Check",
"required": true,
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "namespace",
"in": "path",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "pretty",
"in": "query",
"description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).",
"schema": {
"type": "string",
"uniqueItems": true
}
}
]
},
"/apis/advisor.grafana.app/v0alpha1/namespaces/{namespace}/checktypes": {
"get": {
"tags": [
@@ -1694,6 +1984,296 @@
}
}
]
},
"/apis/advisor.grafana.app/v0alpha1/namespaces/{namespace}/checktypes/{name}/status": {
"get": {
"tags": [
"CheckType"
],
"description": "read status of the specified CheckType",
"operationId": "getCheckTypeStatus",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
}
}
}
},
"x-kubernetes-action": "get",
"x-kubernetes-group-version-kind": {
"group": "advisor.grafana.app",
"version": "v0alpha1",
"kind": "CheckType"
}
},
"put": {
"tags": [
"CheckType"
],
"description": "replace status of the specified CheckType",
"operationId": "replaceCheckTypeStatus",
"parameters": [
{
"name": "dryRun",
"in": "query",
"description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed",
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "fieldManager",
"in": "query",
"description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.",
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "fieldValidation",
"in": "query",
"description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.",
"schema": {
"type": "string",
"uniqueItems": true
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
}
}
},
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
}
}
}
},
"x-kubernetes-action": "put",
"x-kubernetes-group-version-kind": {
"group": "advisor.grafana.app",
"version": "v0alpha1",
"kind": "CheckType"
}
},
"patch": {
"tags": [
"CheckType"
],
"description": "partially update status of the specified CheckType",
"operationId": "updateCheckTypeStatus",
"parameters": [
{
"name": "dryRun",
"in": "query",
"description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed",
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "fieldManager",
"in": "query",
"description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).",
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "fieldValidation",
"in": "query",
"description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.",
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "force",
"in": "query",
"description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.",
"schema": {
"type": "boolean",
"uniqueItems": true
}
}
],
"requestBody": {
"content": {
"application/apply-patch+yaml": {
"schema": {
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
}
},
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
}
},
"application/merge-patch+json": {
"schema": {
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
}
},
"application/strategic-merge-patch+json": {
"schema": {
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
}
}
},
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/vnd.kubernetes.protobuf": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
},
"application/yaml": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType"
}
}
}
}
},
"x-kubernetes-action": "patch",
"x-kubernetes-group-version-kind": {
"group": "advisor.grafana.app",
"version": "v0alpha1",
"kind": "CheckType"
}
},
"parameters": [
{
"name": "name",
"in": "path",
"description": "name of the CheckType",
"required": true,
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "namespace",
"in": "path",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "pretty",
"in": "query",
"description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).",
"schema": {
"type": "string",
"uniqueItems": true
}
}
]
}
},
"components": {

Some files were not shown because too many files have changed in this diff Show More