Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs
This commit is contained in:
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"})
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
+2
-1
@@ -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')
|
||||
|
||||
+2
-1
@@ -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
@@ -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)
|
||||
|
||||
+2
-1
@@ -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')
|
||||
|
||||
+2
-1
@@ -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')
|
||||
|
||||
+2
-1
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
+2
-1
@@ -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')
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,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 +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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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
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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user