K8s: Namespace parsing updates (default + stack-id) (#76310)

Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com>
This commit is contained in:
Ryan McKinley
2023-10-12 11:34:50 -07:00
committed by GitHub
parent be7fe761a3
commit 2a527aa33b
13 changed files with 328 additions and 81 deletions

View File

@@ -20,7 +20,10 @@ type OrgIDAuthorizer struct {
}
func ProvideOrgIDAuthorizer(orgService org.Service) *OrgIDAuthorizer {
return &OrgIDAuthorizer{log: log.New("grafana-apiserver.authorizer.orgid"), org: orgService}
return &OrgIDAuthorizer{
log: log.New("grafana-apiserver.authorizer.orgid"),
org: orgService,
}
}
func (auth OrgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
@@ -29,11 +32,26 @@ func (auth OrgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut
return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil
}
orgID, ok := grafanarequest.ParseOrgID(a.GetNamespace())
if !ok {
info, err := grafanarequest.ParseNamespace(a.GetNamespace())
if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error reading namespace: %v", err), nil
}
// No opinion when the namespace is arbitrary
if info.OrgID == -1 {
return authorizer.DecisionNoOpinion, "", nil
}
if info.StackID != "" {
return authorizer.DecisionDeny, "using a stack namespace requires deployment with a fixed stack id", nil
}
// Quick check that the same org is used
if signedInUser.OrgID == info.OrgID {
return authorizer.DecisionAllow, "", nil
}
// Check if the user has access to the specified org
query := org.GetUserOrgListQuery{UserID: signedInUser.UserID}
result, err := auth.org.GetUserOrgList(ctx, &query)
if err != nil {
@@ -41,10 +59,10 @@ func (auth OrgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut
}
for _, org := range result {
if org.OrgID == orgID {
if org.OrgID == info.OrgID {
return authorizer.DecisionAllow, "", nil
}
}
return authorizer.DecisionDeny, fmt.Sprintf("user %d is not a member of org %d", signedInUser.UserID, orgID), nil
return authorizer.DecisionDeny, fmt.Sprintf("user %d is not a member of org %d", signedInUser.UserID, info.OrgID), nil
}

View File

@@ -4,22 +4,20 @@ import (
"context"
"fmt"
"k8s.io/apiserver/pkg/authorization/authorizer"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/org"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
var _ authorizer.Authorizer = &OrgIDAuthorizer{}
type OrgRoleAuthorizer struct {
log log.Logger
org org.Service
}
func ProvideOrgRoleAuthorizer(orgService org.Service) *OrgRoleAuthorizer {
return &OrgRoleAuthorizer{log: log.New("grafana-apiserver.authorizer.orgrole"), org: orgService}
return &OrgRoleAuthorizer{log: log.New("grafana-apiserver.authorizer.orgrole")}
}
func (auth OrgRoleAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {

View File

@@ -7,16 +7,28 @@ import (
"k8s.io/apiserver/pkg/authorization/union"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/org"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/stack"
"github.com/grafana/grafana/pkg/setting"
)
func ProvideAuthorizer(
orgIDAuthorizer *org.OrgIDAuthorizer,
orgRoleAuthorizer *org.OrgRoleAuthorizer,
stackIDAuthorizer *stack.StackIDAuthorizer,
cfg *setting.Cfg,
) authorizer.Authorizer {
authorizers := []authorizer.Authorizer{
authorizerfactory.NewPrivilegedGroups(user.SystemPrivilegedGroup),
orgIDAuthorizer,
orgRoleAuthorizer,
}
// In Hosted grafana, the StackID replaces the orgID as a valid namespace
if cfg.StackID != "" {
authorizers = append(authorizers, stackIDAuthorizer)
} else {
authorizers = append(authorizers, orgIDAuthorizer)
}
authorizers = append(authorizers, orgRoleAuthorizer)
return union.New(authorizers...)
}

View File

@@ -0,0 +1,56 @@
package stack
import (
"context"
"fmt"
"k8s.io/apiserver/pkg/authorization/authorizer"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/infra/log"
grafanarequest "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/setting"
)
var _ authorizer.Authorizer = &StackIDAuthorizer{}
type StackIDAuthorizer struct {
log log.Logger
stackID string
}
func ProvideStackIDAuthorizer(cfg *setting.Cfg) *StackIDAuthorizer {
return &StackIDAuthorizer{
log: log.New("grafana-apiserver.authorizer.stackid"),
stackID: cfg.StackID, // this lets a single tenant grafana validate stack id (rather than orgs)
}
}
func (auth StackIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
signedInUser, err := appcontext.User(ctx)
if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil
}
info, err := grafanarequest.ParseNamespace(a.GetNamespace())
if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error reading namespace: %v", err), nil
}
// No opinion when the namespace is arbitrary
if info.OrgID == -1 {
return authorizer.DecisionNoOpinion, "", nil
}
if info.StackID != auth.stackID {
return authorizer.DecisionDeny, "wrong stack id is selected", nil
}
if info.OrgID != 1 {
return authorizer.DecisionDeny, "cloud instance requires org 1", nil
}
if signedInUser.OrgID != 1 {
return authorizer.DecisionDeny, "user must be in org 1", nil
}
return authorizer.DecisionAllow, "", nil
}

View File

@@ -0,0 +1,7 @@
package stack
import "github.com/google/wire"
var WireSet = wire.NewSet(
ProvideStackIDAuthorizer,
)

View File

@@ -4,9 +4,11 @@ import (
"github.com/google/wire"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/org"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/stack"
)
var WireSet = wire.NewSet(
org.WireSet,
stack.WireSet,
ProvideAuthorizer,
)

View File

@@ -2,25 +2,58 @@ package request
import (
"context"
"fmt"
"strconv"
"strings"
"k8s.io/apiserver/pkg/endpoints/request"
)
func OrgIDFrom(ctx context.Context) (int64, bool) {
ns := request.NamespaceValue(ctx)
return ParseOrgID(ns)
type NamespaceInfo struct {
// OrgID defined in namespace (1 when using stack ids)
OrgID int64
// The cloud stack ID (must match the value in cfg.Settings)
StackID string
// The original namespace string regardless the input
Value string
}
func ParseOrgID(ns string) (int64, bool) {
if len(ns) < 5 || ns[:4] != "org-" {
return 0, false
func NamespaceInfoFrom(ctx context.Context, requireOrgID bool) (NamespaceInfo, error) {
info, err := ParseNamespace(request.NamespaceValue(ctx))
if err == nil && requireOrgID && info.OrgID < 1 {
return info, fmt.Errorf("expected valid orgId")
}
orgID, err := strconv.Atoi(ns[4:])
if err != nil {
return 0, false
}
return int64(orgID), true
return info, err
}
func ParseNamespace(ns string) (NamespaceInfo, error) {
info := NamespaceInfo{Value: ns, OrgID: -1}
if ns == "default" {
info.OrgID = 1
return info, nil
}
if strings.HasPrefix(ns, "org-") {
id, err := strconv.Atoi(ns[4:])
if id < 1 {
return info, fmt.Errorf("invalid org id")
}
if id == 1 {
return info, fmt.Errorf("use default rather than org-1")
}
info.OrgID = int64(id)
return info, err
}
if strings.HasPrefix(ns, "stack-") {
info.StackID = ns[6:]
if len(info.StackID) < 2 {
return info, fmt.Errorf("invalid stack id")
}
info.OrgID = 1
return info, nil
}
return info, nil
}

View File

@@ -1,61 +1,127 @@
package request_test
import (
"context"
"testing"
"k8s.io/apiserver/pkg/endpoints/request"
grafanarequest "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
)
func TestOrgIDFrom(t *testing.T) {
func TestParseNamespace(t *testing.T) {
tests := []struct {
name string
ctx context.Context
expected int64
ok bool
name string
namespace string
expected grafanarequest.NamespaceInfo
expectErr bool
}{
{
name: "empty namespace",
ctx: context.Background(),
expected: 0,
ok: false,
name: "empty namespace",
expected: grafanarequest.NamespaceInfo{
OrgID: -1,
},
},
{
name: "incorrect number of parts",
ctx: request.WithNamespace(context.Background(), "org-123-a"),
expected: 0,
ok: false,
name: "incorrect number of parts",
namespace: "org-123-a",
expectErr: true,
expected: grafanarequest.NamespaceInfo{
OrgID: -1,
},
},
{
name: "incorrect prefix",
ctx: request.WithNamespace(context.Background(), "abc-123"),
expected: 0,
ok: false,
name: "org id not a number",
namespace: "org-invalid",
expectErr: true,
expected: grafanarequest.NamespaceInfo{
OrgID: -1,
},
},
{
name: "org id not a number",
ctx: request.WithNamespace(context.Background(), "org-invalid"),
expected: 0,
ok: false,
name: "valid org id",
namespace: "org-123",
expected: grafanarequest.NamespaceInfo{
OrgID: 123,
},
},
{
name: "valid org id",
ctx: request.WithNamespace(context.Background(), "org-123"),
expected: 123,
ok: true,
name: "org should not be 1 in the namespace",
namespace: "org-1",
expectErr: true,
expected: grafanarequest.NamespaceInfo{
OrgID: -1,
},
},
{
name: "can not be negative",
namespace: "org--5",
expectErr: true,
expected: grafanarequest.NamespaceInfo{
OrgID: -1,
},
},
{
name: "can not be zero",
namespace: "org-0",
expectErr: true,
expected: grafanarequest.NamespaceInfo{
OrgID: -1,
},
},
{
name: "default is org 1",
namespace: "default",
expected: grafanarequest.NamespaceInfo{
OrgID: 1,
},
},
{
name: "valid stack",
namespace: "stack-abcdef",
expected: grafanarequest.NamespaceInfo{
OrgID: 1,
StackID: "abcdef",
},
},
{
name: "invalid stack id",
namespace: "stack-",
expectErr: true,
expected: grafanarequest.NamespaceInfo{
OrgID: -1,
},
},
{
name: "invalid stack id (too short)",
namespace: "stack-1",
expectErr: true,
expected: grafanarequest.NamespaceInfo{
OrgID: -1,
StackID: "1",
},
},
{
name: "other namespace",
namespace: "anything",
expected: grafanarequest.NamespaceInfo{
OrgID: -1,
Value: "anything",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, ok := grafanarequest.OrgIDFrom(tt.ctx)
if actual != tt.expected {
t.Errorf("OrgIDFrom() returned %d, expected %d", actual, tt.expected)
info, err := grafanarequest.ParseNamespace(tt.namespace)
if tt.expectErr != (err != nil) {
t.Errorf("ParseNamespace() returned %+v, expected an error", info)
}
if ok != tt.ok {
t.Errorf("OrgIDFrom() returned %t, expected %t", ok, tt.ok)
if info.OrgID != tt.expected.OrgID {
t.Errorf("ParseNamespace() [OrgID] returned %d, expected %d", info.OrgID, tt.expected.OrgID)
}
if info.StackID != tt.expected.StackID {
t.Errorf("ParseNamespace() [StackID] returned %s, expected %s", info.StackID, tt.expected.StackID)
}
if info.Value != tt.namespace {
t.Errorf("ParseNamespace() [Value] returned %s, expected %s", info.Value, tt.namespace)
}
})
}