diff --git a/lib/auth/dbobjectimportrule/dbobjectimportrulev1/service.go b/lib/auth/dbobjectimportrule/dbobjectimportrulev1/service.go
index 9dcbd75a88380..6f6503bfc25aa 100644
--- a/lib/auth/dbobjectimportrule/dbobjectimportrulev1/service.go
+++ b/lib/auth/dbobjectimportrule/dbobjectimportrulev1/service.go
@@ -27,9 +27,9 @@ import (
pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1"
"github.com/gravitational/teleport/api/types"
- "github.com/gravitational/teleport/api/types/databaseobjectimportrule"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/services"
+ "github.com/gravitational/teleport/lib/srv/db/common/databaseobjectimportrule"
)
// Backend interface for manipulating DatabaseObjectImportRule resources.
diff --git a/lib/auth/init.go b/lib/auth/init.go
index 7ccaab4a30730..4c3856f1adab3 100644
--- a/lib/auth/init.go
+++ b/lib/auth/init.go
@@ -62,7 +62,7 @@ import (
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
- "github.com/gravitational/teleport/lib/srv/db/common/permissions"
+ "github.com/gravitational/teleport/lib/srv/db/common/databaseobjectimportrule"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/tlsca"
usagereporter "github.com/gravitational/teleport/lib/usagereporter/teleport"
@@ -941,7 +941,7 @@ func createPresetDatabaseObjectImportRule(ctx context.Context, rules services.Da
return nil
}
- rule := permissions.NewPresetImportAllObjectsRule()
+ rule := databaseobjectimportrule.NewPresetImportAllObjectsRule()
if rule == nil {
return nil
}
diff --git a/lib/services/databaseobjectimportrule_test.go b/lib/services/databaseobjectimportrule_test.go
index 3762ba2486eff..97f82aece1ba6 100644
--- a/lib/services/databaseobjectimportrule_test.go
+++ b/lib/services/databaseobjectimportrule_test.go
@@ -22,8 +22,10 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
+ "github.com/gravitational/teleport/api/defaults"
dbobjectimportrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1"
- "github.com/gravitational/teleport/api/types/databaseobjectimportrule"
+ headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
+ "github.com/gravitational/teleport/api/types"
apilabels "github.com/gravitational/teleport/api/types/label"
)
@@ -49,8 +51,15 @@ func TestMarshalDatabaseObjectImportRuleRoundTrip(t *testing.T) {
},
},
}
- obj, err := databaseobjectimportrule.NewDatabaseObjectImportRule("import_all_staging_tables", spec)
- require.NoError(t, err)
+ obj := &dbobjectimportrulev1.DatabaseObjectImportRule{
+ Kind: types.KindDatabaseObjectImportRule,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "import_all_staging_tables",
+ Namespace: defaults.Namespace,
+ },
+ Spec: spec,
+ }
out, err := MarshalDatabaseObjectImportRule(obj)
require.NoError(t, err)
diff --git a/lib/services/local/databaseobjectimportrule_test.go b/lib/services/local/databaseobjectimportrule_test.go
index 67234f2c513cc..4c86eb3bded68 100644
--- a/lib/services/local/databaseobjectimportrule_test.go
+++ b/lib/services/local/databaseobjectimportrule_test.go
@@ -30,9 +30,9 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
databaseobjectimportrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1"
- "github.com/gravitational/teleport/api/types/databaseobjectimportrule"
"github.com/gravitational/teleport/api/types/label"
"github.com/gravitational/teleport/lib/backend/memory"
+ "github.com/gravitational/teleport/lib/srv/db/common/databaseobjectimportrule"
)
// TestDatabaseObjectImportRuleCRUD tests backend operations with DatabaseObject import rule resources.
diff --git a/lib/srv/db/autousers_test.go b/lib/srv/db/autousers_test.go
index 37c132b14ba43..8752823c660cf 100644
--- a/lib/srv/db/autousers_test.go
+++ b/lib/srv/db/autousers_test.go
@@ -28,10 +28,10 @@ import (
dbobjectimportrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1"
"github.com/gravitational/teleport/api/types"
- "github.com/gravitational/teleport/api/types/databaseobjectimportrule"
"github.com/gravitational/teleport/api/types/label"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/auth"
+ "github.com/gravitational/teleport/lib/srv/db/common/databaseobjectimportrule"
"github.com/gravitational/teleport/lib/srv/db/mongodb"
"github.com/gravitational/teleport/lib/srv/db/postgres"
)
diff --git a/api/types/databaseobject/databaseobject.go b/lib/srv/db/common/databaseobject/databaseobject.go
similarity index 100%
rename from api/types/databaseobject/databaseobject.go
rename to lib/srv/db/common/databaseobject/databaseobject.go
diff --git a/api/types/databaseobject/databaseobject_test.go b/lib/srv/db/common/databaseobject/databaseobject_test.go
similarity index 100%
rename from api/types/databaseobject/databaseobject_test.go
rename to lib/srv/db/common/databaseobject/databaseobject_test.go
diff --git a/lib/srv/db/common/databaseobjectimportrule/apply.go b/lib/srv/db/common/databaseobjectimportrule/apply.go
new file mode 100644
index 0000000000000..de180cd3014b1
--- /dev/null
+++ b/lib/srv/db/common/databaseobjectimportrule/apply.go
@@ -0,0 +1,281 @@
+// Teleport
+// Copyright (C) 2023 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package databaseobjectimportrule
+
+import (
+ "regexp"
+ "sort"
+ "strings"
+ "unicode"
+
+ "github.com/gravitational/trace"
+ "github.com/sirupsen/logrus"
+
+ dbobjectv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobject/v1"
+ dbobjectimportrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/api/types/label"
+ "github.com/gravitational/teleport/api/utils"
+ "github.com/gravitational/teleport/lib/services"
+ libutils "github.com/gravitational/teleport/lib/utils"
+ "github.com/gravitational/teleport/lib/utils/typical"
+)
+
+// ApplyDatabaseObjectImportRules applies the given set of rules onto a set of objects coming from a same database.
+// Returns a fresh copy of a subset of supplied objects, filtered and modified.
+// For the object to be returned, it must match at least one rule.
+// The modification consists of application of extra labels, per matching mappings.
+// If there are any errors due to invalid label template, the corresponding objects will be dropped.
+// Final error count is returned.
+func ApplyDatabaseObjectImportRules(logger logrus.FieldLogger, rules []*dbobjectimportrulev1.DatabaseObjectImportRule, database types.Database, objs []*dbobjectv1.DatabaseObject) ([]*dbobjectv1.DatabaseObject, int) {
+ // sort: rules with higher priorities are applied last.
+ sort.Slice(rules, func(i, j int) bool {
+ return rules[i].Spec.Priority < rules[j].Spec.Priority
+ })
+
+ // filter rules: keep those with matching labels
+ // we only need mappings from the rules, so extract those.
+ var mappings []*dbobjectimportrulev1.DatabaseObjectImportRuleMapping
+ for _, rule := range rules {
+ dbLabels := make(types.Labels)
+ mapLabel := label.ToMap(rule.Spec.GetDatabaseLabels())
+ for k, v := range mapLabel {
+ dbLabels[k] = v
+ }
+ if ok, _, _ := services.MatchLabels(dbLabels, database.GetAllLabels()); ok {
+ mappings = append(mappings, rule.Spec.Mappings...)
+ }
+ }
+
+ var objects []*dbobjectv1.DatabaseObject
+ var errCount int
+ // anything to do?
+ if len(mappings) == 0 {
+ return objects, errCount
+ }
+
+ // find all objects that match any of the rules
+ for _, obj := range objs {
+ // prepare object clone
+ objClone := utils.CloneProtoMsg(obj)
+ if objClone.Metadata.Labels == nil {
+ objClone.Metadata.Labels = map[string]string{}
+ }
+
+ // apply each mapping in order.
+ matched := false
+ hadError := false
+ for _, mapping := range mappings {
+ match, err := applyMappingToObject(mapping, objClone.GetSpec(), objClone.Metadata.Labels)
+ if err != nil {
+ logger.WithField("name", obj.GetMetadata().GetName()).WithError(err).Debug("failed to apply label due to template error")
+ errCount++
+ hadError = true
+ break
+ }
+ if match {
+ matched = true
+ }
+ }
+
+ if !hadError && matched {
+ objects = append(objects, objClone)
+ }
+ }
+
+ return objects, errCount
+}
+
+// validateTemplate evaluates the template, checking for potential errors.
+func validateTemplate(template string) error {
+ _, err := evalTemplate(template, &dbobjectv1.DatabaseObjectSpec{})
+ return trace.Wrap(err)
+}
+
+type eval interface {
+ eval(spec *dbobjectv1.DatabaseObjectSpec) (string, error)
+}
+
+type literal struct {
+ text string
+}
+
+func (l literal) eval(_ *dbobjectv1.DatabaseObjectSpec) (string, error) {
+ return l.text, nil
+}
+
+type expression struct {
+ text string
+}
+
+func (e expression) eval(spec *dbobjectv1.DatabaseObjectSpec) (string, error) {
+ type evaluationEnv struct{}
+
+ envVar := map[string]typical.Variable{
+ "true": true,
+ "false": false,
+ "obj": typical.DynamicMapFunction(func(e evaluationEnv, key string) (string, error) {
+ switch key {
+ case "protocol":
+ return spec.GetProtocol(), nil
+ case "database_service_name":
+ return spec.GetDatabaseServiceName(), nil
+ case "object_kind":
+ return spec.GetObjectKind(), nil
+ case "database":
+ return spec.GetDatabase(), nil
+ case "schema":
+ return spec.GetSchema(), nil
+ case "name":
+ return spec.GetName(), nil
+ }
+
+ return "", trace.NotFound("key %v not found", key)
+ }),
+ }
+
+ parser, err := typical.NewParser[evaluationEnv, string](typical.ParserSpec{Variables: envVar})
+ if err != nil {
+ return "", trace.Wrap(err)
+ }
+
+ expr, err := parser.Parse(e.text)
+ if err != nil {
+ return "", trace.Wrap(err)
+ }
+
+ text, err := expr.Evaluate(evaluationEnv{})
+ if err != nil {
+ return "", trace.Wrap(err)
+ }
+
+ return text, nil
+}
+
+var reVariable = regexp.MustCompile(
+ // prefix is anything that is not { or }
+ `^(?P[^}{]*)` +
+ // variable is anything in brackets {{}} that is not { or }
+ `{{(?P\s*[^}{]*\s*)}}` +
+ // suffix is anything that is not { or }
+ `(?P[^}{]*)$`,
+)
+
+// splitExpression splits the template into several parts, to be evaluated separately.
+func splitExpression(value string) ([]eval, error) {
+ match := reVariable.FindStringSubmatch(value)
+ if len(match) == 0 {
+ if strings.Contains(value, "{{") || strings.Contains(value, "}}") {
+ return nil, trace.BadParameter(
+ "%q is using template brackets '{{' or '}}', however expression does not parse, make sure the format is {{expression}}",
+ value,
+ )
+ }
+ return []eval{literal{text: strings.TrimSpace(value)}}, nil
+ }
+
+ return []eval{
+ literal{text: strings.TrimLeftFunc(match[1], unicode.IsSpace)},
+ expression{text: match[2]},
+ literal{text: strings.TrimRightFunc(match[3], unicode.IsSpace)},
+ }, nil
+}
+
+func evalTemplate(template string, spec *dbobjectv1.DatabaseObjectSpec) (string, error) {
+ chunks, err := splitExpression(template)
+ if err != nil {
+ return "", trace.Wrap(err)
+ }
+
+ var sb strings.Builder
+
+ for _, chunk := range chunks {
+ text, err := chunk.eval(spec)
+ if err != nil {
+ return "", trace.Wrap(err)
+ }
+ sb.WriteString(text)
+ }
+
+ return sb.String(), nil
+}
+
+func applyMappingToObject(mapping *dbobjectimportrulev1.DatabaseObjectImportRuleMapping, spec *dbobjectv1.DatabaseObjectSpec, labels map[string]string) (bool, error) {
+ // the matching is applied to the object spec; existing object labels does not matter
+ if !databaseObjectScopeMatch(mapping.GetScope(), spec) {
+ return false, nil
+ }
+ if !databaseObjectImportMatch(mapping.GetMatch(), spec) {
+ return false, nil
+ }
+
+ for key, value := range mapping.AddLabels {
+ out, err := evalTemplate(value, spec)
+ if err != nil {
+ return false, trace.Wrap(err)
+ }
+ labels[key] = out
+ }
+
+ return true, nil
+}
+
+func matchPattern(pattern, value string) bool {
+ re, err := libutils.CompileExpression(pattern)
+ if err != nil {
+ return false
+ }
+ return re.MatchString(value)
+}
+
+func matchAny(patterns []string, value string) bool {
+ return utils.Any(patterns, func(pattern string) bool {
+ return matchPattern(pattern, value)
+ })
+}
+
+func databaseObjectScopeMatch(scope *dbobjectimportrulev1.DatabaseObjectImportScope, spec *dbobjectv1.DatabaseObjectSpec) bool {
+ // require at least one match if there are any names to match against.
+ if len(scope.GetDatabaseNames()) > 0 && !matchAny(scope.GetDatabaseNames(), spec.GetDatabase()) {
+ return false
+ }
+ if len(scope.GetSchemaNames()) > 0 && !matchAny(scope.GetSchemaNames(), spec.GetSchema()) {
+ return false
+ }
+ return true
+}
+
+func databaseObjectImportMatch(match *dbobjectimportrulev1.DatabaseObjectImportMatch, spec *dbobjectv1.DatabaseObjectSpec) bool {
+ switch spec.GetObjectKind() {
+ case ObjectKindTable:
+ return matchAny(match.GetTableNames(), spec.GetName())
+ case ObjectKindView:
+ return matchAny(match.GetViewNames(), spec.GetName())
+ case ObjectKindProcedure:
+ return matchAny(match.GetProcedureNames(), spec.GetName())
+ default:
+ // unknown object kind
+ return false
+ }
+
+}
+
+const (
+ ObjectKindTable = "table"
+ ObjectKindView = "view"
+ ObjectKindProcedure = "procedure"
+)
diff --git a/lib/srv/db/common/permissions/import_rules_test.go b/lib/srv/db/common/databaseobjectimportrule/apply_test.go
similarity index 56%
rename from lib/srv/db/common/permissions/import_rules_test.go
rename to lib/srv/db/common/databaseobjectimportrule/apply_test.go
index 4312eb3505bf0..77d4455902e9b 100644
--- a/lib/srv/db/common/permissions/import_rules_test.go
+++ b/lib/srv/db/common/databaseobjectimportrule/apply_test.go
@@ -14,19 +14,22 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-package permissions
+package databaseobjectimportrule
import (
+ "maps"
"testing"
+ "github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
+ "github.com/gravitational/teleport/api/defaults"
dbobjectv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobject/v1"
databaseobjectimportrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1"
+ headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
"github.com/gravitational/teleport/api/types"
- "github.com/gravitational/teleport/api/types/databaseobject"
- "github.com/gravitational/teleport/api/types/databaseobjectimportrule"
"github.com/gravitational/teleport/api/types/label"
+ "github.com/gravitational/teleport/lib/srv/db/common/databaseobject"
)
func TestApplyDatabaseObjectImportRules(t *testing.T) {
@@ -56,16 +59,31 @@ func TestApplyDatabaseObjectImportRules(t *testing.T) {
}
mkImportRule := func(name string, spec *databaseobjectimportrulev1.DatabaseObjectImportRuleSpec) *databaseobjectimportrulev1.DatabaseObjectImportRule {
- out, err := databaseobjectimportrule.NewDatabaseObjectImportRule(name, spec)
+ out, err := NewDatabaseObjectImportRule(name, spec)
require.NoError(t, err)
return out
}
+
+ mkImportRuleNoValidation := func(name string, spec *databaseobjectimportrulev1.DatabaseObjectImportRuleSpec) *databaseobjectimportrulev1.DatabaseObjectImportRule {
+ out := &databaseobjectimportrulev1.DatabaseObjectImportRule{
+ Kind: types.KindDatabaseObjectImportRule,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: name,
+ Namespace: defaults.Namespace,
+ },
+ Spec: spec,
+ }
+ return out
+ }
+
tests := []struct {
name string
rules []*databaseobjectimportrulev1.DatabaseObjectImportRule
database types.Database
objs []*dbobjectv1.DatabaseObject
want []*dbobjectv1.DatabaseObject
+ errCount int
}{
{
name: "empty inputs",
@@ -173,15 +191,73 @@ func TestApplyDatabaseObjectImportRules(t *testing.T) {
}),
},
},
+ {
+ name: "errors are counted",
+ rules: []*databaseobjectimportrulev1.DatabaseObjectImportRule{
+ mkImportRule("foo", &databaseobjectimportrulev1.DatabaseObjectImportRuleSpec{
+ Priority: 10,
+ DatabaseLabels: label.FromMap(map[string][]string{"*": {"*"}}),
+ Mappings: []*databaseobjectimportrulev1.DatabaseObjectImportRuleMapping{
+ {
+ Match: &databaseobjectimportrulev1.DatabaseObjectImportMatch{
+ TableNames: []string{"*"},
+ },
+ AddLabels: map[string]string{
+ "dev_access": "rw",
+ "flag_from_dev": "dummy",
+ },
+ },
+ },
+ }),
+
+ mkImportRuleNoValidation("bar", &databaseobjectimportrulev1.DatabaseObjectImportRuleSpec{
+ Priority: 20,
+ DatabaseLabels: label.FromMap(map[string][]string{"*": {"*"}}),
+ Mappings: []*databaseobjectimportrulev1.DatabaseObjectImportRuleMapping{
+ {
+ Match: &databaseobjectimportrulev1.DatabaseObjectImportMatch{
+ TableNames: []string{"*"},
+ },
+ AddLabels: map[string]string{
+ "dev_access": "ro",
+ "flag_from_prod": "dummy",
+ },
+ },
+ {
+ Match: &databaseobjectimportrulev1.DatabaseObjectImportMatch{TableNames: []string{"bar", "baz"}},
+ AddLabels: map[string]string{"error label": "{{foo()}}"},
+ },
+ },
+ }),
+ },
+ database: mkDatabase("dummy", map[string]string{}),
+ objs: []*dbobjectv1.DatabaseObject{
+ mkDatabaseObject("foo", &dbobjectv1.DatabaseObjectSpec{ObjectKind: ObjectKindTable, Protocol: "postgres"}),
+ mkDatabaseObject("bar", &dbobjectv1.DatabaseObjectSpec{ObjectKind: ObjectKindTable, Protocol: "postgres"}),
+ mkDatabaseObject("baz", &dbobjectv1.DatabaseObjectSpec{ObjectKind: ObjectKindTable, Protocol: "postgres"}),
+ },
+ want: []*dbobjectv1.DatabaseObject{
+ mkDatabaseObject("foo", &dbobjectv1.DatabaseObjectSpec{ObjectKind: ObjectKindTable, Protocol: "postgres"}, func(db *dbobjectv1.DatabaseObject) error {
+ db.Metadata.Labels = map[string]string{
+ "dev_access": "ro",
+ "flag_from_dev": "dummy",
+ "flag_from_prod": "dummy",
+ }
+ return nil
+ }),
+ },
+ errCount: 2,
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- out := ApplyDatabaseObjectImportRules(tt.rules, tt.database, tt.objs)
+ out, errCount := ApplyDatabaseObjectImportRules(logrus.StandardLogger(), tt.rules, tt.database, tt.objs)
require.Len(t, out, len(tt.want))
for i, obj := range out {
require.Equal(t, tt.want[i].String(), obj.String())
}
+ require.Equal(t, tt.errCount, errCount)
})
}
}
@@ -404,3 +480,226 @@ func Test_databaseObjectScopeMatch(t *testing.T) {
})
}
}
+
+func Test_applyMappingToObject(t *testing.T) {
+ spec := &dbobjectv1.DatabaseObjectSpec{
+ Database: "db3",
+ DatabaseServiceName: "service3",
+ Protocol: "postgres",
+ ObjectKind: ObjectKindTable,
+ Name: "object3",
+ Schema: "schema3",
+ }
+
+ tests := []struct {
+ name string
+ mapping *databaseobjectimportrulev1.DatabaseObjectImportRuleMapping
+ labels map[string]string
+ wantLabels map[string]string
+ wantMatch bool
+ wantError bool
+ }{
+ {
+ name: "simple templates",
+ mapping: &databaseobjectimportrulev1.DatabaseObjectImportRuleMapping{
+ Match: &databaseobjectimportrulev1.DatabaseObjectImportMatch{
+ TableNames: []string{"*"},
+ },
+ AddLabels: map[string]string{
+ "plain_label": "rw",
+ "protocol": "{{obj.protocol}}",
+ "database_service_name": "{{obj.database_service_name}}",
+ "object_kind": "{{obj.object_kind}}",
+ "database": "{{obj.database}}",
+ "schema": "{{obj.schema}}",
+ "name": "{{obj.name}}",
+ },
+ },
+ labels: map[string]string{},
+ wantLabels: map[string]string{
+ "plain_label": "rw",
+ "protocol": "postgres",
+ "database_service_name": "service3",
+ "object_kind": "table",
+ "database": "db3",
+ "schema": "schema3",
+ "name": "object3",
+ },
+ wantMatch: true,
+ },
+ {
+ name: "add prefix",
+ mapping: &databaseobjectimportrulev1.DatabaseObjectImportRuleMapping{
+ Match: &databaseobjectimportrulev1.DatabaseObjectImportMatch{
+ TableNames: []string{"*"},
+ },
+ AddLabels: map[string]string{
+ "plain_label": "rw",
+ "tag": "db-{{obj.object_kind}}",
+ },
+ },
+ labels: map[string]string{},
+ wantLabels: map[string]string{
+ "plain_label": "rw",
+ "tag": "db-table",
+ },
+ wantMatch: true,
+ },
+ {
+ name: "spaces are trimmed prefix",
+ mapping: &databaseobjectimportrulev1.DatabaseObjectImportRuleMapping{
+ Match: &databaseobjectimportrulev1.DatabaseObjectImportMatch{
+ TableNames: []string{"*"},
+ },
+ AddLabels: map[string]string{
+ "plain_label": "rw",
+ "tag": " db-{{ obj.object_kind }}-bar ",
+ },
+ },
+ labels: map[string]string{},
+ wantLabels: map[string]string{
+ "plain_label": "rw",
+ "tag": "db-table-bar",
+ },
+ wantMatch: true,
+ },
+ {
+ name: "invalid object is rejected",
+ mapping: &databaseobjectimportrulev1.DatabaseObjectImportRuleMapping{
+ Match: &databaseobjectimportrulev1.DatabaseObjectImportMatch{
+ TableNames: []string{"*"},
+ },
+ AddLabels: map[string]string{
+ "plain_label": "rw",
+ "tag": "db-{{obj.invalid}}",
+ },
+ },
+ labels: map[string]string{},
+ wantError: true,
+ },
+ {
+ name: "invalid namespace is rejected",
+ mapping: &databaseobjectimportrulev1.DatabaseObjectImportRuleMapping{
+ Match: &databaseobjectimportrulev1.DatabaseObjectImportMatch{
+ TableNames: []string{"*"},
+ },
+ AddLabels: map[string]string{
+ "plain_label": "rw",
+ "tag": "db-{{wrong.object_kind}}",
+ },
+ },
+ labels: map[string]string{},
+ wantError: true,
+ },
+ {
+ name: "empty template is rejected",
+ mapping: &databaseobjectimportrulev1.DatabaseObjectImportRuleMapping{
+ Match: &databaseobjectimportrulev1.DatabaseObjectImportMatch{
+ TableNames: []string{"*"},
+ },
+ AddLabels: map[string]string{
+ "plain_label": "rw",
+ "tag": "db-{{}}",
+ },
+ },
+ labels: map[string]string{},
+ wantError: true,
+ },
+ {
+ name: "multi template is rejected",
+ mapping: &databaseobjectimportrulev1.DatabaseObjectImportRuleMapping{
+ Match: &databaseobjectimportrulev1.DatabaseObjectImportMatch{
+ TableNames: []string{"*"},
+ },
+ AddLabels: map[string]string{
+ "plain_label": "rw",
+ "tag": "db-{{obj.object_kind obj.object_kind}}",
+ },
+ },
+ labels: map[string]string{},
+ wantError: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ labels := maps.Clone(tt.labels)
+ match, err := applyMappingToObject(tt.mapping, spec, labels)
+ if tt.wantError {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tt.wantMatch, match)
+ require.Equal(t, tt.wantLabels, labels)
+ }
+ })
+ }
+}
+
+func Test_splitExpression(t *testing.T) {
+ tests := []struct {
+ name string
+ value string
+ want []eval
+ wantErr bool
+ }{
+ {
+ name: "empty string",
+ value: "",
+ want: []eval{literal{text: ""}},
+ wantErr: false,
+ },
+ {
+ name: "literal",
+ value: "literal",
+ want: []eval{literal{text: "literal"}},
+ wantErr: false,
+ },
+ {
+ name: "literal with whitespace",
+ value: " literal ",
+ want: []eval{literal{text: "literal"}},
+ wantErr: false,
+ },
+ {
+ name: "prefix, expr, suffix",
+ value: "prefix-{{expr}}-suffix",
+ want: []eval{literal{text: "prefix-"}, expression{text: "expr"}, literal{text: "-suffix"}},
+ wantErr: false,
+ },
+ {
+ name: "prefix, expr, suffix with extra whitespace",
+ value: " prefix-{{expr}}-suffix ",
+ want: []eval{literal{text: "prefix-"}, expression{text: "expr"}, literal{text: "-suffix"}},
+ wantErr: false,
+ },
+ {
+ name: "unmatched {{",
+ value: "foo bar {{ baz",
+ want: nil,
+ wantErr: true,
+ },
+ {
+ name: "unmatched }}",
+ value: "foo bar }} baz",
+ want: nil,
+ wantErr: true,
+ },
+ {
+ name: "multiple templates",
+ value: "foo {{bar}} {{baz}}",
+ want: nil,
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := splitExpression(tt.value)
+ if tt.wantErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tt.want, got)
+ }
+ })
+ }
+}
diff --git a/api/types/databaseobjectimportrule/databaseobjectimportrule.go b/lib/srv/db/common/databaseobjectimportrule/create.go
similarity index 88%
rename from api/types/databaseobjectimportrule/databaseobjectimportrule.go
rename to lib/srv/db/common/databaseobjectimportrule/create.go
index 0949cb3652ec1..5dfb56595c671 100644
--- a/api/types/databaseobjectimportrule/databaseobjectimportrule.go
+++ b/lib/srv/db/common/databaseobjectimportrule/create.go
@@ -15,6 +15,8 @@
package databaseobjectimportrule
import (
+ "strings"
+
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/defaults"
@@ -71,5 +73,16 @@ func ValidateDatabaseObjectImportRule(rule *dbobjectimportrulev1.DatabaseObjectI
if len(rule.Spec.Mappings) == 0 {
return trace.BadParameter("missing mappings")
}
+ for _, mapping := range rule.Spec.Mappings {
+ for key, template := range mapping.AddLabels {
+ if strings.TrimSpace(key) == "" {
+ return trace.BadParameter("invalid mapping: label name is empty or whitespace")
+ }
+ err := validateTemplate(template)
+ if err != nil {
+ return trace.Wrap(err, "mapping value failed to parse as template")
+ }
+ }
+ }
return nil
}
diff --git a/api/types/databaseobjectimportrule/databaseobjectimportrule_test.go b/lib/srv/db/common/databaseobjectimportrule/create_test.go
similarity index 78%
rename from api/types/databaseobjectimportrule/databaseobjectimportrule_test.go
rename to lib/srv/db/common/databaseobjectimportrule/create_test.go
index bac708db80fb5..8253fbb7a0c4c 100644
--- a/api/types/databaseobjectimportrule/databaseobjectimportrule_test.go
+++ b/lib/srv/db/common/databaseobjectimportrule/create_test.go
@@ -162,6 +162,42 @@ func TestValidateDatabaseObjectImportRule(t *testing.T) {
},
expectedError: trace.BadParameter("missing mappings"),
},
+ {
+ name: "invalid mapping key",
+ rule: &dbobjectimportrulev1.DatabaseObjectImportRule{
+ Kind: types.KindDatabaseObjectImportRule,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "test",
+ Namespace: defaults.Namespace,
+ },
+ Spec: &dbobjectimportrulev1.DatabaseObjectImportRuleSpec{
+ DatabaseLabels: label.FromMap(map[string][]string{"key": {"value"}}),
+ Mappings: []*dbobjectimportrulev1.DatabaseObjectImportRuleMapping{{
+ AddLabels: map[string]string{" ": "dummy"},
+ }},
+ },
+ },
+ expectedError: trace.BadParameter("invalid mapping: label name is empty or whitespace"),
+ },
+ {
+ name: "invalid template in mapping",
+ rule: &dbobjectimportrulev1.DatabaseObjectImportRule{
+ Kind: types.KindDatabaseObjectImportRule,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: "test",
+ Namespace: defaults.Namespace,
+ },
+ Spec: &dbobjectimportrulev1.DatabaseObjectImportRuleSpec{
+ DatabaseLabels: label.FromMap(map[string][]string{"key": {"value"}}),
+ Mappings: []*dbobjectimportrulev1.DatabaseObjectImportRuleMapping{{
+ AddLabels: map[string]string{"dummy": " {{ "},
+ }},
+ },
+ },
+ expectedError: trace.Wrap(trace.BadParameter("\" {{ \" is using template brackets '{{' or '}}', however expression does not parse, make sure the format is {{expression}}"), "mapping value failed to parse as template"),
+ },
}
for _, tt := range tests {
diff --git a/lib/srv/db/common/permissions/presets.go b/lib/srv/db/common/databaseobjectimportrule/preset.go
similarity index 68%
rename from lib/srv/db/common/permissions/presets.go
rename to lib/srv/db/common/databaseobjectimportrule/preset.go
index 21389479d8ff4..d627e23d0f954 100644
--- a/lib/srv/db/common/permissions/presets.go
+++ b/lib/srv/db/common/databaseobjectimportrule/preset.go
@@ -14,34 +14,36 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-package permissions
+package databaseobjectimportrule
import (
log "github.com/sirupsen/logrus"
dbobjectimportrulev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1"
- "github.com/gravitational/teleport/api/types/databaseobjectimportrule"
"github.com/gravitational/teleport/api/types/label"
)
// NewPresetImportAllObjectsRule creates new "import_all_objects" database object import rule, which applies `kind: