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>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -1,11 +1,23 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/authlib/claims"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/authz/mappers"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/rbac/store"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
)
|
||||
@@ -14,24 +26,180 @@ type Service struct {
|
||||
authzv1.UnimplementedAuthzServiceServer
|
||||
authzextv1.UnimplementedAuthzExtentionServiceServer
|
||||
|
||||
store *store.Store
|
||||
store *store.Store
|
||||
identityStore legacy.LegacyIdentityStore
|
||||
actionMapper *mappers.K8sRbacMapper
|
||||
|
||||
logger log.Logger
|
||||
tracer tracing.Tracer
|
||||
}
|
||||
|
||||
func NewService(sql legacysql.LegacyDatabaseProvider, logger log.Logger, tracer tracing.Tracer) *Service {
|
||||
func NewService(sql legacysql.LegacyDatabaseProvider, identityStore legacy.LegacyIdentityStore, logger log.Logger, tracer tracing.Tracer) *Service {
|
||||
return &Service{
|
||||
store: store.NewStore(sql),
|
||||
logger: logger,
|
||||
tracer: tracer,
|
||||
store: store.NewStore(sql),
|
||||
identityStore: identityStore,
|
||||
actionMapper: mappers.NewK8sRbacMapper(),
|
||||
logger: logger,
|
||||
tracer: tracer,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement Check
|
||||
// func (s *Service) Check(ctx context.Context, req *authzv1.CheckRequest) (*authzv1.CheckResponse, error) {
|
||||
// This needs to be done for the database provider
|
||||
// ns := req.GetNamespace()
|
||||
// ctx = request.WithNamespace(ctx, ns)
|
||||
func (s *Service) Check(ctx context.Context, req *authzv1.CheckRequest) (*authzv1.CheckResponse, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.Check")
|
||||
defer span.End()
|
||||
ctxLogger := s.logger.FromContext(ctx)
|
||||
|
||||
// return nil, nil
|
||||
// }
|
||||
deny := &authzv1.CheckResponse{Allowed: false}
|
||||
|
||||
checkReq, err := s.validateRequest(ctx, req)
|
||||
if err != nil {
|
||||
ctxLogger.Error("invalid request", "error", err)
|
||||
return deny, err
|
||||
}
|
||||
ctx = request.WithNamespace(ctx, req.GetNamespace())
|
||||
|
||||
permissions, err := s.getUserPermissions(ctx, checkReq)
|
||||
if err != nil {
|
||||
ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err)
|
||||
return deny, err
|
||||
}
|
||||
|
||||
allowed, err := s.checkPermission(ctx, permissions, checkReq)
|
||||
if err != nil {
|
||||
ctxLogger.Error("could not check permission", "error", err)
|
||||
return deny, err
|
||||
}
|
||||
return &authzv1.CheckResponse{Allowed: allowed}, nil
|
||||
}
|
||||
|
||||
func (s *Service) validateRequest(ctx context.Context, req *authzv1.CheckRequest) (*CheckRequest, error) {
|
||||
ctxLogger := s.logger.FromContext(ctx)
|
||||
|
||||
if req.GetNamespace() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "namespace is required")
|
||||
}
|
||||
namespace := req.GetNamespace()
|
||||
ns, err := claims.ParseNamespace(namespace)
|
||||
if err != nil {
|
||||
ctxLogger.Error("could not parse namespace", "namespace", namespace, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.GetSubject() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "subject is required")
|
||||
}
|
||||
user := req.GetSubject()
|
||||
identityType, userUID, err := claims.ParseTypeID(user)
|
||||
if err != nil {
|
||||
ctxLogger.Error("could not parse subject", "subject", user, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
// Permission check currently only checks user and service account permissions, so might return a false negative for other types
|
||||
if !(identityType == claims.TypeUser || identityType == claims.TypeServiceAccount) {
|
||||
ctxLogger.Warn("unsupported identity type", "type", identityType)
|
||||
}
|
||||
|
||||
if req.GetGroup() == "" || req.GetResource() == "" || req.GetVerb() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "group, resource and verb are required")
|
||||
}
|
||||
action, ok := s.actionMapper.Action(req.GetGroup(), req.GetResource(), req.GetVerb())
|
||||
if !ok {
|
||||
ctxLogger.Error("could not find associated rbac action", "group", req.GetGroup(), "resource", req.GetResource(), "verb", req.GetVerb())
|
||||
return nil, status.Error(codes.NotFound, "could not find associated rbac action")
|
||||
}
|
||||
|
||||
checkReq := &CheckRequest{
|
||||
Namespace: ns,
|
||||
UserUID: userUID,
|
||||
Action: action,
|
||||
Group: req.GetGroup(),
|
||||
Resource: req.GetResource(),
|
||||
Verb: req.GetVerb(),
|
||||
Name: req.GetName(),
|
||||
}
|
||||
return checkReq, nil
|
||||
}
|
||||
|
||||
func (s *Service) getUserPermissions(ctx context.Context, req *CheckRequest) ([]accesscontrol.Permission, error) {
|
||||
var userIDQuery store.UserIdentifierQuery
|
||||
// Assume that numeric UID is user ID
|
||||
if userID, err := strconv.Atoi(req.UserUID); err == nil {
|
||||
userIDQuery = store.UserIdentifierQuery{UserID: int64(userID)}
|
||||
} else {
|
||||
userIDQuery = store.UserIdentifierQuery{UserUID: req.UserUID}
|
||||
}
|
||||
userIdentifiers, err := s.store.GetUserIdentifiers(ctx, userIDQuery)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get user internal id: %w", err)
|
||||
}
|
||||
|
||||
basicRoles, err := s.store.GetBasicRoles(ctx, req.Namespace, store.BasicRoleQuery{UserID: userIdentifiers.ID})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get basic roles: %w", err)
|
||||
}
|
||||
|
||||
teamIDs := make([]int64, 0, 50)
|
||||
teamQuery := legacy.ListUserTeamsQuery{
|
||||
UserUID: userIdentifiers.UID,
|
||||
Pagination: common.Pagination{Limit: 50},
|
||||
}
|
||||
|
||||
for {
|
||||
teams, err := s.identityStore.ListUserTeams(ctx, req.Namespace, teamQuery)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get user teams: %w", err)
|
||||
}
|
||||
for _, team := range teams.Items {
|
||||
teamIDs = append(teamIDs, team.ID)
|
||||
}
|
||||
teamQuery.Pagination.Continue = teams.Continue
|
||||
if teams.Continue == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
userPermQuery := store.PermissionsQuery{
|
||||
UserID: userIdentifiers.ID,
|
||||
Action: req.Action,
|
||||
TeamIDs: teamIDs,
|
||||
Role: basicRoles.Role,
|
||||
IsServerAdmin: basicRoles.IsAdmin,
|
||||
}
|
||||
|
||||
return s.store.GetUserPermissions(ctx, req.Namespace, userPermQuery)
|
||||
}
|
||||
|
||||
func (s *Service) checkPermission(ctx context.Context, permissions []accesscontrol.Permission, req *CheckRequest) (bool, error) {
|
||||
ctxLogger := s.logger.FromContext(ctx)
|
||||
|
||||
// Only check action if the request doesn't specify scope
|
||||
if req.Name == "" {
|
||||
return len(permissions) > 0, nil
|
||||
}
|
||||
|
||||
scopeMap := getScopeMap(permissions)
|
||||
|
||||
// Wildcard grant, no further checks needed
|
||||
if scopeMap["*"] {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
scope, has := s.actionMapper.Scope(req.Group, req.Resource, req.Name)
|
||||
if !has {
|
||||
ctxLogger.Error("could not get attribute for resource", "resource", req.Resource)
|
||||
return false, fmt.Errorf("could not get attribute for resource")
|
||||
}
|
||||
return scopeMap[scope], nil
|
||||
}
|
||||
|
||||
func getScopeMap(permissions []accesscontrol.Permission) map[string]bool {
|
||||
permMap := make(map[string]bool, len(permissions))
|
||||
for _, perm := range permissions {
|
||||
// If has any wildcard, return immediately
|
||||
if perm.Kind == "*" || perm.Attribute == "*" || perm.Identifier == "*" {
|
||||
return map[string]bool{"*": true}
|
||||
}
|
||||
permMap[perm.Scope] = true
|
||||
}
|
||||
return permMap
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
package store
|
||||
|
||||
import "github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
// TODO (gamab): Implement GetRoles, GetTeams, GetFolders, GetPermissions
|
||||
"github.com/grafana/authlib/claims"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
sql legacysql.LegacyDatabaseProvider
|
||||
@@ -13,3 +20,107 @@ func NewStore(sql legacysql.LegacyDatabaseProvider) *Store {
|
||||
sql: sql,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) GetUserPermissions(ctx context.Context, ns claims.NamespaceInfo, query PermissionsQuery) ([]accesscontrol.Permission, error) {
|
||||
sql, err := s.sql(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query.OrgID = ns.OrgID
|
||||
req := newGetPermissions(sql, &query)
|
||||
q, err := sqltemplate.Execute(sqlUserPerms, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if res != nil {
|
||||
_ = res.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var perms []accesscontrol.Permission
|
||||
for res.Next() {
|
||||
var perm accesscontrol.Permission
|
||||
if err := res.Scan(&perm.Action, &perm.Kind, &perm.Attribute, &perm.Identifier, &perm.Scope); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
perms = append(perms, perm)
|
||||
}
|
||||
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetUserIdentifiers(ctx context.Context, query UserIdentifierQuery) (*UserIdentifiers, error) {
|
||||
sql, err := s.sql(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := newGetUserIdentifiers(sql, &query)
|
||||
q, err := sqltemplate.Execute(sqlUserIdentifiers, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...)
|
||||
defer func() {
|
||||
if rows != nil {
|
||||
_ = rows.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !rows.Next() {
|
||||
return nil, fmt.Errorf("user could not be found")
|
||||
}
|
||||
|
||||
var userIDs UserIdentifiers
|
||||
if err := rows.Scan(&userIDs.ID, &userIDs.UID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userIDs, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetBasicRoles(ctx context.Context, ns claims.NamespaceInfo, query BasicRoleQuery) (*BasicRole, error) {
|
||||
sql, err := s.sql(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query.OrgID = ns.OrgID
|
||||
req := newGetBasicRoles(sql, &query)
|
||||
q, err := sqltemplate.Execute(sqlQueryBasicRoles, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...)
|
||||
defer func() {
|
||||
if rows != nil {
|
||||
_ = rows.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !rows.Next() {
|
||||
return nil, fmt.Errorf("no basic roles found for the user")
|
||||
}
|
||||
|
||||
var role BasicRole
|
||||
if err := rows.Scan(&role.Role, &role.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
+4
@@ -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
|
||||
+7
@@ -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')
|
||||
)
|
||||
Vendored
Executable
+8
@@ -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))
|
||||
)
|
||||
+6
@@ -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))
|
||||
)
|
||||
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
SELECT u.id, u.uid
|
||||
FROM `grafana`.`user` as u
|
||||
WHERE u.id = 1
|
||||
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
SELECT u.id, u.uid
|
||||
FROM `grafana`.`user` as u
|
||||
WHERE u.uid = 'some_uid'
|
||||
Vendored
Executable
+4
@@ -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
|
||||
Vendored
Executable
+7
@@ -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')
|
||||
)
|
||||
Vendored
Executable
+8
@@ -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))
|
||||
)
|
||||
Vendored
Executable
+6
@@ -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))
|
||||
)
|
||||
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
SELECT u.id, u.uid
|
||||
FROM "grafana"."user" as u
|
||||
WHERE u.id = 1
|
||||
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
SELECT u.id, u.uid
|
||||
FROM "grafana"."user" as u
|
||||
WHERE u.uid = 'some_uid'
|
||||
+4
@@ -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
|
||||
+7
@@ -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')
|
||||
)
|
||||
Vendored
Executable
+8
@@ -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))
|
||||
)
|
||||
+6
@@ -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))
|
||||
)
|
||||
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
SELECT u.id, u.uid
|
||||
FROM "grafana"."user" as u
|
||||
WHERE u.id = 1
|
||||
Vendored
Executable
+3
@@ -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 }}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/authz/mappers"
|
||||
@@ -25,7 +26,7 @@ import (
|
||||
)
|
||||
|
||||
func RegisterRBACAuthZService(handler grpcserver.Provider, db legacysql.LegacyDatabaseProvider, tracer tracing.Tracer) {
|
||||
server := rbac.NewService(db, log.New("authz-grpc-server"), tracer)
|
||||
server := rbac.NewService(db, legacy.NewLegacySQLStores(db), log.New("authz-grpc-server"), tracer)
|
||||
|
||||
srv := handler.GetServer()
|
||||
authzv1.RegisterAuthzServiceServer(srv, server)
|
||||
|
||||
Reference in New Issue
Block a user