diff --git a/app/boot_levels.go b/app/boot_levels.go index e05b8969b..ff23bf2ae 100644 --- a/app/boot_levels.go +++ b/app/boot_levels.go @@ -330,12 +330,17 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { // Init DAL and prepare default connection if _, err = dal.InitGlobalService( ctx, - app.Log.Named("DAL"), + app.Log.Named("dal"), app.Opt.Environment.IsDevelopment(), // DB_DSN is the default connection with full capabilities primaryDalConnection.Config.Connection, - primaryDalConnection.ConnectionDefaults(), + dal.ConnectionMeta{ + ConnectionDefaults: primaryDalConnection.ConnectionDefaults(), + // @todo make it configurable from env + SensitivityLevel: 0, + Label: primaryDalConnection.Handle, + }, primaryDalConnection.ActiveCapabilities()...); err != nil { return err } diff --git a/compose/module-field.cue b/compose/module-field.cue index 08bf16df5..6b21014f6 100644 --- a/compose/module-field.cue +++ b/compose/module-field.cue @@ -20,6 +20,7 @@ moduleField: schema.#Resource & { label: {} options: { goType: "types.ModuleFieldOptions" } encoding_strategy: { goType: "types.EncodingStrategy" } + privacy: { goType: "types.DataPrivacyConfig" } private: { goType: "bool", storeIdent: "is_private" } required: { goType: "bool", storeIdent: "is_required" } visible: { goType: "bool", storeIdent: "is_visible" } diff --git a/compose/module.cue b/compose/module.cue index a76976f1d..df8ecb6bb 100644 --- a/compose/module.cue +++ b/compose/module.cue @@ -15,6 +15,7 @@ module: schema.#Resource & { handle: schema.HandleField meta: { goType: "rawJson" } model_config: { goType: "types.ModelConfig" } + privacy: { goType: "types.DataPrivacyConfig" } fields: { goType: "types.ModuleFieldSet", store: false } namespace_id: { ident: "namespaceID", goType: "uint64", storeIdent: "rel_namespace" } name: {} diff --git a/compose/rest.yaml b/compose/rest.yaml index 91f676063..757567f46 100644 --- a/compose/rest.yaml +++ b/compose/rest.yaml @@ -561,6 +561,11 @@ endpoints: required: true title: modelConfig parser: types.ParseModelConfig + - type: types.DataPrivacyConfig + name: privacy + required: true + title: Data privacy config + parser: types.ParseDataPrivacyConfig - type: types.ModuleFieldSet name: fields required: true @@ -607,6 +612,11 @@ endpoints: required: true title: modelConfig parser: types.ParseModelConfig + - type: types.DataPrivacyConfig + name: privacy + required: true + title: Data privacy config + parser: types.ParseDataPrivacyConfig - type: types.ModuleFieldSet name: fields required: true diff --git a/compose/rest/module.go b/compose/rest/module.go index 07bfb20de..6bf6fe656 100644 --- a/compose/rest/module.go +++ b/compose/rest/module.go @@ -108,6 +108,7 @@ func (ctrl *Module) Create(ctx context.Context, r *request.ModuleCreate) (interf mod = &types.Module{ NamespaceID: r.NamespaceID, ModelConfig: r.ModelConfig, + Privacy: r.Privacy, Name: r.Name, Handle: r.Handle, Fields: r.Fields, @@ -127,6 +128,7 @@ func (ctrl *Module) Update(ctx context.Context, r *request.ModuleUpdate) (interf ID: r.ModuleID, NamespaceID: r.NamespaceID, ModelConfig: r.ModelConfig, + Privacy: r.Privacy, Name: r.Name, Handle: r.Handle, Fields: r.Fields, diff --git a/compose/rest/request/module.go b/compose/rest/request/module.go index 8d351029a..b9845279c 100644 --- a/compose/rest/request/module.go +++ b/compose/rest/request/module.go @@ -101,6 +101,11 @@ type ( // modelConfig ModelConfig types.ModelConfig + // Privacy POST parameter + // + // Data privacy config + Privacy types.DataPrivacyConfig + // Fields POST parameter // // Fields JSON @@ -155,6 +160,11 @@ type ( // modelConfig ModelConfig types.ModelConfig + // Privacy POST parameter + // + // Data privacy config + Privacy types.DataPrivacyConfig + // Fields POST parameter // // Fields JSON @@ -382,6 +392,7 @@ func (r ModuleCreate) Auditable() map[string]interface{} { "name": r.Name, "handle": r.Handle, "modelConfig": r.ModelConfig, + "privacy": r.Privacy, "fields": r.Fields, "meta": r.Meta, "labels": r.Labels, @@ -408,6 +419,11 @@ func (r ModuleCreate) GetModelConfig() types.ModelConfig { return r.ModelConfig } +// Auditable returns all auditable/loggable parameters +func (r ModuleCreate) GetPrivacy() types.DataPrivacyConfig { + return r.Privacy +} + // Auditable returns all auditable/loggable parameters func (r ModuleCreate) GetFields() types.ModuleFieldSet { return r.Fields @@ -470,6 +486,18 @@ func (r *ModuleCreate) Fill(req *http.Request) (err error) { } } + if val, ok := req.MultipartForm.Value["privacy[]"]; ok { + r.Privacy, err = types.ParseDataPrivacyConfig(val) + if err != nil { + return err + } + } else if val, ok := req.MultipartForm.Value["privacy"]; ok { + r.Privacy, err = types.ParseDataPrivacyConfig(val) + if err != nil { + return err + } + } + if val, ok := req.MultipartForm.Value["meta"]; ok && len(val) > 0 { r.Meta, err = payload.ParseJSONTextWithErr(val[0]) if err != nil { @@ -524,6 +552,18 @@ func (r *ModuleCreate) Fill(req *http.Request) (err error) { } } + if val, ok := req.Form["privacy[]"]; ok { + r.Privacy, err = types.ParseDataPrivacyConfig(val) + if err != nil { + return err + } + } else if val, ok := req.Form["privacy"]; ok { + r.Privacy, err = types.ParseDataPrivacyConfig(val) + if err != nil { + return err + } + } + //if val, ok := req.Form["fields[]"]; ok && len(val) > 0 { // r.Fields, err = types.ModuleFieldSet(val), nil // if err != nil { @@ -626,6 +666,7 @@ func (r ModuleUpdate) Auditable() map[string]interface{} { "name": r.Name, "handle": r.Handle, "modelConfig": r.ModelConfig, + "privacy": r.Privacy, "fields": r.Fields, "meta": r.Meta, "updatedAt": r.UpdatedAt, @@ -658,6 +699,11 @@ func (r ModuleUpdate) GetModelConfig() types.ModelConfig { return r.ModelConfig } +// Auditable returns all auditable/loggable parameters +func (r ModuleUpdate) GetPrivacy() types.DataPrivacyConfig { + return r.Privacy +} + // Auditable returns all auditable/loggable parameters func (r ModuleUpdate) GetFields() types.ModuleFieldSet { return r.Fields @@ -725,6 +771,18 @@ func (r *ModuleUpdate) Fill(req *http.Request) (err error) { } } + if val, ok := req.MultipartForm.Value["privacy[]"]; ok { + r.Privacy, err = types.ParseDataPrivacyConfig(val) + if err != nil { + return err + } + } else if val, ok := req.MultipartForm.Value["privacy"]; ok { + r.Privacy, err = types.ParseDataPrivacyConfig(val) + if err != nil { + return err + } + } + if val, ok := req.MultipartForm.Value["meta"]; ok && len(val) > 0 { r.Meta, err = payload.ParseJSONTextWithErr(val[0]) if err != nil { @@ -786,6 +844,18 @@ func (r *ModuleUpdate) Fill(req *http.Request) (err error) { } } + if val, ok := req.Form["privacy[]"]; ok { + r.Privacy, err = types.ParseDataPrivacyConfig(val) + if err != nil { + return err + } + } else if val, ok := req.Form["privacy"]; ok { + r.Privacy, err = types.ParseDataPrivacyConfig(val) + if err != nil { + return err + } + } + //if val, ok := req.Form["fields[]"]; ok && len(val) > 0 { // r.Fields, err = types.ModuleFieldSet(val), nil // if err != nil { diff --git a/compose/service/module_dal.go b/compose/service/module_dal.go index bda83762e..fbca0d3ab 100644 --- a/compose/service/module_dal.go +++ b/compose/service/module_dal.go @@ -110,9 +110,12 @@ func (svc *module) moduleToModel(ctx context.Context, ns *types.Namespace, mod * out := &dal.Model{ ConnectionID: mod.ModelConfig.ConnectionID, Ident: svc.formatPartitionIdent(ns, mod, ccfg), + Label: mod.Handle, Attributes: make(dal.AttributeSet, len(mod.Fields)), + SensitivityLevel: mod.Privacy.SensitivityLevel, + ResourceID: mod.ID, ResourceType: types.ModuleResourceType, Resource: mod.RbacResource(), @@ -277,6 +280,9 @@ func (svc *module) moduleFieldToAttribute(getCodec func(f *types.ModuleField) da return nil, fmt.Errorf("invalid field %s: kind %s not supported", f.Name, f.Kind) } + out.SensitivityLevel = f.Privacy.SensitivityLevel + out.Label = f.Name + return } diff --git a/compose/types/module.go b/compose/types/module.go index 7d2a49f91..c33a430cd 100644 --- a/compose/types/module.go +++ b/compose/types/module.go @@ -27,12 +27,18 @@ type ( SystemFieldEncoding SystemFieldEncoding `json:"systemFieldEncoding"` } + DataPrivacyConfig struct { + SensitivityLevel uint64 `json:"sensitivityLevel,string"` + UsageDisclosure string `json:"usageDisclosure"` + } + Module struct { ID uint64 `json:"moduleID,string"` Handle string `json:"handle"` Meta types.JSONText `json:"meta"` - ModelConfig ModelConfig `json:"modelConfig"` + ModelConfig ModelConfig `json:"modelConfig"` + Privacy DataPrivacyConfig `json:"privacy"` Fields ModuleFieldSet `json:"fields"` @@ -181,3 +187,31 @@ func ParseModelConfig(ss []string) (m ModelConfig, err error) { err = json.Unmarshal([]byte(ss[0]), &m) return } + +func (nm *DataPrivacyConfig) Scan(value interface{}) error { + //lint:ignore S1034 This typecast is intentional, we need to get []byte out of a []uint8 + switch value.(type) { + case nil: + *nm = DataPrivacyConfig{} + case []uint8: + b := value.([]byte) + if err := json.Unmarshal(b, nm); err != nil { + return errors.Wrapf(err, "cannot scan '%v' into DataPrivacyConfig", string(b)) + } + } + + return nil +} + +func (nm DataPrivacyConfig) Value() (driver.Value, error) { + return json.Marshal(nm) +} + +func ParseDataPrivacyConfig(ss []string) (dpc DataPrivacyConfig, err error) { + if len(ss) == 0 { + return + } + + err = json.Unmarshal([]byte(ss[0]), &dpc) + return +} diff --git a/compose/types/module_field.go b/compose/types/module_field.go index b9355e867..91312044a 100644 --- a/compose/types/module_field.go +++ b/compose/types/module_field.go @@ -28,6 +28,8 @@ type ( EncodingStrategy EncodingStrategy `json:"encodingStrategy"` + Privacy DataPrivacyConfig `json:"privacy"` + Private bool `json:"isPrivate"` Required bool `json:"isRequired"` Visible bool `json:"isVisible"` diff --git a/pkg/dal/errors.go b/pkg/dal/errors.go new file mode 100644 index 000000000..a53d4d891 --- /dev/null +++ b/pkg/dal/errors.go @@ -0,0 +1,44 @@ +package dal + +import ( + "github.com/cortezaproject/corteza-server/pkg/errors" + "github.com/cortezaproject/corteza-server/pkg/locale" +) + +func errModelHigherSensitivity(model, connection string) error { + return errors.New( + errors.KindSensitiveData, + + "model sensitivity surpasses connection sensitivity", + + errors.Meta("type", "invalid sensitivity"), + + // Translation namespace & key + errors.Meta(locale.ErrorMetaNamespace{}, "internal"), + errors.Meta(locale.ErrorMetaKey{}, "dal.sensitivity.model-exceeds-connection"), + errors.Meta("model", model), + errors.Meta("connection", connection), + + errors.StackSkip(1), + errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"), + ) +} + +func errAttributeHigherSensitivity(model, attribute string) error { + return errors.New( + errors.KindSensitiveData, + + "attribute sensitivity surpasses model sensitivity", + + errors.Meta("type", "invalid sensitivity"), + + // Translation namespace & key + errors.Meta(locale.ErrorMetaNamespace{}, "internal"), + errors.Meta(locale.ErrorMetaKey{}, "dal.sensitivity.attribute-exceeds-model"), + errors.Meta("model", model), + errors.Meta("attribute", attribute), + + errors.StackSkip(1), + errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"), + ) +} diff --git a/pkg/dal/model.go b/pkg/dal/model.go index be95adcbf..1a93974e1 100644 --- a/pkg/dal/model.go +++ b/pkg/dal/model.go @@ -23,11 +23,14 @@ type ( Model struct { ConnectionID uint64 Ident string + Label string Resource string ResourceID uint64 ResourceType string + SensitivityLevel uint64 + Attributes AttributeSet } ModelSet []*Model @@ -35,6 +38,9 @@ type ( // Attribute describes a specific value of the dataset Attribute struct { Ident string + Label string + + SensitivityLevel uint64 MultiValue bool @@ -73,6 +79,7 @@ func PrimaryAttribute(ident string, codec Codec) *Attribute { func FullAttribute(ident string, at Type, codec Codec) *Attribute { return &Attribute{ Ident: ident, + Label: ident, Sortable: true, Filterable: true, Store: codec, diff --git a/pkg/dal/sensitivity_level.go b/pkg/dal/sensitivity_level.go new file mode 100644 index 000000000..50d062c3c --- /dev/null +++ b/pkg/dal/sensitivity_level.go @@ -0,0 +1,53 @@ +package dal + +type ( + SensitivityLevel struct { + Handle string + ID uint64 + } + SensitivityLevelSet []SensitivityLevel + + sensitivityLevelIndex struct { + set SensitivityLevelSet + + byHandle map[string]int + byID map[uint64]int + } +) + +func (sli sensitivityLevelIndex) includes(l uint64) (ok bool) { + _, ok = sli.byID[l] + return +} + +func (sli sensitivityLevelIndex) isSubset(a, b uint64) (ok bool) { + // Edgecases + // If A is zero theneverything is possible + if a == 0 { + return true + } + // If B is zero, then A must also be zero + if b == 0 { + return a == 0 + } + + var lvlA, lvlB int + + if lvlA, ok = sli.byID[a]; !ok { + return false + } + + if lvlB, ok = sli.byID[b]; !ok { + return false + } + + return lvlA <= lvlB +} + +func (ss SensitivityLevelSet) includes(l uint64) (ok bool) { + for _, s := range ss { + ok = ok || s.ID == l + } + + return +} diff --git a/pkg/dal/service.go b/pkg/dal/service.go index e24b28631..66bd818e5 100644 --- a/pkg/dal/service.go +++ b/pkg/dal/service.go @@ -11,10 +11,21 @@ import ( type ( connectionWrap struct { + connectionID uint64 + label string + sensitivityLevel uint64 + connection Connection Defaults ConnectionDefaults } + ConnectionMeta struct { + ConnectionDefaults + + SensitivityLevel uint64 + Label string + } + ConnectionDefaults struct { ModelIdent string AttributeIdent string @@ -31,6 +42,8 @@ type ( logger *zap.Logger inDev bool + + sensitivityLevels sensitivityLevelIndex } ) @@ -43,8 +56,10 @@ var ( ) // InitGlobalService initializes a fresh DAL where the given primary connection -func InitGlobalService(ctx context.Context, log *zap.Logger, inDev bool, cp ConnectionParams, dft ConnectionDefaults, capabilities ...capabilities.Capability) (*service, error) { +func InitGlobalService(ctx context.Context, log *zap.Logger, inDev bool, cp ConnectionParams, cm ConnectionMeta, capabilities ...capabilities.Capability) (*service, error) { if gSvc == nil { + log.Debug("initializing DAL service with primary connection", zap.Any("connection params", cp)) + gSvc = &service{ connections: make(map[uint64]*connectionWrap), models: make(map[uint64]ModelSet), @@ -56,7 +71,9 @@ func InitGlobalService(ctx context.Context, log *zap.Logger, inDev bool, cp Conn var err error cw := &connectionWrap{ - Defaults: dft, + Defaults: cm.ConnectionDefaults, + sensitivityLevel: cm.SensitivityLevel, + label: cm.Label, } cw.connection, err = connect(ctx, log, inDev, cp, capabilities...) if err != nil { @@ -81,7 +98,7 @@ func Service() *service { } // // // // // // // // // // // // // // // // // // // // // // // // // -// driver management +// meta func (svc *service) Drivers() (drivers []Driver) { for _, d := range registeredDrivers { @@ -91,16 +108,37 @@ func (svc *service) Drivers() (drivers []Driver) { return } +func (svc *service) ReloadSensitivityLevels(levels SensitivityLevelSet) (err error) { + svc.logger.Debug("reloading sensitivity levels", zap.Any("sensitivity levels", levels)) + newLevelIndex := svc.newSensitivityLevelIndex(levels) + + // Validate state after sensitivity level change + if err = svc.validateNewSensitivityLevels(newLevelIndex); err != nil { + return + } + + // Replace old ones + svc.sensitivityLevels = newLevelIndex + + return +} + // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // Connection management // AddConnection adds a new connection to the DAL -func (svc *service) AddConnection(ctx context.Context, connectionID uint64, cp ConnectionParams, dft ConnectionDefaults, capabilities ...capabilities.Capability) (err error) { +func (svc *service) AddConnection(ctx context.Context, connectionID uint64, cp ConnectionParams, cm ConnectionMeta, capabilities ...capabilities.Capability) (err error) { + svc.logger.Debug("adding new connection", zap.Uint64("connectionID", connectionID), zap.Any("connection params", cp)) + cw := &connectionWrap{ - Defaults: dft, + connectionID: connectionID, + Defaults: cm.ConnectionDefaults, + sensitivityLevel: cm.SensitivityLevel, + label: cm.Label, } + cw.connection, err = connect(ctx, svc.logger, svc.inDev, cp, capabilities...) if err != nil { return @@ -111,6 +149,8 @@ func (svc *service) AddConnection(ctx context.Context, connectionID uint64, cp C // RemoveConnection removes the given connection from the DAL func (svc *service) RemoveConnection(ctx context.Context, connectionID uint64) (err error) { + svc.logger.Debug("removing connection", zap.Uint64("connectionID", connectionID)) + c := svc.connections[connectionID] if c == nil { return fmt.Errorf("can not remove connection %d: connection does not exist", connectionID) @@ -132,12 +172,15 @@ func (svc *service) RemoveConnection(ctx context.Context, connectionID uint64) ( // UpdateConnection updates the given connection // // @todo make this better; for now remove + add -func (svc *service) UpdateConnection(ctx context.Context, connectionID uint64, cp ConnectionParams, dft ConnectionDefaults, capabilities ...capabilities.Capability) (err error) { +func (svc *service) UpdateConnection(ctx context.Context, connectionID uint64, cp ConnectionParams, cm ConnectionMeta, capabilities ...capabilities.Capability) (err error) { + svc.logger.Debug("updating connection", zap.Uint64("connectionID", connectionID)) + if err = svc.RemoveConnection(ctx, connectionID); err != nil { return } + // @todo check sensitivity level against modules - return svc.AddConnection(ctx, connectionID, cp, dft, capabilities...) + return svc.AddConnection(ctx, connectionID, cp, cm, capabilities...) } // ConnectionDefaultreturns the defaults we can use with this connection @@ -210,7 +253,7 @@ func (svc *service) Truncate(ctx context.Context, mf ModelFilter, capabilities c func (svc *service) storeOpPrep(ctx context.Context, mf ModelFilter, capabilities capabilities.Set) (model *Model, cw *connectionWrap, err error) { model = svc.getModelByFilter(mf) if model == nil { - err = fmt.Errorf("cannot perform operation: model not registered") + err = fmt.Errorf("model not found") return } @@ -229,6 +272,8 @@ func (svc *service) storeOpPrep(ctx context.Context, mf ModelFilter, capabilitie // ReloadModel unregister old models and register the new ones func (svc *service) ReloadModel(ctx context.Context, models ...*Model) (err error) { + svc.logger.Debug("reloading models") + // Clear up the old ones // @todo profile if manually removing nested pointers makes it faster svc.models = make(map[uint64]ModelSet) @@ -237,6 +282,8 @@ func (svc *service) ReloadModel(ctx context.Context, models ...*Model) (err erro // AddModel adds support for a new model func (svc *service) AddModel(ctx context.Context, models ...*Model) (err error) { + svc.logger.Debug("adding model", zap.Int("count", len(models))) + var ( cw *connectionWrap ) @@ -247,7 +294,7 @@ func (svc *service) AddModel(ctx context.Context, models ...*Model) (err error) return err } - err = svc.registerModel(ctx, cw.connection, connectionID, models) + err = svc.registerModel(ctx, cw, connectionID, models) if err != nil { return } @@ -258,12 +305,16 @@ func (svc *service) AddModel(ctx context.Context, models ...*Model) (err error) // RemoveModel removes support for the given model func (svc *service) RemoveModel(ctx context.Context, models ...*Model) (err error) { + svc.logger.Debug("removing models", zap.Int("count", len(models))) + // validation for _, model := range models { + svc.logger.Debug("removing model", zap.String("resource type", model.ResourceType), zap.String("resource model", model.Resource)) + // Validate existence old := svc.GetModelByResource(model.ConnectionID, model.ResourceType, model.Resource) if old == nil { - return fmt.Errorf("cannot remove module %s: not registered", model.Resource) + return fmt.Errorf("cannot remove model %s: model not found", model.Resource) } // Validate no leftover references @@ -271,7 +322,7 @@ func (svc *service) RemoveModel(ctx context.Context, models ...*Model) (err erro for _, registered := range svc.models { refs := registered.FilterByReferenced(model) if len(refs) > 0 { - return fmt.Errorf("cannot remove module %s: referenced by other modules", model.Resource) + return fmt.Errorf("cannot remove model %s: referenced by other models", model.Resource) } } } @@ -329,12 +380,12 @@ func (svc *service) getConnection(ctx context.Context, connectionID uint64, cc . cw = svc.connections[connectionID] } if cw == nil { - return fmt.Errorf("could not get connection %d: store does not exist", connectionID) + return fmt.Errorf("connection %d does not exist", connectionID) } // check if connection supports requested capabilities if !cw.connection.Can(cc...) { - return fmt.Errorf("connection does not support requested capabilities: %v", capabilities.Set(cc).Diff(cw.connection.Capabilities())) + return fmt.Errorf("connection %d does not support requested capabilities %v", connectionID, capabilities.Set(cc).Diff(cw.connection.Capabilities())) } can = cw.connection.Capabilities() return nil @@ -359,26 +410,28 @@ func (svc *service) modelByConnection(models ModelSet) (out map[uint64]ModelSet) return } -func (svc *service) registerModel(ctx context.Context, s Connection, storeID uint64, models ModelSet) (err error) { +func (svc *service) registerModel(ctx context.Context, cw *connectionWrap, connectionID uint64, models ModelSet) (err error) { for _, model := range models { - existing := svc.GetModelByResource(storeID, model.ResourceType, model.Resource) + svc.logger.Debug("adding model for connection", zap.Uint64("connectionID", connectionID), zap.String("resource type", model.ResourceType), zap.String("resource model", model.Resource)) + + existing := svc.GetModelByResource(connectionID, model.ResourceType, model.Resource) if existing != nil { - return fmt.Errorf("cannot add model %s to store %d: already exists", model.Resource, storeID) + return fmt.Errorf("cannot add model %s to store %d: model already exists", model.Resource, connectionID) } - err = svc.registerModelToConnection(ctx, s, model) + err = svc.registerModelToConnection(ctx, cw, model) if err != nil { return } - svc.models[storeID] = append(svc.models[storeID], model) + svc.models[connectionID] = append(svc.models[connectionID], model) } return } -func (svc *service) registerModelToConnection(ctx context.Context, s Connection, model *Model) (err error) { - available, err := s.Models(ctx) +func (svc *service) registerModelToConnection(ctx context.Context, cw *connectionWrap, model *Model) (err error) { + available, err := cw.connection.Models(ctx) if err != nil { return err } @@ -388,14 +441,27 @@ func (svc *service) registerModelToConnection(ctx context.Context, s Connection, // Assert validity diff := existing.Diff(model) if len(diff) > 0 { - return fmt.Errorf("model %s exists: model not compatible: %v", existing.Resource, diff) + return fmt.Errorf("cannot add model %d: model already exists for connection %d: models not compatible: %v", existing.ResourceID, cw.connectionID, diff) } return nil } + // Validate model against connection + { + if !svc.sensitivityLevels.isSubset(model.SensitivityLevel, cw.sensitivityLevel) { + return errModelHigherSensitivity(model.Label, cw.label) + } + + for _, attr := range model.Attributes { + if !svc.sensitivityLevels.isSubset(attr.SensitivityLevel, model.SensitivityLevel) { + return errAttributeHigherSensitivity(model.Label, attr.Label) + } + } + } + // Try to add to store - err = s.CreateModel(ctx, model) + err = cw.connection.CreateModel(ctx, model) if err != nil { return } @@ -409,3 +475,63 @@ func (svc *service) getModelByFilter(mf ModelFilter) *Model { } return svc.GetModelByResource(mf.ConnectionID, mf.ResourceType, mf.Resource) } + +func (svc *service) newSensitivityLevelIndex(levels SensitivityLevelSet) (out sensitivityLevelIndex) { + out = sensitivityLevelIndex{ + byID: make(map[uint64]int), + byHandle: make(map[string]int), + set: make(SensitivityLevelSet, len(levels)), + } + + for i, l := range levels { + out.set[i] = l + + out.byID[l.ID] = i + out.byHandle[l.Handle] = i + } + + return +} + +func (svc *service) validateNewSensitivityLevels(levels sensitivityLevelIndex) (err error) { + err = func() (err error) { + cIndex := make(map[uint64]*connectionWrap) + + // - connections + for _, _c := range svc.connections { + c := _c + cIndex[c.connectionID] = c + + if !levels.includes(c.sensitivityLevel) { + return fmt.Errorf("connection sensitivity level missing %d", c.sensitivityLevel) + } + } + + // - models + for _, mm := range svc.models { + for _, m := range mm { + if !levels.includes(m.SensitivityLevel) { + return fmt.Errorf("model sensitivity level missing %d", m.SensitivityLevel) + } + if !levels.isSubset(m.SensitivityLevel, cIndex[m.ConnectionID].sensitivityLevel) { + return fmt.Errorf("model sensitivity level missing %d", m.SensitivityLevel) + } + + for _, attr := range m.Attributes { + if !levels.includes(attr.SensitivityLevel) { + return fmt.Errorf("attribute sensitivity level missing %d", attr.SensitivityLevel) + } + if !levels.isSubset(attr.SensitivityLevel, m.SensitivityLevel) { + return fmt.Errorf("attribute sensitivity level %d greater then model sensitivity level %d", attr.SensitivityLevel, m.SensitivityLevel) + } + } + } + } + return + }() + + if err != nil { + return fmt.Errorf("cannot reload sensitivity levels: %v", err) + } + return +} diff --git a/pkg/errors/kind.go b/pkg/errors/kind.go index 1d67ab850..e87ec28ed 100644 --- a/pkg/errors/kind.go +++ b/pkg/errors/kind.go @@ -28,6 +28,9 @@ const ( // Data already exists KindDuplicateData + // Data sensitivity related issue + KindSensitiveData + // Access control KindUnauthorized @@ -50,7 +53,7 @@ const ( // translates error kind into http status func (k kind) httpStatus() int { switch k { - case KindInvalidData: + case KindInvalidData, KindSensitiveData: return http.StatusBadRequest case KindNotFound: @@ -101,6 +104,10 @@ func DuplicateData(m string, aa ...interface{}) *Error { return err(KindDuplicateData, fmt.Sprintf(m, aa...)) } +func SensitiveData(m string, aa ...interface{}) *Error { + return err(KindSensitiveData, fmt.Sprintf(m, aa...)) +} + func Unauthorized(m string, aa ...interface{}) *Error { return err(KindUnauthorized, fmt.Sprintf(m, aa...)) } @@ -164,6 +171,10 @@ func IsDuplicateData(err error) bool { return IsKind(err, KindDuplicateData) } +func IsSensitiveData(err error) bool { + return IsKind(err, KindSensitiveData) +} + func IsUnauthorized(err error) bool { return IsKind(err, KindUnauthorized) } diff --git a/store/adapters/rdbms/aux_types.gen.go b/store/adapters/rdbms/aux_types.gen.go index aa73ea28b..d8fa3661d 100644 --- a/store/adapters/rdbms/aux_types.gen.go +++ b/store/adapters/rdbms/aux_types.gen.go @@ -244,15 +244,16 @@ type ( // auxComposeModule is an auxiliary structure used for transporting to/from RDBMS store auxComposeModule struct { - ID uint64 `db:"id"` - Handle string `db:"handle"` - Meta rawJson `db:"meta"` - ModelConfig composeType.ModelConfig `db:"model_config"` - NamespaceID uint64 `db:"namespace_id"` - Name string `db:"name"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt *time.Time `db:"updated_at"` - DeletedAt *time.Time `db:"deleted_at"` + ID uint64 `db:"id"` + Handle string `db:"handle"` + Meta rawJson `db:"meta"` + ModelConfig composeType.ModelConfig `db:"model_config"` + Privacy composeType.DataPrivacyConfig `db:"privacy"` + NamespaceID uint64 `db:"namespace_id"` + Name string `db:"name"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt *time.Time `db:"updated_at"` + DeletedAt *time.Time `db:"deleted_at"` } // auxComposeModuleField is an auxiliary structure used for transporting to/from RDBMS store @@ -265,6 +266,7 @@ type ( Label string `db:"label"` Options composeType.ModuleFieldOptions `db:"options"` EncodingStrategy composeType.EncodingStrategy `db:"encoding_strategy"` + Privacy composeType.DataPrivacyConfig `db:"privacy"` Private bool `db:"private"` Required bool `db:"required"` Visible bool `db:"visible"` @@ -1434,6 +1436,7 @@ func (aux *auxComposeModule) encode(res *composeType.Module) (_ error) { aux.Handle = res.Handle aux.Meta = res.Meta aux.ModelConfig = res.ModelConfig + aux.Privacy = res.Privacy aux.NamespaceID = res.NamespaceID aux.Name = res.Name aux.CreatedAt = res.CreatedAt @@ -1451,6 +1454,7 @@ func (aux auxComposeModule) decode() (res *composeType.Module, _ error) { res.Handle = aux.Handle res.Meta = aux.Meta res.ModelConfig = aux.ModelConfig + res.Privacy = aux.Privacy res.NamespaceID = aux.NamespaceID res.Name = aux.Name res.CreatedAt = aux.CreatedAt @@ -1468,6 +1472,7 @@ func (aux *auxComposeModule) scan(row scanner) error { &aux.Handle, &aux.Meta, &aux.ModelConfig, + &aux.Privacy, &aux.NamespaceID, &aux.Name, &aux.CreatedAt, @@ -1488,6 +1493,7 @@ func (aux *auxComposeModuleField) encode(res *composeType.ModuleField) (_ error) aux.Label = res.Label aux.Options = res.Options aux.EncodingStrategy = res.EncodingStrategy + aux.Privacy = res.Privacy aux.Private = res.Private aux.Required = res.Required aux.Visible = res.Visible @@ -1513,6 +1519,7 @@ func (aux auxComposeModuleField) decode() (res *composeType.ModuleField, _ error res.Label = aux.Label res.Options = aux.Options res.EncodingStrategy = aux.EncodingStrategy + res.Privacy = aux.Privacy res.Private = aux.Private res.Required = aux.Required res.Visible = aux.Visible @@ -1538,6 +1545,7 @@ func (aux *auxComposeModuleField) scan(row scanner) error { &aux.Label, &aux.Options, &aux.EncodingStrategy, + &aux.Privacy, &aux.Private, &aux.Required, &aux.Visible, diff --git a/store/adapters/rdbms/queries.gen.go b/store/adapters/rdbms/queries.gen.go index 041dd55a8..58c82dffa 100644 --- a/store/adapters/rdbms/queries.gen.go +++ b/store/adapters/rdbms/queries.gen.go @@ -1637,6 +1637,7 @@ var ( "handle", "meta", "model_config", + "privacy", "rel_namespace", "name", "created_at", @@ -1655,6 +1656,7 @@ var ( "handle": res.Handle, "meta": res.Meta, "model_config": res.ModelConfig, + "privacy": res.Privacy, "rel_namespace": res.NamespaceID, "name": res.Name, "created_at": res.CreatedAt, @@ -1676,6 +1678,7 @@ var ( "handle": res.Handle, "meta": res.Meta, "model_config": res.ModelConfig, + "privacy": res.Privacy, "rel_namespace": res.NamespaceID, "name": res.Name, "created_at": res.CreatedAt, @@ -1695,6 +1698,7 @@ var ( "handle": res.Handle, "meta": res.Meta, "model_config": res.ModelConfig, + "privacy": res.Privacy, "rel_namespace": res.NamespaceID, "name": res.Name, "created_at": res.CreatedAt, @@ -1745,6 +1749,7 @@ var ( "label", "options", "encoding_strategy", + "privacy", "is_private", "is_required", "is_visible", @@ -1771,6 +1776,7 @@ var ( "label": res.Label, "options": res.Options, "encoding_strategy": res.EncodingStrategy, + "privacy": res.Privacy, "is_private": res.Private, "is_required": res.Required, "is_visible": res.Visible, @@ -1800,6 +1806,7 @@ var ( "label": res.Label, "options": res.Options, "encoding_strategy": res.EncodingStrategy, + "privacy": res.Privacy, "is_private": res.Private, "is_required": res.Required, "is_visible": res.Visible, @@ -1827,6 +1834,7 @@ var ( "label": res.Label, "options": res.Options, "encoding_strategy": res.EncodingStrategy, + "privacy": res.Privacy, "is_private": res.Private, "is_required": res.Required, "is_visible": res.Visible, diff --git a/store/adapters/rdbms/upgrade_tables.go b/store/adapters/rdbms/upgrade_tables.go index 5ae1777b8..f7c6bec83 100644 --- a/store/adapters/rdbms/upgrade_tables.go +++ b/store/adapters/rdbms/upgrade_tables.go @@ -447,6 +447,7 @@ func tableComposeModule() *Table { ColumnDef("handle", ColumnTypeVarchar, ColumnTypeLength(handleLength)), ColumnDef("name", ColumnTypeText), ColumnDef("meta", ColumnTypeJson), + ColumnDef("privacy", ColumnTypeJson), ColumnDef("model_config", ColumnTypeJson), CUDTimestamps, @@ -463,6 +464,7 @@ func tableComposeModuleField() *Table { ColumnDef("place", ColumnTypeInteger), ColumnDef("kind", ColumnTypeText), ColumnDef("options", ColumnTypeJson), + ColumnDef("privacy", ColumnTypeJson), ColumnDef("encoding_strategy", ColumnTypeJson), ColumnDef("default_value", ColumnTypeJson), ColumnDef("expressions", ColumnTypeJson), diff --git a/system/service/dal_connection.go b/system/service/dal_connection.go index 7b29e76b4..20891e2ea 100644 --- a/system/service/dal_connection.go +++ b/system/service/dal_connection.go @@ -33,8 +33,8 @@ type ( } dalConnections interface { - AddConnection(ctx context.Context, connectionID uint64, cp dal.ConnectionParams, dft dal.ConnectionDefaults, capabilities ...capabilities.Capability) (err error) - UpdateConnection(ctx context.Context, connectionID uint64, cp dal.ConnectionParams, dft dal.ConnectionDefaults, capabilities ...capabilities.Capability) (err error) + AddConnection(ctx context.Context, connectionID uint64, cp dal.ConnectionParams, dft dal.ConnectionMeta, capabilities ...capabilities.Capability) (err error) + UpdateConnection(ctx context.Context, connectionID uint64, cp dal.ConnectionParams, dft dal.ConnectionMeta, capabilities ...capabilities.Capability) (err error) RemoveConnection(ctx context.Context, connectionID uint64) (err error) } ) @@ -119,7 +119,13 @@ func (svc *dalConnection) Create(ctx context.Context, new *types.DalConnection) q = new - return svc.dal.AddConnection(ctx, new.ID, new.Config.Connection, new.ConnectionDefaults(), new.ActiveCapabilities()...) + var cm dal.ConnectionMeta + cm, err = svc.makeConnectionMeta(ctx, new) + if err != nil { + return + } + + return svc.dal.AddConnection(ctx, new.ID, new.Config.Connection, cm, new.ActiveCapabilities()...) }() return q, svc.recordAction(ctx, qProps, DalConnectionActionCreate, err) @@ -151,7 +157,13 @@ func (svc *dalConnection) Update(ctx context.Context, upd *types.DalConnection) q = upd - return svc.dal.UpdateConnection(ctx, upd.ID, upd.Config.Connection, upd.ConnectionDefaults(), upd.ActiveCapabilities()...) + var cm dal.ConnectionMeta + cm, err = svc.makeConnectionMeta(ctx, upd) + if err != nil { + return + } + + return svc.dal.UpdateConnection(ctx, upd.ID, upd.Config.Connection, cm, upd.ActiveCapabilities()...) }() return q, svc.recordAction(ctx, qProps, DalConnectionActionUpdate, err) @@ -219,7 +231,13 @@ func (svc *dalConnection) UndeleteByID(ctx context.Context, ID uint64) (err erro return } - return svc.dal.AddConnection(ctx, q.ID, q.Config.Connection, q.ConnectionDefaults(), q.ActiveCapabilities()...) + var cm dal.ConnectionMeta + cm, err = svc.makeConnectionMeta(ctx, q) + if err != nil { + return + } + + return svc.dal.AddConnection(ctx, q.ID, q.Config.Connection, cm, q.ActiveCapabilities()...) }() return svc.recordAction(ctx, qProps, DalConnectionActionDelete, err) @@ -262,10 +280,30 @@ func (svc *dalConnection) reloadConnections(ctx context.Context) (err error) { } for _, c := range cc { - if err = svc.dal.AddConnection(ctx, c.ID, c.Config.Connection, c.ConnectionDefaults(), c.ActiveCapabilities()...); err != nil { + var cm dal.ConnectionMeta + cm, err = svc.makeConnectionMeta(ctx, c) + if err != nil { + return + } + if err = svc.dal.AddConnection(ctx, c.ID, c.Config.Connection, cm, c.ActiveCapabilities()...); err != nil { return } } return } + +func (svc *dalConnection) makeConnectionMeta(ctx context.Context, c *types.DalConnection) (cm dal.ConnectionMeta, err error) { + // @todo we could probably utilize connection params more here + cm = dal.ConnectionMeta{ + ConnectionDefaults: dal.ConnectionDefaults{ + ModelIdent: c.Config.DefaultModelIdent, + AttributeIdent: c.Config.DefaultAttributeIdent, + PartitionFormat: c.Config.DefaultPartitionFormat, + }, + SensitivityLevel: c.SensitivityLevel, + Label: c.Handle, + } + + return +} diff --git a/system/service/dal_sensitivity_level.go b/system/service/dal_sensitivity_level.go index 2a3a8c831..761d3ca88 100644 --- a/system/service/dal_sensitivity_level.go +++ b/system/service/dal_sensitivity_level.go @@ -2,9 +2,13 @@ package service import ( "context" + "fmt" + "sort" "github.com/cortezaproject/corteza-server/pkg/actionlog" a "github.com/cortezaproject/corteza-server/pkg/auth" + "github.com/cortezaproject/corteza-server/pkg/dal" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/types" @@ -15,29 +19,27 @@ type ( actionlog actionlog.Recorder store store.Storer ac sensitivityLevelAccessController - // dal dalSensitivityLevels + dal dalSensitivityLevels } sensitivityLevelAccessController interface { CanManageDalSensitivityLevel(context.Context) bool } - // dalSensitivityLevels interface { - // AddSensitivityLevel(ctx context.Context, sensitivityLevelID uint64, cp dal.SensitivityLevelParams, dft dal.SensitivityLevelDefaults, capabilities ...capabilities.Capability) (err error) - // UpdateSensitivityLevel(ctx context.Context, sensitivityLevelID uint64, cp dal.SensitivityLevelParams, dft dal.SensitivityLevelDefaults, capabilities ...capabilities.Capability) (err error) - // RemoveSensitivityLevel(ctx context.Context, sensitivityLevelID uint64) (err error) - // } + dalSensitivityLevels interface { + ReloadSensitivityLevels(raw dal.SensitivityLevelSet) (err error) + } ) -func SensitivityLevel(ctx context.Context) (*dalSensitivityLevel, error) { +func SensitivityLevel(ctx context.Context, dal dalSensitivityLevels) (*dalSensitivityLevel, error) { out := &dalSensitivityLevel{ ac: DefaultAccessControl, actionlog: DefaultActionlog, store: DefaultStore, - // dal: dal, + dal: dal, } - return out, nil // out.reloadSensitivityLevels(ctx) + return out, out.reloadSensitivityLevels(ctx, out.store) } func (svc *dalSensitivityLevel) FindByID(ctx context.Context, ID uint64) (q *types.DalSensitivityLevel, err error) { @@ -76,18 +78,22 @@ func (svc *dalSensitivityLevel) Create(ctx context.Context, new *types.DalSensit return DalSensitivityLevelErrNotAllowedToManage(qProps) } - new.ID = nextID() new.CreatedAt = *now() new.CreatedBy = a.GetIdentityFromContext(ctx).Identity() + ups, err := svc.prepare(ctx, svc.store, new) + if err != nil { + return + } + new.ID = nextID() - if err = store.CreateDalSensitivityLevel(ctx, svc.store, new); err != nil { - return err + err = store.UpsertDalSensitivityLevel(ctx, svc.store, ups...) + if err != nil { + return } q = new - // return svc.dal.AddSensitivityLevel(ctx, new.ID, new.Config.SensitivityLevel, new.SensitivityLevelDefaults(), new.ActiveCapabilities()...) - return + return svc.reloadSensitivityLevels(ctx, svc.store) }() return q, svc.recordAction(ctx, qProps, DalSensitivityLevelActionCreate, err) @@ -113,14 +119,18 @@ func (svc *dalSensitivityLevel) Update(ctx context.Context, upd *types.DalSensit upd.CreatedAt = qq.CreatedAt upd.UpdatedBy = a.GetIdentityFromContext(ctx).Identity() - if err = store.UpdateDalSensitivityLevel(ctx, svc.store, upd); err != nil { + ups, err := svc.prepare(ctx, svc.store, upd) + if err != nil { + return + } + err = store.UpsertDalSensitivityLevel(ctx, svc.store, ups...) + if err != nil { return } q = upd - // return svc.dal.UpdateSensitivityLevel(ctx, upd.ID, upd.Config.SensitivityLevel, upd.SensitivityLevelDefaults(), upd.ActiveCapabilities()...) - return + return svc.reloadSensitivityLevels(ctx, svc.store) }() return q, svc.recordAction(ctx, qProps, DalSensitivityLevelActionUpdate, err) @@ -150,12 +160,16 @@ func (svc *dalSensitivityLevel) DeleteByID(ctx context.Context, ID uint64) (err q.DeletedAt = now() q.DeletedBy = a.GetIdentityFromContext(ctx).Identity() - if err = store.UpdateDalSensitivityLevel(ctx, svc.store, q); err != nil { + ups, err := svc.prepare(ctx, svc.store, q) + if err != nil { + return + } + err = store.UpsertDalSensitivityLevel(ctx, svc.store, ups...) + if err != nil { return } - // return svc.dal.RemoveSensitivityLevel(ctx, q.ID) - return + return svc.reloadSensitivityLevels(ctx, svc.store) }() return svc.recordAction(ctx, qProps, DalSensitivityLevelActionDelete, err) @@ -189,8 +203,7 @@ func (svc *dalSensitivityLevel) UndeleteByID(ctx context.Context, ID uint64) (er return } - // return svc.dal.AddSensitivityLevel(ctx, q.ID, q.Config.SensitivityLevel, q.SensitivityLevelDefaults(), q.ActiveCapabilities()...) - return + return svc.reloadSensitivityLevels(ctx, svc.store) }() return svc.recordAction(ctx, qProps, DalSensitivityLevelActionDelete, err) @@ -225,19 +238,101 @@ func (svc *dalSensitivityLevel) Search(ctx context.Context, filter types.DalSens return r, f, svc.recordAction(ctx, aProps, DalSensitivityLevelActionSearch, err) } -// func (svc *dalSensitivityLevel) reloadSensitivityLevels(ctx context.Context) (err error) { -// // Get all available sensitivityLevels -// cc, _, err := store.SearchDalSensitivityLevels(ctx, svc.store, types.DalSensitivityLevelFilter{}) -// if err != nil { -// return -// } +func (svc *dalSensitivityLevel) reloadSensitivityLevels(ctx context.Context, s store.Storer) (err error) { + ll, err := svc.getSensitivityLevels(ctx, s) + if err != nil { + return + } -// for _, c := range cc { -// // if err = svc.dal.AddSensitivityLevel(ctx, c.ID, c.Config.SensitivityLevel, c.SensitivityLevelDefaults(), c.ActiveCapabilities()...); err != nil { -// return -// return -// } -// } + return svc.dal.ReloadSensitivityLevels(ll) +} -// return -// } +func (svc *dalSensitivityLevel) getSensitivityLevels(ctx context.Context, s store.Storer) (out dal.SensitivityLevelSet, err error) { + ll, _, err := store.SearchDalSensitivityLevels(ctx, s, types.DalSensitivityLevelFilter{Deleted: filter.StateExcluded}) + if err != nil { + return + } + + sort.Sort(ll) + + for _, l := range ll { + out = append(out, dal.SensitivityLevel{ + ID: l.ID, + Handle: l.Handle, + }) + } + + return +} + +func (svc *dalSensitivityLevel) prepare(ctx context.Context, s store.Storer, sl *types.DalSensitivityLevel) (_ types.DalSensitivityLevelSet, err error) { + set, _, err := store.SearchDalSensitivityLevels(ctx, s, types.DalSensitivityLevelFilter{}) + if err != nil { + return + } + + updating := sl.ID != 0 + deleting := sl.DeletedAt != nil + + // Validation + { + // Assure unique level + for _, crt := range set { + if crt.Level == sl.Level { + return nil, fmt.Errorf("invalid sensitivity level: duplicated level value %d", sl.Level) + } + } + + var current *types.DalSensitivityLevel + for _, crt := range set { + if crt.ID == sl.ID { + current = crt + break + } + } + + if (updating || deleting) && current == nil { + return nil, fmt.Errorf("cannot update sensitivity level %s: does not exist", sl.Handle) + } else if !updating && current != nil { + return nil, fmt.Errorf("cannot create sensitivity level %s: already exists", sl.Handle) + } + } + + // Preparations + { + // Make sure to properly update + for i, s := range set { + if s.ID == sl.ID { + set[i] = sl + break + } + } + + // Make sure it's in there + if !deleting && !updating { + set = append(set, sl) + } + + // Sort by level for easier normalization + sort.Sort(set) + + // Normalize sensitivity level + offset := 0 + for i := range set { + if set[i].DeletedAt != nil { + offset++ + } + + nxtLvl := i + 1 - offset + if nxtLvl != set[i].Level { + set[i].UpdatedAt = now() + // Same user so we can cheat a bit + set[i].UpdatedBy = sl.CreatedBy + } + + set[i].Level = nxtLvl + } + } + + return set, err +} diff --git a/system/service/service.go b/system/service/service.go index 2014d5b36..0617ce0b5 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -155,7 +155,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, primaryCon return } - DefaultDalSensitivityLevel, err = SensitivityLevel(ctx) + DefaultDalSensitivityLevel, err = SensitivityLevel(ctx, dal.Service()) if err != nil { return } diff --git a/system/types/dal_sensitivity_level.go b/system/types/dal_sensitivity_level.go index a0f531afe..5ae5c0355 100644 --- a/system/types/dal_sensitivity_level.go +++ b/system/types/dal_sensitivity_level.go @@ -75,3 +75,11 @@ func (nm *DalSensitivityLevelMeta) Scan(value interface{}) error { func (nm DalSensitivityLevelMeta) Value() (driver.Value, error) { return json.Marshal(nm) } + +func (ss DalSensitivityLevelSet) Len() int { return len(ss) } +func (ss DalSensitivityLevelSet) Less(i, j int) bool { + return ss[i].Level < ss[j].Level +} +func (ss DalSensitivityLevelSet) Swap(i, j int) { + ss[i], ss[j] = ss[j], ss[i] +}