mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
RBAC: Add required component to perform access control checks for user api when running single tenant (#93104)
* Unexport store and create new constructor function * Add ResourceAuthorizer and LegacyAccessClient * Configure checks for user store * List with checks if AccessClient is configured * Allow system user service account to read all users --------- Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
parent
bca8bd3c8b
commit
2e38329026
@ -40,6 +40,10 @@ func (info *ResourceInfo) WithGroupAndShortName(group string, shortName string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (info *ResourceInfo) GetName() string {
|
||||||
|
return info.resourceName
|
||||||
|
}
|
||||||
|
|
||||||
func (info *ResourceInfo) GetSingularName() string {
|
func (info *ResourceInfo) GetSingularName() string {
|
||||||
return info.singularName
|
return info.singularName
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,9 @@ func WithRequester(handler http.Handler) http.Handler {
|
|||||||
Permissions: map[int64]map[string][]string{
|
Permissions: map[int64]map[string][]string{
|
||||||
orgId: {
|
orgId: {
|
||||||
"*": {"*"}, // all resources, all scopes
|
"*": {"*"}, // all resources, all scopes
|
||||||
|
// FIXME(kalleep): We don't support wildcard actions so we need to list all possible actions
|
||||||
|
// for this user. This is not scalable and we should look into how to fix this.
|
||||||
|
"org.users:read": {"*"},
|
||||||
// Dashboards do not support wildcard action
|
// Dashboards do not support wildcard action
|
||||||
// dashboards.ActionDashboardsRead: {"*"},
|
// dashboards.ActionDashboardsRead: {"*"},
|
||||||
// dashboards.ActionDashboardsCreate: {"*"},
|
// dashboards.ActionDashboardsCreate: {"*"},
|
||||||
|
35
pkg/registry/apis/iam/authorizer.go
Normal file
35
pkg/registry/apis/iam/authorizer.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package iam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/claims"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
gfauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIdentityStore) (authorizer.Authorizer, claims.AccessClient) {
|
||||||
|
client := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||||
|
Resource: "users",
|
||||||
|
Attr: "id",
|
||||||
|
Mapping: map[string]string{
|
||||||
|
"get": accesscontrol.ActionOrgUsersRead,
|
||||||
|
"list": accesscontrol.ActionOrgUsersRead,
|
||||||
|
},
|
||||||
|
Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error) {
|
||||||
|
res, err := store.GetUserInternalID(ctx, ns, legacy.GetUserInternalIDQuery{
|
||||||
|
UID: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []string{fmt.Sprintf("users:id:%d", res.ID)}, nil
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
return gfauthorizer.NewResourceAuthorizer(client), client
|
||||||
|
}
|
@ -14,6 +14,7 @@ import (
|
|||||||
type LegacyIdentityStore interface {
|
type LegacyIdentityStore interface {
|
||||||
ListDisplay(ctx context.Context, ns claims.NamespaceInfo, query ListDisplayQuery) (*ListUserResult, error)
|
ListDisplay(ctx context.Context, ns claims.NamespaceInfo, query ListDisplayQuery) (*ListUserResult, error)
|
||||||
|
|
||||||
|
GetUserInternalID(ctx context.Context, ns claims.NamespaceInfo, query GetUserInternalIDQuery) (*GetUserInternalIDResult, error)
|
||||||
ListUsers(ctx context.Context, ns claims.NamespaceInfo, query ListUserQuery) (*ListUserResult, error)
|
ListUsers(ctx context.Context, ns claims.NamespaceInfo, query ListUserQuery) (*ListUserResult, error)
|
||||||
ListUserTeams(ctx context.Context, ns claims.NamespaceInfo, query ListUserTeamsQuery) (*ListUserTeamsResult, error)
|
ListUserTeams(ctx context.Context, ns claims.NamespaceInfo, query ListUserTeamsQuery) (*ListUserTeamsResult, error)
|
||||||
|
|
||||||
@ -37,6 +38,11 @@ func NewLegacySQLStores(sql legacysql.LegacyDatabaseProvider) LegacyIdentityStor
|
|||||||
|
|
||||||
type legacySQLStore struct {
|
type legacySQLStore struct {
|
||||||
sql legacysql.LegacyDatabaseProvider
|
sql legacysql.LegacyDatabaseProvider
|
||||||
|
ac claims.AccessClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *legacySQLStore) WithAccessClient(ac claims.AccessClient) {
|
||||||
|
s.ac = ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// Templates setup.
|
// Templates setup.
|
||||||
|
@ -2,6 +2,7 @@ package legacy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
@ -13,6 +14,79 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GetUserInternalIDQuery struct {
|
||||||
|
OrgID int64
|
||||||
|
UID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUserInternalIDResult struct {
|
||||||
|
ID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var sqlQueryUserInternalIDTemplate = mustTemplate("user_internal_id.sql")
|
||||||
|
|
||||||
|
func newGetUserInternalID(sql *legacysql.LegacyDatabaseHelper, q *GetUserInternalIDQuery) getUserInternalIDQuery {
|
||||||
|
return getUserInternalIDQuery{
|
||||||
|
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
|
||||||
|
UserTable: sql.Table("user"),
|
||||||
|
OrgUserTable: sql.Table("org_user"),
|
||||||
|
Query: q,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type getUserInternalIDQuery struct {
|
||||||
|
sqltemplate.SQLTemplate
|
||||||
|
UserTable string
|
||||||
|
OrgUserTable string
|
||||||
|
Query *GetUserInternalIDQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r getUserInternalIDQuery) Validate() error {
|
||||||
|
return nil // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *legacySQLStore) GetUserInternalID(ctx context.Context, ns claims.NamespaceInfo, query GetUserInternalIDQuery) (*GetUserInternalIDResult, error) {
|
||||||
|
query.OrgID = ns.OrgID
|
||||||
|
if query.OrgID == 0 {
|
||||||
|
return nil, fmt.Errorf("expected non zero org id")
|
||||||
|
}
|
||||||
|
|
||||||
|
sql, err := s.sql(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := newGetUserInternalID(sql, &query)
|
||||||
|
q, err := sqltemplate.Execute(sqlQueryUserInternalIDTemplate, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("execute template %q: %w", sqlQueryUserInternalIDTemplate.Name(), 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, errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GetUserInternalIDResult{
|
||||||
|
id,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type ListUserQuery struct {
|
type ListUserQuery struct {
|
||||||
OrgID int64
|
OrgID int64
|
||||||
UID string
|
UID string
|
||||||
|
7
pkg/registry/apis/iam/legacy/user_internal_id.sql
Normal file
7
pkg/registry/apis/iam/legacy/user_internal_id.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
SELECT u.id
|
||||||
|
FROM {{ .Ident .UserTable }} as u
|
||||||
|
INNER JOIN {{ .Ident .OrgUserTable }} as o ON u.id = o.user_id
|
||||||
|
WHERE o.org_id = {{ .Arg .Query.OrgID }}
|
||||||
|
AND u.uid = {{ .Arg .Query.UID }}
|
||||||
|
AND NOT u.is_service_account
|
||||||
|
LIMIT 1;
|
@ -3,19 +3,6 @@ package iam
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
||||||
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
|
|
||||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount"
|
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/iam/sso"
|
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/iam/team"
|
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/iam/user"
|
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ssosettings"
|
|
||||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
@ -25,14 +12,34 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
common "k8s.io/kube-openapi/pkg/common"
|
common "k8s.io/kube-openapi/pkg/common"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/claims"
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
|
||||||
|
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/iam/sso"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/iam/team"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/iam/user"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||||
|
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ builder.APIGroupBuilder = (*IdentityAccessManagementAPIBuilder)(nil)
|
var _ builder.APIGroupBuilder = (*IdentityAccessManagementAPIBuilder)(nil)
|
||||||
|
|
||||||
// This is used just so wire has something unique to return
|
// This is used just so wire has something unique to return
|
||||||
type IdentityAccessManagementAPIBuilder struct {
|
type IdentityAccessManagementAPIBuilder struct {
|
||||||
Store legacy.LegacyIdentityStore
|
store legacy.LegacyIdentityStore
|
||||||
SSOService ssosettings.Service
|
authorizer authorizer.Authorizer
|
||||||
|
accessClient claims.AccessClient
|
||||||
|
|
||||||
|
// Not set for multi-tenant deployment for now
|
||||||
|
sso ssosettings.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterAPIService(
|
func RegisterAPIService(
|
||||||
@ -40,20 +47,44 @@ func RegisterAPIService(
|
|||||||
apiregistration builder.APIRegistrar,
|
apiregistration builder.APIRegistrar,
|
||||||
ssoService ssosettings.Service,
|
ssoService ssosettings.Service,
|
||||||
sql db.DB,
|
sql db.DB,
|
||||||
|
ac accesscontrol.AccessControl,
|
||||||
) (*IdentityAccessManagementAPIBuilder, error) {
|
) (*IdentityAccessManagementAPIBuilder, error) {
|
||||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||||
return nil, nil // skip registration unless opting into experimental apis
|
// skip registration unless opting into experimental apis
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store := legacy.NewLegacySQLStores(legacysql.NewDatabaseProvider(sql))
|
||||||
|
authorizer, client := newLegacyAuthorizer(ac, store)
|
||||||
|
|
||||||
builder := &IdentityAccessManagementAPIBuilder{
|
builder := &IdentityAccessManagementAPIBuilder{
|
||||||
Store: legacy.NewLegacySQLStores(legacysql.NewDatabaseProvider(sql)),
|
store: store,
|
||||||
SSOService: ssoService,
|
sso: ssoService,
|
||||||
|
authorizer: authorizer,
|
||||||
|
accessClient: client,
|
||||||
}
|
}
|
||||||
apiregistration.RegisterAPI(builder)
|
apiregistration.RegisterAPI(builder)
|
||||||
|
|
||||||
return builder, nil
|
return builder, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewAPIService(store legacy.LegacyIdentityStore) *IdentityAccessManagementAPIBuilder {
|
||||||
|
return &IdentityAccessManagementAPIBuilder{
|
||||||
|
store: store,
|
||||||
|
authorizer: authorizer.AuthorizerFunc(
|
||||||
|
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
user, err := identity.GetRequester(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return authorizer.DecisionDeny, "no identity found", err
|
||||||
|
}
|
||||||
|
if user.GetIsGrafanaAdmin() {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
return authorizer.DecisionDeny, "only grafana admins have access for now", nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *IdentityAccessManagementAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
func (b *IdentityAccessManagementAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
||||||
return iamv0.SchemeGroupVersion
|
return iamv0.SchemeGroupVersion
|
||||||
}
|
}
|
||||||
@ -80,27 +111,27 @@ func (b *IdentityAccessManagementAPIBuilder) GetAPIGroupInfo(
|
|||||||
storage := map[string]rest.Storage{}
|
storage := map[string]rest.Storage{}
|
||||||
|
|
||||||
teamResource := iamv0.TeamResourceInfo
|
teamResource := iamv0.TeamResourceInfo
|
||||||
storage[teamResource.StoragePath()] = team.NewLegacyStore(b.Store)
|
storage[teamResource.StoragePath()] = team.NewLegacyStore(b.store)
|
||||||
storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.Store)
|
storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store)
|
||||||
|
|
||||||
teamBindingResource := iamv0.TeamBindingResourceInfo
|
teamBindingResource := iamv0.TeamBindingResourceInfo
|
||||||
storage[teamBindingResource.StoragePath()] = team.NewLegacyBindingStore(b.Store)
|
storage[teamBindingResource.StoragePath()] = team.NewLegacyBindingStore(b.store)
|
||||||
|
|
||||||
userResource := iamv0.UserResourceInfo
|
userResource := iamv0.UserResourceInfo
|
||||||
storage[userResource.StoragePath()] = user.NewLegacyStore(b.Store)
|
storage[userResource.StoragePath()] = user.NewLegacyStore(b.store, b.accessClient)
|
||||||
storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.Store)
|
storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store)
|
||||||
|
|
||||||
serviceaccountResource := iamv0.ServiceAccountResourceInfo
|
serviceaccountResource := iamv0.ServiceAccountResourceInfo
|
||||||
storage[serviceaccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.Store)
|
storage[serviceaccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store)
|
||||||
storage[serviceaccountResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.Store)
|
storage[serviceaccountResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store)
|
||||||
|
|
||||||
if b.SSOService != nil {
|
if b.sso != nil {
|
||||||
ssoResource := iamv0.SSOSettingResourceInfo
|
ssoResource := iamv0.SSOSettingResourceInfo
|
||||||
storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.SSOService)
|
storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The display endpoint -- NOTE, this uses a rewrite hack to allow requests without a name parameter
|
// The display endpoint -- NOTE, this uses a rewrite hack to allow requests without a name parameter
|
||||||
storage["display"] = user.NewLegacyDisplayREST(b.Store)
|
storage["display"] = user.NewLegacyDisplayREST(b.store)
|
||||||
|
|
||||||
apiGroupInfo.VersionedResourcesStorageMap[iamv0.VERSION] = storage
|
apiGroupInfo.VersionedResourcesStorageMap[iamv0.VERSION] = storage
|
||||||
return &apiGroupInfo, nil
|
return &apiGroupInfo, nil
|
||||||
@ -116,16 +147,5 @@ func (b *IdentityAccessManagementAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *IdentityAccessManagementAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
func (b *IdentityAccessManagementAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||||
// TODO: handle authorization based in entity.
|
return b.authorizer
|
||||||
return authorizer.AuthorizerFunc(
|
|
||||||
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
|
||||||
user, err := identity.GetRequester(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return authorizer.DecisionDeny, "no identity found", err
|
|
||||||
}
|
|
||||||
if user.GetIsGrafanaAdmin() {
|
|
||||||
return authorizer.DecisionAllow, "", nil
|
|
||||||
}
|
|
||||||
return authorizer.DecisionDeny, "only grafana admins have access for now", nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/claims"
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
|
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
|
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
|
||||||
@ -28,12 +30,13 @@ var (
|
|||||||
|
|
||||||
var resource = iamv0.UserResourceInfo
|
var resource = iamv0.UserResourceInfo
|
||||||
|
|
||||||
func NewLegacyStore(store legacy.LegacyIdentityStore) *LegacyStore {
|
func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient) *LegacyStore {
|
||||||
return &LegacyStore{store}
|
return &LegacyStore{store, ac}
|
||||||
}
|
}
|
||||||
|
|
||||||
type LegacyStore struct {
|
type LegacyStore struct {
|
||||||
store legacy.LegacyIdentityStore
|
store legacy.LegacyIdentityStore
|
||||||
|
ac claims.AccessClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LegacyStore) New() runtime.Object {
|
func (s *LegacyStore) New() runtime.Object {
|
||||||
@ -64,23 +67,92 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{
|
if s.ac != nil {
|
||||||
OrgID: ns.OrgID,
|
return s.listWithCheck(ctx, ns, common.PaginationFromListOptions(options))
|
||||||
Pagination: common.PaginationFromListOptions(options),
|
}
|
||||||
|
|
||||||
|
return s.listWithoutCheck(ctx, ns, common.PaginationFromListOptions(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LegacyStore) listWithCheck(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (runtime.Object, error) {
|
||||||
|
ident, err := identity.GetRequester(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
check, err := s.ac.Compile(ctx, ident, claims.AccessRequest{
|
||||||
|
Verb: "list",
|
||||||
|
Resource: resource.GetName(),
|
||||||
|
Namespace: ns.Value,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
list := func(p common.Pagination) ([]iamv0.User, int64, int64, error) {
|
||||||
|
found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{
|
||||||
|
Pagination: p,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]iamv0.User, 0, len(found.Users))
|
||||||
|
for _, u := range found.Users {
|
||||||
|
if check(ns.Value, strconv.FormatInt(u.ID, 10)) {
|
||||||
|
out = append(out, toUserItem(&u, ns.Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, found.Continue, found.RV, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
items, c, rv, err := list(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outer:
|
||||||
|
for len(items) < int(p.Limit) && c != 0 {
|
||||||
|
var more []iamv0.User
|
||||||
|
more, c, _, err = list(common.Pagination{Limit: p.Limit, Continue: c})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range more {
|
||||||
|
if len(items) == int(p.Limit) {
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
items = append(items, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := &iamv0.UserList{Items: items}
|
||||||
|
obj.ListMeta.Continue = common.OptionalFormatInt(c)
|
||||||
|
obj.ListMeta.ResourceVersion = common.OptionalFormatInt(rv)
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LegacyStore) listWithoutCheck(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (runtime.Object, error) {
|
||||||
|
found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{
|
||||||
|
Pagination: p,
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
list := &iamv0.UserList{}
|
list := &iamv0.UserList{}
|
||||||
for _, item := range found.Users {
|
for _, item := range found.Users {
|
||||||
list.Items = append(list.Items, *toUserItem(&item, ns.Value))
|
list.Items = append(list.Items, toUserItem(&item, ns.Value))
|
||||||
}
|
}
|
||||||
|
|
||||||
list.ListMeta.Continue = common.OptionalFormatInt(found.Continue)
|
list.ListMeta.Continue = common.OptionalFormatInt(found.Continue)
|
||||||
list.ListMeta.ResourceVersion = common.OptionalFormatInt(found.RV)
|
list.ListMeta.ResourceVersion = common.OptionalFormatInt(found.RV)
|
||||||
|
return list, nil
|
||||||
return list, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||||
@ -100,10 +172,12 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO
|
|||||||
if len(found.Users) < 1 {
|
if len(found.Users) < 1 {
|
||||||
return nil, resource.NewNotFound(name)
|
return nil, resource.NewNotFound(name)
|
||||||
}
|
}
|
||||||
return toUserItem(&found.Users[0], ns.Value), nil
|
|
||||||
|
obj := toUserItem(&found.Users[0], ns.Value)
|
||||||
|
return &obj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func toUserItem(u *user.User, ns string) *iamv0.User {
|
func toUserItem(u *user.User, ns string) iamv0.User {
|
||||||
item := &iamv0.User{
|
item := &iamv0.User{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: u.UID,
|
Name: u.UID,
|
||||||
@ -125,5 +199,5 @@ func toUserItem(u *user.User, ns string) *iamv0.User {
|
|||||||
Name: "SQL",
|
Name: "SQL",
|
||||||
Path: strconv.FormatInt(u.ID, 10),
|
Path: strconv.FormatInt(u.ID, 10),
|
||||||
})
|
})
|
||||||
return item
|
return *item
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,10 @@ type AccessControl interface {
|
|||||||
// RegisterScopeAttributeResolver allows the caller to register a scope resolver for a
|
// RegisterScopeAttributeResolver allows the caller to register a scope resolver for a
|
||||||
// specific scope prefix (ex: datasources:name:)
|
// specific scope prefix (ex: datasources:name:)
|
||||||
RegisterScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver)
|
RegisterScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver)
|
||||||
|
// WithoutResolvers copies AccessControl without any configured resolvers.
|
||||||
|
// This is useful when we don't want to reuse any pre-configured resolvers
|
||||||
|
// for a authorization call.
|
||||||
|
WithoutResolvers() AccessControl
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
|
@ -205,6 +205,16 @@ func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver a
|
|||||||
a.resolvers.AddScopeAttributeResolver(prefix, resolver)
|
a.resolvers.AddScopeAttributeResolver(prefix, resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AccessControl) WithoutResolvers() accesscontrol.AccessControl {
|
||||||
|
return &AccessControl{
|
||||||
|
features: a.features,
|
||||||
|
log: a.log,
|
||||||
|
zclient: a.zclient,
|
||||||
|
metrics: a.metrics,
|
||||||
|
resolvers: accesscontrol.NewResolvers(a.log),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *AccessControl) debug(ctx context.Context, ident identity.Requester, msg string, eval accesscontrol.Evaluator) {
|
func (a *AccessControl) debug(ctx context.Context, ident identity.Requester, msg string, eval accesscontrol.Evaluator) {
|
||||||
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.debug")
|
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.debug")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
@ -75,6 +75,10 @@ func (f FakeAccessControl) Evaluate(ctx context.Context, user identity.Requester
|
|||||||
func (f FakeAccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) {
|
func (f FakeAccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f FakeAccessControl) WithoutResolvers() accesscontrol.AccessControl {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
type FakeStore struct {
|
type FakeStore struct {
|
||||||
ExpectedUserPermissions []accesscontrol.Permission
|
ExpectedUserPermissions []accesscontrol.Permission
|
||||||
ExpectedBasicRolesPermissions []accesscontrol.Permission
|
ExpectedBasicRolesPermissions []accesscontrol.Permission
|
||||||
|
131
pkg/services/accesscontrol/authorizer.go
Normal file
131
pkg/services/accesscontrol/authorizer.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package accesscontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/claims"
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceResolver is called before authorization is performed.
|
||||||
|
// It can be used to translate resoruce name into one or more valid scopes that
|
||||||
|
// will be used for authorization. If more than one scope is returned from a resolver
|
||||||
|
// only one needs to match to allow call to be authorized.
|
||||||
|
type ResourceResolver interface {
|
||||||
|
Resolve(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceResolverFunc is an adapter so that functions can implement ResourceResolver.
|
||||||
|
type ResourceResolverFunc func(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error)
|
||||||
|
|
||||||
|
func (r ResourceResolverFunc) Resolve(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error) {
|
||||||
|
return r(ctx, ns, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceAuthorizerOptions struct {
|
||||||
|
// Resource is the resource name in plural.
|
||||||
|
Resource string
|
||||||
|
// Attr is attribute used for resource scope. It's usually 'id' or 'uid'
|
||||||
|
// depending on what is stored for the resource.
|
||||||
|
Attr string
|
||||||
|
// Mapping is used to translate k8s verb to rbac action.
|
||||||
|
// Key is the desired verb and value the rbac action it should be translated into.
|
||||||
|
Mapping map[string]string
|
||||||
|
// Resolver if passed can translate into one or more scopes used to authorize resource.
|
||||||
|
// This is useful when stored scopes are based on something else than k8s name or
|
||||||
|
// for resources that inherit permission from folder.
|
||||||
|
Resolver ResourceResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ claims.AccessClient = (*LegacyAccessClient)(nil)
|
||||||
|
|
||||||
|
func NewLegacyAccessClient(ac AccessControl, opts ...ResourceAuthorizerOptions) *LegacyAccessClient {
|
||||||
|
stored := map[string]ResourceAuthorizerOptions{}
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
if o.Mapping == nil {
|
||||||
|
o.Mapping = map[string]string{}
|
||||||
|
}
|
||||||
|
stored[o.Resource] = o
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LegacyAccessClient{ac.WithoutResolvers(), stored}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LegacyAccessClient struct {
|
||||||
|
ac AccessControl
|
||||||
|
opts map[string]ResourceAuthorizerOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasAccess implements claims.AccessClient.
|
||||||
|
func (c *LegacyAccessClient) HasAccess(ctx context.Context, id claims.AuthInfo, req claims.AccessRequest) (bool, error) {
|
||||||
|
ident, ok := id.(identity.Requester)
|
||||||
|
if !ok {
|
||||||
|
return false, errors.New("expected identity.Requester for legacy access control")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, ok := c.opts[req.Resource]
|
||||||
|
if !ok {
|
||||||
|
// For now we fallback to grafana admin if no options are found for resource.
|
||||||
|
if ident.GetIsGrafanaAdmin() {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
action, ok := opts.Mapping[req.Verb]
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("missing action for %s %s", req.Verb, req.Resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, err := claims.ParseNamespace(req.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var eval Evaluator
|
||||||
|
if req.Name != "" {
|
||||||
|
if opts.Resolver != nil {
|
||||||
|
scopes, err := opts.Resolver.Resolve(ctx, ns, req.Name)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
eval = EvalPermission(action, scopes...)
|
||||||
|
} else {
|
||||||
|
eval = EvalPermission(action, fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, req.Name))
|
||||||
|
}
|
||||||
|
} else if req.Verb == "list" {
|
||||||
|
// For list request we need to filter out in storage layer.
|
||||||
|
eval = EvalPermission(action)
|
||||||
|
} else {
|
||||||
|
// Assuming that all non list request should have a valid name
|
||||||
|
return false, fmt.Errorf("unhandled authorization: %s %s", req.Group, req.Verb)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.ac.Evaluate(ctx, ident, eval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile implements claims.AccessClient.
|
||||||
|
func (c *LegacyAccessClient) Compile(ctx context.Context, id claims.AuthInfo, req claims.AccessRequest) (claims.AccessChecker, error) {
|
||||||
|
ident, ok := id.(identity.Requester)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("expected identity.Requester for legacy access control")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, ok := c.opts[req.Resource]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unsupported resource: %s", req.Resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
action, ok := opts.Mapping[req.Verb]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("missing action for %s %s", req.Verb, req.Resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
check := Checker(ident, action)
|
||||||
|
return func(_, name string) bool {
|
||||||
|
return check(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, name))
|
||||||
|
}, nil
|
||||||
|
}
|
118
pkg/services/accesscontrol/authorizer_test.go
Normal file
118
pkg/services/accesscontrol/authorizer_test.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package accesscontrol_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/claims"
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||||
|
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResourceAuthorizer_HasAccess(t *testing.T) {
|
||||||
|
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||||
|
|
||||||
|
t.Run("should have no opinion for non resource requests", func(t *testing.T) {
|
||||||
|
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||||
|
Resource: "dashboards",
|
||||||
|
Attr: "uid",
|
||||||
|
})
|
||||||
|
|
||||||
|
ok, err := a.HasAccess(context.Background(), &identity.StaticRequester{}, claims.AccessRequest{
|
||||||
|
Verb: "get",
|
||||||
|
Resource: "dashboards",
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "1",
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, false, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should reject when user don't have correct scope", func(t *testing.T) {
|
||||||
|
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||||
|
Resource: "dashboards",
|
||||||
|
Attr: "uid",
|
||||||
|
Mapping: map[string]string{
|
||||||
|
"get": "dashboards:read",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ident := newIdent(
|
||||||
|
accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:2"},
|
||||||
|
)
|
||||||
|
|
||||||
|
ok, err := a.HasAccess(context.Background(), ident, claims.AccessRequest{
|
||||||
|
Verb: "get",
|
||||||
|
Namespace: "default",
|
||||||
|
Resource: "dashboards",
|
||||||
|
Name: "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, false, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should just check action for list requests", func(t *testing.T) {
|
||||||
|
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||||
|
Resource: "dashboards",
|
||||||
|
Attr: "uid",
|
||||||
|
Mapping: map[string]string{
|
||||||
|
"list": "dashboards:read",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ident := newIdent(
|
||||||
|
accesscontrol.Permission{Action: "dashboards:read"},
|
||||||
|
)
|
||||||
|
|
||||||
|
ok, err := a.HasAccess(context.Background(), ident, claims.AccessRequest{
|
||||||
|
Verb: "list",
|
||||||
|
Namespace: "default",
|
||||||
|
Resource: "dashboards",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, true, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should allow when user have correct scope", func(t *testing.T) {
|
||||||
|
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||||
|
Resource: "dashboards",
|
||||||
|
Attr: "uid",
|
||||||
|
Mapping: map[string]string{
|
||||||
|
"get": "dashboards:read",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ident := newIdent(
|
||||||
|
accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
ok, err := a.HasAccess(context.Background(), ident, claims.AccessRequest{
|
||||||
|
Verb: "get",
|
||||||
|
Namespace: "default",
|
||||||
|
Resource: "dashboards",
|
||||||
|
Name: "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, true, ok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIdent(permissions ...accesscontrol.Permission) *identity.StaticRequester {
|
||||||
|
pmap := map[string][]string{}
|
||||||
|
for _, p := range permissions {
|
||||||
|
pmap[p.Action] = append(pmap[p.Action], p.Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &identity.StaticRequester{
|
||||||
|
OrgID: 1,
|
||||||
|
Permissions: map[int64]map[string][]string{1: pmap},
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,12 @@
|
|||||||
package accesscontrol
|
package accesscontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Checker(user *user.SignedInUser, action string) func(scopes ...string) bool {
|
func Checker(user identity.Requester, action string) func(scopes ...string) bool {
|
||||||
if user.Permissions == nil || user.Permissions[user.OrgID] == nil {
|
permissions := user.GetPermissions()
|
||||||
return func(scopes ...string) bool { return false }
|
userScopes, ok := permissions[action]
|
||||||
}
|
|
||||||
|
|
||||||
userScopes, ok := user.Permissions[user.OrgID][action]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return func(scopes ...string) bool { return false }
|
return func(scopes ...string) bool { return false }
|
||||||
}
|
}
|
||||||
|
@ -265,3 +265,8 @@ func (m *Mock) SyncUserRoles(ctx context.Context, orgID int64, cmd accesscontrol
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithoutResolvers implements fullAccessControl.
|
||||||
|
func (m *Mock) WithoutResolvers() accesscontrol.AccessControl {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
49
pkg/services/apiserver/auth/authorizer/resource.go
Normal file
49
pkg/services/apiserver/auth/authorizer/resource.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package authorizer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/claims"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewResourceAuthorizer(c claims.AccessClient) authorizer.Authorizer {
|
||||||
|
return ResourceAuthorizer{c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceAuthorizer is used to translate authorizer.Authorizer calls to claims.AccessClient calls
|
||||||
|
type ResourceAuthorizer struct {
|
||||||
|
c claims.AccessClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ResourceAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
if !attr.IsResourceRequest() {
|
||||||
|
return authorizer.DecisionNoOpinion, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ident, ok := claims.From(ctx)
|
||||||
|
if !ok {
|
||||||
|
return authorizer.DecisionDeny, "", errors.New("no identity found for request")
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := r.c.HasAccess(ctx, ident, claims.AccessRequest{
|
||||||
|
Verb: attr.GetVerb(),
|
||||||
|
Group: attr.GetAPIGroup(),
|
||||||
|
Resource: attr.GetResource(),
|
||||||
|
Namespace: attr.GetNamespace(),
|
||||||
|
Name: attr.GetName(),
|
||||||
|
Subresource: attr.GetSubresource(),
|
||||||
|
Path: attr.GetPath(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return authorizer.DecisionDeny, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return authorizer.DecisionDeny, "unauthorized request", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
@ -32,8 +32,8 @@ func (a *recordingAccessControlFake) RegisterScopeAttributeResolver(prefix strin
|
|||||||
panic("implement me")
|
panic("implement me")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *recordingAccessControlFake) IsDisabled() bool {
|
func (a *recordingAccessControlFake) WithoutResolvers() accesscontrol.AccessControl {
|
||||||
return a.Disabled
|
panic("unimplemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ accesscontrol.AccessControl = &recordingAccessControlFake{}
|
var _ accesscontrol.AccessControl = &recordingAccessControlFake{}
|
||||||
|
@ -129,6 +129,11 @@ func (a *recordingAccessControlFake) RegisterScopeAttributeResolver(prefix strin
|
|||||||
panic("implement me")
|
panic("implement me")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *recordingAccessControlFake) WithoutResolvers() ac.AccessControl {
|
||||||
|
// TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
func (a *recordingAccessControlFake) IsDisabled() bool {
|
func (a *recordingAccessControlFake) IsDisabled() bool {
|
||||||
return a.Disabled
|
return a.Disabled
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user