From 76e803cc3c8bb1912662f8e8d78ffd8245af74aa Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Fri, 6 Nov 2020 08:33:24 +0100 Subject: [PATCH] Implement support for labels on system resources --- store/rdbms/applications.go | 4 ++ store/rdbms/roles.go | 4 ++ store/rdbms/users.go | 4 ++ system/rest.yaml | 40 +++++++++++ system/rest/application.go | 7 +- system/rest/request/application.go | 69 +++++++++++++++++++ system/rest/request/role.go | 69 +++++++++++++++++++ system/rest/request/user.go | 69 +++++++++++++++++++ system/rest/role.go | 5 +- system/rest/user.go | 3 + system/service/application.go | 59 +++++++++++++++-- system/service/role.go | 96 ++++++++++++++++++++++++--- system/service/user.go | 103 ++++++++++++++++++++++++++--- system/types/applications.go | 5 ++ system/types/role.go | 12 +++- system/types/type_labels.gen.go | 81 +++++++++++++++++++++++ system/types/types.yaml | 3 + system/types/user.go | 5 ++ tests/system/application_test.go | 83 +++++++++++++++++++++++ tests/system/role_test.go | 83 +++++++++++++++++++++++ tests/system/user_test.go | 83 +++++++++++++++++++++++ 21 files changed, 859 insertions(+), 28 deletions(-) create mode 100644 system/types/type_labels.gen.go diff --git a/store/rdbms/applications.go b/store/rdbms/applications.go index f539926ee..11e43ff5e 100644 --- a/store/rdbms/applications.go +++ b/store/rdbms/applications.go @@ -12,6 +12,10 @@ func (s Store) convertApplicationFilter(f types.ApplicationFilter) (query squirr query = filter.StateCondition(query, "app.deleted_at", f.Deleted) + if len(f.LabeledIDs) > 0 { + query = query.Where(squirrel.Eq{"app.id": f.LabeledIDs}) + } + if f.Query != "" { qs := f.Query + "%" query = query.Where(squirrel.Or{ diff --git a/store/rdbms/roles.go b/store/rdbms/roles.go index 2aa15467f..017c17d62 100644 --- a/store/rdbms/roles.go +++ b/store/rdbms/roles.go @@ -17,6 +17,10 @@ func (s Store) convertRoleFilter(f types.RoleFilter) (query squirrel.SelectBuild query = query.Where(squirrel.Eq{"rl.ID": f.RoleID}) } + if len(f.LabeledIDs) > 0 { + query = query.Where(squirrel.Eq{"rl.id": f.LabeledIDs}) + } + if f.MemberID > 0 { query = query.Where(squirrel.Expr("rl.ID IN (SELECT rel_role FROM role_members AS m WHERE m.rel_user = ?)", f.MemberID)) } diff --git a/store/rdbms/users.go b/store/rdbms/users.go index 186a5afc2..c3c43457c 100644 --- a/store/rdbms/users.go +++ b/store/rdbms/users.go @@ -18,6 +18,10 @@ func (s Store) convertUserFilter(f types.UserFilter) (query squirrel.SelectBuild query = query.Where(squirrel.Eq{"usr.ID": f.UserID}) } + if len(f.LabeledIDs) > 0 { + query = query.Where(squirrel.Eq{"usr.id": f.LabeledIDs}) + } + if len(f.RoleID) > 0 { or := squirrel.Or{} // Due to lack of support for more exotic expressions (slice of values inside subquery) diff --git a/system/rest.yaml b/system/rest.yaml index fa6c3ba19..b530a47df 100644 --- a/system/rest.yaml +++ b/system/rest.yaml @@ -234,6 +234,8 @@ endpoints: authentication: - Client ID - Session ID + imports: + - github.com/cortezaproject/corteza-server/pkg/label apis: - name: list method: GET @@ -253,6 +255,10 @@ endpoints: required: false title: Exclude (0, default), include (1) or return only (2) achived roles type: uint + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: uint name: limit title: Limit @@ -280,6 +286,10 @@ endpoints: name: members required: false title: Role member IDs + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - name: update method: PUT title: Update role details @@ -303,6 +313,10 @@ endpoints: name: members required: false title: Role member IDs + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - name: read method: GET title: Read role details and memberships @@ -443,6 +457,7 @@ endpoints: - Client ID - Session ID imports: + - github.com/cortezaproject/corteza-server/pkg/label - github.com/cortezaproject/corteza-server/system/types apis: - name: list @@ -495,6 +510,10 @@ endpoints: required: false title: Exclude (0, default), include (1) or return only (2) suspended users type: uint + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: uint name: limit title: Limit @@ -526,6 +545,10 @@ endpoints: type: types.UserKind required: false title: Kind (normal, bot) + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - name: update method: PUT title: Update user details @@ -553,6 +576,10 @@ endpoints: type: types.UserKind required: false title: Kind (normal, bot) + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - name: read method: GET title: Read user details @@ -677,6 +704,7 @@ endpoints: entrypoint: application authentication: [] imports: + - github.com/cortezaproject/corteza-server/pkg/label - sqlxTypes github.com/jmoiron/sqlx/types apis: - name: list @@ -697,6 +725,10 @@ endpoints: required: false title: Exclude (0, default), include (1) or return only (2) deleted roles type: uint + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: uint name: limit title: Limit @@ -728,6 +760,10 @@ endpoints: type: sqlxTypes.JSONText required: false title: Arbitrary JSON holding application configuration + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - name: update method: PUT title: Update user details @@ -755,6 +791,10 @@ endpoints: type: sqlxTypes.JSONText required: false title: Arbitrary JSON holding application configuration + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - name: read method: GET title: Read application details diff --git a/system/rest/application.go b/system/rest/application.go index 7b9a95959..525e31458 100644 --- a/system/rest/application.go +++ b/system/rest/application.go @@ -61,8 +61,9 @@ func (ctrl *Application) List(ctx context.Context, r *request.ApplicationList) ( var ( err error f = types.ApplicationFilter{ - Name: r.Name, - Query: r.Query, + Name: r.Name, + Query: r.Query, + Labels: r.Labels, Deleted: filter.State(r.Deleted), } @@ -86,6 +87,7 @@ func (ctrl *Application) Create(ctx context.Context, r *request.ApplicationCreat app = &types.Application{ Name: r.Name, Enabled: r.Enabled, + Labels: r.Labels, } ) @@ -107,6 +109,7 @@ func (ctrl *Application) Update(ctx context.Context, r *request.ApplicationUpdat ID: r.ApplicationID, Name: r.Name, Enabled: r.Enabled, + Labels: r.Labels, } ) diff --git a/system/rest/request/application.go b/system/rest/request/application.go index 1e21e5257..0f0045e7c 100644 --- a/system/rest/request/application.go +++ b/system/rest/request/application.go @@ -11,6 +11,7 @@ package request import ( "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/go-chi/chi" sqlxTypes "github.com/jmoiron/sqlx/types" @@ -46,6 +47,11 @@ type ( // Exclude (0, default), include (1) or return only (2) deleted roles Deleted uint + // Labels GET parameter + // + // Labels + Labels map[string]string + // Limit GET parameter // // Limit @@ -82,6 +88,11 @@ type ( // // Arbitrary JSON holding application configuration Config sqlxTypes.JSONText + + // Labels POST parameter + // + // Labels + Labels map[string]string } ApplicationUpdate struct { @@ -109,6 +120,11 @@ type ( // // Arbitrary JSON holding application configuration Config sqlxTypes.JSONText + + // Labels POST parameter + // + // Labels + Labels map[string]string } ApplicationRead struct { @@ -156,6 +172,7 @@ func (r ApplicationList) Auditable() map[string]interface{} { "name": r.Name, "query": r.Query, "deleted": r.Deleted, + "labels": r.Labels, "limit": r.Limit, "pageCursor": r.PageCursor, "sort": r.Sort, @@ -177,6 +194,11 @@ func (r ApplicationList) GetDeleted() uint { return r.Deleted } +// Auditable returns all auditable/loggable parameters +func (r ApplicationList) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r ApplicationList) GetLimit() uint { return r.Limit @@ -227,6 +249,17 @@ func (r *ApplicationList) Fill(req *http.Request) (err error) { return err } } + if val, ok := tmp["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := tmp["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } if val, ok := tmp["limit"]; ok && len(val) > 0 { r.Limit, err = payload.ParseUint(val[0]), nil if err != nil { @@ -262,6 +295,7 @@ func (r ApplicationCreate) Auditable() map[string]interface{} { "enabled": r.Enabled, "unify": r.Unify, "config": r.Config, + "labels": r.Labels, } } @@ -285,6 +319,11 @@ func (r ApplicationCreate) GetConfig() sqlxTypes.JSONText { return r.Config } +// Auditable returns all auditable/loggable parameters +func (r ApplicationCreate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *ApplicationCreate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -332,6 +371,18 @@ func (r *ApplicationCreate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } return err @@ -350,6 +401,7 @@ func (r ApplicationUpdate) Auditable() map[string]interface{} { "enabled": r.Enabled, "unify": r.Unify, "config": r.Config, + "labels": r.Labels, } } @@ -378,6 +430,11 @@ func (r ApplicationUpdate) GetConfig() sqlxTypes.JSONText { return r.Config } +// Auditable returns all auditable/loggable parameters +func (r ApplicationUpdate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *ApplicationUpdate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -425,6 +482,18 @@ func (r *ApplicationUpdate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } { diff --git a/system/rest/request/role.go b/system/rest/request/role.go index 0e2dd5e17..f91206f67 100644 --- a/system/rest/request/role.go +++ b/system/rest/request/role.go @@ -11,6 +11,7 @@ package request import ( "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/go-chi/chi" "io" @@ -45,6 +46,11 @@ type ( // Exclude (0, default), include (1) or return only (2) achived roles Archived uint + // Labels GET parameter + // + // Labels + Labels map[string]string + // Limit GET parameter // // Limit @@ -76,6 +82,11 @@ type ( // // Role member IDs Members []string + + // Labels POST parameter + // + // Labels + Labels map[string]string } RoleUpdate struct { @@ -98,6 +109,11 @@ type ( // // Role member IDs Members []string + + // Labels POST parameter + // + // Labels + Labels map[string]string } RoleRead struct { @@ -214,6 +230,7 @@ func (r RoleList) Auditable() map[string]interface{} { "query": r.Query, "deleted": r.Deleted, "archived": r.Archived, + "labels": r.Labels, "limit": r.Limit, "pageCursor": r.PageCursor, "sort": r.Sort, @@ -235,6 +252,11 @@ func (r RoleList) GetArchived() uint { return r.Archived } +// Auditable returns all auditable/loggable parameters +func (r RoleList) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r RoleList) GetLimit() uint { return r.Limit @@ -285,6 +307,17 @@ func (r *RoleList) Fill(req *http.Request) (err error) { return err } } + if val, ok := tmp["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := tmp["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } if val, ok := tmp["limit"]; ok && len(val) > 0 { r.Limit, err = payload.ParseUint(val[0]), nil if err != nil { @@ -319,6 +352,7 @@ func (r RoleCreate) Auditable() map[string]interface{} { "name": r.Name, "handle": r.Handle, "members": r.Members, + "labels": r.Labels, } } @@ -337,6 +371,11 @@ func (r RoleCreate) GetMembers() []string { return r.Members } +// Auditable returns all auditable/loggable parameters +func (r RoleCreate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *RoleCreate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -377,6 +416,18 @@ func (r *RoleCreate) Fill(req *http.Request) (err error) { // return err // } //} + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } return err @@ -394,6 +445,7 @@ func (r RoleUpdate) Auditable() map[string]interface{} { "name": r.Name, "handle": r.Handle, "members": r.Members, + "labels": r.Labels, } } @@ -417,6 +469,11 @@ func (r RoleUpdate) GetMembers() []string { return r.Members } +// Auditable returns all auditable/loggable parameters +func (r RoleUpdate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *RoleUpdate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -457,6 +514,18 @@ func (r *RoleUpdate) Fill(req *http.Request) (err error) { // return err // } //} + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } { diff --git a/system/rest/request/user.go b/system/rest/request/user.go index 23925ef63..cdb8dd36d 100644 --- a/system/rest/request/user.go +++ b/system/rest/request/user.go @@ -11,6 +11,7 @@ package request import ( "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/cortezaproject/corteza-server/system/types" "github.com/go-chi/chi" @@ -86,6 +87,11 @@ type ( // Exclude (0, default), include (1) or return only (2) suspended users Suspended uint + // Labels GET parameter + // + // Labels + Labels map[string]string + // Limit GET parameter // // Limit @@ -122,6 +128,11 @@ type ( // // Kind (normal, bot) Kind types.UserKind + + // Labels POST parameter + // + // Labels + Labels map[string]string } UserUpdate struct { @@ -149,6 +160,11 @@ type ( // // Kind (normal, bot) Kind types.UserKind + + // Labels POST parameter + // + // Labels + Labels map[string]string } UserRead struct { @@ -261,6 +277,7 @@ func (r UserList) Auditable() map[string]interface{} { "incSuspended": r.IncSuspended, "deleted": r.Deleted, "suspended": r.Suspended, + "labels": r.Labels, "limit": r.Limit, "pageCursor": r.PageCursor, "sort": r.Sort, @@ -322,6 +339,11 @@ func (r UserList) GetSuspended() uint { return r.Suspended } +// Auditable returns all auditable/loggable parameters +func (r UserList) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r UserList) GetLimit() uint { return r.Limit @@ -430,6 +452,17 @@ func (r *UserList) Fill(req *http.Request) (err error) { return err } } + if val, ok := tmp["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := tmp["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } if val, ok := tmp["limit"]; ok && len(val) > 0 { r.Limit, err = payload.ParseUint(val[0]), nil if err != nil { @@ -465,6 +498,7 @@ func (r UserCreate) Auditable() map[string]interface{} { "name": r.Name, "handle": r.Handle, "kind": r.Kind, + "labels": r.Labels, } } @@ -488,6 +522,11 @@ func (r UserCreate) GetKind() types.UserKind { return r.Kind } +// Auditable returns all auditable/loggable parameters +func (r UserCreate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *UserCreate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -535,6 +574,18 @@ func (r *UserCreate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } return err @@ -553,6 +604,7 @@ func (r UserUpdate) Auditable() map[string]interface{} { "name": r.Name, "handle": r.Handle, "kind": r.Kind, + "labels": r.Labels, } } @@ -581,6 +633,11 @@ func (r UserUpdate) GetKind() types.UserKind { return r.Kind } +// Auditable returns all auditable/loggable parameters +func (r UserUpdate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *UserUpdate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -628,6 +685,18 @@ func (r *UserUpdate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } { diff --git a/system/rest/role.go b/system/rest/role.go index 1b7d8c9e1..d973de566 100644 --- a/system/rest/role.go +++ b/system/rest/role.go @@ -58,7 +58,8 @@ func (ctrl Role) List(ctx context.Context, r *request.RoleList) (interface{}, er var ( err error f = types.RoleFilter{ - Query: r.Query, + Query: r.Query, + Labels: r.Labels, Archived: filter.State(r.Archived), Deleted: filter.State(r.Deleted), @@ -83,6 +84,7 @@ func (ctrl Role) Create(ctx context.Context, r *request.RoleCreate) (interface{} role = &types.Role{ Name: r.Name, Handle: r.Handle, + Labels: r.Labels, } ) @@ -107,6 +109,7 @@ func (ctrl Role) Update(ctx context.Context, r *request.RoleUpdate) (interface{} ID: r.RoleID, Name: r.Name, Handle: r.Handle, + Labels: r.Labels, } ) diff --git a/system/rest/user.go b/system/rest/user.go index f049cb5f6..07f9fea04 100644 --- a/system/rest/user.go +++ b/system/rest/user.go @@ -45,6 +45,7 @@ func (ctrl User) List(ctx context.Context, r *request.UserList) (interface{}, er Username: r.Username, Handle: r.Handle, Kind: r.Kind, + Labels: r.Labels, Suspended: filter.State(r.Suspended), Deleted: filter.State(r.Deleted), } @@ -76,6 +77,7 @@ func (ctrl User) Create(ctx context.Context, r *request.UserCreate) (interface{} Name: r.Name, Handle: r.Handle, Kind: r.Kind, + Labels: r.Labels, } return ctrl.user.With(ctx).Create(user) @@ -88,6 +90,7 @@ func (ctrl User) Update(ctx context.Context, r *request.UserUpdate) (interface{} Name: r.Name, Handle: r.Handle, Kind: r.Kind, + Labels: r.Labels, } return ctrl.user.With(ctx).Update(user) diff --git a/system/service/application.go b/system/service/application.go index 25a9092c5..e0f518d3b 100644 --- a/system/service/application.go +++ b/system/service/application.go @@ -4,6 +4,7 @@ import ( "context" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/filter" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/service/event" "github.com/cortezaproject/corteza-server/system/types" @@ -14,7 +15,7 @@ type ( ac applicationAccessController eventbus eventDispatcher actionlog actionlog.Recorder - store store.Applications + store store.Storer } applicationAccessController interface { @@ -27,7 +28,7 @@ type ( ) // Application is a default application service initializer -func Application(s store.Applications, ac applicationAccessController, al actionlog.Recorder, eb eventDispatcher) *application { +func Application(s store.Storer, ac applicationAccessController, al actionlog.Recorder, eb eventDispatcher) *application { return &application{store: s, ac: ac, actionlog: al, eventbus: eb} } @@ -83,8 +84,29 @@ func (svc *application) Search(ctx context.Context, af types.ApplicationFilter) } } - aa, f, err = store.SearchApplications(ctx, svc.store, af) - return err + if len(af.Labels) > 0 { + af.LabeledIDs, err = label.Search( + ctx, + svc.store, + types.Application{}.LabelResourceKind(), + af.Labels, + ) + + if err != nil { + return err + } + } + + if aa, f, err = store.SearchApplications(ctx, svc.store, af); err != nil { + return err + } + + if err = label.Load(ctx, svc.store, toLabeledApplications(aa)...); err != nil { + return err + } + + return nil + }() return aa, f, svc.recordAction(ctx, aaProps, ApplicationActionSearch, err) @@ -116,7 +138,11 @@ func (svc *application) Create(ctx context.Context, new *types.Application) (app return } - aaProps.setApplication(app) + if err = label.Create(ctx, svc.store, new); err != nil { + return + } + + app = new _ = svc.eventbus.WaitFor(ctx, event.ApplicationAfterCreate(new, nil)) return nil @@ -162,6 +188,13 @@ func (svc *application) Update(ctx context.Context, upd *types.Application) (app return err } + if label.Changed(app.Labels, upd.Labels) { + if err = label.Update(ctx, svc.store, upd); err != nil { + return + } + app.Labels = upd.Labels + } + _ = svc.eventbus.WaitFor(ctx, event.ApplicationAfterUpdate(upd, app)) return nil }() @@ -244,3 +277,19 @@ func (svc *application) Undelete(ctx context.Context, ID uint64) (err error) { return svc.recordAction(ctx, aaProps, ApplicationActionUndelete, err) } + +// toLabeledApplications converts to []label.LabeledResource +// +// This function is auto-generated. +func toLabeledApplications(set []*types.Application) []label.LabeledResource { + if len(set) == 0 { + return nil + } + + ll := make([]label.LabeledResource, len(set)) + for i := range set { + ll[i] = set[i] + } + + return ll +} diff --git a/system/service/role.go b/system/service/role.go index 946ed2148..8c4d9a6e1 100644 --- a/system/service/role.go +++ b/system/service/role.go @@ -5,6 +5,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/handle" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/service/event" @@ -112,8 +113,33 @@ func (svc role) Find(filter types.RoleFilter) (rr types.RoleSet, f types.RoleFil } } - rr, f, err = store.SearchRoles(svc.ctx, svc.store, filter) - return err + if len(filter.Labels) > 0 { + filter.LabeledIDs, err = label.Search( + svc.ctx, + svc.store, + types.Role{}.LabelResourceKind(), + filter.Labels, + ) + + if err != nil { + return err + } + + // labels specified but no labeled resources found + if len(filter.LabeledIDs) == 0 { + return nil + } + } + + if rr, f, err = store.SearchRoles(svc.ctx, svc.store, filter); err != nil { + return err + } + + if err = label.Load(svc.ctx, svc.store, toLabeledRoles(rr)...); err != nil { + return err + } + + return nil }() return rr, f, svc.recordAction(svc.ctx, raProps, RoleActionSearch, err) @@ -125,9 +151,17 @@ func (svc role) FindByID(roleID uint64) (r *types.Role, err error) { ) err = func() error { - r, err = svc.findByID(roleID) + if r, err = svc.findByID(roleID); err != nil { + return err + } + raProps.setRole(r) - return err + + if err = label.Load(svc.ctx, svc.store, r); err != nil { + return err + } + + return nil }() return r, svc.recordAction(svc.ctx, raProps, RoleActionLookup, err) @@ -147,9 +181,17 @@ func (svc role) FindByName(name string) (r *types.Role, err error) { ) err = func() error { - r, err = store.LookupRoleByName(svc.ctx, svc.store, name) + if r, err = store.LookupRoleByName(svc.ctx, svc.store, name); err != nil { + return err + } + raProps.setRole(r) - return err + + if err = label.Load(svc.ctx, svc.store, r); err != nil { + return err + } + + return nil }() return r, svc.recordAction(svc.ctx, raProps, RoleActionLookup, err) @@ -161,9 +203,17 @@ func (svc role) FindByHandle(h string) (r *types.Role, err error) { ) err = func() error { - r, err = store.LookupRoleByName(svc.ctx, svc.store, h) + if r, err = store.LookupRoleByHandle(svc.ctx, svc.store, h); err != nil { + return err + } + raProps.setRole(r) - return err + + if err = label.Load(svc.ctx, svc.store, r); err != nil { + return err + } + + return nil }() return r, svc.recordAction(svc.ctx, raProps, RoleActionLookup, err) @@ -218,7 +268,11 @@ func (svc role) Create(new *types.Role) (r *types.Role, err error) { return } - raProps.setRole(r) + if err = label.Create(svc.ctx, svc.store, new); err != nil { + return + } + + r = new _ = svc.eventbus.WaitFor(svc.ctx, event.RoleAfterCreate(new, r)) return @@ -269,6 +323,14 @@ func (svc role) Update(upd *types.Role) (r *types.Role, err error) { return err } + if label.Changed(r.Labels, upd.Labels) { + if err = label.Update(svc.ctx, svc.store, upd); err != nil { + return + } + + r.Labels = upd.Labels + } + _ = svc.eventbus.WaitFor(svc.ctx, event.RoleAfterUpdate(upd, r)) return nil @@ -547,3 +609,19 @@ func (svc role) MemberRemove(roleID, memberID uint64) (err error) { return svc.recordAction(svc.ctx, raProps, RoleActionMemberRemove, err) } + +// toLabeledRoles converts to []label.LabeledResource +// +// This function is auto-generated. +func toLabeledRoles(set []*types.Role) []label.LabeledResource { + if len(set) == 0 { + return nil + } + + ll := make([]label.LabeledResource, len(set)) + for i := range set { + ll[i] = set[i] + } + + return ll +} diff --git a/system/service/user.go b/system/service/user.go index 1591c428d..7914be21c 100644 --- a/system/service/user.go +++ b/system/service/user.go @@ -8,6 +8,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/handle" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/service/event" "github.com/cortezaproject/corteza-server/system/types" @@ -142,8 +143,17 @@ func (svc user) FindByID(userID uint64) (u *types.User, err error) { return nil } - u, err = svc.proc(store.LookupUserByID(svc.ctx, svc.store, userID)) - return err + if u, err = svc.proc(store.LookupUserByID(svc.ctx, svc.store, userID)); err != nil { + return err + } + + uaProps.setUser(u) + + if err = label.Load(svc.ctx, svc.store, u); err != nil { + return err + } + + return nil }() return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err) @@ -155,8 +165,17 @@ func (svc user) FindByEmail(email string) (u *types.User, err error) { ) err = func() error { - u, err = svc.proc(store.LookupUserByEmail(svc.ctx, svc.store, email)) - return err + if u, err = svc.proc(store.LookupUserByEmail(svc.ctx, svc.store, email)); err != nil { + return err + } + + uaProps.setUser(u) + + if err = label.Load(svc.ctx, svc.store, u); err != nil { + return err + } + + return nil }() return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err) @@ -168,8 +187,17 @@ func (svc user) FindByUsername(username string) (u *types.User, err error) { ) err = func() error { - u, err = svc.proc(store.LookupUserByUsername(svc.ctx, svc.store, username)) - return err + if u, err = svc.proc(store.LookupUserByUsername(svc.ctx, svc.store, username)); err != nil { + return err + } + + uaProps.setUser(u) + + if err = label.Load(svc.ctx, svc.store, u); err != nil { + return err + } + + return nil }() return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err) @@ -181,8 +209,17 @@ func (svc user) FindByHandle(handle string) (u *types.User, err error) { ) err = func() error { - u, err = svc.proc(store.LookupUserByHandle(svc.ctx, svc.store, handle)) - return err + if u, err = svc.proc(store.LookupUserByHandle(svc.ctx, svc.store, handle)); err != nil { + return err + } + + uaProps.setUser(u) + + if err = label.Load(svc.ctx, svc.store, u); err != nil { + return err + } + + return nil }() return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err) @@ -273,11 +310,33 @@ func (svc user) Find(filter types.UserFilter) (uu types.UserSet, f types.UserFil } } + if len(filter.Labels) > 0 { + filter.LabeledIDs, err = label.Search( + svc.ctx, + svc.store, + types.User{}.LabelResourceKind(), + filter.Labels, + ) + + if err != nil { + return err + } + + // labels specified but no labeled resources found + if len(filter.LabeledIDs) == 0 { + return nil + } + } + uu, f, err = store.SearchUsers(svc.ctx, svc.store, filter) if err != nil { return err } + if err = label.Load(svc.ctx, svc.store, toLabeledUsers(uu)...); err != nil { + return err + } + return uu.Walk(func(u *types.User) error { svc.handlePrivateData(u) return nil @@ -343,6 +402,10 @@ func (svc user) Create(new *types.User) (u *types.User, err error) { return } + if err = label.Create(svc.ctx, svc.store, new); err != nil { + return + } + _ = svc.eventbus.WaitFor(svc.ctx, event.UserAfterCreate(new, u)) return }() @@ -405,6 +468,14 @@ func (svc user) Update(upd *types.User) (u *types.User, err error) { return } + if label.Changed(u.Labels, upd.Labels) { + if err = label.Update(svc.ctx, svc.store, upd); err != nil { + return + } + + u.Labels = upd.Labels + } + _ = svc.eventbus.WaitFor(svc.ctx, event.UserAfterUpdate(upd, u)) return }() @@ -726,3 +797,19 @@ func createHandle(ctx context.Context, s store.Users, u *types.User) { ) } } + +// toLabeledUsers converts to []label.LabeledResource +// +// This function is auto-generated. +func toLabeledUsers(set []*types.User) []label.LabeledResource { + if len(set) == 0 { + return nil + } + + ll := make([]label.LabeledResource, len(set)) + for i := range set { + ll[i] = set[i] + } + + return ll +} diff --git a/system/types/applications.go b/system/types/applications.go index e718c7f0e..957b6f661 100644 --- a/system/types/applications.go +++ b/system/types/applications.go @@ -20,6 +20,8 @@ type ( Unify *ApplicationUnify `json:"unify,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` UpdatedAt *time.Time `json:"updatedAt,omitempty"` DeletedAt *time.Time `json:"deletedAt,omitempty"` @@ -39,6 +41,9 @@ type ( Name string `json:"name"` Query string `json:"query"` + LabeledIDs []uint64 `json:"-"` + Labels map[string]string `json:"labels,omitempty"` + Deleted filter.State `json:"deleted"` // Check fn is called by store backend for each resource found function can diff --git a/system/types/role.go b/system/types/role.go index c12385faa..09d3a6a0e 100644 --- a/system/types/role.go +++ b/system/types/role.go @@ -10,9 +10,12 @@ import ( type ( // Role - An organisation may have many roles. Roles may have many channels available. Access to channels may be shared between roles. Role struct { - ID uint64 `json:"roleID,string"` - Name string `json:"name"` - Handle string `json:"handle"` + ID uint64 `json:"roleID,string"` + Name string `json:"name"` + Handle string `json:"handle"` + + Labels map[string]string `json:"labels,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` UpdatedAt *time.Time `json:"updatedAt,omitempty"` ArchivedAt *time.Time `json:"archivedAt,omitempty"` @@ -31,6 +34,9 @@ type ( Deleted filter.State `json:"deleted"` Archived filter.State `json:"archived"` + LabeledIDs []uint64 `json:"-"` + Labels map[string]string `json:"labels,omitempty"` + // Check fn is called by store backend for each resource found function can // modify the resource and return false if store should not return it // diff --git a/system/types/type_labels.gen.go b/system/types/type_labels.gen.go new file mode 100644 index 000000000..ab1ca52b6 --- /dev/null +++ b/system/types/type_labels.gen.go @@ -0,0 +1,81 @@ +package types + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// system/types/types.yaml + +// SetLabel adds new label to label map +func (m *Application) SetLabel(key string, value string) { + if m.Labels == nil { + m.Labels = make(map[string]string) + } + + m.Labels[key] = value +} + +// GetLabels adds new label to label map +func (m Application) GetLabels() map[string]string { + return m.Labels +} + +// GetLabels adds new label to label map +func (Application) LabelResourceKind() string { + return "application" +} + +// GetLabels adds new label to label map +func (m Application) LabelResourceID() uint64 { + return m.ID +} + +// SetLabel adds new label to label map +func (m *Role) SetLabel(key string, value string) { + if m.Labels == nil { + m.Labels = make(map[string]string) + } + + m.Labels[key] = value +} + +// GetLabels adds new label to label map +func (m Role) GetLabels() map[string]string { + return m.Labels +} + +// GetLabels adds new label to label map +func (Role) LabelResourceKind() string { + return "role" +} + +// GetLabels adds new label to label map +func (m Role) LabelResourceID() uint64 { + return m.ID +} + +// SetLabel adds new label to label map +func (m *User) SetLabel(key string, value string) { + if m.Labels == nil { + m.Labels = make(map[string]string) + } + + m.Labels[key] = value +} + +// GetLabels adds new label to label map +func (m User) GetLabels() map[string]string { + return m.Labels +} + +// GetLabels adds new label to label map +func (User) LabelResourceKind() string { + return "user" +} + +// GetLabels adds new label to label map +func (m User) LabelResourceID() uint64 { + return m.ID +} diff --git a/system/types/types.yaml b/system/types/types.yaml index a6557686d..80c1e7221 100644 --- a/system/types/types.yaml +++ b/system/types/types.yaml @@ -1,7 +1,10 @@ types: User: + labelResourceType: user Application: + labelResourceType: application Role: + labelResourceType: role RoleMember: noIdField: true Credentials: diff --git a/system/types/user.go b/system/types/user.go index f71157f90..04bce5d8a 100644 --- a/system/types/user.go +++ b/system/types/user.go @@ -25,6 +25,8 @@ type ( EmailConfirmed bool `json:"-"` + Labels map[string]string `json:"labels,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` UpdatedAt *time.Time `json:"updatedAt,omitempty"` SuspendedAt *time.Time `json:"suspendedAt,omitempty"` @@ -49,6 +51,9 @@ type ( Handle string `json:"handle"` Kind UserKind `json:"kind"` + LabeledIDs []uint64 `json:"-"` + Labels map[string]string `json:"labels,omitempty"` + Deleted filter.State `json:"deleted"` Suspended filter.State `json:"suspended"` diff --git a/tests/system/application_test.go b/tests/system/application_test.go index bcc955189..9e9c7173d 100644 --- a/tests/system/application_test.go +++ b/tests/system/application_test.go @@ -9,7 +9,9 @@ import ( "github.com/cortezaproject/corteza-server/system/types" "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest-jsonpath" + "github.com/stretchr/testify/require" "net/http" + "net/url" "testing" "time" ) @@ -188,3 +190,84 @@ func TestApplicationDelete(t *testing.T) { h.a.NotNil(res) h.a.NotNil(res.DeletedAt) } + +func TestApplicationLabels(t *testing.T) { + h := newHelper(t) + h.clearApplications() + + h.allow(types.SystemRBACResource, "application.create") + h.allow(types.ApplicationRBACResource.AppendWildcard(), "read") + h.allow(types.ApplicationRBACResource.AppendWildcard(), "update") + h.allow(types.ApplicationRBACResource.AppendWildcard(), "delete") + + var ( + ID uint64 + ) + + t.Run("create", func(t *testing.T) { + var ( + req = require.New(t) + payload = &types.Application{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + "/application/", + types.Application{Labels: map[string]string{"foo": "bar", "bar": "42"}}, + payload, + ) + req.NotZero(payload.ID) + + h.a.Equal(payload.Labels["foo"], "bar", + "labels must contain foo with value bar") + h.a.Equal(payload.Labels["bar"], "42", + "labels must contain bar with value 42") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + + ID = payload.ID + }) + + t.Run("update", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + payload = &types.Application{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("PUT /application/%d", ID), + &types.Application{Labels: map[string]string{"foo": "baz", "baz": "123"}}, + payload, + ) + req.NotZero(payload.ID) + //req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels") + + req.Equal(payload.Labels["foo"], "baz", + "labels must contain foo with value baz") + req.NotContains(payload.Labels, "bar", + "labels must not contain bar") + req.Equal(payload.Labels["baz"], "123", + "labels must contain baz with value 123") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + }) + + t.Run("search", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + set = types.ApplicationSet{} + ) + + helpers.SearchWithLabelsViaAPI(h.apiInit(), t, "/application/", &set, url.Values{"labels": []string{"baz=123"}}) + req.NotEmpty(set) + req.NotNil(set.FindByID(ID)) + req.NotNil(set.FindByID(ID).Labels) + }) +} diff --git a/tests/system/role_test.go b/tests/system/role_test.go index a55bf1f86..d62fb753f 100644 --- a/tests/system/role_test.go +++ b/tests/system/role_test.go @@ -9,7 +9,9 @@ import ( "github.com/cortezaproject/corteza-server/system/types" "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest-jsonpath" + "github.com/stretchr/testify/require" "net/http" + "net/url" "testing" "time" ) @@ -219,3 +221,84 @@ func TestRoleDelete(t *testing.T) { h.a.NotNil(res) h.a.NotNil(res.DeletedAt) } + +func TestRoleLabels(t *testing.T) { + h := newHelper(t) + h.clearRoles() + + h.allow(types.SystemRBACResource, "role.create") + h.allow(types.RoleRBACResource.AppendWildcard(), "read") + h.allow(types.RoleRBACResource.AppendWildcard(), "update") + h.allow(types.RoleRBACResource.AppendWildcard(), "delete") + + var ( + ID uint64 + ) + + t.Run("create", func(t *testing.T) { + var ( + req = require.New(t) + payload = &types.Role{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + "/roles/", + types.Role{Labels: map[string]string{"foo": "bar", "bar": "42"}}, + payload, + ) + req.NotZero(payload.ID) + + h.a.Equal(payload.Labels["foo"], "bar", + "labels must contain foo with value bar") + h.a.Equal(payload.Labels["bar"], "42", + "labels must contain bar with value 42") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + + ID = payload.ID + }) + + t.Run("update", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + payload = &types.Role{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("PUT /roles/%d", ID), + &types.Role{Labels: map[string]string{"foo": "baz", "baz": "123"}}, + payload, + ) + req.NotZero(payload.ID) + //req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels") + + req.Equal(payload.Labels["foo"], "baz", + "labels must contain foo with value baz") + req.NotContains(payload.Labels, "bar", + "labels must not contain bar") + req.Equal(payload.Labels["baz"], "123", + "labels must contain baz with value 123") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + }) + + t.Run("search", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + set = types.RoleSet{} + ) + + helpers.SearchWithLabelsViaAPI(h.apiInit(), t, "/roles/", &set, url.Values{"labels": []string{"baz=123"}}) + req.NotEmpty(set) + req.NotNil(set.FindByID(ID)) + req.NotNil(set.FindByID(ID).Labels) + }) +} diff --git a/tests/system/user_test.go b/tests/system/user_test.go index 124a61367..c9730f066 100644 --- a/tests/system/user_test.go +++ b/tests/system/user_test.go @@ -9,7 +9,9 @@ import ( "github.com/cortezaproject/corteza-server/system/types" "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest-jsonpath" + "github.com/stretchr/testify/require" "net/http" + "net/url" "testing" "time" ) @@ -380,3 +382,84 @@ func TestUserDelete(t *testing.T) { Assert(helpers.AssertNoErrors). End() } + +func TestUserLabels(t *testing.T) { + h := newHelper(t) + h.clearUsers() + + h.allow(types.SystemRBACResource, "user.create") + h.allow(types.UserRBACResource.AppendWildcard(), "read") + h.allow(types.UserRBACResource.AppendWildcard(), "update") + h.allow(types.UserRBACResource.AppendWildcard(), "delete") + + var ( + ID uint64 + ) + + t.Run("create", func(t *testing.T) { + var ( + req = require.New(t) + payload = &types.User{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + "/users/", + types.User{Email: h.randEmail(), Labels: map[string]string{"foo": "bar", "bar": "42"}}, + payload, + ) + req.NotZero(payload.ID) + + h.a.Equal(payload.Labels["foo"], "bar", + "labels must contain foo with value bar") + h.a.Equal(payload.Labels["bar"], "42", + "labels must contain bar with value 42") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + + ID = payload.ID + }) + + t.Run("update", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + payload = &types.User{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("PUT /users/%d", ID), + &types.User{ID: ID, Email: h.randEmail(), Labels: map[string]string{"foo": "baz", "baz": "123"}}, + payload, + ) + req.NotZero(payload.ID) + //req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels") + + req.Equal(payload.Labels["foo"], "baz", + "labels must contain foo with value baz") + req.NotContains(payload.Labels, "bar", + "labels must not contain bar") + req.Equal(payload.Labels["baz"], "123", + "labels must contain baz with value 123") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + }) + + t.Run("search", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + set = types.UserSet{} + ) + + helpers.SearchWithLabelsViaAPI(h.apiInit(), t, "/users/", &set, url.Values{"labels": []string{"baz=123"}}) + req.NotEmpty(set) + req.NotNil(set.FindByID(ID)) + req.NotNil(set.FindByID(ID).Labels) + }) +}