From 9a205ef5c791a94b4f6634a67c7e33190c2eac25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Tue, 14 May 2024 14:59:05 +0200 Subject: [PATCH 1/3] Escape attribute as identifier or string literal --- docs/changelog.md | 1 + internal/pyfmt/format.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 12903777..8b731feb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -37,6 +37,7 @@ pages](https://github.com/dalibo/ldap2pg/pulls?utf8=%E2%9C%93&q=is%3Apr%20is%3Am - Suggest --verbose on error. - Suggest --real on dry run with changes. - New final metrics: roles, grants, inspect time. +- Escape attribute with : `{sAMAccountName.identifier()}` and `{sAMAccountName.string()}`. diff --git a/internal/pyfmt/format.go b/internal/pyfmt/format.go index 45d78db3..5f440749 100644 --- a/internal/pyfmt/format.go +++ b/internal/pyfmt/format.go @@ -138,6 +138,10 @@ func (f Format) Format(values map[string]string) string { v = strings.ToLower(v) case "upper()": v = strings.ToUpper(v) + case "identifier()": + v = fmt.Sprintf("\"%s\"", v) + case "string()": + v = fmt.Sprintf("'%s'", strings.ReplaceAll(v, "'", "''")) default: v = "!INVALID_METHOD" } From c41eaaa24bda1820a88d210b7cd4732ea41679ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Tue, 14 May 2024 15:07:36 +0200 Subject: [PATCH 2/3] Unify listing rule formats --- internal/wanted/rules.go | 23 ++++++++++++++--------- internal/wanted/step.go | 15 ++------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/internal/wanted/rules.go b/internal/wanted/rules.go index 638d53ab..430d2f32 100644 --- a/internal/wanted/rules.go +++ b/internal/wanted/rules.go @@ -18,12 +18,11 @@ type GrantRule struct { } func (r GrantRule) IsStatic() bool { - return r.Database.IsStatic() && - r.Object.IsStatic() && - r.Owner.IsStatic() && - r.Schema.IsStatic() && - r.To.IsStatic() && - r.Privilege.IsStatic() + return lists.And(r.Formats(), func(f pyfmt.Format) bool { return f.IsStatic() }) +} + +func (r GrantRule) Formats() []pyfmt.Format { + return []pyfmt.Format{r.Owner, r.Privilege, r.Database, r.Schema, r.Object, r.To} } func (r GrantRule) Generate(results *ldap.Result, privileges privilege.RefMap) <-chan privilege.Grant { @@ -94,9 +93,15 @@ type RoleRule struct { } func (r RoleRule) IsStatic() bool { - return r.Name.IsStatic() && - r.Comment.IsStatic() && - lists.And(r.Parents, func(m MembershipRule) bool { return m.IsStatic() }) + return lists.And(r.Formats(), func(f pyfmt.Format) bool { return f.IsStatic() }) +} + +func (r RoleRule) Formats() []pyfmt.Format { + fmts := []pyfmt.Format{r.Name, r.Comment} + for _, p := range r.Parents { + fmts = append(fmts, p.Name) + } + return fmts } func (r RoleRule) Generate(results *ldap.Result) <-chan role.Role { diff --git a/internal/wanted/step.go b/internal/wanted/step.go index 7e77249a..a5a512ef 100644 --- a/internal/wanted/step.go +++ b/internal/wanted/step.go @@ -109,25 +109,14 @@ func (s Step) IterFields() <-chan *pyfmt.Field { go func() { defer close(ch) for _, rule := range s.RoleRules { - allFormats := []pyfmt.Format{ - rule.Name, rule.Comment, - } - for _, p := range rule.Parents { - allFormats = append(allFormats, p.Name) - } - - for _, f := range allFormats { + for _, f := range rule.Formats() { for _, field := range f.Fields { ch <- field } } } for _, rule := range s.GrantRules { - allFormats := []pyfmt.Format{ - rule.Privilege, rule.Database, rule.Schema, rule.Object, rule.To, - } - - for _, f := range allFormats { + for _, f := range rule.Formats() { for _, field := range f.Fields { ch <- field } From 188757e328dc490a68d62df700ddfff7126abd55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Tue, 14 May 2024 15:15:38 +0200 Subject: [PATCH 3/3] Execute before and after create hook --- docs/changelog.md | 1 + docs/config.md | 30 +++++++++++++++++++++++++++++ internal/config/role.go | 2 +- internal/role/role.go | 41 ++++++++++++++++++++++++++++------------ internal/wanted/rules.go | 30 +++++++++++++++++------------ test/extra.ldap2pg.yml | 10 ++++++++++ test/test_extra.py | 4 ++++ 7 files changed, 93 insertions(+), 25 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 8b731feb..8af9759f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -37,6 +37,7 @@ pages](https://github.com/dalibo/ldap2pg/pulls?utf8=%E2%9C%93&q=is%3Apr%20is%3Am - Suggest --verbose on error. - Suggest --real on dry run with changes. - New final metrics: roles, grants, inspect time. +- Execute arbitrary SQL snippet before and after role creation. - Escape attribute with : `{sAMAccountName.identifier()}` and `{sAMAccountName.string()}`. diff --git a/docs/config.md b/docs/config.md index 3aea0b69..d939db45 100644 --- a/docs/config.md +++ b/docs/config.md @@ -509,6 +509,36 @@ rules: ``` +#### `before_create` { #role-before-create } + +SQL snippet to execute before role creation. +`before_create` accepts LDAP attributes injection using curly braces. +You are responsible to escape attribute with either `.identifier()` or `.string()`. + +``` yaml +rules: +- ldapsearch: ... + role: + name: "{cn}" + before_create: "INSERT INTO log VALUES ({cn.string()})" +``` + + +#### `after_create` { #role-after-create } + +SQL snippet to execute after role creation. +`after_create` accepts LDAP attributes injection using curly braces. +You are responsible to escape attribute with either `.identifier()` or `.string()`. + +``` yaml +rules: +- ldapsearch: ... + role: + name: "{sAMAccountName}" + after_create: "CREATE SCHEMA {sAMAccountName.identifier()} AUTHORIZATION {sAMAccountName.identifier()}" +``` + + ### `grant` { #rules-grant } [grant rule]: #rules-grant diff --git a/internal/config/role.go b/internal/config/role.go index 7f97f0dd..8f8d518b 100644 --- a/internal/config/role.go +++ b/internal/config/role.go @@ -52,7 +52,7 @@ func NormalizeRoleRule(yaml interface{}) (rule map[string]interface{}, err error return nil, fmt.Errorf("bad type: %T", yaml) } - err = CheckSpuriousKeys(&rule, "names", "comment", "parents", "options", "config") + err = CheckSpuriousKeys(&rule, "names", "comment", "parents", "options", "config", "before_create", "after_create") return } diff --git a/internal/role/role.go b/internal/role/role.go index da45d348..9e46940a 100644 --- a/internal/role/role.go +++ b/internal/role/role.go @@ -8,11 +8,13 @@ import ( ) type Role struct { - Name string - Comment string - Parents []Membership - Options Options - Config *Config + Name string + Comment string + Parents []Membership + Options Options + Config *Config + BeforeCreate string + AfterCreate string } func New() Role { @@ -182,6 +184,14 @@ func (r *Role) Alter(wanted Role) (out []postgres.SyncQuery) { func (r *Role) Create() (out []postgres.SyncQuery) { identifier := pgx.Identifier{r.Name} + if r.BeforeCreate != "" { + out = append(out, postgres.SyncQuery{ + Description: "Run before create hook.", + LogArgs: []interface{}{"role", r.Name, "sql", r.BeforeCreate}, + Query: r.BeforeCreate, + }) + } + if len(r.Parents) > 0 { parents := []interface{}{} for _, parent := range r.Parents { @@ -211,18 +221,25 @@ func (r *Role) Create() (out []postgres.SyncQuery) { QueryArgs: []interface{}{identifier, r.Comment}, }) - if nil == r.Config { - return + if r.Config != nil { + for k, v := range *r.Config { + out = append(out, postgres.SyncQuery{ + Description: "Set role config.", + LogArgs: []interface{}{"role", r.Name, "config", k, "value", v}, + Query: `ALTER ROLE %s SET %s TO %s`, + QueryArgs: []interface{}{identifier, pgx.Identifier{k}, v}, + }) + } } - for k, v := range *r.Config { + if r.AfterCreate != "" { out = append(out, postgres.SyncQuery{ - Description: "Set role config.", - LogArgs: []interface{}{"role", r.Name, "config", k, "value", v}, - Query: `ALTER ROLE %s SET %s TO %s`, - QueryArgs: []interface{}{identifier, pgx.Identifier{k}, v}, + Description: "Run after create hook.", + LogArgs: []interface{}{"role", r.Name, "sql", r.AfterCreate}, + Query: r.AfterCreate, }) } + return } diff --git a/internal/wanted/rules.go b/internal/wanted/rules.go index 430d2f32..ec7d7f02 100644 --- a/internal/wanted/rules.go +++ b/internal/wanted/rules.go @@ -85,11 +85,13 @@ func (r GrantRule) Generate(results *ldap.Result, privileges privilege.RefMap) < } type RoleRule struct { - Name pyfmt.Format - Options role.Options - Comment pyfmt.Format - Parents []MembershipRule - Config *role.Config + Name pyfmt.Format + Options role.Options + Comment pyfmt.Format + Parents []MembershipRule + Config *role.Config + BeforeCreate pyfmt.Format `mapstructure:"before_create"` + AfterCreate pyfmt.Format `mapstructure:"after_create"` } func (r RoleRule) IsStatic() bool { @@ -97,7 +99,7 @@ func (r RoleRule) IsStatic() bool { } func (r RoleRule) Formats() []pyfmt.Format { - fmts := []pyfmt.Format{r.Name, r.Comment} + fmts := []pyfmt.Format{r.Name, r.Comment, r.BeforeCreate, r.AfterCreate} for _, p := range r.Parents { fmts = append(fmts, p.Name) } @@ -124,21 +126,25 @@ func (r RoleRule) Generate(results *ldap.Result) <-chan role.Role { if nil == results.Entry { // Case static rule. role := role.Role{ - Name: r.Name.String(), - Comment: r.Comment.String(), - Options: r.Options, - Parents: parents, - Config: r.Config, + Name: r.Name.String(), + Comment: r.Comment.String(), + Options: r.Options, + Parents: parents, + Config: r.Config, + BeforeCreate: r.BeforeCreate.String(), + AfterCreate: r.AfterCreate.String(), } ch <- role } else { // Case dynamic rule. - for values := range results.GenerateValues(r.Name, r.Comment) { + for values := range results.GenerateValues(r.Name, r.Comment, r.BeforeCreate, r.AfterCreate) { role := role.Role{} role.Name = r.Name.Format(values) role.Comment = r.Comment.Format(values) role.Options = r.Options role.Parents = append(parents[0:0], parents...) // copy + role.BeforeCreate = r.BeforeCreate.Format(values) + role.AfterCreate = r.AfterCreate.Format(values) ch <- role } } diff --git a/test/extra.ldap2pg.yml b/test/extra.ldap2pg.yml index b040f44a..e874f913 100644 --- a/test/extra.ldap2pg.yml +++ b/test/extra.ldap2pg.yml @@ -81,3 +81,13 @@ rules: comment: "group: {memberOf.sAMAccountName}" options: LOGIN SUPERUSER parents: [ldap_roles] + +- description: "Hooks" + ldapsearch: + base: "cn=users,dc=bridoulou,dc=fr" + filter: "(cn=corinne)" + role: + name: "{cn}" + parent: ldap_roles + before_create: "CREATE SCHEMA {cn.identifier()};" + after_create: "CREATE TABLE {cn.identifier()}.username AS SELECT {cn.string()}::regrole AS username;" diff --git a/test/test_extra.py b/test/test_extra.py index 46e112a3..a74f627a 100644 --- a/test/test_extra.py +++ b/test/test_extra.py @@ -68,3 +68,7 @@ def test_role_config(extrarun, psql): 'application_name': 'keep-me', } assert expected_unmodified_config == psql.config('nicolas') + + +def test_role_hook(extrarun, psql): + assert psql.scalar("SELECT username FROM corinne.username;") == "corinne"