mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s: Namespace parsing updates (default + stack-id) (#76310)
Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package stack
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var WireSet = wire.NewSet(
|
||||
ProvideStackIDAuthorizer,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user