Teams: Add apis for team membership (#92204)

* Add TeamBinding resource

* Implement read api:s for TeamBindings
pull/92387/head
Karl Persson 9 months ago committed by GitHub
parent 2ba930ab1f
commit 2872e11c13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 26
      pkg/apis/identity/v0alpha1/register.go
  2. 49
      pkg/apis/identity/v0alpha1/types_team.go
  3. 114
      pkg/apis/identity/v0alpha1/zz_generated.deepcopy.go
  4. 171
      pkg/apis/identity/v0alpha1/zz_generated.openapi.go
  5. 1
      pkg/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list
  6. 30
      pkg/registry/apis/identity/common/common.go
  7. 92
      pkg/registry/apis/identity/legacy/legacy_sql.go
  8. 29
      pkg/registry/apis/identity/legacy/queries.go
  9. 30
      pkg/registry/apis/identity/legacy/queries_test.go
  10. 18
      pkg/registry/apis/identity/legacy/query_team_bindings.sql
  11. 7
      pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_1_bindings.sql
  12. 11
      pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_bindings_page_1.sql
  13. 12
      pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_bindings_page_2.sql
  14. 7
      pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_1_bindings.sql
  15. 11
      pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_bindings_page_1.sql
  16. 12
      pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_bindings_page_2.sql
  17. 7
      pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_1_bindings.sql
  18. 11
      pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_bindings_page_1.sql
  19. 12
      pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_bindings_page_2.sql
  20. 48
      pkg/registry/apis/identity/legacy/types.go
  21. 3
      pkg/registry/apis/identity/register.go
  22. 171
      pkg/registry/apis/identity/team/store_binding.go
  23. 0
      pkg/registry/apis/identity/team/store_user_team.go

@ -121,6 +121,30 @@ var SSOSettingResourceInfo = common.NewResourceInfo(
},
)
var TeamBindingResourceInfo = common.NewResourceInfo(
GROUP, VERSION, "teambindings", "teambinding", "TeamBinding",
func() runtime.Object { return &TeamBinding{} },
func() runtime.Object { return &TeamBindingList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Team", Type: "string"},
{Name: "Created At", Type: "string", Format: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*TeamBinding)
if !ok {
return nil, fmt.Errorf("expected team binding")
}
return []interface{}{
m.Name,
m.Spec.TeamRef.Name,
m.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
},
},
)
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
@ -144,6 +168,8 @@ func AddKnownTypes(scheme *runtime.Scheme, version string) {
&IdentityDisplayResults{},
&SSOSetting{},
&SSOSettingList{},
&TeamBinding{},
&TeamBindingList{},
)
}

@ -1,6 +1,8 @@
package v0alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Team struct {
@ -9,6 +11,7 @@ type Team struct {
Spec TeamSpec `json:"spec,omitempty"`
}
type TeamSpec struct {
Title string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
@ -21,3 +24,47 @@ type TeamList struct {
Items []Team `json:"items,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TeamBinding struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TeamBindingSpec `json:"spec,omitempty"`
}
type TeamBindingSpec struct {
Subjects []TeamSubject `json:"subjects,omitempty"`
TeamRef TeamRef `json:"teamRef,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TeamBindingList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TeamBinding `json:"items,omitempty"`
}
type TeamSubject struct {
// Name is the unique identifier for subject.
Name string `json:"name,omitempty"`
// Permission subject has in permission.
// Can be either admin or member.
Permission TeamPermission `json:"permission,omitempty"`
}
type TeamRef struct {
// Name is the unique identifier for a team.
Name string `json:"name,omitempty"`
}
// TeamPermission for subject
// +enum
type TeamPermission string
const (
TeamPermissionAdmin TeamPermission = "admin"
TeamPermissionMember TeamPermission = "member"
)

@ -247,6 +247,88 @@ func (in *Team) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamBinding) DeepCopyInto(out *TeamBinding) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamBinding.
func (in *TeamBinding) DeepCopy() *TeamBinding {
if in == nil {
return nil
}
out := new(TeamBinding)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TeamBinding) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamBindingList) DeepCopyInto(out *TeamBindingList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TeamBinding, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamBindingList.
func (in *TeamBindingList) DeepCopy() *TeamBindingList {
if in == nil {
return nil
}
out := new(TeamBindingList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TeamBindingList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamBindingSpec) DeepCopyInto(out *TeamBindingSpec) {
*out = *in
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]TeamSubject, len(*in))
copy(*out, *in)
}
out.TeamRef = in.TeamRef
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamBindingSpec.
func (in *TeamBindingSpec) DeepCopy() *TeamBindingSpec {
if in == nil {
return nil
}
out := new(TeamBindingSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamList) DeepCopyInto(out *TeamList) {
*out = *in
@ -280,6 +362,22 @@ func (in *TeamList) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamRef) DeepCopyInto(out *TeamRef) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamRef.
func (in *TeamRef) DeepCopy() *TeamRef {
if in == nil {
return nil
}
out := new(TeamRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamSpec) DeepCopyInto(out *TeamSpec) {
*out = *in
@ -296,6 +394,22 @@ func (in *TeamSpec) DeepCopy() *TeamSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamSubject) DeepCopyInto(out *TeamSubject) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamSubject.
func (in *TeamSubject) DeepCopy() *TeamSubject {
if in == nil {
return nil
}
out := new(TeamSubject)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *User) DeepCopyInto(out *User) {
*out = *in

@ -23,8 +23,13 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.ServiceAccountList": schema_pkg_apis_identity_v0alpha1_ServiceAccountList(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.ServiceAccountSpec": schema_pkg_apis_identity_v0alpha1_ServiceAccountSpec(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.Team": schema_pkg_apis_identity_v0alpha1_Team(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBinding": schema_pkg_apis_identity_v0alpha1_TeamBinding(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBindingList": schema_pkg_apis_identity_v0alpha1_TeamBindingList(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBindingSpec": schema_pkg_apis_identity_v0alpha1_TeamBindingSpec(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamList": schema_pkg_apis_identity_v0alpha1_TeamList(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamRef": schema_pkg_apis_identity_v0alpha1_TeamRef(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamSpec": schema_pkg_apis_identity_v0alpha1_TeamSpec(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamSubject": schema_pkg_apis_identity_v0alpha1_TeamSubject(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.User": schema_pkg_apis_identity_v0alpha1_User(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserList": schema_pkg_apis_identity_v0alpha1_UserList(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserSpec": schema_pkg_apis_identity_v0alpha1_UserSpec(ref),
@ -449,6 +454,126 @@ func schema_pkg_apis_identity_v0alpha1_Team(ref common.ReferenceCallback) common
}
}
func schema_pkg_apis_identity_v0alpha1_TeamBinding(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBindingSpec"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBindingSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_identity_v0alpha1_TeamBindingList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBinding"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBinding", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_identity_v0alpha1_TeamBindingSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"subjects": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamSubject"),
},
},
},
},
},
"teamRef": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamRef"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamRef", "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamSubject"},
}
}
func schema_pkg_apis_identity_v0alpha1_TeamList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@ -496,6 +621,25 @@ func schema_pkg_apis_identity_v0alpha1_TeamList(ref common.ReferenceCallback) co
}
}
func schema_pkg_apis_identity_v0alpha1_TeamRef(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Description: "Name is the unique identifier for a team.",
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
func schema_pkg_apis_identity_v0alpha1_TeamSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@ -520,6 +664,33 @@ func schema_pkg_apis_identity_v0alpha1_TeamSpec(ref common.ReferenceCallback) co
}
}
func schema_pkg_apis_identity_v0alpha1_TeamSubject(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Description: "Name is the unique identifier for subject.",
Type: []string{"string"},
Format: "",
},
},
"permission": {
SchemaProps: spec.SchemaProps{
Description: "Permission subject has in permission. Can be either admin or member.\n\nPossible enum values:\n - `\"admin\"`\n - `\"member\"`",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"admin", "member"},
},
},
},
},
},
}
}
func schema_pkg_apis_identity_v0alpha1_User(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

@ -1,3 +1,4 @@
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,TeamBindingSpec,Subjects
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,IdentityDisplay,IdentityType
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,IdentityDisplay,InternalID
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,TeamSpec,Title

@ -0,0 +1,30 @@
package common
import (
"fmt"
"strconv"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
)
// GetContinueID is a helper to parse options.Continue as int64.
// If no continue token is provided 0 is returned.
func GetContinueID(options *internalversion.ListOptions) (int64, error) {
if options.Continue != "" {
continueID, err := strconv.ParseInt(options.Continue, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid continue token: %w", err)
}
return continueID, nil
}
return 0, nil
}
// OptonalFormatInt formats num as a string. If num is less or equal than 0
// an empty string is returned.
func OptionalFormatInt(num int64) string {
if num > 0 {
return strconv.FormatInt(num, 10)
}
return ""
}

@ -16,16 +16,16 @@ var (
_ LegacyIdentityStore = (*legacySQLStore)(nil)
)
type legacySQLStore struct {
sql legacysql.LegacyDatabaseProvider
}
func NewLegacySQLStores(sql legacysql.LegacyDatabaseProvider) LegacyIdentityStore {
return &legacySQLStore{
sql: sql,
}
}
type legacySQLStore struct {
sql legacysql.LegacyDatabaseProvider
}
// ListTeams implements LegacyIdentityStore.
func (s *legacySQLStore) ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error) {
if query.Limit < 1 {
@ -51,8 +51,6 @@ func (s *legacySQLStore) ListTeams(ctx context.Context, ns claims.NamespaceInfo,
}
q := rawQuery
// fmt.Printf("%s // %v\n", rawQuery, req.GetArgs())
res := &ListTeamResult{}
rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...)
defer func() {
@ -126,13 +124,10 @@ func (s *legacySQLStore) GetDisplay(ctx context.Context, ns claims.NamespaceInfo
}
func (s *legacySQLStore) queryUsers(ctx context.Context, sql *legacysql.LegacyDatabaseHelper, t *template.Template, req sqltemplate.Args, limit int) (*ListUserResult, error) {
rawQuery, err := sqltemplate.Execute(t, req)
q, err := sqltemplate.Execute(t, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlQueryUsers.Name(), err)
}
q := rawQuery
// fmt.Printf("%s // %v\n", rawQuery, req.GetArgs())
res := &ListUserResult{}
rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...)
@ -163,6 +158,83 @@ func (s *legacySQLStore) queryUsers(ctx context.Context, sql *legacysql.LegacyDa
return res, err
}
// ListTeamsBindings implements LegacyIdentityStore.
func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error) {
if query.Limit < 1 {
query.Limit = 50
}
limit := int(query.Limit)
query.Limit += 1 // for continue
query.OrgID = ns.OrgID
if query.OrgID == 0 {
return nil, fmt.Errorf("expected non zero orgID")
}
sql, err := s.sql(ctx)
if err != nil {
return nil, err
}
req := newListTeamBindings(sql, &query)
q, err := sqltemplate.Execute(sqlQueryTeamBindings, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeams.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
}
res := &ListTeamBindingsResult{}
grouped := map[string][]TeamMember{}
var lastID int64
var atTeamLimit bool
for rows.Next() {
m := TeamMember{}
err = rows.Scan(&m.ID, &m.TeamUID, &m.TeamID, &m.UserUID, &m.Created, &m.Updated, &m.Permission)
if err != nil {
return res, err
}
lastID = m.TeamID
members, ok := grouped[m.TeamUID]
if ok {
grouped[m.TeamUID] = append(members, m)
} else if !atTeamLimit {
grouped[m.TeamUID] = []TeamMember{m}
}
if len(grouped) >= limit {
atTeamLimit = true
res.ContinueID = lastID
}
}
if query.UID == "" {
res.RV, err = sql.GetResourceVersion(ctx, "team_member", "updated")
}
res.Bindings = make([]TeamBinding, 0, len(grouped))
for uid, members := range grouped {
res.Bindings = append(res.Bindings, TeamBinding{
TeamUID: uid,
Members: members,
})
}
return res, err
}
// GetUserTeams implements LegacyIdentityStore.
func (s *legacySQLStore) GetUserTeams(ctx context.Context, ns claims.NamespaceInfo, uid string) ([]team.Team, error) {
panic("unimplemented")

@ -26,9 +26,10 @@ func mustTemplate(filename string) *template.Template {
// Templates.
var (
sqlQueryTeams = mustTemplate("query_teams.sql")
sqlQueryUsers = mustTemplate("query_users.sql")
sqlQueryDisplay = mustTemplate("query_display.sql")
sqlQueryTeams = mustTemplate("query_teams.sql")
sqlQueryUsers = mustTemplate("query_users.sql")
sqlQueryDisplay = mustTemplate("query_display.sql")
sqlQueryTeamBindings = mustTemplate("query_team_bindings.sql")
)
type sqlQueryListUsers struct {
@ -88,3 +89,25 @@ func newGetDisplay(sql *legacysql.LegacyDatabaseHelper, q *GetUserDisplayQuery)
func (r sqlQueryGetDisplay) Validate() error {
return nil // TODO
}
type sqlQueryListTeamBindings struct {
sqltemplate.SQLTemplate
Query *ListTeamBindingsQuery
UserTable string
TeamTable string
TeamMemberTable string
}
func (r sqlQueryListTeamBindings) Validate() error {
return nil // TODO
}
func newListTeamBindings(sql *legacysql.LegacyDatabaseHelper, q *ListTeamBindingsQuery) sqlQueryListTeamBindings {
return sqlQueryListTeamBindings{
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
UserTable: sql.Table("user"),
TeamTable: sql.Table("team"),
TeamMemberTable: sql.Table("team_member"),
Query: q,
}
}

@ -35,6 +35,12 @@ func TestQueries(t *testing.T) {
return &v
}
listTeamBindings := func(q *ListTeamBindingsQuery) sqltemplate.SQLTemplate {
v := newListTeamBindings(nodb, q)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
RootDir: "testdata",
Templates: map[*template.Template][]mocks.TemplateTestCase{
@ -104,6 +110,30 @@ func TestQueries(t *testing.T) {
}),
},
},
sqlQueryTeamBindings: {
{
Name: "team_1_bindings",
Data: listTeamBindings(&ListTeamBindingsQuery{
OrgID: 1,
UID: "team-1",
}),
},
{
Name: "team_bindings_page_1",
Data: listTeamBindings(&ListTeamBindingsQuery{
OrgID: 1,
Limit: 5,
}),
},
{
Name: "team_bindings_page_2",
Data: listTeamBindings(&ListTeamBindingsQuery{
OrgID: 1,
Limit: 5,
ContinueID: 5,
}),
},
},
},
})
}

@ -0,0 +1,18 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission FROM {{ .Ident .TeamMemberTable }} tm
INNER JOIN {{ .Ident .TeamTable }} t ON tm.team_id = t.id
INNER JOIN {{ .Ident .UserTable }} u ON tm.user_id = u.id
WHERE
{{ if .Query.UID }}
t.uid = {{ .Arg .Query.UID }}
{{ else }}
t.uid IN(
SELECT uid
FROM {{ .Ident .TeamTable }} t
{{ if .Query.ContinueID }}
WHERE t.id >= {{ .Arg .Query.ContinueID }}
{{ end }}
ORDER BY t.id ASC LIMIT {{ .Arg .Query.Limit }}
)
{{ end }}
AND tm.org_id = {{ .Arg .Query.OrgID}}
ORDER BY t.id ASC;

@ -0,0 +1,7 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission FROM "grafana.team_member" tm
INNER JOIN "grafana.team" t ON tm.team_id = t.id
INNER JOIN "grafana.user" u ON tm.user_id = u.id
WHERE
t.uid = 'team-1'
AND tm.org_id = 1
ORDER BY t.id ASC;

@ -0,0 +1,11 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission FROM "grafana.team_member" tm
INNER JOIN "grafana.team" t ON tm.team_id = t.id
INNER JOIN "grafana.user" u ON tm.user_id = u.id
WHERE
t.uid IN(
SELECT uid
FROM "grafana.team" t
ORDER BY t.id ASC LIMIT 5
)
AND tm.org_id = 1
ORDER BY t.id ASC;

@ -0,0 +1,12 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission FROM "grafana.team_member" tm
INNER JOIN "grafana.team" t ON tm.team_id = t.id
INNER JOIN "grafana.user" u ON tm.user_id = u.id
WHERE
t.uid IN(
SELECT uid
FROM "grafana.team" t
WHERE t.id >= 5
ORDER BY t.id ASC LIMIT 5
)
AND tm.org_id = 1
ORDER BY t.id ASC;

@ -0,0 +1,7 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission FROM "grafana.team_member" tm
INNER JOIN "grafana.team" t ON tm.team_id = t.id
INNER JOIN "grafana.user" u ON tm.user_id = u.id
WHERE
t.uid = 'team-1'
AND tm.org_id = 1
ORDER BY t.id ASC;

@ -0,0 +1,11 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission FROM "grafana.team_member" tm
INNER JOIN "grafana.team" t ON tm.team_id = t.id
INNER JOIN "grafana.user" u ON tm.user_id = u.id
WHERE
t.uid IN(
SELECT uid
FROM "grafana.team" t
ORDER BY t.id ASC LIMIT 5
)
AND tm.org_id = 1
ORDER BY t.id ASC;

@ -0,0 +1,12 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission FROM "grafana.team_member" tm
INNER JOIN "grafana.team" t ON tm.team_id = t.id
INNER JOIN "grafana.user" u ON tm.user_id = u.id
WHERE
t.uid IN(
SELECT uid
FROM "grafana.team" t
WHERE t.id >= 5
ORDER BY t.id ASC LIMIT 5
)
AND tm.org_id = 1
ORDER BY t.id ASC;

@ -0,0 +1,7 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission FROM "grafana.team_member" tm
INNER JOIN "grafana.team" t ON tm.team_id = t.id
INNER JOIN "grafana.user" u ON tm.user_id = u.id
WHERE
t.uid = 'team-1'
AND tm.org_id = 1
ORDER BY t.id ASC;

@ -0,0 +1,11 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission FROM "grafana.team_member" tm
INNER JOIN "grafana.team" t ON tm.team_id = t.id
INNER JOIN "grafana.user" u ON tm.user_id = u.id
WHERE
t.uid IN(
SELECT uid
FROM "grafana.team" t
ORDER BY t.id ASC LIMIT 5
)
AND tm.org_id = 1
ORDER BY t.id ASC;

@ -0,0 +1,12 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission FROM "grafana.team_member" tm
INNER JOIN "grafana.team" t ON tm.team_id = t.id
INNER JOIN "grafana.user" u ON tm.user_id = u.id
WHERE
t.uid IN(
SELECT uid
FROM "grafana.team" t
WHERE t.id >= 5
ORDER BY t.id ASC LIMIT 5
)
AND tm.org_id = 1
ORDER BY t.id ASC;

@ -2,8 +2,10 @@ package legacy
import (
"context"
"time"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
)
@ -22,6 +24,12 @@ type ListUserResult struct {
RV int64
}
type GetUserDisplayQuery struct {
OrgID int64
UIDs []string
IDs []int64
}
type ListTeamQuery struct {
OrgID int64
UID string
@ -35,16 +43,46 @@ type ListTeamResult struct {
RV int64
}
type GetUserDisplayQuery struct {
OrgID int64
UIDs []string
IDs []int64
type TeamMember struct {
ID int64
TeamID int64
TeamUID string
UserUID string
Updated time.Time
Created time.Time
Permission team.PermissionType
}
func (m TeamMember) MemberID() string {
return identity.NewTypedIDString(claims.TypeUser, m.UserUID)
}
type TeamBinding struct {
TeamUID string
Members []TeamMember
}
type ListTeamBindingsQuery struct {
// UID is team uid to list bindings for. If not set store should list bindings for all teams
UID string
OrgID int64
ContinueID int64 // ContinueID
Limit int64
}
type ListTeamBindingsResult struct {
Bindings []TeamBinding
ContinueID int64
RV int64
}
// In every case, RBAC should be applied before calling, or before returning results to the requester
type LegacyIdentityStore interface {
ListUsers(ctx context.Context, ns claims.NamespaceInfo, query ListUserQuery) (*ListUserResult, error)
ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error)
GetDisplay(ctx context.Context, ns claims.NamespaceInfo, query GetUserDisplayQuery) (*ListUserResult, error)
ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error)
ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error)
GetUserTeams(ctx context.Context, ns claims.NamespaceInfo, uid string) ([]team.Team, error)
}

@ -83,6 +83,9 @@ func (b *IdentityAPIBuilder) GetAPIGroupInfo(
teamResource := identityv0.TeamResourceInfo
storage[teamResource.StoragePath()] = team.NewLegacyStore(b.Store)
teamBindingResource := identityv0.TeamBindingResourceInfo
storage[teamBindingResource.StoragePath()] = team.NewLegacyBindingStore(b.Store)
userResource := identityv0.UserResourceInfo
storage[userResource.StoragePath()] = user.NewLegacyStore(b.Store)
storage[userResource.StoragePath("teams")] = team.NewLegacyUserTeamsStore(b.Store)

@ -0,0 +1,171 @@
package team
import (
"context"
"strconv"
"time"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/authlib/claims"
identityv0 "github.com/grafana/grafana/pkg/apis/identity/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/identity/common"
"github.com/grafana/grafana/pkg/registry/apis/identity/legacy"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/team"
)
var bindingResource = identityv0.TeamBindingResourceInfo
var (
_ rest.Storage = (*LegacyBindingStore)(nil)
_ rest.Scoper = (*LegacyBindingStore)(nil)
_ rest.SingularNameProvider = (*LegacyBindingStore)(nil)
_ rest.Getter = (*LegacyBindingStore)(nil)
_ rest.Lister = (*LegacyBindingStore)(nil)
)
func NewLegacyBindingStore(store legacy.LegacyIdentityStore) *LegacyBindingStore {
return &LegacyBindingStore{store}
}
type LegacyBindingStore struct {
store legacy.LegacyIdentityStore
}
// Destroy implements rest.Storage.
func (l *LegacyBindingStore) Destroy() {}
// New implements rest.Storage.
func (l *LegacyBindingStore) New() runtime.Object {
return bindingResource.NewFunc()
}
// NewList implements rest.Lister.
func (l *LegacyBindingStore) NewList() runtime.Object {
return bindingResource.NewListFunc()
}
// NamespaceScoped implements rest.Scoper.
func (l *LegacyBindingStore) NamespaceScoped() bool {
return true
}
// GetSingularName implements rest.SingularNameProvider.
func (l *LegacyBindingStore) GetSingularName() string {
return bindingResource.GetSingularName()
}
// ConvertToTable implements rest.Lister.
func (l *LegacyBindingStore) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return bindingResource.TableConverter().ConvertToTable(ctx, object, tableOptions)
}
// Get implements rest.Getter.
func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
res, err := l.store.ListTeamBindings(ctx, ns, legacy.ListTeamBindingsQuery{
UID: name,
Limit: 1,
})
if err != nil {
return nil, err
}
if len(res.Bindings) != 1 {
// FIXME: maybe empty result?
return nil, resource.NewNotFound(name)
}
obj := mapToBindingObject(ns, res.Bindings[0])
return &obj, nil
}
// List implements rest.Lister.
func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
continueID, err := common.GetContinueID(options)
if err != nil {
return nil, err
}
res, err := l.store.ListTeamBindings(ctx, ns, legacy.ListTeamBindingsQuery{
ContinueID: continueID,
Limit: options.Limit,
})
if err != nil {
return nil, err
}
list := identityv0.TeamBindingList{
Items: make([]identityv0.TeamBinding, 0, len(res.Bindings)),
}
for _, b := range res.Bindings {
list.Items = append(list.Items, mapToBindingObject(ns, b))
}
list.ListMeta.Continue = common.OptionalFormatInt(res.ContinueID)
list.ListMeta.ResourceVersion = common.OptionalFormatInt(res.RV)
return &list, nil
}
func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) identityv0.TeamBinding {
rv := time.Time{}
ct := time.Now()
for _, m := range b.Members {
if m.Updated.After(rv) {
rv = m.Updated
}
if m.Created.Before(ct) {
ct = m.Created
}
}
return identityv0.TeamBinding{
ObjectMeta: metav1.ObjectMeta{
Name: b.TeamUID,
Namespace: ns.Value,
ResourceVersion: strconv.FormatInt(rv.UnixMilli(), 10),
CreationTimestamp: metav1.NewTime(ct),
},
Spec: identityv0.TeamBindingSpec{
TeamRef: identityv0.TeamRef{
Name: b.TeamUID,
},
Subjects: mapToSubjects(b.Members),
},
}
}
func mapToSubjects(members []legacy.TeamMember) []identityv0.TeamSubject {
out := make([]identityv0.TeamSubject, 0, len(members))
for _, m := range members {
out = append(out, identityv0.TeamSubject{
Name: m.MemberID(),
Permission: mapPermisson(m.Permission),
})
}
return out
}
func mapPermisson(p team.PermissionType) identityv0.TeamPermission {
if p == team.PermissionTypeAdmin {
return identityv0.TeamPermissionAdmin
} else {
return identityv0.TeamPermissionMember
}
}
Loading…
Cancel
Save