RBAC: Add legacy authorization checks to teams (#94524)

* Setup team authorization for teams

* Add list filter for teams
This commit is contained in:
Karl Persson 2024-10-10 16:47:31 +02:00 committed by GitHub
parent 315778227b
commit 86fc8da703
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 170 additions and 55 deletions

View File

@ -1,6 +1,8 @@
package v0alpha1
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -15,6 +17,13 @@ type Team struct {
type TeamSpec struct {
Title string `json:"title,omitempty"`
Email string `json:"email,omitempty"`
// This is currently used for authorization checks but we don't want to expose it
InternalID int64 `json:"-"`
}
func (t Team) AuthID() string {
return fmt.Sprintf("%d", t.Spec.InternalID)
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@ -54,6 +54,19 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden
return []string{fmt.Sprintf("serviceaccounts:id:%d", res.ID)}, nil
}),
},
accesscontrol.ResourceAuthorizerOptions{
Resource: iamv0.TeamResourceInfo.GetName(),
Attr: "id",
Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error) {
res, err := store.GetTeamInternalID(ctx, ns, legacy.GetTeamInternalIDQuery{
UID: name,
})
if err != nil {
return nil, err
}
return []string{fmt.Sprintf("teams:id:%d", res.ID)}, nil
}),
},
)
return gfauthorizer.NewResourceAuthorizer(client), client

View File

@ -22,6 +22,7 @@ type LegacyIdentityStore interface {
ListServiceAccounts(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountsQuery) (*ListServiceAccountResult, error)
ListServiceAccountTokens(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountTokenQuery) (*ListServiceAccountTokenResult, error)
GetTeamInternalID(ctx context.Context, ns claims.NamespaceInfo, query GetTeamInternalIDQuery) (*GetTeamInternalIDResult, error)
ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error)
ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error)
ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error)

View File

@ -3,6 +3,7 @@ package legacy
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
@ -14,6 +15,79 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
type GetTeamInternalIDQuery struct {
OrgID int64
UID string
}
type GetTeamInternalIDResult struct {
ID int64
}
var sqlQueryTeamInternalIDTemplate = mustTemplate("team_internal_id.sql")
func newGetTeamInternalID(sql *legacysql.LegacyDatabaseHelper, q *GetTeamInternalIDQuery) getTeamInternalIDQuery {
return getTeamInternalIDQuery{
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
TeamTable: sql.Table("team"),
Query: q,
}
}
type getTeamInternalIDQuery struct {
sqltemplate.SQLTemplate
TeamTable string
Query *GetTeamInternalIDQuery
}
func (r getTeamInternalIDQuery) Validate() error { return nil }
func (s *legacySQLStore) GetTeamInternalID(
ctx context.Context,
ns claims.NamespaceInfo,
query GetTeamInternalIDQuery,
) (*GetTeamInternalIDResult, 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 := newGetTeamInternalID(sql, &query)
q, err := sqltemplate.Execute(sqlQueryTeamInternalIDTemplate, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeamInternalIDTemplate.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("team not found")
}
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
return &GetTeamInternalIDResult{
id,
}, nil
}
type ListTeamQuery struct {
OrgID int64
UID string

View File

@ -0,0 +1,5 @@
SELECT t.id
FROM {{ .Ident .TeamTable }} as t
WHERE t.org_id = {{ .Arg .Query.OrgID }}
AND t.uid = {{ .Arg .Query.UID }}
LIMIT 1;

View File

@ -97,7 +97,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
storage := map[string]rest.Storage{}
teamResource := iamv0.TeamResourceInfo
storage[teamResource.StoragePath()] = team.NewLegacyStore(b.store)
storage[teamResource.StoragePath()] = team.NewLegacyStore(b.store, b.accessClient)
storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store)
teamBindingResource := iamv0.TeamBindingResourceInfo

View File

@ -2,6 +2,7 @@ package team
import (
"context"
"fmt"
"strconv"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
@ -15,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/team"
)
var (
@ -27,12 +29,13 @@ var (
var resource = iamv0.TeamResourceInfo
func NewLegacyStore(store legacy.LegacyIdentityStore) *LegacyStore {
return &LegacyStore{store}
func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient) *LegacyStore {
return &LegacyStore{store, ac}
}
type LegacyStore struct {
store legacy.LegacyIdentityStore
ac claims.AccessClient
}
func (s *LegacyStore) New() runtime.Object {
@ -58,74 +61,84 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object,
return resource.TableConverter().ConvertToTable(ctx, object, tableOptions)
}
func (s *LegacyStore) doList(ctx context.Context, ns claims.NamespaceInfo, query legacy.ListTeamQuery) (*iamv0.TeamList, error) {
rsp, err := s.store.ListTeams(ctx, ns, query)
if err != nil {
return nil, err
}
list := &iamv0.TeamList{
ListMeta: metav1.ListMeta{
ResourceVersion: strconv.FormatInt(rsp.RV, 10),
func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
res, err := common.List(
ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options),
func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.Team], error) {
found, err := s.store.ListTeams(ctx, ns, legacy.ListTeamQuery{
Pagination: p,
})
if err != nil {
return nil, err
}
teams := make([]iamv0.Team, 0, len(found.Teams))
for _, t := range found.Teams {
teams = append(teams, toTeamObject(t, ns))
}
return &common.ListResponse[iamv0.Team]{
Items: teams,
RV: found.RV,
Continue: found.Continue,
}, nil
},
}
for _, team := range rsp.Teams {
item := iamv0.Team{
ObjectMeta: metav1.ObjectMeta{
Name: team.UID,
Namespace: ns.Value,
CreationTimestamp: metav1.NewTime(team.Created),
ResourceVersion: strconv.FormatInt(team.Updated.UnixMilli(), 10),
},
Spec: iamv0.TeamSpec{
Title: team.Name,
Email: team.Email,
},
}
meta, err := utils.MetaAccessor(&item)
if err != nil {
return nil, err
}
meta.SetUpdatedTimestamp(&team.Updated)
meta.SetOriginInfo(&utils.ResourceOriginInfo{
Name: "SQL",
Path: strconv.FormatInt(team.ID, 10),
})
list.Items = append(list.Items, item)
)
if err != nil {
return nil, fmt.Errorf("failed to list teams: %w", err)
}
list.ListMeta.Continue = common.OptionalFormatInt(rsp.Continue)
list.ListMeta.ResourceVersion = common.OptionalFormatInt(rsp.RV)
list := &iamv0.TeamList{Items: res.Items}
list.ListMeta.Continue = common.OptionalFormatInt(res.Continue)
list.ListMeta.ResourceVersion = common.OptionalFormatInt(res.RV)
return list, nil
}
func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
return s.doList(ctx, ns, legacy.ListTeamQuery{
OrgID: ns.OrgID,
Pagination: common.PaginationFromListOptions(options),
})
}
func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
rsp, err := s.doList(ctx, ns, legacy.ListTeamQuery{
found, err := s.store.ListTeams(ctx, ns, legacy.ListTeamQuery{
OrgID: ns.OrgID,
UID: name,
Pagination: common.Pagination{Limit: 1},
})
if err != nil {
return nil, err
if found == nil || err != nil {
return nil, resource.NewNotFound(name)
}
if len(rsp.Items) > 0 {
return &rsp.Items[0], nil
if len(found.Teams) < 1 {
return nil, resource.NewNotFound(name)
}
return nil, resource.NewNotFound(name)
obj := toTeamObject(found.Teams[0], ns)
return &obj, nil
}
func toTeamObject(t team.Team, ns claims.NamespaceInfo) iamv0.Team {
obj := iamv0.Team{
ObjectMeta: metav1.ObjectMeta{
Name: t.UID,
Namespace: ns.Value,
CreationTimestamp: metav1.NewTime(t.Created),
ResourceVersion: strconv.FormatInt(t.Updated.UnixMilli(), 10),
},
Spec: iamv0.TeamSpec{
Title: t.Name,
Email: t.Email,
InternalID: t.ID,
},
}
meta, _ := utils.MetaAccessor(&obj)
meta.SetUpdatedTimestamp(&t.Updated)
meta.SetOriginInfo(&utils.ResourceOriginInfo{
Name: "SQL",
Path: strconv.FormatInt(t.ID, 10),
})
return obj
}