AuthZ: Extend /api/search to work with self-contained permissions (#70749)

* Search sql filter draft, unfinished

* Search works for empty roles

* Add current AuthModule to SignedInUser

* clean up, changes to the search

* Use constant prefixes

* Change AuthModule to AuthenticatedBy

* Add tests for using the permissions from the SignedInUser

* Refactor and simplify code

* Fix sql generation for pg and mysql

* Fixes, clean up

* Add test for empty permission list

* Fix

* Fix any vs all in case of edit permission

* Update pkg/services/authn/authn.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* Update pkg/services/sqlstore/permissions/dashboard_test.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* Fixes, changes based on the review

---------

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
Misi
2023-07-12 12:31:36 +02:00
committed by GitHub
co-authored by Gabriel MABILLE
parent e56b2cae00
commit 5efc3386d3
23 changed files with 650 additions and 235 deletions
+37 -35
View File
@@ -200,9 +200,9 @@ type Identity struct {
Email string
// IsGrafanaAdmin is true if the entity is a Grafana admin.
IsGrafanaAdmin *bool
// AuthModule is the name of the external system. For example, "auth_ldap" or "auth_saml".
// Empty if the identity is provided by Grafana.
AuthModule string
// AuthenticatedBy is the name of the authentication client that was used to authenticate the current Identity.
// For example, "password", "apikey", "auth_ldap" or "auth_azuread".
AuthenticatedBy string
// AuthId is the unique identifier for the entity in the external system.
// Empty if the identity is provided by Grafana.
AuthID string
@@ -262,21 +262,22 @@ func (i *Identity) SignedInUser() *user.SignedInUser {
}
u := &user.SignedInUser{
UserID: 0,
OrgID: i.OrgID,
OrgName: i.OrgName,
OrgRole: i.Role(),
Login: i.Login,
Name: i.Name,
Email: i.Email,
OrgCount: i.OrgCount,
IsGrafanaAdmin: isGrafanaAdmin,
IsAnonymous: i.IsAnonymous,
IsDisabled: i.IsDisabled,
HelpFlags1: i.HelpFlags1,
LastSeenAt: i.LastSeenAt,
Teams: i.Teams,
Permissions: i.Permissions,
UserID: 0,
OrgID: i.OrgID,
OrgName: i.OrgName,
OrgRole: i.Role(),
Login: i.Login,
Name: i.Name,
Email: i.Email,
AuthenticatedBy: i.AuthenticatedBy,
OrgCount: i.OrgCount,
IsGrafanaAdmin: isGrafanaAdmin,
IsAnonymous: i.IsAnonymous,
IsDisabled: i.IsDisabled,
HelpFlags1: i.HelpFlags1,
LastSeenAt: i.LastSeenAt,
Teams: i.Teams,
Permissions: i.Permissions,
}
namespace, id := i.NamespacedID()
@@ -294,7 +295,7 @@ func (i *Identity) ExternalUserInfo() login.ExternalUserInfo {
_, id := i.NamespacedID()
return login.ExternalUserInfo{
OAuthToken: i.OAuthToken,
AuthModule: i.AuthModule,
AuthModule: i.AuthenticatedBy,
AuthId: i.AuthID,
UserId: id,
Email: i.Email,
@@ -308,23 +309,24 @@ func (i *Identity) ExternalUserInfo() login.ExternalUserInfo {
}
// IdentityFromSignedInUser creates an identity from a SignedInUser.
func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams) *Identity {
func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams, authenticatedBy string) *Identity {
return &Identity{
ID: id,
OrgID: usr.OrgID,
OrgName: usr.OrgName,
OrgRoles: map[int64]org.RoleType{usr.OrgID: usr.OrgRole},
Login: usr.Login,
Name: usr.Name,
Email: usr.Email,
OrgCount: usr.OrgCount,
IsGrafanaAdmin: &usr.IsGrafanaAdmin,
IsDisabled: usr.IsDisabled,
HelpFlags1: usr.HelpFlags1,
LastSeenAt: usr.LastSeenAt,
Teams: usr.Teams,
ClientParams: params,
Permissions: usr.Permissions,
ID: id,
OrgID: usr.OrgID,
OrgName: usr.OrgName,
OrgRoles: map[int64]org.RoleType{usr.OrgID: usr.OrgRole},
Login: usr.Login,
Name: usr.Name,
Email: usr.Email,
AuthenticatedBy: authenticatedBy,
OrgCount: usr.OrgCount,
IsGrafanaAdmin: &usr.IsGrafanaAdmin,
IsDisabled: usr.IsDisabled,
HelpFlags1: usr.HelpFlags1,
LastSeenAt: usr.LastSeenAt,
Teams: usr.Teams,
ClientParams: params,
Permissions: usr.Permissions,
}
}
+13 -13
View File
@@ -76,13 +76,13 @@ func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *auth
// Does user exist in the database?
usr, userAuth, errUserInDB := s.getUser(ctx, id)
if errUserInDB != nil && !errors.Is(errUserInDB, user.ErrUserNotFound) {
s.log.FromContext(ctx).Error("Failed to fetch user", "error", errUserInDB, "auth_module", id.AuthModule, "auth_id", id.AuthID)
s.log.FromContext(ctx).Error("Failed to fetch user", "error", errUserInDB, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
return errSyncUserInternal.Errorf("unable to retrieve user")
}
if errors.Is(errUserInDB, user.ErrUserNotFound) {
if !id.ClientParams.AllowSignUp {
s.log.FromContext(ctx).Warn("Failed to create user, signup is not allowed for module", "auth_module", id.AuthModule, "auth_id", id.AuthID)
s.log.FromContext(ctx).Warn("Failed to create user, signup is not allowed for module", "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
return errUserSignupDisabled.Errorf("%w", login.ErrSignupNotAllowed)
}
@@ -90,13 +90,13 @@ func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *auth
var errCreate error
usr, errCreate = s.createUser(ctx, id)
if errCreate != nil {
s.log.FromContext(ctx).Error("Failed to create user", "error", errCreate, "auth_module", id.AuthModule, "auth_id", id.AuthID)
s.log.FromContext(ctx).Error("Failed to create user", "error", errCreate, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
return errSyncUserInternal.Errorf("unable to create user")
}
} else {
// update user
if errUpdate := s.updateUserAttributes(ctx, usr, id, userAuth); errUpdate != nil {
s.log.FromContext(ctx).Error("Failed to update user", "error", errUpdate, "auth_module", id.AuthModule, "auth_id", id.AuthID)
s.log.FromContext(ctx).Error("Failed to update user", "error", errUpdate, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
return errSyncUserInternal.Errorf("unable to update user")
}
}
@@ -174,7 +174,7 @@ func (s *UserSync) EnableDisabledUserHook(ctx context.Context, identity *authn.I
}
func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, identity *authn.Identity, createConnection bool) error {
if identity.AuthModule == "" {
if identity.AuthenticatedBy == "" {
return nil
}
@@ -184,7 +184,7 @@ func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, ident
if createConnection {
return s.authInfoService.SetAuthInfo(ctx, &login.SetAuthInfoCommand{
UserId: userID,
AuthModule: identity.AuthModule,
AuthModule: identity.AuthenticatedBy,
AuthId: identity.AuthID,
OAuthToken: identity.OAuthToken,
})
@@ -194,13 +194,13 @@ func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, ident
return s.authInfoService.UpdateAuthInfo(ctx, &login.UpdateAuthInfoCommand{
UserId: userID,
AuthId: identity.AuthID,
AuthModule: identity.AuthModule,
AuthModule: identity.AuthenticatedBy,
OAuthToken: identity.OAuthToken,
})
}
func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id *authn.Identity, userAuth *login.UserAuth) error {
if errProtection := s.userProtectionService.AllowUserMapping(usr, id.AuthModule); errProtection != nil {
if errProtection := s.userProtectionService.AllowUserMapping(usr, id.AuthenticatedBy); errProtection != nil {
return errUserProtection.Errorf("user mapping not allowed: %w", errProtection)
}
// sync user info
@@ -286,8 +286,8 @@ func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.Us
func (s *UserSync) getUser(ctx context.Context, identity *authn.Identity) (*user.User, *login.UserAuth, error) {
// Check auth info fist
if identity.AuthID != "" && identity.AuthModule != "" {
query := &login.GetAuthInfoQuery{AuthId: identity.AuthID, AuthModule: identity.AuthModule}
if identity.AuthID != "" && identity.AuthenticatedBy != "" {
query := &login.GetAuthInfoQuery{AuthId: identity.AuthID, AuthModule: identity.AuthenticatedBy}
authInfo, errGetAuthInfo := s.authInfoService.GetAuthInfo(ctx, query)
if errGetAuthInfo != nil && !errors.Is(errGetAuthInfo, user.ErrUserNotFound) {
@@ -307,7 +307,7 @@ func (s *UserSync) getUser(ctx context.Context, identity *authn.Identity) (*user
// if the user connected to user auth does not exist try to clean it up
if errors.Is(errGetByID, user.ErrUserNotFound) {
if err := s.authInfoService.DeleteUserAuthInfo(ctx, authInfo.UserId); err != nil {
s.log.FromContext(ctx).Error("Failed to clean up user auth", "error", err, "auth_module", identity.AuthModule, "auth_id", identity.AuthID)
s.log.FromContext(ctx).Error("Failed to clean up user auth", "error", err, "auth_module", identity.AuthenticatedBy, "auth_id", identity.AuthID)
}
}
}
@@ -322,8 +322,8 @@ func (s *UserSync) getUser(ctx context.Context, identity *authn.Identity) (*user
var userAuth *login.UserAuth
// Special case for generic oauth: generic oauth does not store authID,
// so we need to find the user first then check for the userAuth connection by module and userID
if identity.AuthModule == login.GenericOAuthModule {
query := &login.GetAuthInfoQuery{AuthModule: identity.AuthModule, UserId: usr.ID}
if identity.AuthenticatedBy == login.GenericOAuthModule {
query := &login.GetAuthInfoQuery{AuthModule: identity.AuthenticatedBy, UserId: usr.ID}
userAuth, err = s.authInfoService.GetAuthInfo(ctx, query)
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
return nil, nil, err
@@ -268,12 +268,12 @@ func TestUserSync_SyncUserHook(t *testing.T) {
args: args{
ctx: context.Background(),
id: &authn.Identity{
ID: "",
AuthID: "2032",
AuthModule: "oauth",
Login: "test",
Name: "test",
Email: "test",
ID: "",
AuthID: "2032",
AuthenticatedBy: "oauth",
Login: "test",
Name: "test",
Email: "test",
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: login.UserLookupParams{
@@ -286,13 +286,13 @@ func TestUserSync_SyncUserHook(t *testing.T) {
},
wantErr: false,
wantID: &authn.Identity{
ID: "user:1",
AuthID: "2032",
AuthModule: "oauth",
Login: "test",
Name: "test",
Email: "test",
IsGrafanaAdmin: ptrBool(false),
ID: "user:1",
AuthID: "2032",
AuthenticatedBy: "oauth",
Login: "test",
Name: "test",
Email: "test",
IsGrafanaAdmin: ptrBool(false),
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: login.UserLookupParams{
@@ -313,12 +313,12 @@ func TestUserSync_SyncUserHook(t *testing.T) {
args: args{
ctx: context.Background(),
id: &authn.Identity{
ID: "",
Login: "test",
Name: "test",
Email: "test",
AuthModule: "oauth",
AuthID: "2032",
ID: "",
Login: "test",
Name: "test",
Email: "test",
AuthenticatedBy: "oauth",
AuthID: "2032",
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: login.UserLookupParams{
@@ -341,13 +341,13 @@ func TestUserSync_SyncUserHook(t *testing.T) {
args: args{
ctx: context.Background(),
id: &authn.Identity{
ID: "",
Login: "test_create",
Name: "test_create",
IsGrafanaAdmin: ptrBool(true),
Email: "test_create",
AuthModule: "oauth",
AuthID: "2032",
ID: "",
Login: "test_create",
Name: "test_create",
IsGrafanaAdmin: ptrBool(true),
Email: "test_create",
AuthenticatedBy: "oauth",
AuthID: "2032",
ClientParams: authn.ClientParams{
SyncUser: true,
AllowSignUp: true,
@@ -362,13 +362,13 @@ func TestUserSync_SyncUserHook(t *testing.T) {
},
wantErr: false,
wantID: &authn.Identity{
ID: "user:2",
Login: "test_create",
Name: "test_create",
Email: "test_create",
AuthModule: "oauth",
AuthID: "2032",
IsGrafanaAdmin: ptrBool(true),
ID: "user:2",
Login: "test_create",
Name: "test_create",
Email: "test_create",
AuthenticatedBy: "oauth",
AuthID: "2032",
IsGrafanaAdmin: ptrBool(true),
ClientParams: authn.ClientParams{
SyncUser: true,
AllowSignUp: true,
+7 -5
View File
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
@@ -64,10 +65,11 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide
// if the api key don't belong to a service account construct the identity and return it
if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 {
return &authn.Identity{
ID: authn.NamespacedID(authn.NamespaceAPIKey, apiKey.ID),
OrgID: apiKey.OrgID,
OrgRoles: map[int64]org.RoleType{apiKey.OrgID: apiKey.Role},
ClientParams: authn.ClientParams{SyncPermissions: true},
ID: authn.NamespacedID(authn.NamespaceAPIKey, apiKey.ID),
OrgID: apiKey.OrgID,
OrgRoles: map[int64]org.RoleType{apiKey.OrgID: apiKey.Role},
ClientParams: authn.ClientParams{SyncPermissions: true},
AuthenticatedBy: login.APIKeyAuthModule,
}, nil
}
@@ -80,7 +82,7 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide
return nil, err
}
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceServiceAccount, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}), nil
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceServiceAccount, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}, login.APIKeyAuthModule), nil
}
func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) {
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/apikey/apikeytest"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
@@ -55,6 +56,7 @@ func TestAPIKey_Authenticate(t *testing.T) {
ClientParams: authn.ClientParams{
SyncPermissions: true,
},
AuthenticatedBy: login.APIKeyAuthModule,
},
},
{
@@ -88,6 +90,7 @@ func TestAPIKey_Authenticate(t *testing.T) {
ClientParams: authn.ClientParams{
SyncPermissions: true,
},
AuthenticatedBy: login.APIKeyAuthModule,
},
},
{
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/oauthserver"
"github.com/grafana/grafana/pkg/services/signingkeys"
"github.com/grafana/grafana/pkg/services/user"
@@ -100,7 +101,7 @@ func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*auth
signedInUser.Permissions[s.getDefaultOrgID()] = claims.Entitlements
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: false}), nil
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: false}, login.ExtendedJWTModule), nil
}
func (s *ExtendedJWT) Test(ctx context.Context, r *authn.Request) bool {
+13 -13
View File
@@ -150,19 +150,19 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
}
},
want: &authn.Identity{
OrgID: 1,
OrgCount: 0,
OrgName: "",
OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin},
ID: "user:2",
Login: "johndoe",
Name: "John Doe",
Email: "johndoe@grafana.com",
IsGrafanaAdmin: boolPtr(false),
AuthModule: "",
AuthID: "",
IsDisabled: false,
HelpFlags1: 0,
OrgID: 1,
OrgCount: 0,
OrgName: "",
OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin},
ID: "user:2",
Login: "johndoe",
Name: "John Doe",
Email: "johndoe@grafana.com",
IsGrafanaAdmin: boolPtr(false),
AuthenticatedBy: login.ExtendedJWTModule,
AuthID: "",
IsDisabled: false,
HelpFlags1: 0,
Permissions: map[int64]map[string][]string{
1: {
"dashboards:create": {
+3 -3
View File
@@ -32,8 +32,8 @@ func (c *Grafana) String() string {
func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) {
identity := &authn.Identity{
AuthModule: login.AuthProxyAuthModule,
AuthID: username,
AuthenticatedBy: login.AuthProxyAuthModule,
AuthID: username,
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeams: true,
@@ -109,7 +109,7 @@ func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, us
return nil, err
}
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: true}), nil
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: true}, login.PasswordAuthModule), nil
}
func comparePassword(password, salt, hash string) bool {
+19 -17
View File
@@ -40,13 +40,13 @@ func TestGrafana_AuthenticateProxy(t *testing.T) {
proxyFieldEmail: "email@email.com",
},
expectedIdentity: &authn.Identity{
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
Login: "test",
Name: "name",
Email: "email@email.com",
AuthModule: "authproxy",
AuthID: "test",
Groups: []string{"grp1", "grp2"},
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
Login: "test",
Name: "name",
Email: "email@email.com",
AuthenticatedBy: login.AuthProxyAuthModule,
AuthID: "test",
Groups: []string{"grp1", "grp2"},
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeams: true,
@@ -66,10 +66,10 @@ func TestGrafana_AuthenticateProxy(t *testing.T) {
req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{}}},
additional: map[string]string{},
expectedIdentity: &authn.Identity{
Login: "test@test.com",
Email: "test@test.com",
AuthModule: "authproxy",
AuthID: "test@test.com",
Login: "test@test.com",
Email: "test@test.com",
AuthenticatedBy: login.AuthProxyAuthModule,
AuthID: "test@test.com",
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeams: true,
@@ -106,7 +106,7 @@ func TestGrafana_AuthenticateProxy(t *testing.T) {
assert.Equal(t, tt.expectedIdentity.Name, identity.Name)
assert.Equal(t, tt.expectedIdentity.Email, identity.Email)
assert.Equal(t, tt.expectedIdentity.AuthID, identity.AuthID)
assert.Equal(t, tt.expectedIdentity.AuthModule, identity.AuthModule)
assert.Equal(t, tt.expectedIdentity.AuthenticatedBy, identity.AuthenticatedBy)
assert.Equal(t, tt.expectedIdentity.Groups, identity.Groups)
assert.Equal(t, tt.expectedIdentity.ClientParams.SyncUser, identity.ClientParams.SyncUser)
@@ -143,11 +143,13 @@ func TestGrafana_AuthenticatePassword(t *testing.T) {
findUser: true,
expectedSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: "Viewer"},
expectedIdentity: &authn.Identity{
ID: "user:1",
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: "Viewer"},
IsGrafanaAdmin: boolPtr(false),
ClientParams: authn.ClientParams{SyncPermissions: true}},
ID: "user:1",
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: "Viewer"},
IsGrafanaAdmin: boolPtr(false),
ClientParams: authn.ClientParams{SyncPermissions: true},
AuthenticatedBy: login.PasswordAuthModule,
},
},
{
desc: "should fail for incorrect password",
+3 -3
View File
@@ -66,9 +66,9 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi
}
id := &authn.Identity{
AuthModule: login.JWTModule,
AuthID: sub,
OrgRoles: map[int64]org.RoleType{},
AuthenticatedBy: login.JWTModule,
AuthID: sub,
OrgRoles: map[int64]org.RoleType{},
ClientParams: authn.ClientParams{
SyncUser: true,
FetchSyncedUser: true,
+13 -13
View File
@@ -35,19 +35,19 @@ func TestAuthenticateJWT(t *testing.T) {
}
jwtHeaderName := "X-Forwarded-User"
wantID := &authn.Identity{
OrgID: 0,
OrgCount: 0,
OrgName: "",
OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin},
ID: "",
Login: "eai-doe",
Name: "Eai Doe",
Email: "eai.doe@cor.po",
IsGrafanaAdmin: boolPtr(false),
AuthModule: "jwt",
AuthID: "1234567890",
IsDisabled: false,
HelpFlags1: 0,
OrgID: 0,
OrgCount: 0,
OrgName: "",
OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin},
ID: "",
Login: "eai-doe",
Name: "Eai Doe",
Email: "eai.doe@cor.po",
IsGrafanaAdmin: boolPtr(false),
AuthenticatedBy: login.JWTModule,
AuthID: "1234567890",
IsDisabled: false,
HelpFlags1: 0,
ClientParams: authn.ClientParams{
SyncUser: true,
AllowSignUp: true,
+9 -9
View File
@@ -71,15 +71,15 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern
func (c *LDAP) identityFromLDAPInfo(orgID int64, info *login.ExternalUserInfo) *authn.Identity {
return &authn.Identity{
OrgID: orgID,
OrgRoles: info.OrgRoles,
Login: info.Login,
Name: info.Name,
Email: info.Email,
IsGrafanaAdmin: info.IsGrafanaAdmin,
AuthModule: info.AuthModule,
AuthID: info.AuthId,
Groups: info.Groups,
OrgID: orgID,
OrgRoles: info.OrgRoles,
Login: info.Login,
Name: info.Name,
Email: info.Email,
IsGrafanaAdmin: info.IsGrafanaAdmin,
AuthenticatedBy: info.AuthModule,
AuthID: info.AuthId,
Groups: info.Groups,
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeams: true,
+16 -16
View File
@@ -39,14 +39,14 @@ func TestLDAP_AuthenticateProxy(t *testing.T) {
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
},
expectedIdentity: &authn.Identity{
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
Login: "test",
Name: "test test",
Email: "test@test.com",
AuthModule: login.LDAPAuthModule,
AuthID: "123",
Groups: []string{"1", "2"},
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
Login: "test",
Name: "test test",
Email: "test@test.com",
AuthenticatedBy: login.LDAPAuthModule,
AuthID: "123",
Groups: []string{"1", "2"},
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeams: true,
@@ -105,14 +105,14 @@ func TestLDAP_AuthenticatePassword(t *testing.T) {
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
},
expectedIdentity: &authn.Identity{
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
Login: "test",
Name: "test test",
Email: "test@test.com",
AuthModule: login.LDAPAuthModule,
AuthID: "123",
Groups: []string{"1", "2"},
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
Login: "test",
Name: "test test",
Email: "test@test.com",
AuthenticatedBy: login.LDAPAuthModule,
AuthID: "123",
Groups: []string{"1", "2"},
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeams: true,
+9 -9
View File
@@ -146,15 +146,15 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
}
return &authn.Identity{
Login: userInfo.Login,
Name: userInfo.Name,
Email: userInfo.Email,
IsGrafanaAdmin: isGrafanaAdmin,
AuthModule: c.moduleName,
AuthID: userInfo.Id,
Groups: userInfo.Groups,
OAuthToken: token,
OrgRoles: orgRoles,
Login: userInfo.Login,
Name: userInfo.Name,
Email: userInfo.Email,
IsGrafanaAdmin: isGrafanaAdmin,
AuthenticatedBy: c.moduleName,
AuthID: userInfo.Id,
Groups: userInfo.Groups,
OAuthToken: token,
OrgRoles: orgRoles,
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeams: true,
+15 -15
View File
@@ -129,13 +129,13 @@ func TestOAuth_Authenticate(t *testing.T) {
Groups: []string{"grp1", "grp2"},
},
expectedIdentity: &authn.Identity{
Email: "some@email.com",
AuthModule: "oauth_azuread",
AuthID: "123",
Name: "name",
Groups: []string{"grp1", "grp2"},
OAuthToken: &oauth2.Token{},
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
Email: "some@email.com",
AuthenticatedBy: login.AzureADAuthModule,
AuthID: "123",
Name: "name",
Groups: []string{"grp1", "grp2"},
OAuthToken: &oauth2.Token{},
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeams: true,
@@ -168,13 +168,13 @@ func TestOAuth_Authenticate(t *testing.T) {
Groups: []string{"grp1", "grp2"},
},
expectedIdentity: &authn.Identity{
Email: "some@email.com",
AuthModule: "oauth_azuread",
AuthID: "123",
Name: "name",
Groups: []string{"grp1", "grp2"},
OAuthToken: &oauth2.Token{},
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
Email: "some@email.com",
AuthenticatedBy: login.AzureADAuthModule,
AuthID: "123",
Name: "name",
Groups: []string{"grp1", "grp2"},
OAuthToken: &oauth2.Token{},
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeams: true,
@@ -221,7 +221,7 @@ func TestOAuth_Authenticate(t *testing.T) {
assert.Equal(t, tt.expectedIdentity.Name, identity.Name)
assert.Equal(t, tt.expectedIdentity.Email, identity.Email)
assert.Equal(t, tt.expectedIdentity.AuthID, identity.AuthID)
assert.Equal(t, tt.expectedIdentity.AuthModule, identity.AuthModule)
assert.Equal(t, tt.expectedIdentity.AuthenticatedBy, identity.AuthenticatedBy)
assert.Equal(t, tt.expectedIdentity.Groups, identity.Groups)
assert.Equal(t, tt.expectedIdentity.ClientParams.SyncUser, identity.ClientParams.SyncUser)
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@@ -99,7 +100,7 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
// and perform syncs
if usr != nil {
c.log.FromContext(ctx).Debug("User was loaded from cache, skip syncs", "userId", usr.UserID)
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}), nil
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}, login.AuthProxyAuthModule), nil
}
}
}
+2 -2
View File
@@ -56,11 +56,11 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide
return nil, err
}
identity = authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true})
identity = authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}, login.RenderModule)
}
identity.LastSeenAt = time.Now()
identity.AuthModule = login.RenderModule
identity.AuthenticatedBy = login.RenderModule
return identity, nil
}
+12 -12
View File
@@ -38,11 +38,11 @@ func TestRender_Authenticate(t *testing.T) {
},
},
expectedIdentity: &authn.Identity{
ID: "user:0",
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
AuthModule: login.RenderModule,
ClientParams: authn.ClientParams{SyncPermissions: true},
ID: "user:0",
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
AuthenticatedBy: login.RenderModule,
ClientParams: authn.ClientParams{SyncPermissions: true},
},
expectedRenderUsr: &rendering.RenderUser{
OrgID: 1,
@@ -59,13 +59,13 @@ func TestRender_Authenticate(t *testing.T) {
},
},
expectedIdentity: &authn.Identity{
ID: "user:1",
OrgID: 1,
OrgName: "test",
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
IsGrafanaAdmin: boolPtr(false),
AuthModule: login.RenderModule,
ClientParams: authn.ClientParams{SyncPermissions: true},
ID: "user:1",
OrgID: 1,
OrgName: "test",
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
IsGrafanaAdmin: boolPtr(false),
AuthenticatedBy: login.RenderModule,
ClientParams: authn.ClientParams{SyncPermissions: true},
},
expectedRenderUsr: &rendering.RenderUser{
OrgID: 1,
@@ -185,7 +185,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
reqContext.UserToken = identity.SessionToken
reqContext.IsSignedIn = !identity.IsAnonymous
reqContext.AllowAnonymous = identity.IsAnonymous
reqContext.IsRenderCall = identity.AuthModule == login.RenderModule
reqContext.IsRenderCall = identity.AuthenticatedBy == login.RenderModule
}
} else {
const headerName = "X-Grafana-Org-Id"
+3
View File
@@ -36,10 +36,13 @@ type Store interface {
const (
// modules
PasswordAuthModule = "password"
APIKeyAuthModule = "apikey"
SAMLAuthModule = "auth.saml"
LDAPAuthModule = "ldap"
AuthProxyAuthModule = "authproxy"
JWTModule = "jwt"
ExtendedJWTModule = "extendedjwt"
RenderModule = "render"
// OAuth provider modules
AzureADAuthModule = "oauth_azuread"
+122 -34
View File
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
@@ -172,36 +173,68 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
permSelector := strings.Builder{}
var permSelectorArgs []interface{}
// useSelfContainedPermissions is true if the user's permissions are stored and set from the JWT token
// currently it's used for the extended JWT module (when the user is authenticated via a JWT token generated by Grafana)
useSelfContainedPermissions := f.user.AuthenticatedBy == login.ExtendedJWTModule
if len(f.dashboardActions) > 0 {
toCheck := actionsToCheck(f.dashboardActions, f.user.Permissions[f.user.OrgID], dashWildcards, folderWildcards)
if len(toCheck) > 0 {
builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 16) FROM permission WHERE scope LIKE 'dashboards:uid:%'")
builder.WriteString(rolesFilter)
args = append(args, params...)
if !useSelfContainedPermissions {
builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 16) FROM permission WHERE scope LIKE 'dashboards:uid:%'")
builder.WriteString(rolesFilter)
args = append(args, params...)
if len(toCheck) == 1 {
builder.WriteString(" AND action = ?")
args = append(args, toCheck[0])
if len(toCheck) == 1 {
builder.WriteString(" AND action = ?")
args = append(args, toCheck[0])
} else {
builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
args = append(args, toCheck...)
args = append(args, len(toCheck))
}
builder.WriteString(") AND NOT dashboard.is_folder)")
} else {
builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
args = append(args, toCheck...)
args = append(args, len(toCheck))
actions := parseStringSliceFromInterfaceSlice(toCheck)
args = getAllowedUIDs(actions, f.user, dashboards.ScopeDashboardsPrefix)
// Only add the IN clause if we have any dashboards to check
if len(args) > 0 {
builder.WriteString("(dashboard.uid IN (?" + strings.Repeat(", ?", len(args)-1) + "")
builder.WriteString(") AND NOT dashboard.is_folder)")
} else {
builder.WriteString("(1 = 0)")
}
}
builder.WriteString(") AND NOT dashboard.is_folder)")
builder.WriteString(" OR ")
permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
if len(toCheck) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
if !useSelfContainedPermissions {
permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
if len(toCheck) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
permSelectorArgs = append(permSelectorArgs, toCheck...)
permSelectorArgs = append(permSelectorArgs, len(toCheck))
}
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
permSelectorArgs = append(permSelectorArgs, toCheck...)
permSelectorArgs = append(permSelectorArgs, len(toCheck))
actions := parseStringSliceFromInterfaceSlice(toCheck)
permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix)
// Only add the IN clause if we have any folders to check
if len(permSelectorArgs) > 0 {
permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "")
} else {
permSelector.WriteString("(")
}
}
permSelector.WriteRune(')')
@@ -221,9 +254,13 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
}
default:
builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
builder.WriteString("WHERE d.uid IN ")
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
if len(permSelectorArgs) > 0 {
builder.WriteString("WHERE d.uid IN ")
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
} else {
builder.WriteString("WHERE 1 = 0")
}
}
builder.WriteString(") AND NOT dashboard.is_folder)")
} else {
@@ -242,17 +279,30 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
toCheck := actionsToCheck(f.folderActions, f.user.Permissions[f.user.OrgID], folderWildcards)
if len(toCheck) > 0 {
permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
if len(toCheck) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
if !useSelfContainedPermissions {
permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
if len(toCheck) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
permSelectorArgs = append(permSelectorArgs, toCheck...)
permSelectorArgs = append(permSelectorArgs, len(toCheck))
}
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
permSelectorArgs = append(permSelectorArgs, toCheck...)
permSelectorArgs = append(permSelectorArgs, len(toCheck))
actions := parseStringSliceFromInterfaceSlice(toCheck)
permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix)
if len(permSelectorArgs) > 0 {
permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "")
} else {
permSelector.WriteString("(")
}
}
permSelector.WriteRune(')')
switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) {
@@ -271,15 +321,20 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
args = append(args, nestedFoldersArgs...)
}
default:
builder.WriteString("(dashboard.uid IN ")
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
if len(permSelectorArgs) > 0 {
builder.WriteString("(dashboard.uid IN ")
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
} else {
builder.WriteString("(1 = 0")
}
}
builder.WriteString(" AND dashboard.is_folder)")
} else {
builder.WriteString("dashboard.is_folder")
}
}
builder.WriteRune(')')
f.where = clause{string: builder.String(), params: args}
@@ -365,3 +420,36 @@ func nestedFoldersSelectors(permSelector string, permSelectorArgs []interface{},
return strings.Join(wheres, ") OR "), args
}
func parseStringSliceFromInterfaceSlice(slice []interface{}) []string {
result := make([]string, 0, len(slice))
for _, s := range slice {
result = append(result, s.(string))
}
return result
}
func getAllowedUIDs(actions []string, user *user.SignedInUser, scopePrefix string) []interface{} {
uidToActions := make(map[string]map[string]struct{})
for _, action := range actions {
for _, uidScope := range user.Permissions[user.OrgID][action] {
if !strings.HasPrefix(uidScope, scopePrefix) {
continue
}
uid := strings.TrimPrefix(uidScope, scopePrefix)
if _, exists := uidToActions[uid]; !exists {
uidToActions[uid] = make(map[string]struct{})
}
uidToActions[uid][action] = struct{}{}
}
}
// args max capacity is the length of the different uids
args := make([]interface{}, 0, len(uidToActions))
for uid, assignedActions := range uidToActions {
if len(assignedActions) == len(actions) {
args = append(args, uid)
}
}
return args
}
@@ -21,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -111,6 +112,30 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) {
},
expectedResult: 2,
},
{
desc: "Should return the dashboards that the User has dashboards:write permission on in case of 'edit' permission",
permission: dashboards.PERMISSION_EDIT,
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:31"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"},
{Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33"},
},
expectedResult: 1,
},
{
desc: "Should return the folders that the User has dashboards:create permission on in case of 'edit' permission",
permission: dashboards.PERMISSION_EDIT,
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"},
{Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3"},
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:4"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"},
},
expectedResult: 1,
},
{
desc: "Should return folders that users can read alerts from",
permission: dashboards.PERMISSION_VIEW,
@@ -160,6 +185,167 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) {
}
}
func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
type testCase struct {
desc string
queryType string
permission dashboards.PermissionType
signedInUserPermissions []accesscontrol.Permission
expectedResult int
}
tests := []testCase{
{
desc: "Should be able to view all dashboards with wildcard scope",
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll},
},
expectedResult: 100,
},
{
desc: "Should be able to view all dashboards with folder wildcard scope",
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll},
},
expectedResult: 100,
},
{
desc: "Should not be able to view any dashboards or folders without any permissions",
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{},
expectedResult: 0,
},
{
desc: "Should be able to view a subset of dashboards with dashboard scopes",
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:110"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:40"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:22"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:13"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:55"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:99"},
},
expectedResult: 6,
},
{
desc: "Should be able to view a subset of dashboards with dashboard action and folder scope",
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:8"},
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:10"},
},
expectedResult: 20,
},
{
desc: "Should be able to view all folders with folder wildcard",
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:*"},
},
expectedResult: 10,
},
{
desc: "Should be able to view a subset folders",
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"},
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:6"},
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:9"},
},
expectedResult: 3,
},
{
desc: "Should return folders and dashboard with 'edit' permission",
permission: dashboards.PERMISSION_EDIT,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"},
{Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"},
{Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33"},
},
expectedResult: 2,
},
{
desc: "Should return the dashboards that the User has dashboards:write permission on in case of 'edit' permission",
permission: dashboards.PERMISSION_EDIT,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:31"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"},
{Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33"},
},
expectedResult: 1,
},
{
desc: "Should return the folders that the User has dashboards:create permission on in case of 'edit' permission",
permission: dashboards.PERMISSION_EDIT,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"},
{Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3"},
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:4"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"},
},
expectedResult: 1,
},
{
desc: "Should return folders that users can read alerts from",
permission: dashboards.PERMISSION_VIEW,
queryType: searchstore.TypeAlertFolder,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"},
{Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:3"},
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:8"},
{Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:8"},
},
expectedResult: 2,
},
{
desc: "Should return folders that users can read alerts when user has read wildcard",
permission: dashboards.PERMISSION_VIEW,
queryType: searchstore.TypeAlertFolder,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "*"},
{Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:3"},
{Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:8"},
},
expectedResult: 2,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
store := setupTest(t, 10, 100, []accesscontrol.Permission{})
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
require.NoError(t, err)
usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.signedInUserPermissions)}}
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType, featuremgmt.WithFeatures(), recursiveQueriesAreSupported)
var result int
err = store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
q, params := filter.Where()
recQry, recQryParams := filter.With()
params = append(recQryParams, params...)
_, err := sess.SQL(recQry+"\nSELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result)
return err
})
require.NoError(t, err)
assert.Equal(t, tt.expectedResult, result)
})
}
}
func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
testCases := []struct {
desc string
@@ -266,6 +452,132 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
}
}
func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermissions(t *testing.T) {
testCases := []struct {
desc string
queryType string
permission dashboards.PermissionType
signedInUserPermissions []accesscontrol.Permission
expectedResult []string
features featuremgmt.FeatureToggles
}{
{
desc: "Should be able to view dashboards under inherited folders if nested folders are enabled",
queryType: searchstore.TypeDashboard,
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
},
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedResult: []string{"dashboard under parent folder", "dashboard under subfolder"},
},
{
desc: "Should not be able to view dashboards under inherited folders if nested folders are not enabled",
queryType: searchstore.TypeDashboard,
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
},
features: featuremgmt.WithFeatures(),
expectedResult: []string{"dashboard under parent folder"},
},
{
desc: "Should be able to view inherited folders if nested folders are enabled",
queryType: searchstore.TypeFolder,
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
},
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedResult: []string{"parent", "subfolder"},
},
{
desc: "Should not be able to view inherited folders if nested folders are not enabled",
queryType: searchstore.TypeFolder,
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
},
features: featuremgmt.WithFeatures(),
expectedResult: []string{"parent"},
},
{
desc: "Should be able to view inherited dashboards and folders if nested folders are enabled",
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
},
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedResult: []string{"parent", "subfolder", "dashboard under parent folder", "dashboard under subfolder"},
},
{
desc: "Should not be able to view inherited dashboards and folders if nested folders are not enabled",
permission: dashboards.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
},
features: featuremgmt.WithFeatures(),
expectedResult: []string{"parent", "dashboard under parent folder"},
},
{
desc: "Should be able to edit inherited dashboards and folders if nested folders are enabled",
permission: dashboards.PERMISSION_EDIT,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:subfolder"},
{Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:subfolder"},
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:subfolder"},
{Action: dashboards.ActionDashboardsWrite, Scope: "folders:uid:subfolder"},
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
{Action: dashboards.ActionDashboardsWrite, Scope: "folders:uid:parent"},
},
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedResult: []string{"subfolder", "dashboard under parent folder", "dashboard under subfolder"},
},
}
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
t.Cleanup(func() {
guardian.New = origNewGuardian
})
var orgID int64 = 1
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
helperUser := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule,
Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
{
Action: dashboards.ActionFoldersCreate,
},
{
Action: dashboards.ActionFoldersWrite,
Scope: dashboards.ScopeFoldersAll,
},
}),
},
}
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.signedInUserPermissions)}}
db := setupNestedTest(t, helperUser, []accesscontrol.Permission{}, orgID, tc.features)
recursiveQueriesAreSupported, err := db.RecursiveQueriesAreSupported()
require.NoError(t, err)
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, tc.features, recursiveQueriesAreSupported)
var result []string
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
q, params := filter.Where()
recQry, recQryParams := filter.With()
params = append(recQryParams, params...)
err := sess.SQL(recQry+"\nSELECT title FROM dashboard WHERE "+q, params...).Find(&result)
return err
})
require.NoError(t, err)
assert.Equal(t, tc.expectedResult, result)
})
}
}
func setupTest(t *testing.T, numFolders, numDashboards int, permissions []accesscontrol.Permission) db.DB {
t.Helper()
+1
View File
@@ -206,6 +206,7 @@ type SignedInUser struct {
Login string
Name string
Email string
AuthenticatedBy string
ApiKeyID int64 `xorm:"api_key_id"`
IsServiceAccount bool `xorm:"is_service_account"`
OrgCount int