mirror of https://github.com/grafana/grafana
App platform: Implement perm check with direct db access (#97579)
* implement perm check with direct db access * add tests * more tests * Update pkg/services/authz/rbac/service.go Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * Update pkg/services/authz/rbac/service.go Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * allow fetching permissions for a user who is not a member of the org * linting * fix typo --------- Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>pull/97151/head
parent
3c78fb1aa4
commit
ded90fa28d
@ -0,0 +1,13 @@ |
||||
package rbac |
||||
|
||||
import "github.com/grafana/authlib/claims" |
||||
|
||||
type CheckRequest struct { |
||||
Namespace claims.NamespaceInfo |
||||
UserUID string |
||||
Action string |
||||
Group string |
||||
Resource string |
||||
Verb string |
||||
Name string |
||||
} |
@ -0,0 +1,151 @@ |
||||
package rbac |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
"github.com/grafana/grafana/pkg/services/authz/mappers" |
||||
) |
||||
|
||||
func TestService_checkPermission(t *testing.T) { |
||||
type testCase struct { |
||||
name string |
||||
permissions []accesscontrol.Permission |
||||
check CheckRequest |
||||
expected bool |
||||
} |
||||
|
||||
testCases := []testCase{ |
||||
{ |
||||
name: "should return true if user has permission", |
||||
permissions: []accesscontrol.Permission{ |
||||
{ |
||||
Action: "dashboards:read", |
||||
Scope: "dashboards:uid:some_dashboard", |
||||
Kind: "dashboards", |
||||
Attribute: "uid", |
||||
Identifier: "some_dashboard", |
||||
}, |
||||
}, |
||||
check: CheckRequest{ |
||||
Action: "dashboards:read", |
||||
Group: "dashboard.grafana.app", |
||||
Resource: "dashboards", |
||||
Name: "some_dashboard", |
||||
}, |
||||
expected: true, |
||||
}, |
||||
{ |
||||
name: "should return false if user has permission on a different resource", |
||||
permissions: []accesscontrol.Permission{ |
||||
{ |
||||
Action: "dashboards:read", |
||||
Scope: "dashboards:uid:another_dashboard", |
||||
Kind: "dashboards", |
||||
Attribute: "uid", |
||||
Identifier: "another_dashboard", |
||||
}, |
||||
}, |
||||
check: CheckRequest{ |
||||
Action: "dashboards:read", |
||||
Group: "dashboard.grafana.app", |
||||
Resource: "dashboards", |
||||
Name: "some_dashboard", |
||||
}, |
||||
expected: false, |
||||
}, |
||||
{ |
||||
name: "should return true if user has wildcard permission on identifier", |
||||
permissions: []accesscontrol.Permission{ |
||||
{ |
||||
Action: "dashboards:read", |
||||
Scope: "dashboards:uid:*", |
||||
Kind: "dashboards", |
||||
Attribute: "uid", |
||||
Identifier: "*", |
||||
}, |
||||
}, |
||||
check: CheckRequest{ |
||||
Action: "dashboards:read", |
||||
Group: "dashboard.grafana.app", |
||||
Resource: "dashboards", |
||||
Name: "some_dashboard", |
||||
}, |
||||
expected: true, |
||||
}, |
||||
{ |
||||
name: "should return true if user has wildcard permission on attribute", |
||||
permissions: []accesscontrol.Permission{ |
||||
{ |
||||
Action: "dashboards:read", |
||||
Scope: "dashboards:*", |
||||
Kind: "dashboards", |
||||
Attribute: "*", |
||||
}, |
||||
}, |
||||
check: CheckRequest{ |
||||
Action: "dashboards:read", |
||||
Group: "dashboard.grafana.app", |
||||
Resource: "dashboards", |
||||
Name: "some_dashboard", |
||||
}, |
||||
expected: true, |
||||
}, |
||||
{ |
||||
name: "should return true if user has wildcard permission on kind", |
||||
permissions: []accesscontrol.Permission{ |
||||
{ |
||||
Action: "dashboards:read", |
||||
Scope: "*", |
||||
Kind: "*", |
||||
}, |
||||
}, |
||||
check: CheckRequest{ |
||||
Action: "dashboards:read", |
||||
Group: "dashboard.grafana.app", |
||||
Resource: "dashboards", |
||||
Name: "some_dashboard", |
||||
}, |
||||
expected: true, |
||||
}, |
||||
{ |
||||
name: "should return true if no resource is specified", |
||||
permissions: []accesscontrol.Permission{ |
||||
{ |
||||
Action: "folders:create", |
||||
}, |
||||
}, |
||||
check: CheckRequest{ |
||||
Action: "folders:create", |
||||
Group: "folder.grafana.app", |
||||
Resource: "folders", |
||||
}, |
||||
expected: true, |
||||
}, |
||||
{ |
||||
name: "should return false if user has no permissions on resource", |
||||
permissions: []accesscontrol.Permission{}, |
||||
check: CheckRequest{ |
||||
Action: "dashboards:read", |
||||
Group: "dashboard.grafana.app", |
||||
Resource: "dashboards", |
||||
Name: "some_dashboard", |
||||
}, |
||||
expected: false, |
||||
}, |
||||
} |
||||
|
||||
for _, tc := range testCases { |
||||
t.Run(tc.name, func(t *testing.T) { |
||||
s := &Service{logger: log.New("test"), actionMapper: mappers.NewK8sRbacMapper()} |
||||
got, err := s.checkPermission(context.Background(), tc.permissions, &tc.check) |
||||
require.NoError(t, err) |
||||
assert.Equal(t, tc.expected, got) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
SELECT COALESCE(ou.role, 'None') AS role, u.is_admin |
||||
FROM {{ .Ident .UserTable }} as u |
||||
LEFT JOIN {{ .Ident .OrgUserTable }} as ou ON ou.user_id = u.id AND ou.org_id = {{ .Arg .Query.OrgID }} |
||||
WHERE u.id = {{ .Arg .Query.UserID }} |
@ -0,0 +1,30 @@ |
||||
package store |
||||
|
||||
type UserIdentifiers struct { |
||||
ID int64 |
||||
UID string |
||||
} |
||||
|
||||
type BasicRole struct { |
||||
Role string |
||||
IsAdmin bool |
||||
} |
||||
|
||||
type PermissionsQuery struct { |
||||
OrgID int64 |
||||
UserID int64 |
||||
Action string |
||||
TeamIDs []int64 |
||||
Role string |
||||
IsServerAdmin bool |
||||
} |
||||
|
||||
type BasicRoleQuery struct { |
||||
UserID int64 |
||||
OrgID int64 |
||||
} |
||||
|
||||
type UserIdentifierQuery struct { |
||||
UserID int64 |
||||
UserUID string |
||||
} |
@ -0,0 +1,13 @@ |
||||
SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM {{ .Ident .PermissionTable }} as p |
||||
WHERE p.action = {{ .Arg .Query.Action }} AND p.role_id IN ( |
||||
SELECT role_id FROM {{ .Ident .UserRoleTable }} as ur WHERE ur.user_id = {{ .Arg .Query.UserID }} AND (ur.org_id = {{ .Arg .Query.OrgID }} OR ur.org_id = 0) |
||||
{{ if .Query.TeamIDs }} |
||||
UNION |
||||
SELECT role_id FROM {{ .Ident .TeamRoleTable }} as tr WHERE tr.team_id IN ({{ .ArgList .Query.TeamIDs }}) AND tr.org_id = {{ .Arg .Query.OrgID }} |
||||
{{ end }} |
||||
UNION ALL |
||||
SELECT role_id FROM {{ .Ident .BuiltinRoleTable }} as br WHERE (br.role = {{ .Arg .Query.Role }} AND (br.org_id = {{ .Arg .Query.OrgID }} OR br.org_id = 0)) |
||||
{{ if .Query.IsServerAdmin }} |
||||
OR (br.role = 'Grafana Admin') |
||||
{{ end }} |
||||
) |
@ -0,0 +1,92 @@ |
||||
package store |
||||
|
||||
import ( |
||||
"embed" |
||||
"fmt" |
||||
"text/template" |
||||
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql" |
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" |
||||
) |
||||
|
||||
var ( |
||||
//go:embed *.sql
|
||||
sqlTemplatesFS embed.FS |
||||
sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `*.sql`)) |
||||
|
||||
sqlUserPerms = mustTemplate("permission_query.sql") |
||||
sqlQueryBasicRoles = mustTemplate("basic_role_query.sql") |
||||
sqlUserIdentifiers = mustTemplate("user_identifier_query.sql") |
||||
) |
||||
|
||||
func mustTemplate(filename string) *template.Template { |
||||
if t := sqlTemplates.Lookup(filename); t != nil { |
||||
return t |
||||
} |
||||
panic(fmt.Sprintf("template file not found: %s", filename)) |
||||
} |
||||
|
||||
type getUserIdentifiers struct { |
||||
sqltemplate.SQLTemplate |
||||
Query *UserIdentifierQuery |
||||
|
||||
UserTable string |
||||
} |
||||
|
||||
func (r getUserIdentifiers) Validate() error { |
||||
return nil |
||||
} |
||||
|
||||
func newGetUserIdentifiers(sql *legacysql.LegacyDatabaseHelper, q *UserIdentifierQuery) getUserIdentifiers { |
||||
return getUserIdentifiers{ |
||||
SQLTemplate: sqltemplate.New(sql.DialectForDriver()), |
||||
Query: q, |
||||
UserTable: sql.Table("user"), |
||||
} |
||||
} |
||||
|
||||
type getBasicRolesQuery struct { |
||||
sqltemplate.SQLTemplate |
||||
Query *BasicRoleQuery |
||||
|
||||
UserTable string |
||||
OrgUserTable string |
||||
} |
||||
|
||||
func (r getBasicRolesQuery) Validate() error { |
||||
return nil |
||||
} |
||||
|
||||
func newGetBasicRoles(sql *legacysql.LegacyDatabaseHelper, q *BasicRoleQuery) getBasicRolesQuery { |
||||
return getBasicRolesQuery{ |
||||
SQLTemplate: sqltemplate.New(sql.DialectForDriver()), |
||||
Query: q, |
||||
UserTable: sql.Table("user"), |
||||
OrgUserTable: sql.Table("org_user"), |
||||
} |
||||
} |
||||
|
||||
type getPermissionsQuery struct { |
||||
sqltemplate.SQLTemplate |
||||
Query *PermissionsQuery |
||||
|
||||
PermissionTable string |
||||
UserRoleTable string |
||||
TeamRoleTable string |
||||
BuiltinRoleTable string |
||||
} |
||||
|
||||
func (r getPermissionsQuery) Validate() error { |
||||
return nil |
||||
} |
||||
|
||||
func newGetPermissions(sql *legacysql.LegacyDatabaseHelper, q *PermissionsQuery) getPermissionsQuery { |
||||
return getPermissionsQuery{ |
||||
SQLTemplate: sqltemplate.New(sql.DialectForDriver()), |
||||
Query: q, |
||||
PermissionTable: sql.Table("permission"), |
||||
UserRoleTable: sql.Table("user_role"), |
||||
TeamRoleTable: sql.Table("team_role"), |
||||
BuiltinRoleTable: sql.Table("builtin_role"), |
||||
} |
||||
} |
@ -0,0 +1,97 @@ |
||||
package store |
||||
|
||||
import ( |
||||
"testing" |
||||
"text/template" |
||||
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql" |
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" |
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks" |
||||
) |
||||
|
||||
func TestIdentityQueries(t *testing.T) { |
||||
// prefix tables with grafana
|
||||
nodb := &legacysql.LegacyDatabaseHelper{ |
||||
Table: func(n string) string { |
||||
return "grafana." + n |
||||
}, |
||||
} |
||||
|
||||
getIdentifiers := func(q *UserIdentifierQuery) sqltemplate.SQLTemplate { |
||||
v := newGetUserIdentifiers(nodb, q) |
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate() |
||||
return &v |
||||
} |
||||
|
||||
getBasicRoles := func(q *BasicRoleQuery) sqltemplate.SQLTemplate { |
||||
v := newGetBasicRoles(nodb, q) |
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate() |
||||
return &v |
||||
} |
||||
|
||||
getPermissions := func(q *PermissionsQuery) sqltemplate.SQLTemplate { |
||||
v := newGetPermissions(nodb, q) |
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate() |
||||
return &v |
||||
} |
||||
|
||||
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ |
||||
RootDir: "testdata", |
||||
Templates: map[*template.Template][]mocks.TemplateTestCase{ |
||||
sqlUserIdentifiers: { |
||||
{ |
||||
Name: "id_specified", |
||||
Data: getIdentifiers(&UserIdentifierQuery{ |
||||
UserID: 1, |
||||
}), |
||||
}, |
||||
{ |
||||
Name: "uid_specified", |
||||
Data: getIdentifiers(&UserIdentifierQuery{ |
||||
UserUID: "some_uid", |
||||
}), |
||||
}, |
||||
}, |
||||
sqlQueryBasicRoles: { |
||||
{ |
||||
Name: "basic_roles", |
||||
Data: getBasicRoles(&BasicRoleQuery{ |
||||
UserID: 1, |
||||
OrgID: 1, |
||||
}), |
||||
}, |
||||
}, |
||||
sqlUserPerms: { |
||||
{ |
||||
Name: "viewer_user", |
||||
Data: getPermissions(&PermissionsQuery{ |
||||
UserID: 1, |
||||
OrgID: 1, |
||||
Action: "folders:read", |
||||
Role: "Viewer", |
||||
}), |
||||
}, |
||||
{ |
||||
Name: "admin_user", |
||||
Data: getPermissions(&PermissionsQuery{ |
||||
UserID: 1, |
||||
OrgID: 1, |
||||
Action: "folders:read", |
||||
Role: "Admin", |
||||
IsServerAdmin: true, |
||||
}), |
||||
}, |
||||
{ |
||||
Name: "user_with_teams", |
||||
Data: getPermissions(&PermissionsQuery{ |
||||
UserID: 1, |
||||
OrgID: 1, |
||||
Action: "folders:read", |
||||
Role: "None", |
||||
TeamIDs: []int64{1, 2}, |
||||
}), |
||||
}, |
||||
}, |
||||
}, |
||||
}) |
||||
} |
@ -0,0 +1,4 @@ |
||||
SELECT COALESCE(ou.role, 'None') AS role, u.is_admin |
||||
FROM `grafana`.`user` as u |
||||
LEFT JOIN `grafana`.`org_user` as ou ON ou.user_id = u.id AND ou.org_id = 1 |
||||
WHERE u.id = 1 |
@ -0,0 +1,7 @@ |
||||
SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p |
||||
WHERE p.action = 'folders:read' AND p.role_id IN ( |
||||
SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) |
||||
UNION ALL |
||||
SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) |
||||
OR (br.role = 'Grafana Admin') |
||||
) |
@ -0,0 +1,8 @@ |
||||
SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p |
||||
WHERE p.action = 'folders:read' AND p.role_id IN ( |
||||
SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) |
||||
UNION |
||||
SELECT role_id FROM `grafana`.`team_role` as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 |
||||
UNION ALL |
||||
SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) |
||||
) |
@ -0,0 +1,6 @@ |
||||
SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p |
||||
WHERE p.action = 'folders:read' AND p.role_id IN ( |
||||
SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) |
||||
UNION ALL |
||||
SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) |
||||
) |
@ -0,0 +1,3 @@ |
||||
SELECT u.id, u.uid |
||||
FROM `grafana`.`user` as u |
||||
WHERE u.id = 1 |
@ -0,0 +1,3 @@ |
||||
SELECT u.id, u.uid |
||||
FROM `grafana`.`user` as u |
||||
WHERE u.uid = 'some_uid' |
@ -0,0 +1,4 @@ |
||||
SELECT COALESCE(ou.role, 'None') AS role, u.is_admin |
||||
FROM "grafana"."user" as u |
||||
LEFT JOIN "grafana"."org_user" as ou ON ou.user_id = u.id AND ou.org_id = 1 |
||||
WHERE u.id = 1 |
@ -0,0 +1,7 @@ |
||||
SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p |
||||
WHERE p.action = 'folders:read' AND p.role_id IN ( |
||||
SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) |
||||
UNION ALL |
||||
SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) |
||||
OR (br.role = 'Grafana Admin') |
||||
) |
@ -0,0 +1,8 @@ |
||||
SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p |
||||
WHERE p.action = 'folders:read' AND p.role_id IN ( |
||||
SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) |
||||
UNION |
||||
SELECT role_id FROM "grafana"."team_role" as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 |
||||
UNION ALL |
||||
SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) |
||||
) |
@ -0,0 +1,6 @@ |
||||
SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p |
||||
WHERE p.action = 'folders:read' AND p.role_id IN ( |
||||
SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) |
||||
UNION ALL |
||||
SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) |
||||
) |
@ -0,0 +1,3 @@ |
||||
SELECT u.id, u.uid |
||||
FROM "grafana"."user" as u |
||||
WHERE u.id = 1 |
@ -0,0 +1,3 @@ |
||||
SELECT u.id, u.uid |
||||
FROM "grafana"."user" as u |
||||
WHERE u.uid = 'some_uid' |
@ -0,0 +1,4 @@ |
||||
SELECT COALESCE(ou.role, 'None') AS role, u.is_admin |
||||
FROM "grafana"."user" as u |
||||
LEFT JOIN "grafana"."org_user" as ou ON ou.user_id = u.id AND ou.org_id = 1 |
||||
WHERE u.id = 1 |
@ -0,0 +1,7 @@ |
||||
SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p |
||||
WHERE p.action = 'folders:read' AND p.role_id IN ( |
||||
SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) |
||||
UNION ALL |
||||
SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) |
||||
OR (br.role = 'Grafana Admin') |
||||
) |
@ -0,0 +1,8 @@ |
||||
SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p |
||||
WHERE p.action = 'folders:read' AND p.role_id IN ( |
||||
SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) |
||||
UNION |
||||
SELECT role_id FROM "grafana"."team_role" as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 |
||||
UNION ALL |
||||
SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) |
||||
) |
@ -0,0 +1,6 @@ |
||||
SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p |
||||
WHERE p.action = 'folders:read' AND p.role_id IN ( |
||||
SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) |
||||
UNION ALL |
||||
SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) |
||||
) |
@ -0,0 +1,3 @@ |
||||
SELECT u.id, u.uid |
||||
FROM "grafana"."user" as u |
||||
WHERE u.id = 1 |
@ -0,0 +1,3 @@ |
||||
SELECT u.id, u.uid |
||||
FROM "grafana"."user" as u |
||||
WHERE u.uid = 'some_uid' |
@ -0,0 +1,7 @@ |
||||
SELECT u.id, u.uid |
||||
FROM {{ .Ident .UserTable }} as u |
||||
{{ if .Query.UserUID }} |
||||
WHERE u.uid = {{ .Arg .Query.UserUID }} |
||||
{{ else }} |
||||
WHERE u.id = {{ .Arg .Query.UserID }} |
||||
{{ end }} |
Loading…
Reference in new issue