From 67a8afc340fb52853eaa468d27b700ab78cb3c7d Mon Sep 17 00:00:00 2001 From: Brad Sickles Date: Wed, 18 Oct 2023 09:10:35 -0400 Subject: [PATCH] Added support for aws_lambda_invocation (#16) --- README.md | 1 - aws/main.go | 20 ++++++--- crud-invoke/handle.go | 88 +++++++++++++++++++++++++++++++++++++++ postgresql/database.go | 4 ++ postgresql/role.go | 4 ++ postgresql/role_member.go | 7 ++++ 6 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 crud-invoke/handle.go diff --git a/README.md b/README.md index 12c7738..744c7bd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ This is a utility to administer postgres databases that are behind a firewall. -The published docker image runs with a lambda entrypoint. Using a lambda that is on the same VPC as the database, this utility can ensure a database exists with a specific owner. This utilizes AWS IAM to secure administration instead of using an SSH Tunnel or VPN. This also limits the actions that a user can take, making it extremely hard to perform malicious commands. diff --git a/aws/main.go b/aws/main.go index 2cc4591..eac36b6 100644 --- a/aws/main.go +++ b/aws/main.go @@ -8,6 +8,7 @@ import ( "github.com/nullstone-io/go-lambda-api-sdk/function_url" "github.com/nullstone-modules/pg-db-admin/api" "github.com/nullstone-modules/pg-db-admin/aws/secrets" + crud_invoke "github.com/nullstone-modules/pg-db-admin/crud-invoke" "github.com/nullstone-modules/pg-db-admin/legacy" "github.com/nullstone-modules/pg-db-admin/postgresql" "github.com/nullstone-modules/pg-db-admin/setup" @@ -27,11 +28,16 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() - setupConnUrlSecretId := os.Getenv(dbSetupConnUrlSecretIdEnvVar) - log.Printf("Retrieving setup connection url secret (%s)\n", setupConnUrlSecretId) - dbSetupConnUrl, err := secrets.GetString(ctx, setupConnUrlSecretId) - if err != nil { - log.Println(err.Error()) + var dbSetupConnUrl string + if setupConnUrlSecretId := os.Getenv(dbSetupConnUrlSecretIdEnvVar); setupConnUrlSecretId == "" { + log.Println("Skipping setup connection url secret") + } else { + log.Printf("Retrieving setup connection url secret (%s)\n", setupConnUrlSecretId) + var err error + dbSetupConnUrl, err = secrets.GetString(ctx, setupConnUrlSecretId) + if err != nil { + log.Println(err.Error()) + } } adminConnUrlSecretId := os.Getenv(dbAdminConnUrlSecretIdEnvVar) log.Printf("Retrieving admin connection url secret (%s)\n", adminConnUrlSecretId) @@ -54,6 +60,10 @@ func HandleRequest(setupStore, adminStore *postgresql.Store) func(ctx context.Co log.Println("Initial Setup Event") return setup.Handle(ctx, event, setupStore, os.Getenv(dbAdminConnUrlSecretIdEnvVar)) } + if ok, event := crud_invoke.IsEvent(rawEvent); ok { + log.Println("Invocation (CRUD) Event", event.Tf.Action, event.Type) + return crud_invoke.Handle(ctx, event, adminStore) + } if ok, event := isFunctionUrlEvent(rawEvent); ok { router := api.CreateRouter(adminStore) diff --git a/crud-invoke/handle.go b/crud-invoke/handle.go new file mode 100644 index 0000000..7082509 --- /dev/null +++ b/crud-invoke/handle.go @@ -0,0 +1,88 @@ +package crud_invoke + +import ( + "context" + "encoding/json" + "fmt" + "github.com/nullstone-io/go-rest-api" + "github.com/nullstone-modules/pg-db-admin/postgresql" +) + +// This package handles invocations from a Terraform `aws_lambda_invocation` CRUD resource +// When the resource has an attribute `lifecycle_scope = "CRUD"`, +// the payload will contain `tf` member with information about the action and previous input + +type Event struct { + Type string `json:"type"` + Data json.RawMessage `json:"data"` + Tf EventTf `json:"tf"` +} + +type EventTf struct { + Action string `json:"action"` + PrevInput any `json:"prev_input"` +} + +func IsEvent(rawEvent json.RawMessage) (bool, Event) { + var event Event + if err := json.Unmarshal(rawEvent, &event); err != nil { + return false, event + } + return event.Tf.Action != "", event +} + +func Handle(ctx context.Context, event Event, store *postgresql.Store) (any, error) { + crudHandler := CrudByName(store, event.Type) + if crudHandler == nil { + return nil, fmt.Errorf("unknown event 'type' %q", event.Type) + } + + return crudHandler.Handle(event.Tf.Action, event.Data) +} + +func CrudByName(s *postgresql.Store, name string) CrudHandler { + switch name { + case "databases": + return Crud[string, postgresql.Database]{DataAccess: s.Databases} + case "roles": + return Crud[string, postgresql.Role]{DataAccess: s.Roles} + case "role_members": + return Crud[postgresql.RoleMemberKey, postgresql.RoleMember]{DataAccess: s.RoleMembers} + case "schema_privileges": + return Crud[postgresql.SchemaPrivilegeKey, postgresql.SchemaPrivilege]{DataAccess: s.SchemaPrivileges} + case "default_grants": + return Crud[postgresql.DefaultGrantKey, postgresql.DefaultGrant]{DataAccess: s.DefaultGrants} + default: + return nil + } +} + +type CrudHandler interface { + Handle(action string, raw json.RawMessage) (any, error) +} + +type Keyer[TKey any] interface { + Key() TKey +} + +type Crud[TKey any, T Keyer[TKey]] struct { + DataAccess rest.DataAccess[TKey, T] +} + +func (h Crud[TKey, T]) Handle(action string, raw json.RawMessage) (any, error) { + var obj T + if err := json.Unmarshal(raw, &obj); err != nil { + return nil, fmt.Errorf("unable to parse input payload: %w", err) + } + + switch action { + case "create": + return h.DataAccess.Create(obj) + case "update": + return h.DataAccess.Update(obj.Key(), obj) + case "delete": + return h.DataAccess.Drop(obj.Key()) + default: + return nil, fmt.Errorf("unknown event 'action' %q", action) + } +} diff --git a/postgresql/database.go b/postgresql/database.go index f301551..8f5edd8 100644 --- a/postgresql/database.go +++ b/postgresql/database.go @@ -28,6 +28,10 @@ type Database struct { UseExisting bool `json:"useExisting"` } +func (d Database) Key() string { + return d.Name +} + var _ rest.DataAccess[string, Database] = &Databases{} type Databases struct { diff --git a/postgresql/role.go b/postgresql/role.go index 8cc4275..535f483 100644 --- a/postgresql/role.go +++ b/postgresql/role.go @@ -23,6 +23,10 @@ type Role struct { Attributes RoleAttributes `json:"attributes"` } +func (r Role) Key() string { + return r.Name +} + type RoleAttributes struct { CreateDb bool `json:"createDb"` CreateRole bool `json:"createRole"` diff --git a/postgresql/role_member.go b/postgresql/role_member.go index dbb0f2e..b53d612 100644 --- a/postgresql/role_member.go +++ b/postgresql/role_member.go @@ -24,6 +24,13 @@ type RoleMember struct { UseExisting bool `json:"useExisting"` } +func (r RoleMember) Key() RoleMemberKey { + return RoleMemberKey{ + Member: r.Member, + Target: r.Target, + } +} + type RoleMemberKey struct { Member string Target string