Add support for resource flagging; system/application
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user