Access control: Use ResolveIdentity() for authorizing in org (#85549)
* Access control: Use ResolveIdentity() for authorizing in org * Fix tests * Fix middleware tests * Use ResolveIdentity in HasGlobalAccess() function * remove makeTmpUser * Cleanup * Fix linter errors * Fix test build * Remove GetUserPermissionsInOrg()
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -28,8 +29,6 @@ type Service interface {
|
||||
GetRoleByName(ctx context.Context, orgID int64, roleName string) (*RoleDTO, error)
|
||||
// GetUserPermissions returns user permissions with only action and scope fields set.
|
||||
GetUserPermissions(ctx context.Context, user identity.Requester, options Options) ([]Permission, error)
|
||||
// GetUserPermissionsInOrg return user permission in a specific organization
|
||||
GetUserPermissionsInOrg(ctx context.Context, user identity.Requester, orgID int64) ([]Permission, error)
|
||||
// SearchUsersPermissions returns all users' permissions filtered by an action prefix
|
||||
SearchUsersPermissions(ctx context.Context, user identity.Requester, options SearchOptions) (map[int64][]Permission, error)
|
||||
// ClearUserPermissionCache removes the permission cache entry for the given user
|
||||
@@ -171,22 +170,26 @@ type User struct {
|
||||
}
|
||||
|
||||
// HasGlobalAccess checks user access with globally assigned permissions only
|
||||
func HasGlobalAccess(ac AccessControl, service Service, c *contextmodel.ReqContext) func(evaluator Evaluator) bool {
|
||||
func HasGlobalAccess(ac AccessControl, authnService authn.Service, c *contextmodel.ReqContext) func(evaluator Evaluator) bool {
|
||||
return func(evaluator Evaluator) bool {
|
||||
var targetOrgID int64 = GlobalOrgID
|
||||
tmpUser, err := makeTmpUser(c.Req.Context(), service, nil, nil, c.SignedInUser, targetOrgID)
|
||||
orgUser, err := authnService.ResolveIdentity(c.Req.Context(), targetOrgID, c.SignedInUser.GetID())
|
||||
if err != nil {
|
||||
deny(c, nil, fmt.Errorf("failed to authenticate user in target org: %w", err))
|
||||
}
|
||||
|
||||
hasAccess, err := ac.Evaluate(c.Req.Context(), tmpUser, evaluator)
|
||||
hasAccess, err := ac.Evaluate(c.Req.Context(), orgUser, evaluator)
|
||||
if err != nil {
|
||||
c.Logger.Error("Error from access control system", "error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// guard against nil map
|
||||
if c.SignedInUser.Permissions == nil {
|
||||
c.SignedInUser.Permissions = make(map[int64]map[string][]string)
|
||||
}
|
||||
// set on user so we don't fetch global permissions every time this is called
|
||||
c.SignedInUser.Permissions[tmpUser.GetOrgID()] = tmpUser.GetPermissions()
|
||||
c.SignedInUser.Permissions[orgUser.GetOrgID()] = orgUser.GetPermissions()
|
||||
|
||||
return hasAccess
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
@@ -161,48 +160,6 @@ func (s *Service) getCachedUserPermissions(ctx context.Context, user identity.Re
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetUserPermissionsInOrg(ctx context.Context, user identity.Requester, orgID int64) ([]accesscontrol.Permission, error) {
|
||||
permissions := make([]accesscontrol.Permission, 0)
|
||||
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
|
||||
permissions = append(permissions, SharedWithMeFolderPermission)
|
||||
}
|
||||
|
||||
namespace, id := user.GetNamespacedID()
|
||||
userID, err := identity.UserIdentifier(namespace, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get permissions for user's basic roles from RAM
|
||||
roleList, err := s.store.GetUsersBasicRoles(ctx, []int64{userID}, orgID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not fetch basic roles for the user: %w", err)
|
||||
}
|
||||
var roles []string
|
||||
var ok bool
|
||||
if roles, ok = roleList[userID]; !ok {
|
||||
return nil, fmt.Errorf("found no basic roles for user %d in organisation %d", userID, orgID)
|
||||
}
|
||||
for _, builtin := range roles {
|
||||
if basicRole, ok := s.roles[builtin]; ok {
|
||||
permissions = append(permissions, basicRole.Permissions...)
|
||||
}
|
||||
}
|
||||
|
||||
dbPermissions, err := s.store.SearchUsersPermissions(ctx, orgID, accesscontrol.SearchOptions{
|
||||
NamespacedID: authn.NamespacedID(namespace, userID),
|
||||
// Query only basic, managed and plugin roles in OSS
|
||||
RolePrefixes: OSSRolesPrefixes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userPermissions := dbPermissions[userID]
|
||||
return append(permissions, userPermissions...), nil
|
||||
}
|
||||
|
||||
func (s *Service) ClearUserPermissionCache(user identity.Requester) {
|
||||
s.cache.Delete(permissionCacheKey(user))
|
||||
}
|
||||
|
||||
@@ -943,59 +943,3 @@ func TestService_DeleteExternalServiceRole(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_GetUserPermissionsInOrg(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
orgID int64
|
||||
ramRoles map[string]*accesscontrol.RoleDTO // BasicRole => RBAC BasicRole
|
||||
storedPerms map[int64][]accesscontrol.Permission // UserID => Permissions
|
||||
storedRoles map[int64][]string // UserID => Roles
|
||||
want []accesscontrol.Permission
|
||||
}{
|
||||
{
|
||||
name: "should get correct permissions from another org",
|
||||
orgID: 2,
|
||||
ramRoles: map[string]*accesscontrol.RoleDTO{
|
||||
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{}},
|
||||
string(roletype.RoleAdmin): {Permissions: []accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
|
||||
}},
|
||||
},
|
||||
storedPerms: map[int64][]accesscontrol.Permission{
|
||||
1: {
|
||||
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
|
||||
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"},
|
||||
},
|
||||
2: {
|
||||
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:2"},
|
||||
},
|
||||
},
|
||||
storedRoles: map[int64][]string{
|
||||
1: {string(roletype.RoleAdmin)},
|
||||
2: {string(roletype.RoleEditor)},
|
||||
},
|
||||
want: []accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ac := setupTestEnv(t)
|
||||
|
||||
ac.roles = tt.ramRoles
|
||||
ac.store = actest.FakeStore{
|
||||
ExpectedUsersPermissions: tt.storedPerms,
|
||||
ExpectedUsersRoles: tt.storedRoles,
|
||||
}
|
||||
user := &user.SignedInUser{OrgID: 1, UserID: 2}
|
||||
|
||||
got, err := ac.GetUserPermissionsInOrg(ctx, user, 2)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.ElementsMatch(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,6 @@ func (f FakeService) GetUserPermissions(ctx context.Context, user identity.Reque
|
||||
return f.ExpectedPermissions, f.ExpectedErr
|
||||
}
|
||||
|
||||
func (f FakeService) GetUserPermissionsInOrg(ctx context.Context, user identity.Requester, orgID int64) ([]accesscontrol.Permission, error) {
|
||||
return f.ExpectedPermissions, f.ExpectedErr
|
||||
}
|
||||
|
||||
func (f FakeService) SearchUsersPermissions(ctx context.Context, user identity.Requester, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
|
||||
return f.ExpectedUsersPermissions, f.ExpectedErr
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/team"
|
||||
"github.com/grafana/grafana/pkg/services/team/teamtest"
|
||||
@@ -27,141 +29,102 @@ func TestAuthorizeInOrgMiddleware(t *testing.T) {
|
||||
|
||||
// Define test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
orgIDGetter accesscontrol.OrgIDGetter
|
||||
evaluator accesscontrol.Evaluator
|
||||
accessControl accesscontrol.AccessControl
|
||||
acService accesscontrol.Service
|
||||
userCache user.Service
|
||||
ctxSignedInUser *user.SignedInUser
|
||||
teamService team.Service
|
||||
expectedStatus int
|
||||
name string
|
||||
targetOrgId int64
|
||||
targerOrgPermissions []accesscontrol.Permission
|
||||
orgIDGetter accesscontrol.OrgIDGetter
|
||||
evaluator accesscontrol.Evaluator
|
||||
accessControl accesscontrol.AccessControl
|
||||
acService accesscontrol.Service
|
||||
userCache user.Service
|
||||
ctxSignedInUser *user.SignedInUser
|
||||
teamService team.Service
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "should authorize user with global org ID - fetch",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return accesscontrol.GlobalOrgID, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
acService: &actest.FakeService{
|
||||
ExpectedPermissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
|
||||
},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusOK,
|
||||
name: "should authorize user with global org ID - fetch",
|
||||
targetOrgId: accesscontrol.GlobalOrgID,
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
targerOrgPermissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "should authorize user with non-global org ID - no fetch",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return 1, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
acService: &actest.FakeService{},
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusOK,
|
||||
name: "should authorize user with non-global org ID - no fetch",
|
||||
targetOrgId: 1,
|
||||
targerOrgPermissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "should return 403 when user has no permissions for the org",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return 1, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
acService: &actest.FakeService{},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
name: "should return 403 when user has no permissions for the org",
|
||||
targetOrgId: 1,
|
||||
targerOrgPermissions: []accesscontrol.Permission{},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "should return 200 when user has permissions for a global org",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return accesscontrol.GlobalOrgID, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
acService: &actest.FakeService{
|
||||
ExpectedPermissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
name: "should return 200 when user has permissions for a global org",
|
||||
targetOrgId: accesscontrol.GlobalOrgID,
|
||||
targerOrgPermissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "should return 403 when user has no permissions for a global org",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return accesscontrol.GlobalOrgID, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
acService: &actest.FakeService{},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
name: "should return 403 when user has no permissions for a global org",
|
||||
targetOrgId: accesscontrol.GlobalOrgID,
|
||||
targerOrgPermissions: []accesscontrol.Permission{},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "should return 403 when user org ID doesn't match and user does not exist in org 2",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return 2, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{ExpectedError: fmt.Errorf("user not found")},
|
||||
acService: &actest.FakeService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
name: "should return 403 when user org ID doesn't match and user does not exist in org 2",
|
||||
targetOrgId: 2,
|
||||
targerOrgPermissions: []accesscontrol.Permission{},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{ExpectedError: fmt.Errorf("user not found")},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "should return 403 early when api key org ID doesn't match",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return 2, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
acService: &actest.FakeService{},
|
||||
ctxSignedInUser: &user.SignedInUser{ApiKeyID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
name: "should return 403 early when api key org ID doesn't match",
|
||||
targetOrgId: 2,
|
||||
targerOrgPermissions: []accesscontrol.Permission{},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{ApiKeyID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "should fetch user permissions when they are empty",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return 1, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
acService: &actest.FakeService{
|
||||
ExpectedPermissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
|
||||
},
|
||||
teamService: &teamtest.FakeService{},
|
||||
userCache: &usertest.FakeUserService{
|
||||
GetSignedInUserFn: func(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) {
|
||||
return &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: nil}, nil
|
||||
},
|
||||
},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: nil},
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "should fetch user permissions when org ID doesn't match",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return 2, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
teamService: &teamtest.FakeService{},
|
||||
acService: &actest.FakeService{
|
||||
ExpectedPermissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
|
||||
},
|
||||
name: "should fetch user permissions when org ID doesn't match",
|
||||
targetOrgId: 2,
|
||||
targerOrgPermissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
teamService: &teamtest.FakeService{},
|
||||
userCache: &usertest.FakeUserService{
|
||||
GetSignedInUserFn: func(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) {
|
||||
return &user.SignedInUser{UserID: 1, OrgID: 2, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}}, nil
|
||||
@@ -171,13 +134,12 @@ func TestAuthorizeInOrgMiddleware(t *testing.T) {
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "fails to fetch user permissions when org ID doesn't match",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return 2, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
teamService: &teamtest.FakeService{},
|
||||
name: "fails to fetch user permissions when org ID doesn't match",
|
||||
targetOrgId: 2,
|
||||
targerOrgPermissions: []accesscontrol.Permission{},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
teamService: &teamtest.FakeService{},
|
||||
acService: &actest.FakeService{
|
||||
ExpectedErr: fmt.Errorf("failed to get user permissions"),
|
||||
},
|
||||
@@ -196,22 +158,17 @@ func TestAuthorizeInOrgMiddleware(t *testing.T) {
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
acService: &actest.FakeService{},
|
||||
userCache: &usertest.FakeUserService{},
|
||||
ctxSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {"users:read": {"users:*"}}}},
|
||||
teamService: &teamtest.FakeService{},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "should fetch global user permissions when user is not a member of the target org",
|
||||
orgIDGetter: func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return 2, nil
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
acService: &actest.FakeService{
|
||||
ExpectedPermissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
|
||||
},
|
||||
name: "should fetch global user permissions when user is not a member of the target org",
|
||||
targetOrgId: 2,
|
||||
targerOrgPermissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
|
||||
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
|
||||
accessControl: ac,
|
||||
userCache: &usertest.FakeUserService{
|
||||
GetSignedInUserFn: func(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) {
|
||||
return &user.SignedInUser{UserID: 1, OrgID: -1, Permissions: map[int64]map[string][]string{}}, nil
|
||||
@@ -228,15 +185,28 @@ func TestAuthorizeInOrgMiddleware(t *testing.T) {
|
||||
// Create test context
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/endpoint", nil)
|
||||
|
||||
service := tc.acService
|
||||
expectedIdentity := &authn.Identity{
|
||||
ID: fmt.Sprintf("user:%v", tc.ctxSignedInUser.UserID),
|
||||
OrgID: tc.targetOrgId,
|
||||
Permissions: map[int64]map[string][]string{},
|
||||
}
|
||||
expectedIdentity.Permissions[tc.targetOrgId] = accesscontrol.GroupScopesByAction(tc.targerOrgPermissions)
|
||||
|
||||
authnService := &authntest.FakeService{
|
||||
ExpectedIdentity: expectedIdentity,
|
||||
}
|
||||
|
||||
var orgIDGetter accesscontrol.OrgIDGetter
|
||||
if tc.orgIDGetter != nil {
|
||||
orgIDGetter = tc.orgIDGetter
|
||||
} else {
|
||||
orgIDGetter = func(c *contextmodel.ReqContext) (int64, error) {
|
||||
return tc.targetOrgId, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Create middleware
|
||||
middleware := accesscontrol.AuthorizeInOrgMiddleware(
|
||||
tc.accessControl,
|
||||
service,
|
||||
tc.userCache,
|
||||
tc.teamService,
|
||||
)(tc.orgIDGetter, tc.evaluator)
|
||||
middleware := accesscontrol.AuthorizeInOrgMiddleware(tc.accessControl, authnService)(orgIDGetter, tc.evaluator)
|
||||
|
||||
// Create test server
|
||||
server := web.New()
|
||||
|
||||
@@ -20,8 +20,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/team"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -177,15 +175,7 @@ func newID() string {
|
||||
|
||||
type OrgIDGetter func(c *contextmodel.ReqContext) (int64, error)
|
||||
|
||||
type userCache interface {
|
||||
GetSignedInUserWithCacheCtx(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error)
|
||||
}
|
||||
|
||||
type teamService interface {
|
||||
GetTeamIDsByUser(ctx context.Context, query *team.GetTeamIDsByUserQuery) ([]int64, error)
|
||||
}
|
||||
|
||||
func AuthorizeInOrgMiddleware(ac AccessControl, service Service, userService userCache, teamService teamService) func(OrgIDGetter, Evaluator) web.Handler {
|
||||
func AuthorizeInOrgMiddleware(ac AccessControl, authnService authn.Service) func(OrgIDGetter, Evaluator) web.Handler {
|
||||
return func(getTargetOrg OrgIDGetter, evaluator Evaluator) web.Handler {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
targetOrgID, err := getTargetOrg(c)
|
||||
@@ -194,104 +184,25 @@ func AuthorizeInOrgMiddleware(ac AccessControl, service Service, userService use
|
||||
return
|
||||
}
|
||||
|
||||
tmpUser, err := makeTmpUser(c.Req.Context(), service, userService, teamService, c.SignedInUser, targetOrgID)
|
||||
if err != nil {
|
||||
deny(c, nil, fmt.Errorf("failed to authenticate user in target org: %w", err))
|
||||
return
|
||||
var orgUser identity.Requester = c.SignedInUser
|
||||
if targetOrgID != c.SignedInUser.GetOrgID() {
|
||||
orgUser, err = authnService.ResolveIdentity(c.Req.Context(), targetOrgID, c.SignedInUser.GetID())
|
||||
if err != nil {
|
||||
deny(c, nil, fmt.Errorf("failed to authenticate user in target org: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
authorize(c, ac, tmpUser, evaluator)
|
||||
authorize(c, ac, orgUser, evaluator)
|
||||
|
||||
// guard against nil map
|
||||
if c.SignedInUser.Permissions == nil {
|
||||
c.SignedInUser.Permissions = make(map[int64]map[string][]string)
|
||||
}
|
||||
c.SignedInUser.Permissions[tmpUser.GetOrgID()] = tmpUser.GetPermissions()
|
||||
c.SignedInUser.Permissions[orgUser.GetOrgID()] = orgUser.GetPermissions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// makeTmpUser creates a temporary user that can be used to evaluate access across orgs.
|
||||
func makeTmpUser(ctx context.Context, service Service, cache userCache,
|
||||
teamService teamService, reqUser identity.Requester, targetOrgID int64) (identity.Requester, error) {
|
||||
tmpUser := &user.SignedInUser{
|
||||
OrgID: reqUser.GetOrgID(),
|
||||
OrgName: reqUser.GetOrgName(),
|
||||
OrgRole: reqUser.GetOrgRole(),
|
||||
IsGrafanaAdmin: reqUser.GetIsGrafanaAdmin(),
|
||||
Login: reqUser.GetLogin(),
|
||||
Teams: reqUser.GetTeams(),
|
||||
Permissions: map[int64]map[string][]string{
|
||||
reqUser.GetOrgID(): reqUser.GetPermissions(),
|
||||
},
|
||||
}
|
||||
|
||||
namespace, identifier := reqUser.GetNamespacedID()
|
||||
id, _ := identity.IntIdentifier(namespace, identifier)
|
||||
switch namespace {
|
||||
case identity.NamespaceUser:
|
||||
tmpUser.UserID = id
|
||||
case identity.NamespaceAPIKey:
|
||||
tmpUser.ApiKeyID = id
|
||||
if tmpUser.OrgID != targetOrgID {
|
||||
return nil, errors.New("API key does not belong to target org")
|
||||
}
|
||||
case identity.NamespaceServiceAccount:
|
||||
tmpUser.UserID = id
|
||||
tmpUser.IsServiceAccount = true
|
||||
}
|
||||
|
||||
if tmpUser.OrgID != targetOrgID {
|
||||
switch targetOrgID {
|
||||
case GlobalOrgID:
|
||||
tmpUser.OrgID = GlobalOrgID
|
||||
tmpUser.OrgRole = org.RoleNone
|
||||
tmpUser.OrgName = ""
|
||||
tmpUser.Teams = []int64{}
|
||||
default:
|
||||
if cache == nil {
|
||||
return nil, errors.New("user cache is nil")
|
||||
}
|
||||
query := user.GetSignedInUserQuery{UserID: tmpUser.UserID, OrgID: targetOrgID}
|
||||
queryResult, err := cache.GetSignedInUserWithCacheCtx(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmpUser.OrgID = queryResult.OrgID
|
||||
tmpUser.OrgName = queryResult.OrgName
|
||||
tmpUser.OrgRole = queryResult.OrgRole
|
||||
|
||||
// Only fetch the team membership is the user is a member of the organization
|
||||
if queryResult.OrgID == targetOrgID {
|
||||
if teamService != nil {
|
||||
teamIDs, err := teamService.GetTeamIDsByUser(ctx, &team.GetTeamIDsByUserQuery{OrgID: targetOrgID, UserID: tmpUser.UserID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmpUser.Teams = teamIDs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the user is not a member of the organization
|
||||
// evaluation must happen based on global permissions.
|
||||
evaluationOrg := targetOrgID
|
||||
if tmpUser.OrgID == NoOrgID {
|
||||
evaluationOrg = GlobalOrgID
|
||||
}
|
||||
if tmpUser.Permissions[evaluationOrg] == nil || len(tmpUser.Permissions[evaluationOrg]) == 0 {
|
||||
permissions, err := service.GetUserPermissions(ctx, tmpUser, Options{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tmpUser.Permissions[evaluationOrg] = GroupScopesByAction(permissions)
|
||||
}
|
||||
|
||||
return tmpUser, nil
|
||||
}
|
||||
|
||||
func UseOrgFromContextParams(c *contextmodel.ReqContext) (int64, error) {
|
||||
orgID, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
|
||||
|
||||
@@ -339,7 +250,7 @@ func UseOrgFromRequestData(c *contextmodel.ReqContext) (int64, error) {
|
||||
|
||||
// UseGlobalOrgFromRequestData returns global org if `global` flag is set or the org where user is logged in.
|
||||
// If RBACSingleOrganization is set, the org where user is logged in is returned - this is intended only for cloud workflows, where instances are limited to a single organization.
|
||||
func UseGlobalOrgFromRequestData(cfg *setting.Cfg) func(*contextmodel.ReqContext) (int64, error) {
|
||||
func UseGlobalOrgFromRequestData(cfg *setting.Cfg) OrgIDGetter {
|
||||
return func(c *contextmodel.ReqContext) (int64, error) {
|
||||
query, err := getOrgQueryFromRequest(c)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,7 +22,6 @@ type Calls struct {
|
||||
Evaluate []interface{}
|
||||
GetRoleByName []interface{}
|
||||
GetUserPermissions []interface{}
|
||||
GetUserPermissionsInOrg []interface{}
|
||||
ClearUserPermissionCache []interface{}
|
||||
DeclareFixedRoles []interface{}
|
||||
DeclarePluginRoles []interface{}
|
||||
@@ -50,7 +49,6 @@ type Mock struct {
|
||||
EvaluateFunc func(context.Context, identity.Requester, accesscontrol.Evaluator) (bool, error)
|
||||
GetRoleByNameFunc func(context.Context, int64, string) (*accesscontrol.RoleDTO, error)
|
||||
GetUserPermissionsFunc func(context.Context, identity.Requester, accesscontrol.Options) ([]accesscontrol.Permission, error)
|
||||
GetUserPermissionsInOrgFunc func(context.Context, identity.Requester, int64) ([]accesscontrol.Permission, error)
|
||||
ClearUserPermissionCacheFunc func(identity.Requester)
|
||||
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
|
||||
DeclarePluginRolesFunc func(context.Context, string, string, []plugins.RoleRegistration) error
|
||||
@@ -152,16 +150,6 @@ func (m *Mock) GetUserPermissions(ctx context.Context, user identity.Requester,
|
||||
return m.permissions, nil
|
||||
}
|
||||
|
||||
func (m *Mock) GetUserPermissionsInOrg(ctx context.Context, user identity.Requester, orgID int64) ([]accesscontrol.Permission, error) {
|
||||
m.Calls.GetUserPermissionsInOrg = append(m.Calls.GetUserPermissionsInOrg, []interface{}{ctx, user, orgID})
|
||||
// Use override if provided
|
||||
if m.GetUserPermissionsInOrgFunc != nil {
|
||||
return m.GetUserPermissionsInOrgFunc(ctx, user, orgID)
|
||||
}
|
||||
// Otherwise return the Permissions list
|
||||
return m.permissions, nil
|
||||
}
|
||||
|
||||
func (m *Mock) ClearUserPermissionCache(user identity.Requester) {
|
||||
m.Calls.ClearUserPermissionCache = append(m.Calls.ClearUserPermissionCache, []interface{}{user})
|
||||
// Use override if provided
|
||||
|
||||
Reference in New Issue
Block a user