Add support for resource flagging; system/application

This commit is contained in:
Tomaž Jerman
2021-03-03 12:33:28 +01:00
parent 2b56633c39
commit b39a93f135
21 changed files with 1825 additions and 3 deletions
+288
View File
@@ -0,0 +1,288 @@
package flag
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/flag/types"
"github.com/cortezaproject/corteza-server/store"
)
type (
FlaggedResource interface {
GetFlags() []string
SetFlags([]string)
FlagResourceKind() string
FlagResourceID() uint64
}
)
// Search returns a slice of IDs corresponding to the filtered flags
func Search(ctx context.Context, s store.Storer, owner uint64, kind string, flags ...string) ([]uint64, error) {
rr := make([]uint64, 0, 100)
ff, _, err := store.SearchFlags(ctx, s, types.FlagFilter{
Kind: kind,
OwnedBy: []uint64{0, owner},
Name: flags,
})
if err != nil {
return nil, err
}
// Little helper to generate a map index for the fetched label resource
mix := func(resID uint64) string {
return fmt.Sprintf("%s:%d", kind, resID)
}
// Firstly get all of the flags for the given user.
// Take note of inactive flags so we can filter them out of the global set
out := make(map[string]bool)
for _, f := range ff {
if f.OwnedBy != 0 {
if f.Active {
rr = append(rr, f.ResourceID)
} else {
out[mix(f.ResourceID)] = true
}
}
}
// Go over global flags, exclude any ignored flags
for _, f := range ff {
if f.OwnedBy == 0 {
if f.Active && !out[mix(f.ResourceID)] {
rr = append(rr, f.ResourceID)
}
}
}
return rr, nil
}
// Create creates a new flag for the given resource
//
// If that flag for that owner for this resource already exists, it's skipped.
// Access control and any other validations should be performed by the caller.
func Create(ctx context.Context, s store.Storer, r FlaggedResource, ownedBy uint64, flag string) error {
// Try to preload existing own flag
own, err := store.LookupFlagByKindResourceIDOwnedByName(ctx, s, r.FlagResourceKind(), r.FlagResourceID(), ownedBy, flag)
if err != nil && err != store.ErrNotFound {
return err
}
if own != nil && own.Active {
return fmt.Errorf("flag %s for resource %s %d already exists", flag, r.FlagResourceKind(), r.FlagResourceID())
}
// If we have an inactive flag, mark it as active
if own != nil && !own.Active {
own.Active = true
return store.UpdateFlag(ctx, s, own)
}
own = &types.Flag{
Kind: r.FlagResourceKind(),
ResourceID: r.FlagResourceID(),
OwnedBy: ownedBy,
Name: flag,
Active: true,
}
return store.CreateFlag(ctx, s, own)
}
// Delete removes the flag from this resource
//
// Access control and any other validations should be performed by the caller.
//
// This operation has two outcomes:
// * if we are removing a flag defined for a specifc user, it is deleted
// * if we are removing a flag defined globally (no owner), we create a new inactive flag
func Delete(ctx context.Context, s store.Storer, r FlaggedResource, ownedBy uint64, flag string) error {
var (
own *types.Flag
global *types.Flag
err error
)
// Try to find the global flag
global, err = store.LookupFlagByKindResourceIDOwnedByName(ctx, s, r.FlagResourceKind(), r.FlagResourceID(), 0, flag)
if err != nil && err != store.ErrNotFound {
return err
}
// Try to find own flag
own, err = store.LookupFlagByKindResourceIDOwnedByName(ctx, s, r.FlagResourceKind(), r.FlagResourceID(), ownedBy, flag)
if err != nil && err != store.ErrNotFound {
return err
}
if own == nil && global == nil {
return fmt.Errorf("flag not found for resource %s %d", r.FlagResourceKind(), r.FlagResourceID())
}
// If we're deleting global flag, do it
if ownedBy == 0 {
if global == nil {
return fmt.Errorf("global flag not found for %s %d", r.FlagResourceKind(), r.FlagResourceID())
}
return store.DeleteFlag(ctx, s, global)
}
// If we're deleting own flag and there is no global flag, delete own flag
if own != nil && global == nil {
return store.DeleteFlag(ctx, s, own)
}
// If we're deleting own flag and there is a global flag, mark own flag as inactive
if own != nil && global != nil {
own.Active = false
return store.UpdateFlag(ctx, s, own)
}
// This can't happen, but just to be safe
return fmt.Errorf("invalid flag removal state")
}
// Load updates the provided resources with storreed flags
//
// 1. All global flags for this resource are fetched
// 2. All user-specifc flags for this resource are fetched, overwriting global flags
func Load(ctx context.Context, s store.Storer, incFlags uint, userID uint64, rr ...FlaggedResource) error {
for _, r := range rr {
var (
flags []string
err error
)
if incFlags == 0 {
flags, err = loadCalculated(ctx, s, userID, r)
} else if incFlags == 1 {
flags, err = loadGlobal(ctx, s, userID, r)
} else if incFlags == 2 {
flags, err = loadOwn(ctx, s, userID, r)
} else {
return fmt.Errorf("unknown flag inclusion: %d", incFlags)
}
if err != nil {
return err
}
r.SetFlags(flags)
}
return nil
}
func loadCalculated(ctx context.Context, s store.Storer, userID uint64, r FlaggedResource) ([]string, error) {
var (
ff types.FlagSet
err error
fMap = make(map[string]bool)
)
// Get flags for all users
ff, _, err = store.SearchFlags(ctx, s, types.FlagFilter{
Kind: r.FlagResourceKind(),
ResourceID: []uint64{r.FlagResourceID()},
OwnedBy: []uint64{0},
})
if err != nil {
return nil, err
}
for _, f := range ff {
fMap[f.Name] = f.Active
}
// Get flags for the given user & merge with general flags
ff, _, err = store.SearchFlags(ctx, s, types.FlagFilter{
Kind: r.FlagResourceKind(),
ResourceID: []uint64{r.FlagResourceID()},
OwnedBy: []uint64{userID},
})
if err != nil {
return nil, err
}
for _, f := range ff {
fMap[f.Name] = f.Active
}
// convert to a slice
rr := make([]string, 0, len(fMap))
for k, v := range fMap {
if v {
rr = append(rr, k)
}
}
return rr, nil
}
func loadGlobal(ctx context.Context, s store.Storer, userID uint64, r FlaggedResource) ([]string, error) {
var (
ff types.FlagSet
err error
fMap = make(map[string]bool)
)
// Get flags for all users
ff, _, err = store.SearchFlags(ctx, s, types.FlagFilter{
Kind: r.FlagResourceKind(),
ResourceID: []uint64{r.FlagResourceID()},
OwnedBy: []uint64{0},
})
if err != nil {
return nil, err
}
for _, f := range ff {
fMap[f.Name] = f.Active
}
// convert to a slice
rr := make([]string, 0, len(fMap))
for _, f := range ff {
if f.Active {
rr = append(rr, f.Name)
}
}
return rr, nil
}
func loadOwn(ctx context.Context, s store.Storer, userID uint64, r FlaggedResource) ([]string, error) {
var (
ff types.FlagSet
err error
fMap = make(map[string]bool)
)
// Get flags for all users
ff, _, err = store.SearchFlags(ctx, s, types.FlagFilter{
Kind: r.FlagResourceKind(),
ResourceID: []uint64{r.FlagResourceID()},
OwnedBy: []uint64{userID},
})
if err != nil {
return nil, err
}
for _, f := range ff {
fMap[f.Name] = f.Active
}
// convert to a slice
rr := make([]string, 0, len(fMap))
for _, f := range ff {
if f.Active {
rr = append(rr, f.Name)
}
}
return rr, nil
}
+25
View File
@@ -0,0 +1,25 @@
package types
type (
Flag struct {
// Kind of the flagged resource
Kind string
// ID if the flagged resource
ResourceID uint64
// The owner of this flag; 0 = everyone
OwnedBy uint64
Name string
Active bool
}
// @todo codegen this thing
FlagSet []*Flag
FlagFilter struct {
Kind string
ResourceID []uint64
OwnedBy []uint64
Name []string
}
)
+93
View File
@@ -0,0 +1,93 @@
package store
// This file is auto-generated.
//
// Template: pkg/codegen/assets/store_base.gen.go.tpl
// Definitions: store/flags.yaml
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/flag/types"
)
type (
Flags interface {
SearchFlags(ctx context.Context, f types.FlagFilter) (types.FlagSet, types.FlagFilter, error)
LookupFlagByKindResourceIDName(ctx context.Context, kind string, resource_id uint64, name string) (*types.Flag, error)
LookupFlagByKindResourceID(ctx context.Context, kind string, resource_id uint64) (*types.Flag, error)
LookupFlagByKindResourceIDOwnedBy(ctx context.Context, kind string, resource_id uint64, owned_by uint64) (*types.Flag, error)
LookupFlagByKindResourceIDOwnedByName(ctx context.Context, kind string, resource_id uint64, owned_by uint64, name string) (*types.Flag, error)
CreateFlag(ctx context.Context, rr ...*types.Flag) error
UpdateFlag(ctx context.Context, rr ...*types.Flag) error
UpsertFlag(ctx context.Context, rr ...*types.Flag) error
DeleteFlag(ctx context.Context, rr ...*types.Flag) error
DeleteFlagByKindResourceIDOwnedByName(ctx context.Context, kind string, resourceID uint64, ownedBy uint64, name string) error
TruncateFlags(ctx context.Context) error
}
)
var _ *types.Flag
var _ context.Context
// SearchFlags returns all matching Flags from store
func SearchFlags(ctx context.Context, s Flags, f types.FlagFilter) (types.FlagSet, types.FlagFilter, error) {
return s.SearchFlags(ctx, f)
}
// LookupFlagByKindResourceIDName Flag lookup by kind, resource, name
func LookupFlagByKindResourceIDName(ctx context.Context, s Flags, kind string, resource_id uint64, name string) (*types.Flag, error) {
return s.LookupFlagByKindResourceIDName(ctx, kind, resource_id, name)
}
// LookupFlagByKindResourceID Flag lookup by kind, resource
func LookupFlagByKindResourceID(ctx context.Context, s Flags, kind string, resource_id uint64) (*types.Flag, error) {
return s.LookupFlagByKindResourceID(ctx, kind, resource_id)
}
// LookupFlagByKindResourceIDOwnedBy Flag lookup by kind, resource, owner
func LookupFlagByKindResourceIDOwnedBy(ctx context.Context, s Flags, kind string, resource_id uint64, owned_by uint64) (*types.Flag, error) {
return s.LookupFlagByKindResourceIDOwnedBy(ctx, kind, resource_id, owned_by)
}
// LookupFlagByKindResourceIDOwnedByName Flag lookup by kind, resource, owner, name
func LookupFlagByKindResourceIDOwnedByName(ctx context.Context, s Flags, kind string, resource_id uint64, owned_by uint64, name string) (*types.Flag, error) {
return s.LookupFlagByKindResourceIDOwnedByName(ctx, kind, resource_id, owned_by, name)
}
// CreateFlag creates one or more Flags in store
func CreateFlag(ctx context.Context, s Flags, rr ...*types.Flag) error {
return s.CreateFlag(ctx, rr...)
}
// UpdateFlag updates one or more (existing) Flags in store
func UpdateFlag(ctx context.Context, s Flags, rr ...*types.Flag) error {
return s.UpdateFlag(ctx, rr...)
}
// UpsertFlag creates new or updates existing one or more Flags in store
func UpsertFlag(ctx context.Context, s Flags, rr ...*types.Flag) error {
return s.UpsertFlag(ctx, rr...)
}
// DeleteFlag Deletes one or more Flags from store
func DeleteFlag(ctx context.Context, s Flags, rr ...*types.Flag) error {
return s.DeleteFlag(ctx, rr...)
}
// DeleteFlagByKindResourceIDOwnedByName Deletes Flag from store
func DeleteFlagByKindResourceIDOwnedByName(ctx context.Context, s Flags, kind string, resourceID uint64, ownedBy uint64, name string) error {
return s.DeleteFlagByKindResourceIDOwnedByName(ctx, kind, resourceID, ownedBy, name)
}
// TruncateFlags Deletes all Flags from store
func TruncateFlags(ctx context.Context, s Flags) error {
return s.TruncateFlags(ctx)
}
+48
View File
@@ -0,0 +1,48 @@
import:
- github.com/cortezaproject/corteza-server/pkg/flag/types
types:
type: types.Flag
fields:
- { field: Kind, isPrimaryKey: true }
- { field: ResourceID, isPrimaryKey: true }
- { field: OwnedBy, isPrimaryKey: true }
- { field: Name, isPrimaryKey: true, lookupFilterPreprocessor: lower }
- { field: Active }
lookups:
- fields: [ Kind, ResourceID, Name ]
uniqueConstraintCheck: true
description: |-
Flag lookup by kind, resource, name
- fields: [ Kind, ResourceID ]
uniqueConstraintCheck: true
description: |-
Flag lookup by kind, resource
- fields: [ Kind, ResourceID, OwnedBy ]
uniqueConstraintCheck: true
description: |-
Flag lookup by kind, resource, owner
- fields: [ Kind, ResourceID, OwnedBy, Name ]
uniqueConstraintCheck: true
description: |-
Flag lookup by kind, resource, owner, name
search:
enablePaging: false
enableSorting: false
enableFilterCheckFunction: false
upsert:
enable: true
rdbms:
alias: flg
table: flags
customFilterConverter: true
+2
View File
@@ -25,6 +25,7 @@ package store
// - store/federation_nodes.yaml
// - store/federation_nodes_sync.yaml
// - store/federation_shared_modules.yaml
// - store/flags.yaml
// - store/labels.yaml
// - store/messaging_attachments.yaml
// - store/messaging_channel_members.yaml
@@ -70,6 +71,7 @@ type (
FederationNodes
FederationNodesSyncs
FederationSharedModules
Flags
Labels
MessagingAttachments
MessagingChannelMembers
+4
View File
@@ -18,6 +18,10 @@ func (s Store) convertApplicationFilter(f types.ApplicationFilter) (query squirr
query = query.Where(squirrel.Eq{"app.id": f.LabeledIDs})
}
if len(f.FlaggedIDs) > 0 {
query = query.Where(squirrel.Eq{"app.id": f.FlaggedIDs})
}
if f.Query != "" {
qs := f.Query + "%"
query = query.Where(squirrel.Or{
+417
View File
@@ -0,0 +1,417 @@
package rdbms
// This file is an auto-generated file
//
// Template: pkg/codegen/assets/store_rdbms.gen.go.tpl
// Definitions: store/flags.yaml
//
// Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated.
import (
"context"
"database/sql"
"github.com/Masterminds/squirrel"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/flag/types"
"github.com/cortezaproject/corteza-server/store"
)
var _ = errors.Is
// SearchFlags returns all matching rows
//
// This function calls convertFlagFilter with the given
// types.FlagFilter and expects to receive a working squirrel.SelectBuilder
func (s Store) SearchFlags(ctx context.Context, f types.FlagFilter) (types.FlagSet, types.FlagFilter, error) {
var (
err error
set []*types.Flag
q squirrel.SelectBuilder
)
return set, f, func() error {
q, err = s.convertFlagFilter(f)
if err != nil {
return err
}
set, err = s.QueryFlags(ctx, q, nil)
return err
}()
}
// QueryFlags queries the database, converts and checks each row and
// returns collected set
//
// Fn also returns total number of fetched items and last fetched item so that the caller can construct cursor
// for next page of results
func (s Store) QueryFlags(
ctx context.Context,
q squirrel.Sqlizer,
check func(*types.Flag) (bool, error),
) ([]*types.Flag, error) {
var (
set = make([]*types.Flag, 0, DefaultSliceCapacity)
res *types.Flag
// Query rows with
rows, err = s.Query(ctx, q)
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
if err = rows.Err(); err == nil {
res, err = s.internalFlagRowScanner(rows)
}
if err != nil {
return nil, err
}
set = append(set, res)
}
return set, rows.Err()
}
// LookupFlagByKindResourceIDName Flag lookup by kind, resource, name
func (s Store) LookupFlagByKindResourceIDName(ctx context.Context, kind string, resource_id uint64, name string) (*types.Flag, error) {
return s.execLookupFlag(ctx, squirrel.Eq{
s.preprocessColumn("flg.kind", ""): store.PreprocessValue(kind, ""),
s.preprocessColumn("flg.rel_resource", ""): store.PreprocessValue(resource_id, ""),
s.preprocessColumn("flg.name", "lower"): store.PreprocessValue(name, "lower"),
})
}
// LookupFlagByKindResourceID Flag lookup by kind, resource
func (s Store) LookupFlagByKindResourceID(ctx context.Context, kind string, resource_id uint64) (*types.Flag, error) {
return s.execLookupFlag(ctx, squirrel.Eq{
s.preprocessColumn("flg.kind", ""): store.PreprocessValue(kind, ""),
s.preprocessColumn("flg.rel_resource", ""): store.PreprocessValue(resource_id, ""),
})
}
// LookupFlagByKindResourceIDOwnedBy Flag lookup by kind, resource, owner
func (s Store) LookupFlagByKindResourceIDOwnedBy(ctx context.Context, kind string, resource_id uint64, owned_by uint64) (*types.Flag, error) {
return s.execLookupFlag(ctx, squirrel.Eq{
s.preprocessColumn("flg.kind", ""): store.PreprocessValue(kind, ""),
s.preprocessColumn("flg.rel_resource", ""): store.PreprocessValue(resource_id, ""),
s.preprocessColumn("flg.owned_by", ""): store.PreprocessValue(owned_by, ""),
})
}
// LookupFlagByKindResourceIDOwnedByName Flag lookup by kind, resource, owner, name
func (s Store) LookupFlagByKindResourceIDOwnedByName(ctx context.Context, kind string, resource_id uint64, owned_by uint64, name string) (*types.Flag, error) {
return s.execLookupFlag(ctx, squirrel.Eq{
s.preprocessColumn("flg.kind", ""): store.PreprocessValue(kind, ""),
s.preprocessColumn("flg.rel_resource", ""): store.PreprocessValue(resource_id, ""),
s.preprocessColumn("flg.owned_by", ""): store.PreprocessValue(owned_by, ""),
s.preprocessColumn("flg.name", "lower"): store.PreprocessValue(name, "lower"),
})
}
// CreateFlag creates one or more rows in flags table
func (s Store) CreateFlag(ctx context.Context, rr ...*types.Flag) (err error) {
for _, res := range rr {
err = s.checkFlagConstraints(ctx, res)
if err != nil {
return err
}
err = s.execCreateFlags(ctx, s.internalFlagEncoder(res))
if err != nil {
return err
}
}
return
}
// UpdateFlag updates one or more existing rows in flags
func (s Store) UpdateFlag(ctx context.Context, rr ...*types.Flag) error {
return s.partialFlagUpdate(ctx, nil, rr...)
}
// partialFlagUpdate updates one or more existing rows in flags
func (s Store) partialFlagUpdate(ctx context.Context, onlyColumns []string, rr ...*types.Flag) (err error) {
for _, res := range rr {
err = s.checkFlagConstraints(ctx, res)
if err != nil {
return err
}
err = s.execUpdateFlags(
ctx,
squirrel.Eq{
s.preprocessColumn("flg.kind", ""): store.PreprocessValue(res.Kind, ""), s.preprocessColumn("flg.rel_resource", ""): store.PreprocessValue(res.ResourceID, ""), s.preprocessColumn("flg.owned_by", ""): store.PreprocessValue(res.OwnedBy, ""), s.preprocessColumn("flg.name", "lower"): store.PreprocessValue(res.Name, "lower"),
},
s.internalFlagEncoder(res).Skip("kind", "rel_resource", "owned_by", "name").Only(onlyColumns...))
if err != nil {
return err
}
}
return
}
// UpsertFlag updates one or more existing rows in flags
func (s Store) UpsertFlag(ctx context.Context, rr ...*types.Flag) (err error) {
for _, res := range rr {
err = s.checkFlagConstraints(ctx, res)
if err != nil {
return err
}
err = s.execUpsertFlags(ctx, s.internalFlagEncoder(res))
if err != nil {
return err
}
}
return nil
}
// DeleteFlag Deletes one or more rows from flags table
func (s Store) DeleteFlag(ctx context.Context, rr ...*types.Flag) (err error) {
for _, res := range rr {
err = s.execDeleteFlags(ctx, squirrel.Eq{
s.preprocessColumn("flg.kind", ""): store.PreprocessValue(res.Kind, ""), s.preprocessColumn("flg.rel_resource", ""): store.PreprocessValue(res.ResourceID, ""), s.preprocessColumn("flg.owned_by", ""): store.PreprocessValue(res.OwnedBy, ""), s.preprocessColumn("flg.name", "lower"): store.PreprocessValue(res.Name, "lower"),
})
if err != nil {
return err
}
}
return nil
}
// DeleteFlagByKindResourceIDOwnedByName Deletes row from the flags table
func (s Store) DeleteFlagByKindResourceIDOwnedByName(ctx context.Context, kind string, resourceID uint64, ownedBy uint64, name string) error {
return s.execDeleteFlags(ctx, squirrel.Eq{
s.preprocessColumn("flg.kind", ""): store.PreprocessValue(kind, ""),
s.preprocessColumn("flg.rel_resource", ""): store.PreprocessValue(resourceID, ""),
s.preprocessColumn("flg.owned_by", ""): store.PreprocessValue(ownedBy, ""),
s.preprocessColumn("flg.name", "lower"): store.PreprocessValue(name, "lower"),
})
}
// TruncateFlags Deletes all rows from the flags table
func (s Store) TruncateFlags(ctx context.Context) error {
return s.Truncate(ctx, s.flagTable())
}
// execLookupFlag prepares Flag query and executes it,
// returning types.Flag (or error)
func (s Store) execLookupFlag(ctx context.Context, cnd squirrel.Sqlizer) (res *types.Flag, err error) {
var (
row rowScanner
)
row, err = s.QueryRow(ctx, s.flagsSelectBuilder().Where(cnd))
if err != nil {
return
}
res, err = s.internalFlagRowScanner(row)
if err != nil {
return
}
return res, nil
}
// execCreateFlags updates all matched (by cnd) rows in flags with given data
func (s Store) execCreateFlags(ctx context.Context, payload store.Payload) error {
return s.Exec(ctx, s.InsertBuilder(s.flagTable()).SetMap(payload))
}
// execUpdateFlags updates all matched (by cnd) rows in flags with given data
func (s Store) execUpdateFlags(ctx context.Context, cnd squirrel.Sqlizer, set store.Payload) error {
return s.Exec(ctx, s.UpdateBuilder(s.flagTable("flg")).Where(cnd).SetMap(set))
}
// execUpsertFlags inserts new or updates matching (by-primary-key) rows in flags with given data
func (s Store) execUpsertFlags(ctx context.Context, set store.Payload) error {
upsert, err := s.config.UpsertBuilder(
s.config,
s.flagTable(),
set,
s.preprocessColumn("kind", ""),
s.preprocessColumn("rel_resource", ""),
s.preprocessColumn("owned_by", ""),
s.preprocessColumn("name", "lower"),
)
if err != nil {
return err
}
return s.Exec(ctx, upsert)
}
// execDeleteFlags Deletes all matched (by cnd) rows in flags with given data
func (s Store) execDeleteFlags(ctx context.Context, cnd squirrel.Sqlizer) error {
return s.Exec(ctx, s.DeleteBuilder(s.flagTable("flg")).Where(cnd))
}
func (s Store) internalFlagRowScanner(row rowScanner) (res *types.Flag, err error) {
res = &types.Flag{}
if _, has := s.config.RowScanners["flag"]; has {
scanner := s.config.RowScanners["flag"].(func(_ rowScanner, _ *types.Flag) error)
err = scanner(row, res)
} else {
err = row.Scan(
&res.Kind,
&res.ResourceID,
&res.OwnedBy,
&res.Name,
&res.Active,
)
}
if err == sql.ErrNoRows {
return nil, store.ErrNotFound.Stack(1)
}
if err != nil {
return nil, errors.Store("could not scan flag db row").Wrap(err)
} else {
return res, nil
}
}
// QueryFlags returns squirrel.SelectBuilder with set table and all columns
func (s Store) flagsSelectBuilder() squirrel.SelectBuilder {
return s.SelectBuilder(s.flagTable("flg"), s.flagColumns("flg")...)
}
// flagTable name of the db table
func (Store) flagTable(aa ...string) string {
var alias string
if len(aa) > 0 {
alias = " AS " + aa[0]
}
return "flags" + alias
}
// FlagColumns returns all defined table columns
//
// With optional string arg, all columns are returned aliased
func (Store) flagColumns(aa ...string) []string {
var alias string
if len(aa) > 0 {
alias = aa[0] + "."
}
return []string{
alias + "kind",
alias + "rel_resource",
alias + "owned_by",
alias + "name",
alias + "active",
}
}
// {true true false false false false}
// internalFlagEncoder encodes fields from types.Flag to store.Payload (map)
//
// Encoding is done by using generic approach or by calling encodeFlag
// func when rdbms.customEncoder=true
func (s Store) internalFlagEncoder(res *types.Flag) store.Payload {
return store.Payload{
"kind": res.Kind,
"rel_resource": res.ResourceID,
"owned_by": res.OwnedBy,
"name": res.Name,
"active": res.Active,
}
}
// checkFlagConstraints performs lookups (on valid) resource to check if any of the values on unique fields
// already exists in the store
//
// Using built-in constraint checking would be more performant but unfortunately we can not rely
// on the full support (MySQL does not support conditional indexes)
func (s *Store) checkFlagConstraints(ctx context.Context, res *types.Flag) error {
// Consider resource valid when all fields in unique constraint check lookups
// have valid (non-empty) value
//
// Only string and uint64 are supported for now
// feel free to add additional types if needed
var valid = true
valid = valid && len(res.Kind) > 0
valid = valid && res.ResourceID > 0
valid = valid && len(res.Name) > 0
valid = valid && len(res.Kind) > 0
valid = valid && res.ResourceID > 0
valid = valid && len(res.Kind) > 0
valid = valid && res.ResourceID > 0
valid = valid && res.OwnedBy > 0
valid = valid && len(res.Kind) > 0
valid = valid && res.ResourceID > 0
valid = valid && res.OwnedBy > 0
valid = valid && len(res.Name) > 0
if !valid {
return nil
}
{
ex, err := s.LookupFlagByKindResourceIDName(ctx, res.Kind, res.ResourceID, res.Name)
if err == nil && ex != nil && ex.Kind != res.Kind && ex.ResourceID != res.ResourceID && ex.OwnedBy != res.OwnedBy && ex.Name != res.Name {
return store.ErrNotUnique.Stack(1)
} else if !errors.IsNotFound(err) {
return err
}
}
{
ex, err := s.LookupFlagByKindResourceID(ctx, res.Kind, res.ResourceID)
if err == nil && ex != nil && ex.Kind != res.Kind && ex.ResourceID != res.ResourceID && ex.OwnedBy != res.OwnedBy && ex.Name != res.Name {
return store.ErrNotUnique.Stack(1)
} else if !errors.IsNotFound(err) {
return err
}
}
{
ex, err := s.LookupFlagByKindResourceIDOwnedBy(ctx, res.Kind, res.ResourceID, res.OwnedBy)
if err == nil && ex != nil && ex.Kind != res.Kind && ex.ResourceID != res.ResourceID && ex.OwnedBy != res.OwnedBy && ex.Name != res.Name {
return store.ErrNotUnique.Stack(1)
} else if !errors.IsNotFound(err) {
return err
}
}
{
ex, err := s.LookupFlagByKindResourceIDOwnedByName(ctx, res.Kind, res.ResourceID, res.OwnedBy, res.Name)
if err == nil && ex != nil && ex.Kind != res.Kind && ex.ResourceID != res.ResourceID && ex.OwnedBy != res.OwnedBy && ex.Name != res.Name {
return store.ErrNotUnique.Stack(1)
} else if !errors.IsNotFound(err) {
return err
}
}
return nil
}
+26
View File
@@ -0,0 +1,26 @@
package rdbms
import (
"github.com/Masterminds/squirrel"
"github.com/cortezaproject/corteza-server/pkg/flag/types"
)
func (s Store) convertFlagFilter(f types.FlagFilter) (query squirrel.SelectBuilder, err error) {
query = s.flagsSelectBuilder()
query = query.Where(squirrel.Eq{"flg.kind": f.Kind})
if len(f.ResourceID) > 0 {
query = query.Where(squirrel.Eq{"flg.rel_resource": f.ResourceID})
}
if len(f.OwnedBy) > 0 {
query = query.Where(squirrel.Eq{"flg.owned_by": f.OwnedBy})
}
if len(f.Name) > 0 {
query = query.Where(squirrel.Eq{"flg.name": f.Name})
}
return
}
+13
View File
@@ -62,6 +62,7 @@ func (s Schema) Tables() []*Table {
s.RbacRules(),
s.Settings(),
s.Labels(),
s.Flags(),
s.Templates(),
s.ComposeAttachment(),
s.ComposeChart(),
@@ -317,6 +318,18 @@ func (Schema) Labels() *Table {
)
}
func (Schema) Flags() *Table {
return TableDef("flags",
ColumnDef("kind", ColumnTypeVarchar, ColumnTypeLength(handleLength)),
ColumnDef("rel_resource", ColumnTypeIdentifier),
ColumnDef("owned_by", ColumnTypeIdentifier),
ColumnDef("name", ColumnTypeVarchar, ColumnTypeLength(resourceLength)),
ColumnDef("active", ColumnTypeBoolean),
AddIndex("unique_kind_res_owner_name", IColumn("kind", "rel_resource", "owned_by"), IExpr("LOWER(name)")),
)
}
func (Schema) Templates() *Table {
return TableDef("templates",
ID,
+53
View File
@@ -0,0 +1,53 @@
package tests
import (
"context"
"testing"
"github.com/cortezaproject/corteza-server/pkg/flag/types"
"github.com/cortezaproject/corteza-server/store"
_ "github.com/joho/godotenv/autoload"
"github.com/stretchr/testify/require"
)
func testFlags(t *testing.T, s store.Flags) {
var (
ctx = context.Background()
)
t.Run("create", func(t *testing.T) {
req := require.New(t)
req.NoError(s.TruncateFlags(ctx))
req.NoError(s.CreateFlag(ctx, &types.Flag{
Kind: "kind",
ResourceID: 1,
OwnedBy: 2,
Name: "fname",
Active: true,
}))
})
t.Run("update", func(t *testing.T) {
req := require.New(t)
req.NoError(s.TruncateFlags(ctx))
req.NoError(s.UpdateFlag(ctx, &types.Flag{
Kind: "kind",
ResourceID: 1,
OwnedBy: 2,
Name: "fname",
Active: false,
}))
})
t.Run("upsert", func(t *testing.T) {
req := require.New(t)
req.NoError(s.TruncateFlags(ctx))
req.NoError(s.UpsertFlag(ctx, &types.Flag{
Kind: "kind",
ResourceID: 1,
OwnedBy: 2,
Name: "fname",
Active: true,
}))
})
}
+6
View File
@@ -23,6 +23,7 @@ package tests
// - store/federation_nodes.yaml
// - store/federation_nodes_sync.yaml
// - store/federation_shared_modules.yaml
// - store/flags.yaml
// - store/labels.yaml
// - store/messaging_attachments.yaml
// - store/messaging_channel_members.yaml
@@ -156,6 +157,11 @@ func testAllGenerated(t *testing.T, s store.Storer) {
testFederationSharedModules(t, s)
})
// Run generated tests for Flags
t.Run("Flags", func(t *testing.T) {
testFlags(t, s)
})
// Run generated tests for Labels
t.Run("Labels", func(t *testing.T) {
testLabels(t, s)
+51
View File
@@ -721,6 +721,13 @@ endpoints:
name: labels
title: Labels
parser: label.ParseStrings
- name: flags
type: "[]string"
title: Flags
- name: incFlags
required: false
title: Calculated (0, default), global (1) or return only (2) own flags
type: uint
- type: uint
name: limit
title: Limit
@@ -795,6 +802,45 @@ endpoints:
name: labels
title: Labels
parser: label.ParseStrings
- name: flagCreate
method: POST
title: Flag application
path: "/{applicationID}/flag/{ownedBy}/{flag}"
parameters:
path:
- type: uint64
name: applicationID
required: true
title: Application ID
- type: string
name: flag
required: true
title: Flag
- type: uint64
name: ownedBy
required: false
title: Owner; 0 = everyone
- name: flagDelete
method: DELETE
title: Unflag application
path: "/{applicationID}/flag/{ownedBy}/{flag}"
parameters:
path:
- type: uint64
name: applicationID
required: true
title: Application ID
- type: string
name: flag
required: true
title: Flag
- type: uint64
name: ownedBy
required: false
title: Owner; 0 = everyone
- name: read
method: GET
title: Read application details
@@ -805,6 +851,11 @@ endpoints:
name: applicationID
required: true
title: Application ID
get:
- name: incFlags
required: false
title: Calculated (0, default), global (1) or return only (2) own flags
type: uint
- name: delete
method: DELETE
title: Remove application
+50 -3
View File
@@ -5,8 +5,10 @@ import (
"strconv"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/corredor"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/flag"
"github.com/cortezaproject/corteza-server/system/rest/request"
"github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/system/service/event"
@@ -64,9 +66,11 @@ func (ctrl *Application) List(ctx context.Context, r *request.ApplicationList) (
var (
err error
f = types.ApplicationFilter{
Name: r.Name,
Query: r.Query,
Labels: r.Labels,
Name: r.Name,
Query: r.Query,
Labels: r.Labels,
Flags: r.Flags,
IncFlags: r.IncFlags,
Deleted: filter.State(r.Deleted),
}
@@ -131,6 +135,11 @@ func (ctrl *Application) Update(ctx context.Context, r *request.ApplicationUpdat
func (ctrl *Application) Read(ctx context.Context, r *request.ApplicationRead) (interface{}, error) {
app, err := ctrl.application.LookupByID(ctx, r.ApplicationID)
if err != nil {
return ctrl.makePayload(ctx, app, err)
}
err = flag.Load(ctx, service.DefaultStore, r.IncFlags, auth.GetIdentityFromContext(ctx).Identity(), app)
return ctrl.makePayload(ctx, app, err)
}
@@ -171,6 +180,44 @@ func (ctrl *Application) Reorder(ctx context.Context, r *request.ApplicationReor
return api.OK(), ctrl.application.Reorder(ctx, order)
}
func (ctrl *Application) FlagCreate(ctx context.Context, r *request.ApplicationFlagCreate) (interface{}, error) {
app, err := ctrl.application.LookupByID(ctx, r.ApplicationID)
if err != nil {
return nil, err
}
if r.OwnedBy == 0 {
if !service.DefaultAccessControl.CanGlobalFlagApplication(ctx) {
return nil, service.ApplicationErrNotAllowedToManageFlagGlobal()
}
} else {
if !service.DefaultAccessControl.CanSelfFlagApplication(ctx) {
return nil, service.ApplicationErrNotAllowedToManageFlag()
}
}
return api.OK(), flag.Create(ctx, service.DefaultStore, app, r.OwnedBy, r.Flag)
}
func (ctrl *Application) FlagDelete(ctx context.Context, r *request.ApplicationFlagDelete) (interface{}, error) {
app, err := ctrl.application.LookupByID(ctx, r.ApplicationID)
if err != nil {
return nil, err
}
if r.OwnedBy == 0 {
if !service.DefaultAccessControl.CanGlobalFlagApplication(ctx) {
return nil, service.ApplicationErrNotAllowedToManageFlagGlobal()
}
} else {
if !service.DefaultAccessControl.CanSelfFlagApplication(ctx) {
return nil, service.ApplicationErrNotAllowedToManageFlag()
}
}
return api.OK(), flag.Delete(ctx, service.DefaultStore, app, r.OwnedBy, r.Flag)
}
func (ctrl Application) makePayload(ctx context.Context, m *types.Application, err error) (*applicationPayload, error) {
if err != nil || m == nil {
return nil, err
+38
View File
@@ -22,6 +22,8 @@ type (
List(context.Context, *request.ApplicationList) (interface{}, error)
Create(context.Context, *request.ApplicationCreate) (interface{}, error)
Update(context.Context, *request.ApplicationUpdate) (interface{}, error)
FlagCreate(context.Context, *request.ApplicationFlagCreate) (interface{}, error)
FlagDelete(context.Context, *request.ApplicationFlagDelete) (interface{}, error)
Read(context.Context, *request.ApplicationRead) (interface{}, error)
Delete(context.Context, *request.ApplicationDelete) (interface{}, error)
Undelete(context.Context, *request.ApplicationUndelete) (interface{}, error)
@@ -34,6 +36,8 @@ type (
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
FlagCreate func(http.ResponseWriter, *http.Request)
FlagDelete func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Undelete func(http.ResponseWriter, *http.Request)
@@ -92,6 +96,38 @@ func NewApplication(h ApplicationAPI) *Application {
api.Send(w, r, value)
},
FlagCreate: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewApplicationFlagCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.FlagCreate(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
FlagDelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewApplicationFlagDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.FlagDelete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewApplicationRead()
@@ -181,6 +217,8 @@ func (h Application) MountRoutes(r chi.Router, middlewares ...func(http.Handler)
r.Get("/application/", h.List)
r.Post("/application/", h.Create)
r.Put("/application/{applicationID}", h.Update)
r.Post("/application/{applicationID}/flag/{ownedBy}/{flag}", h.FlagCreate)
r.Delete("/application/{applicationID}/flag/{ownedBy}/{flag}", h.FlagDelete)
r.Get("/application/{applicationID}", h.Read)
r.Delete("/application/{applicationID}", h.Delete)
r.Post("/application/{applicationID}/undelete", h.Undelete)
+234
View File
@@ -52,6 +52,16 @@ type (
// Labels
Labels map[string]string
// Flags GET parameter
//
// Flags
Flags []string
// IncFlags GET parameter
//
// Calculated (0, default), global (1) or return only (2) own flags
IncFlags uint
// Limit GET parameter
//
// Limit
@@ -137,11 +147,50 @@ type (
Labels map[string]string
}
ApplicationFlagCreate struct {
// ApplicationID PATH parameter
//
// Application ID
ApplicationID uint64 `json:",string"`
// Flag PATH parameter
//
// Flag
Flag string
// OwnedBy PATH parameter
//
// Owner; 0 = everyone
OwnedBy uint64 `json:",string"`
}
ApplicationFlagDelete struct {
// ApplicationID PATH parameter
//
// Application ID
ApplicationID uint64 `json:",string"`
// Flag PATH parameter
//
// Flag
Flag string
// OwnedBy PATH parameter
//
// Owner; 0 = everyone
OwnedBy uint64 `json:",string"`
}
ApplicationRead struct {
// ApplicationID PATH parameter
//
// Application ID
ApplicationID uint64 `json:",string"`
// IncFlags GET parameter
//
// Calculated (0, default), global (1) or return only (2) own flags
IncFlags uint
}
ApplicationDelete struct {
@@ -190,6 +239,8 @@ func (r ApplicationList) Auditable() map[string]interface{} {
"query": r.Query,
"deleted": r.Deleted,
"labels": r.Labels,
"flags": r.Flags,
"incFlags": r.IncFlags,
"limit": r.Limit,
"pageCursor": r.PageCursor,
"sort": r.Sort,
@@ -216,6 +267,16 @@ func (r ApplicationList) GetLabels() map[string]string {
return r.Labels
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationList) GetFlags() []string {
return r.Flags
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationList) GetIncFlags() uint {
return r.IncFlags
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationList) GetLimit() uint {
return r.Limit
@@ -277,6 +338,23 @@ func (r *ApplicationList) Fill(req *http.Request) (err error) {
return err
}
}
if val, ok := tmp["flags[]"]; ok {
r.Flags, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["flags"]; ok {
r.Flags, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["incFlags"]; ok && len(val) > 0 {
r.IncFlags, err = payload.ParseUint(val[0]), nil
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 {
@@ -554,6 +632,144 @@ func (r *ApplicationUpdate) Fill(req *http.Request) (err error) {
return err
}
// NewApplicationFlagCreate request
func NewApplicationFlagCreate() *ApplicationFlagCreate {
return &ApplicationFlagCreate{}
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationFlagCreate) Auditable() map[string]interface{} {
return map[string]interface{}{
"applicationID": r.ApplicationID,
"flag": r.Flag,
"ownedBy": r.OwnedBy,
}
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationFlagCreate) GetApplicationID() uint64 {
return r.ApplicationID
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationFlagCreate) GetFlag() string {
return r.Flag
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationFlagCreate) GetOwnedBy() uint64 {
return r.OwnedBy
}
// Fill processes request and fills internal variables
func (r *ApplicationFlagCreate) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
var val string
// path params
val = chi.URLParam(req, "applicationID")
r.ApplicationID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "flag")
r.Flag, err = val, nil
if err != nil {
return err
}
val = chi.URLParam(req, "ownedBy")
r.OwnedBy, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewApplicationFlagDelete request
func NewApplicationFlagDelete() *ApplicationFlagDelete {
return &ApplicationFlagDelete{}
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationFlagDelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"applicationID": r.ApplicationID,
"flag": r.Flag,
"ownedBy": r.OwnedBy,
}
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationFlagDelete) GetApplicationID() uint64 {
return r.ApplicationID
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationFlagDelete) GetFlag() string {
return r.Flag
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationFlagDelete) GetOwnedBy() uint64 {
return r.OwnedBy
}
// Fill processes request and fills internal variables
func (r *ApplicationFlagDelete) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
var val string
// path params
val = chi.URLParam(req, "applicationID")
r.ApplicationID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "flag")
r.Flag, err = val, nil
if err != nil {
return err
}
val = chi.URLParam(req, "ownedBy")
r.OwnedBy, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewApplicationRead request
func NewApplicationRead() *ApplicationRead {
return &ApplicationRead{}
@@ -563,6 +779,7 @@ func NewApplicationRead() *ApplicationRead {
func (r ApplicationRead) Auditable() map[string]interface{} {
return map[string]interface{}{
"applicationID": r.ApplicationID,
"incFlags": r.IncFlags,
}
}
@@ -571,6 +788,11 @@ func (r ApplicationRead) GetApplicationID() uint64 {
return r.ApplicationID
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationRead) GetIncFlags() uint {
return r.IncFlags
}
// Fill processes request and fills internal variables
func (r *ApplicationRead) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
@@ -584,6 +806,18 @@ func (r *ApplicationRead) Fill(req *http.Request) (err error) {
}
}
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["incFlags"]; ok && len(val) > 0 {
r.IncFlags, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
}
{
var val string
// path params
+12
View File
@@ -43,6 +43,8 @@ func (svc accessControl) Effective(ctx context.Context) (ee rbac.EffectiveSet) {
ee.Push(types.SystemRBACResource, "settings.read", svc.CanReadSettings(ctx))
ee.Push(types.SystemRBACResource, "settings.manage", svc.CanManageSettings(ctx))
ee.Push(types.SystemRBACResource, "application.create", svc.CanCreateApplication(ctx))
ee.Push(types.SystemRBACResource, "application.flag.self", svc.CanSelfFlagApplication(ctx))
ee.Push(types.SystemRBACResource, "application.flag.global", svc.CanGlobalFlagApplication(ctx))
ee.Push(types.SystemRBACResource, "template.create", svc.CanCreateTemplate(ctx))
ee.Push(types.SystemRBACResource, "role.create", svc.CanCreateRole(ctx))
@@ -73,6 +75,14 @@ func (svc accessControl) CanCreateApplication(ctx context.Context) bool {
return svc.can(ctx, types.SystemRBACResource, "application.create")
}
func (svc accessControl) CanSelfFlagApplication(ctx context.Context) bool {
return svc.can(ctx, types.SystemRBACResource, "application.flag.self", rbac.Allowed)
}
func (svc accessControl) CanGlobalFlagApplication(ctx context.Context) bool {
return svc.can(ctx, types.SystemRBACResource, "application.flag.global")
}
func (svc accessControl) CanCreateAuthClient(ctx context.Context) bool {
return svc.can(ctx, types.SystemRBACResource, "authClient.create")
}
@@ -262,6 +272,8 @@ func (svc accessControl) Whitelist() rbac.Whitelist {
"role.create",
"user.create",
"application.create",
"application.flag.self",
"application.flag.global",
"template.create",
"reminder.assign",
)
+41
View File
@@ -4,7 +4,9 @@ import (
"context"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
a "github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/flag"
"github.com/cortezaproject/corteza-server/pkg/label"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/service/event"
@@ -102,6 +104,25 @@ func (svc *application) Search(ctx context.Context, af types.ApplicationFilter)
}
}
if len(af.Flags) > 0 {
af.FlaggedIDs, err = flag.Search(
ctx,
svc.store,
a.GetIdentityFromContext(ctx).Identity(),
(&types.Application{}).FlagResourceKind(),
af.Flags...,
)
if err != nil {
return err
}
// flags specified byt no flagged resources found
if len(af.FlaggedIDs) == 0 {
return nil
}
}
if aa, f, err = store.SearchApplications(ctx, svc.store, af); err != nil {
return err
}
@@ -110,6 +131,10 @@ func (svc *application) Search(ctx context.Context, af types.ApplicationFilter)
return err
}
if err = flag.Load(ctx, svc.store, f.IncFlags, a.GetIdentityFromContext(ctx).Identity(), toFlaggedApplications(aa)...); err != nil {
return err
}
return nil
}()
@@ -324,3 +349,19 @@ func toLabeledApplications(set []*types.Application) []label.LabeledResource {
return ll
}
// toFlaggedApplications converts to []flag.FlaggedResource
//
// This function is auto-generated.
func toFlaggedApplications(set []*types.Application) []flag.FlaggedResource {
if len(set) == 0 {
return nil
}
ll := make([]flag.FlaggedResource, len(set))
for i := range set {
ll[i] = set[i]
}
return ll
}
+104
View File
@@ -385,6 +385,46 @@ func ApplicationActionUndelete(props ...*applicationActionProps) *applicationAct
return a
}
// ApplicationActionFlagManage returns "system:application.flagManage" action
//
// This function is auto-generated.
//
func ApplicationActionFlagManage(props ...*applicationActionProps) *applicationAction {
a := &applicationAction{
timestamp: time.Now(),
resource: "system:application",
action: "flagManage",
log: "managed flags for application {application}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ApplicationActionFlagManageGlobal returns "system:application.flagManageGlobal" action
//
// This function is auto-generated.
//
func ApplicationActionFlagManageGlobal(props ...*applicationActionProps) *applicationAction {
a := &applicationAction{
timestamp: time.Now(),
resource: "system:application",
action: "flagManageGlobal",
log: "managed global flags for application {application}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
@@ -673,6 +713,70 @@ func ApplicationErrNotAllowedToUndelete(mm ...*applicationActionProps) *errors.E
return e
}
// ApplicationErrNotAllowedToManageFlag returns "system:application.notAllowedToManageFlag" as *errors.Error
//
//
// This function is auto-generated.
//
func ApplicationErrNotAllowedToManageFlag(mm ...*applicationActionProps) *errors.Error {
var p = &applicationActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to manage flags for applications", nil),
errors.Meta("type", "notAllowedToManageFlag"),
errors.Meta("resource", "system:application"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(applicationLogMetaKey{}, "failed to manage flags {application.name}; insufficient permissions"),
errors.Meta(applicationPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ApplicationErrNotAllowedToManageFlagGlobal returns "system:application.notAllowedToManageFlagGlobal" as *errors.Error
//
//
// This function is auto-generated.
//
func ApplicationErrNotAllowedToManageFlagGlobal(mm ...*applicationActionProps) *errors.Error {
var p = &applicationActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to manage global flags for applications", nil),
errors.Meta("type", "notAllowedToManageFlagGlobal"),
errors.Meta("resource", "system:application"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(applicationLogMetaKey{}, "failed to manage global flags {application.name}; insufficient permissions"),
errors.Meta(applicationPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
+14
View File
@@ -50,6 +50,12 @@ actions:
- action: undelete
log: "undeleted {application}"
- action: flagManage
log: "managed flags for application {application}"
- action: flagManageGlobal
log: "managed global flags for application {application}"
errors:
- error: notFound
message: "application not found"
@@ -82,3 +88,11 @@ errors:
- error: notAllowedToUndelete
message: "not allowed to undelete this application"
log: "failed to undelete {application.name}; insufficient permissions"
- error: notAllowedToManageFlag
message: "not allowed to manage flags for applications"
log: "failed to manage flags {application.name}; insufficient permissions"
- error: notAllowedToManageFlagGlobal
message: "not allowed to manage global flags for applications"
log: "failed to manage global flags {application.name}; insufficient permissions"
+27
View File
@@ -23,6 +23,7 @@ type (
Unify *ApplicationUnify `json:"unify,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Flags []string `json:"flags,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
@@ -45,6 +46,10 @@ type (
LabeledIDs []uint64 `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
FlaggedIDs []uint64 `json:"-"`
Flags []string `json:"flags,omitempty"`
IncFlags uint `json:"-"`
Deleted filter.State `json:"deleted"`
// Check fn is called by store backend for each resource found function can
@@ -95,3 +100,25 @@ func (au *ApplicationUnify) Scan(value interface{}) error {
func (au ApplicationUnify) Value() (driver.Value, error) {
return json.Marshal(au)
}
// // // These will get generated later on
// SetFlags adds new label to label map
func (a *Application) SetFlags(flags []string) {
a.Flags = flags
}
// GetFlags returns current flags on the resource
func (a *Application) GetFlags() []string {
return a.Flags
}
// FlagResourceKind returns the resource kind for the flag
func (*Application) FlagResourceKind() string {
return "system:application"
}
// GetLabels adds new label to label map
func (a *Application) FlagResourceID() uint64 {
return a.ID
}
+279
View File
@@ -8,6 +8,7 @@ import (
"testing"
"time"
ft "github.com/cortezaproject/corteza-server/pkg/flag/types"
"github.com/cortezaproject/corteza-server/pkg/id"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/service"
@@ -39,12 +40,37 @@ func (h helper) repoMakeApplication(ss ...string) *types.Application {
return res
}
func (h helper) repoFlagApplication(appID, owner uint64, flag string, active bool) *ft.Flag {
res := &ft.Flag{
Kind: "system:application",
ResourceID: appID,
OwnedBy: owner,
Name: flag,
Active: active,
}
h.a.NoError(store.CreateFlag(context.Background(), service.DefaultStore, res))
return res
}
func (h helper) lookupApplicationByID(ID uint64) *types.Application {
res, err := store.LookupApplicationByID(context.Background(), service.DefaultStore, ID)
h.noError(err)
return res
}
func (h helper) searchApplicationFlags(ID, owner uint64, flag string) ft.FlagSet {
res, _, err := store.SearchFlags(context.Background(), service.DefaultStore, ft.FlagFilter{
Kind: "system:application",
ResourceID: []uint64{ID},
OwnedBy: []uint64{owner},
Name: []string{flag},
})
h.noError(err)
return res
}
func (h helper) lookupApplicationByName(name string) *types.Application {
res, _, err := store.SearchApplications(context.Background(), service.DefaultStore, types.ApplicationFilter{
Name: name,
@@ -371,3 +397,256 @@ func TestApplicationLabels(t *testing.T) {
req.NotNil(set.FindByID(ID).Labels)
})
}
func TestApplicationFlags(t *testing.T) {
h := newHelper(t)
h.clearApplications()
h.allow(types.SystemRBACResource, "application.create")
t.Run("create", func(t *testing.T) {
h.allow(types.SystemRBACResource, "application.flag.global")
res := h.repoMakeApplication()
h.apiInit().
Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, 0)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
ff := h.searchApplicationFlags(res.ID, 0, "testFlag")
h.a.NotNil(ff)
h.a.Len(ff, 1)
})
t.Run("create; not allowed", func(t *testing.T) {
h.deny(types.SystemRBACResource, "application.flag.global")
res := h.repoMakeApplication()
h.apiInit().
Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, 0)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to manage global flags for applications")).
End()
})
t.Run("create own", func(t *testing.T) {
h.allow(types.SystemRBACResource, "application.flag.self")
res := h.repoMakeApplication()
h.repoFlagApplication(res.ID, 0, "testFlag", true)
h.apiInit().
Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, 10)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
ff := h.searchApplicationFlags(res.ID, 0, "testFlag")
h.a.NotNil(ff)
h.a.Len(ff, 1)
ff = h.searchApplicationFlags(res.ID, 10, "testFlag")
h.a.NotNil(ff)
h.a.Len(ff, 1)
})
t.Run("create own; not allowed", func(t *testing.T) {
h.deny(types.SystemRBACResource, "application.flag.self")
res := h.repoMakeApplication()
h.repoFlagApplication(res.ID, 0, "testFlag", true)
h.apiInit().
Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, 10)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to manage flags for applications")).
End()
})
t.Run("read application", func(t *testing.T) {
h.allow(types.ApplicationRBACResource.AppendWildcard(), "read")
res := h.repoMakeApplication()
h.repoFlagApplication(res.ID, 0, "testFlag", true)
h.apiInit().
Get(fmt.Sprintf("/application/%d", res.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present(`$.response.flags`)).
Assert(jsonpath.Len(`$.response.flags`, 1)).
Assert(jsonpath.Equal(`$.response.flags[0]`, "testFlag")).
End()
})
t.Run("list applications", func(t *testing.T) {
h.allow(types.ApplicationRBACResource.AppendWildcard(), "read")
h.clearApplications()
res := h.repoMakeApplication()
h.repoFlagApplication(res.ID, 0, "testFlag", true)
h.apiInit().
Get("/application/").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Len(`$.response.set`, 1)).
Assert(jsonpath.Len(`$.response.set[0].flags`, 1)).
Assert(jsonpath.Equal(`$.response.set[0].flags[0]`, "testFlag")).
End()
})
t.Run("read application; with own flag", func(t *testing.T) {
h.allow(types.ApplicationRBACResource.AppendWildcard(), "read")
res := h.repoMakeApplication()
h.repoFlagApplication(res.ID, 0, "testFlag", true)
h.repoFlagApplication(res.ID, h.cUser.ID, "testFlagOwn", true)
h.apiInit().
Get(fmt.Sprintf("/application/%d", res.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present(`$.response.flags`)).
Assert(jsonpath.Len(`$.response.flags`, 2)).
End()
})
t.Run("read application; overwrite global", func(t *testing.T) {
h.allow(types.ApplicationRBACResource.AppendWildcard(), "read")
res := h.repoMakeApplication()
h.repoFlagApplication(res.ID, 0, "testFlag", true)
h.repoFlagApplication(res.ID, h.cUser.ID, "testFlag", false)
h.apiInit().
Get(fmt.Sprintf("/application/%d", res.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.NotPresent(`$.response.flags`)).
End()
})
t.Run("filter by flags", func(t *testing.T) {
flag := rs()
h.allow(types.ApplicationRBACResource.AppendWildcard(), "read")
h.repoMakeApplication()
h.repoMakeApplication()
res := h.repoMakeApplication()
h.repoFlagApplication(res.ID, 0, flag, true)
h.apiInit().
Get(fmt.Sprintf("/application/")).
QueryCollection(url.Values{"flags": []string{flag}}).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Len("$.response.set", 1)).
End()
})
t.Run("filter by flags; self inactive", func(t *testing.T) {
flag := rs()
h.allow(types.ApplicationRBACResource.AppendWildcard(), "read")
h.repoMakeApplication()
h.repoMakeApplication()
res := h.repoMakeApplication()
h.repoFlagApplication(res.ID, 0, flag, true)
h.repoFlagApplication(res.ID, h.cUser.ID, flag, false)
h.apiInit().
Get(fmt.Sprintf("/application/")).
QueryCollection(url.Values{"flags": []string{flag}}).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Len("$.response.set", 0)).
End()
})
}
func TestApplicationFlags_Flow1(t *testing.T) {
h := newHelper(t)
h.clearApplications()
h.allow(types.SystemRBACResource, "application.create")
t.Run("create", func(t *testing.T) {
h.allow(types.SystemRBACResource, "application.flag.global")
h.allow(types.SystemRBACResource, "application.flag.self")
res := h.repoMakeApplication()
a := h.apiInit()
a.Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, h.cUser.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
a.Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, 0)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
a.Delete(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, h.cUser.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
ff := h.searchApplicationFlags(res.ID, 0, "testFlag")
h.a.NotNil(ff)
h.a.Len(ff, 1)
h.a.True(ff[0].Active)
ff = h.searchApplicationFlags(res.ID, h.cUser.ID, "testFlag")
h.a.NotNil(ff)
h.a.Len(ff, 1)
h.a.False(ff[0].Active)
a.Delete(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, h.cUser.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
ff = h.searchApplicationFlags(res.ID, 0, "testFlag")
h.a.NotNil(ff)
h.a.Len(ff, 1)
h.a.True(ff[0].Active)
ff = h.searchApplicationFlags(res.ID, h.cUser.ID, "testFlag")
h.a.NotNil(ff)
h.a.Len(ff, 1)
h.a.False(ff[0].Active)
a.Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, h.cUser.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
ff = h.searchApplicationFlags(res.ID, 0, "testFlag")
h.a.NotNil(ff)
h.a.Len(ff, 1)
h.a.True(ff[0].Active)
ff = h.searchApplicationFlags(res.ID, h.cUser.ID, "testFlag")
h.a.NotNil(ff)
h.a.Len(ff, 1)
h.a.True(ff[0].Active)
})
}