GRN parsing service (#56750)

* GRN parsing service
* move GRN package into infra and update fields
* remove orgID from GRNs (collapse into tenantID)
This commit is contained in:
Kristin Laemmert
2022-10-25 09:07:24 -04:00
committed by GitHub
parent 5daa87d7a1
commit 225ed4cc0a
4 changed files with 174 additions and 0 deletions

13
pkg/infra/grn/doc.go Normal file
View File

@@ -0,0 +1,13 @@
// package GRN provides utilities for working with Grafana Resource Names
// (GRNs).
// A GRN is an identifier which encodes all data necessary to retrieve a given
// resource from its respective service.
// A GRN string is expressed in the format:
//
// grn:${tenant_id}:${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.
package grn

9
pkg/infra/grn/errors.go Normal file
View File

@@ -0,0 +1,9 @@
package grn
import (
"github.com/grafana/grafana/pkg/util/errutil"
)
var (
ErrInvalidGRN = errutil.NewBase(errutil.StatusValidationFailed, "grn.InvalidGRN")
)

75
pkg/infra/grn/grn.go Normal file
View File

@@ -0,0 +1,75 @@
package grn
import (
"fmt"
"strings"
"cuelang.org/go/pkg/strconv"
)
type GRN struct {
// TenantID contains the ID of the tenant (in hosted grafana) or
// organization (in other environments) the resource belongs to. This field
// may be omitted for global Grafana resources which are not associated with
// an organization.
TenantID int
// The kind of resource being identified, for e.g. "dashboard" or "user".
// The caller is responsible for validating the value.
ResourceKind string
// ResourceIdentifier is used by the underlying service to identify the
// resource.
ResourceIdentifier string
}
// 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.
func ParseStr(str string) (GRN, error) {
ret := GRN{}
parts := strings.Split(str, ":")
if len(parts) != 3 {
return ret, ErrInvalidGRN.Errorf("%q is not a complete GRN", str)
}
if parts[0] != "grn" {
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.Atoi(parts[1])
if err != nil {
return ret, ErrInvalidGRN.Errorf("ID segment cannot be converted to an integer")
} else {
ret.TenantID = tID
}
}
return ret, nil
}
// MustParseStr is a wrapper around ParseStr that panics if the given input is
// not a valid GRN. This is intended for use in tests.
func MustParseStr(str string) GRN {
grn, err := ParseStr(str)
if err != nil {
panic("bad grn!")
}
return grn
}
// String returns a string representation of a grn in the format
// grn:tenantID:kind/resourceIdentifier
func (g *GRN) String() string {
return fmt.Sprintf("grn:%d:%s/%s", g.TenantID, g.ResourceKind, g.ResourceIdentifier)
}

77
pkg/infra/grn/grn_test.go Normal file
View File

@@ -0,0 +1,77 @@
package grn
import (
"fmt"
"testing"
)
func TestParseGRNStr(t *testing.T) {
tests := []struct {
input string
expect GRN
expectErr bool
}{
{ // empty
"",
GRN{},
true,
},
{ // too few parts
"grn:dashboards",
GRN{},
true,
},
{ // too many parts
"grn::dashboards:user:orgs:otherthings:hello:stillgoing",
GRN{},
true,
},
{ // Does not look like a GRN
"hrn:grafana::123:dashboards/foo",
GRN{},
true,
},
{ // Missing Kind
"grn::foo",
GRN{},
true,
},
{ // good!
"grn::roles/Admin",
GRN{TenantID: 0, ResourceKind: "roles", ResourceIdentifier: "Admin"},
false,
},
{ // good!
"grn::roles/Admin/with/some/slashes",
GRN{TenantID: 0, ResourceKind: "roles", ResourceIdentifier: "Admin/with/some/slashes"},
false,
},
{ // good!
"grn:123456789:roles/Admin/with/some/slashes",
GRN{TenantID: 123456789, ResourceKind: "roles", 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"},
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("ParseStr(%q)", test.input), func(t *testing.T) {
got, err := ParseStr(test.input)
if test.expectErr && err == nil {
t.Fatal("wrong result. Expected error, got success")
}
if err != nil && !test.expectErr {
t.Fatalf("wrong result. Expected success, got error %s", err.Error())
}
if got != test.expect {
t.Fatalf("wrong result. Wanted %s, got %s\n", test.expect.String(), got.String())
}
})
}
}