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 package v0alpha1
import ( import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@ -15,6 +17,13 @@ type Team struct {
type TeamSpec struct { type TeamSpec struct {
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Email string `json:"email,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 // +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 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 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) ListServiceAccounts(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountsQuery) (*ListServiceAccountResult, error)
ListServiceAccountTokens(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountTokenQuery) (*ListServiceAccountTokenResult, 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) ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error)
ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error) ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error)
ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error) ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error)

View File

@ -3,6 +3,7 @@ package legacy
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"time" "time"
@ -14,6 +15,79 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "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 { type ListTeamQuery struct {
OrgID int64 OrgID int64
UID string 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{} 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, b.accessClient)
storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store)
teamBindingResource := iamv0.TeamBindingResourceInfo teamBindingResource := iamv0.TeamBindingResourceInfo

View File

@ -2,6 +2,7 @@ package team
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"k8s.io/apimachinery/pkg/apis/meta/internalversion" "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/common"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/team"
) )
var ( var (
@ -27,12 +29,13 @@ var (
var resource = iamv0.TeamResourceInfo var resource = iamv0.TeamResourceInfo
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 {
@ -58,74 +61,84 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object,
return resource.TableConverter().ConvertToTable(ctx, object, tableOptions) return resource.TableConverter().ConvertToTable(ctx, object, tableOptions)
} }
func (s *LegacyStore) doList(ctx context.Context, ns claims.NamespaceInfo, query legacy.ListTeamQuery) (*iamv0.TeamList, error) { func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
rsp, err := s.store.ListTeams(ctx, ns, query) res, err := common.List(
if err != nil { ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options),
return nil, err func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.Team], error) {
} found, err := s.store.ListTeams(ctx, ns, legacy.ListTeamQuery{
list := &iamv0.TeamList{ Pagination: p,
ListMeta: metav1.ListMeta{ })
ResourceVersion: strconv.FormatInt(rsp.RV, 10),
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{ if err != nil {
ObjectMeta: metav1.ObjectMeta{ return nil, fmt.Errorf("failed to list teams: %w", err)
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)
} }
list.ListMeta.Continue = common.OptionalFormatInt(rsp.Continue) list := &iamv0.TeamList{Items: res.Items}
list.ListMeta.ResourceVersion = common.OptionalFormatInt(rsp.RV) list.ListMeta.Continue = common.OptionalFormatInt(res.Continue)
list.ListMeta.ResourceVersion = common.OptionalFormatInt(res.RV)
return list, nil 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) { func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
ns, err := request.NamespaceInfoFrom(ctx, true) ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rsp, err := s.doList(ctx, ns, legacy.ListTeamQuery{
found, err := s.store.ListTeams(ctx, ns, legacy.ListTeamQuery{
OrgID: ns.OrgID, OrgID: ns.OrgID,
UID: name, UID: name,
Pagination: common.Pagination{Limit: 1}, Pagination: common.Pagination{Limit: 1},
}) })
if err != nil { if found == nil || err != nil {
return nil, err return nil, resource.NewNotFound(name)
} }
if len(rsp.Items) > 0 { if len(found.Teams) < 1 {
return &rsp.Items[0], nil 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
} }