diff --git a/pkg/infra/grn/doc.go b/pkg/infra/grn/doc.go index d6f6ef834e1..ba6e88698c0 100644 --- a/pkg/infra/grn/doc.go +++ b/pkg/infra/grn/doc.go @@ -6,7 +6,7 @@ // A GRN string is expressed in the format: // -// grn:${tenant_id}:${kind}/${id} +// grn:${tenant_id}:${group}/${kind}/${id} // // The format of the final id is defined by the owning service and not // validated by the GRN parser. Prefer using UIDs where possible. diff --git a/pkg/infra/grn/grn.go b/pkg/infra/grn/grn.go index a2d2cfb8f6b..3faf14c26d7 100644 --- a/pkg/infra/grn/grn.go +++ b/pkg/infra/grn/grn.go @@ -8,10 +8,16 @@ import ( // ParseStr attempts to parse a string into a GRN. It returns an error if the // given string does not match the GRN format, but does not validate the values. +// grn::// +// No component of the GRN may contain a colon +// The TenantID is optional, but must be an integer string if specified +// The ResourceGroup, ResourceKind and ResourceIdentifier must be non-empty strings +// The ResourceGroup and ResourceKind may not contain slashes +// The ResourceIdentifier may contain slashes func ParseStr(str string) (*GRN, error) { ret := &GRN{} - parts := strings.Split(str, ":") + parts := strings.Split(str, ":") if len(parts) != 3 { return ret, ErrInvalidGRN.Errorf("%q is not a complete GRN", str) } @@ -20,22 +26,33 @@ func ParseStr(str string) (*GRN, error) { return ret, ErrInvalidGRN.Errorf("%q does not look like a GRN", str) } - // split the final segment into Kind and ID. This only splits after the - // first occurrence of "/"; a ResourceIdentifier may contain "/" - kind, id, found := strings.Cut(parts[2], "/") - if !found { // missing "/" - return ret, ErrInvalidGRN.Errorf("invalid resource identifier in GRN %q", str) - } - ret.ResourceIdentifier = id - ret.ResourceKind = kind - if parts[1] != "" { tID, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { - return ret, ErrInvalidGRN.Errorf("ID segment cannot be converted to an integer") - } else { - ret.TenantID = tID + return ret, ErrInvalidGRN.Errorf("Tenant ID segment cannot be converted to an integer") } + + ret.TenantID = tID + } + + // split the rest of the GRN into Group, Kind and Identifier + parts = strings.SplitN(parts[2], "/", 3) + if len(parts) != 3 { + return ret, ErrInvalidGRN.Errorf("%q is not a complete GRN", str) + } + + ret.ResourceGroup = parts[0] + ret.ResourceKind = parts[1] + ret.ResourceIdentifier = parts[2] + + if ret.ResourceGroup == "" { + return ret, ErrInvalidGRN.Errorf("Cannot find resource group in GRN %q", str) + } + if ret.ResourceKind == "" { + return ret, ErrInvalidGRN.Errorf("Cannot find resource kind in GRN %q", str) + } + if ret.ResourceIdentifier == "" { + return ret, ErrInvalidGRN.Errorf("Cannot find resource identifier in GRN %q", str) } return ret, nil @@ -54,7 +71,7 @@ func MustParseStr(str string) *GRN { // ToGRNString returns a string representation of a grn in the format // grn:tenantID:kind/resourceIdentifier func (g *GRN) ToGRNString() string { - return fmt.Sprintf("grn:%d:%s/%s", g.TenantID, g.ResourceKind, g.ResourceIdentifier) + return fmt.Sprintf("grn:%d:%s/%s/%s", g.TenantID, g.ResourceGroup, g.ResourceKind, g.ResourceIdentifier) } // Check if the two GRNs reference to the same object @@ -64,6 +81,7 @@ func (g *GRN) Equal(b *GRN) bool { return false } return g == b || (g.TenantID == b.TenantID && + g.ResourceGroup == b.ResourceGroup && g.ResourceKind == b.ResourceKind && g.ResourceIdentifier == b.ResourceIdentifier) } diff --git a/pkg/infra/grn/grn.pb.go b/pkg/infra/grn/grn.pb.go index bc551198897..e4d82a178f5 100644 --- a/pkg/infra/grn/grn.pb.go +++ b/pkg/infra/grn/grn.pb.go @@ -36,6 +36,9 @@ type GRN struct { // ResourceIdentifier is used by the underlying service to identify the // resource. ResourceIdentifier string `protobuf:"bytes,4,opt,name=ResourceIdentifier,proto3" json:"ResourceIdentifier,omitempty"` + // The group represents the specific resource group the resource belongs to. + // This is a unique value for each plugin and maps to the k8s Group + ResourceGroup string `protobuf:"bytes,5,opt,name=ResourceGroup,proto3" json:"ResourceGroup,omitempty"` } func (x *GRN) Reset() { @@ -91,21 +94,31 @@ func (x *GRN) GetResourceIdentifier() string { return "" } +func (x *GRN) GetResourceGroup() string { + if x != nil { + return x.ResourceGroup + } + return "" +} + var File_grn_proto protoreflect.FileDescriptor var file_grn_proto_rawDesc = []byte{ 0x0a, 0x09, 0x67, 0x72, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x67, 0x72, 0x6e, - 0x22, 0x75, 0x0a, 0x03, 0x47, 0x52, 0x4e, 0x12, 0x1a, 0x0a, 0x08, 0x54, 0x65, 0x6e, 0x61, 0x6e, - 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x54, 0x65, 0x6e, 0x61, 0x6e, - 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, - 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x2e, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x2a, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, - 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2f, - 0x67, 0x72, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x22, 0x9b, 0x01, 0x0a, 0x03, 0x47, 0x52, 0x4e, 0x12, 0x1a, 0x0a, 0x08, 0x54, 0x65, 0x6e, 0x61, + 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x54, 0x65, 0x6e, 0x61, + 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x2e, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x24, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x2a, + 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, + 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, + 0x2f, 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2f, 0x67, 0x72, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/pkg/infra/grn/grn.proto b/pkg/infra/grn/grn.proto index 45a8bce4ffc..c3c21210e56 100644 --- a/pkg/infra/grn/grn.proto +++ b/pkg/infra/grn/grn.proto @@ -17,4 +17,8 @@ message GRN { // ResourceIdentifier is used by the underlying service to identify the // resource. string ResourceIdentifier = 4; + + // The group represents the specific resource group the resource belongs to. + // This is a unique value for each plugin and maps to the k8s Group + string ResourceGroup = 5; } diff --git a/pkg/infra/grn/grn_test.go b/pkg/infra/grn/grn_test.go index a9be304083d..c20a82a4a13 100644 --- a/pkg/infra/grn/grn_test.go +++ b/pkg/infra/grn/grn_test.go @@ -38,24 +38,29 @@ func TestParseGRNStr(t *testing.T) { &GRN{}, true, }, + { // Missing Group + "grn::foo/Bar", + &GRN{}, + true, + }, { // good! - "grn::roles/Admin", - &GRN{TenantID: 0, ResourceKind: "roles", ResourceIdentifier: "Admin"}, + "grn::core.grafana.com/Role/Admin", + &GRN{TenantID: 0, ResourceGroup: "core.grafana.com", ResourceKind: "Role", ResourceIdentifier: "Admin"}, false, }, { // good! - "grn::roles/Admin/with/some/slashes", - &GRN{TenantID: 0, ResourceKind: "roles", ResourceIdentifier: "Admin/with/some/slashes"}, + "grn::core.grafana.com/Role/Admin/with/some/slashes", + &GRN{TenantID: 0, ResourceGroup: "core.grafana.com", ResourceKind: "Role", ResourceIdentifier: "Admin/with/some/slashes"}, false, }, { // good! - "grn:123456789:roles/Admin/with/some/slashes", - &GRN{TenantID: 123456789, ResourceKind: "roles", ResourceIdentifier: "Admin/with/some/slashes"}, + "grn:123456789:core.grafana.com/Role/Admin/with/some/slashes", + &GRN{TenantID: 123456789, ResourceGroup: "core.grafana.com", ResourceKind: "Role", ResourceIdentifier: "Admin/with/some/slashes"}, false, }, { // Weird, but valid. - "grn::roles///Admin/with/leading/slashes", - &GRN{TenantID: 0, ResourceKind: "roles", ResourceIdentifier: "//Admin/with/leading/slashes"}, + "grn::core.grafana.com/Role///Admin/with/leading/slashes", + &GRN{TenantID: 0, ResourceGroup: "core.grafana.com", ResourceKind: "Role", ResourceIdentifier: "//Admin/with/leading/slashes"}, false, }, }