diff --git a/pkg/apis/iam/v0alpha1/types_team.go b/pkg/apis/iam/v0alpha1/types_team.go index 9def089d78f..eff4e770f84 100644 --- a/pkg/apis/iam/v0alpha1/types_team.go +++ b/pkg/apis/iam/v0alpha1/types_team.go @@ -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 diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index 8cc8cb68aba..6ea63508d4c 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -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 diff --git a/pkg/registry/apis/iam/legacy/sql.go b/pkg/registry/apis/iam/legacy/sql.go index 5e0e3f470ed..cdabeb6ac7c 100644 --- a/pkg/registry/apis/iam/legacy/sql.go +++ b/pkg/registry/apis/iam/legacy/sql.go @@ -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) diff --git a/pkg/registry/apis/iam/legacy/team.go b/pkg/registry/apis/iam/legacy/team.go index b5e1e5d0e12..be7cb9ab61a 100644 --- a/pkg/registry/apis/iam/legacy/team.go +++ b/pkg/registry/apis/iam/legacy/team.go @@ -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 diff --git a/pkg/registry/apis/iam/legacy/team_internal_id.sql b/pkg/registry/apis/iam/legacy/team_internal_id.sql new file mode 100644 index 00000000000..f2f4e3e5fad --- /dev/null +++ b/pkg/registry/apis/iam/legacy/team_internal_id.sql @@ -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; diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 1a69dcf136c..1ef15561291 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -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 diff --git a/pkg/registry/apis/iam/team/store.go b/pkg/registry/apis/iam/team/store.go index ae6f78291fd..864e5a36df8 100644 --- a/pkg/registry/apis/iam/team/store.go +++ b/pkg/registry/apis/iam/team/store.go @@ -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 }