Skip to content

Commit 80419e4

Browse files
authored
Merge pull request #427 from Dwolla/pgadmin
Grant all privileges on all tables in schema public to pgadmin
2 parents c17efcb + 9a6dd15 commit 80419e4

File tree

12 files changed

+130
-29
lines changed

12 files changed

+130
-29
lines changed

build.sbt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@ lazy val `postgresql-init-core` = (project in file("."))
6262
"com.dwolla" %% "natchez-tagless" % "0.2.6-131-d6a1c7c-SNAPSHOT",
6363
"org.typelevel" %% "mouse" % "1.4.0",
6464
"com.comcast" %% "ip4s-core" % "3.7.0",
65+
"org.typelevel" %% "literally" % "1.2.0",
6566
"org.scalameta" %% "munit" % "1.2.1" % Test,
6667
"org.scalameta" %% "munit-scalacheck" % "1.2.0" % Test,
6768
"io.circe" %% "circe-literal" % circeV % Test,
6869
"com.dwolla" %% "dwolla-otel-natchez" % "0.2.8" % Test,
70+
"org.typelevel" %% "discipline-munit" % "2.0.0" % Test,
71+
"org.typelevel" %% "cats-laws" % "2.13.0" % Test,
6972
)
7073
},
7174
buildInfoPackage := "com.dwolla.buildinfo.postgres.init",

serverless.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ provider:
1515
iam:
1616
deploymentRole: "arn:aws:iam::${env:ACCOUNT}:role/cloudformation/deployer/cloudformation-deployer"
1717
role:
18+
managedPolicies:
19+
- arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess
1820
statements:
1921
- Effect: Allow
2022
Action:

src/main/scala/com/dwolla/postgres/init/PostgresqlDatabaseInitHandler.scala

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,14 @@ class PostgresqlDatabaseInitHandler
3636
given Random[F] <- Resource.eval(Random.scalaUtilRandom[F])
3737
client <- httpClient[F]
3838
entryPoint <- entryPointOverride.toOptionT[Resource[F, *]].getOrElseF {
39-
XRayEnvironment[F].daemonAddress.toResource.flatMap {
40-
case Some(addr) => XRay.entryPoint(addr)
41-
case None => XRay.entryPoint[F]()
42-
}
39+
XRayEnvironment[F]
40+
.daemonAddress
41+
.flatTap(maybeAddress => Console[F].println(s"found daemonAddress: $maybeAddress"))
42+
.toResource
43+
.flatMap {
44+
case Some(addr) => XRay.entryPoint(addr)
45+
case None => XRay.entryPoint[F]()
46+
}
4347
}
4448
region <- Env[F].get("AWS_REGION").liftEitherT(new RuntimeException("missing AWS_REGION environment variable")).map(AwsRegion(_)).rethrowT.toResource
4549
awsEnv <- AwsEnvironment.default(client, region)

src/main/scala/com/dwolla/postgres/init/PostgresqlDatabaseInitHandlerImpl.scala

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,27 @@ object PostgresqlDatabaseInitHandlerImpl {
4545
dbId <- removeUsersFromDatabase(usernames, event.name).inSession(event.host, event.port, event.username, event.password)
4646
} yield HandlerResponse(dbId, None)
4747

48-
private def createOrUpdate(userPasswords: List[UserConnectionInfo], input: DatabaseMetadata): InSession[F, PhysicalResourceId] =
48+
private def databaseScopedCreateOrUpdateOperations(userPasswords: List[UserConnectionInfo], input: DatabaseMetadata): InSession[F, Unit] =
4949
for {
50-
db <- databaseAsPhysicalResourceId[InSession[F, *]](input.name)
51-
_ <- databaseRepository.createDatabase(input)
50+
_ <- databaseRepository.grantAllPrivilegesOnAllTablesInSchemaPublicToPgadmin
5251
_ <- roleRepository.createRole(input.name)
5352
_ <- userPasswords.traverse { userPassword =>
5453
userRepository.addOrUpdateUser(userPassword) >> roleRepository.addUserToRole(userPassword.user, userPassword.database)
5554
}
55+
} yield ()
56+
57+
private def createOrUpdate(userPasswords: List[UserConnectionInfo], input: DatabaseMetadata): F[PhysicalResourceId] =
58+
for {
59+
db <- databaseAsPhysicalResourceId[F](input.name)
60+
scope <- databaseRepository.createDatabase(input).inSession(input.host, input.port, input.username, input.password)
61+
_ <- databaseScopedCreateOrUpdateOperations(userPasswords, input).inSession(input.host, input.port, input.username, input.password, scope)
5662
} yield db
5763

5864
private def handleCreateOrUpdate(input: DatabaseMetadata)
59-
(f: List[UserConnectionInfo] => InSession[F, PhysicalResourceId]): F[PhysicalResourceId] =
65+
(f: List[UserConnectionInfo] => F[PhysicalResourceId]): F[PhysicalResourceId] =
6066
for {
6167
userPasswords <- input.secretIds.traverse(secretsManagerAlg.getSecretAs[UserConnectionInfo])
62-
id <- f(userPasswords).inSession(input.host, input.port, input.username, input.password)
68+
id <- f(userPasswords)
6369
} yield id
6470

6571
private def getUsernamesFromSecrets(secretIds: List[SecretIdType], fallback: Username): F[List[Username]] =

src/main/scala/com/dwolla/postgres/init/package.scala

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,48 @@
11
package com.dwolla.postgres.init
22

3+
import cats.*
34
import cats.syntax.all.*
45
import com.comcast.ip4s.{Host, Port}
56
import eu.timepit.refined.*
67
import eu.timepit.refined.api.*
78
import eu.timepit.refined.string.*
89
import io.circe.Decoder
9-
import monix.newtypes.{HasExtractor, NewtypeWrapped}
1010
import monix.newtypes.integrations.DerivedCirceCodec
11+
import monix.newtypes.{HasExtractor, NewtypeWrapped}
1112
import natchez.TraceableValue
13+
import org.typelevel.literally.Literally
1214

1315
given [A: TraceableValue, P]: TraceableValue[A Refined P] = TraceableValue[A].contramap(_.value)
1416

1517
type SqlIdentifierPredicate = MatchesRegex["[A-Za-z][A-Za-z0-9_]*"]
1618

1719
type SqlIdentifier = String Refined SqlIdentifierPredicate
1820
object SqlIdentifier extends RefinedTypeOps[SqlIdentifier, String]
21+
given Semigroup[SqlIdentifier] = Semigroup.instance((a, b) => SqlIdentifier.unsafeFrom(a.value + b.value))
22+
given Eq[SqlIdentifier] = Eq.by(_.value)
23+
24+
type SqlIdentifierTailPredicate = MatchesRegex["[A-Za-z0-9_]*"]
25+
type SqlIdentifierTail = String Refined SqlIdentifierTailPredicate
26+
object SqlIdentifierTail extends RefinedTypeOps[SqlIdentifierTail, String]
27+
extension (base: SqlIdentifier)
28+
def append(tail: SqlIdentifierTail): SqlIdentifier =
29+
SqlIdentifier.unsafeFrom(base.value + tail.value)
30+
31+
extension (inline ctx: StringContext)
32+
inline def sqlIdentifier(inline args: Any*): SqlIdentifier =
33+
${ SqlIdentifierLiteral('ctx, 'args) }
34+
inline def sqlIdentifierTail(inline args: Any*): SqlIdentifierTail =
35+
${ SqlIdentifierTailLiteral('ctx, 'args) }
36+
37+
object SqlIdentifierLiteral extends Literally[SqlIdentifier]:
38+
def validate(s: String)(using Quotes): Either[String, Expr[SqlIdentifier]] =
39+
SqlIdentifier.from(s).as:
40+
'{ SqlIdentifier.from(${ Expr(s) }).toOption.get }
41+
42+
object SqlIdentifierTailLiteral extends Literally[SqlIdentifierTail]:
43+
def validate(s: String)(using Quotes): Either[String, Expr[SqlIdentifierTail]] =
44+
SqlIdentifierTail.from(s).as:
45+
'{ SqlIdentifierTail.from(${ Expr(s) }).toOption.get }
1946

2047
type GeneratedPasswordPredicate = MatchesRegex["""[-A-Za-z0-9!"#$%&()*+,./:<=>?@\[\]\\^_{|}~]+"""]
2148

src/main/scala/com/dwolla/postgres/init/repositories/CreateSkunkSession.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,29 @@ object CreateSkunkSession {
4242
password: MasterDatabasePassword,
4343
)
4444
(using CreateSkunkSession[F], MonadCancelThrow[F]): F[A] =
45+
impl(host, port, username, password, none)
46+
47+
def inSession(host: Host,
48+
port: Port,
49+
username: MasterDatabaseUsername,
50+
password: MasterDatabasePassword,
51+
database: Database,
52+
)
53+
(using CreateSkunkSession[F], MonadCancelThrow[F]): F[A] =
54+
impl(host, port, username, password, database.some)
55+
56+
private def impl(host: Host,
57+
port: Port,
58+
username: MasterDatabaseUsername,
59+
password: MasterDatabasePassword,
60+
database: Option[Database],
61+
)
62+
(using CreateSkunkSession[F], MonadCancelThrow[F]): F[A] =
4563
CreateSkunkSession[F].single(
4664
host = host.show,
4765
port = port.value,
4866
user = username.value.value,
49-
database = "postgres",
67+
database = database.map(_.value).getOrElse(sqlIdentifier"postgres").value,
5068
password = password.value.some,
5169
ssl = if (host == host"localhost") SSL.None else SSL.System,
5270
).use(kleisli.run)

src/main/scala/com/dwolla/postgres/init/repositories/DatabaseRepository.scala

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ import skunk.implicits.*
1717
trait DatabaseRepository[F[_]] {
1818
def createDatabase(db: DatabaseMetadata): F[Database]
1919
def removeDatabase(database: Database): F[Database]
20+
def grantAllPrivilegesOnAllTablesInSchemaPublicToPgadmin: F[Unit]
2021
}
2122

2223
@annotation.experimental
2324
object DatabaseRepository {
24-
given Aspect[DatabaseRepository, TraceableValue, TraceableValue] = Derive.aspect
25+
given Aspect[DatabaseRepository, TraceableValue, TraceableValue] = {
26+
import com.dwolla.tracing.LowPriorityTraceableValueInstances.unitTraceableValue
27+
Derive.aspect
28+
}
29+
30+
val pgadminRole = RoleName(sqlIdentifier"pgadmin")
2531

2632
def apply[F[_] : {MonadCancelThrow, Logger, Trace}]: DatabaseRepository[InSession[F, *]] = new DatabaseRepository[InSession[F, *]] {
2733
override def createDatabase(db: DatabaseMetadata): Kleisli[F, Session[F], Database] =
@@ -56,6 +62,15 @@ object DatabaseRepository {
5662
.as(database)
5763
.recoverUndefinedAs(database)
5864
}
65+
66+
override def grantAllPrivilegesOnAllTablesInSchemaPublicToPgadmin: InSession[F, Unit] = Kleisli { (session: Session[F]) =>
67+
List(
68+
DatabaseQueries.grantAllPrivilegesOnAllTablesInSchemaPublicTo(_),
69+
DatabaseQueries.alterDefaultPrivilegesToGrantAllPrivilegesOnAllTablesInSchemaPublicTo(_),
70+
).traverse { (f: RoleName => Command[Void]) =>
71+
session.execute(f(pgadminRole))
72+
}
73+
}.void
5974
}.traceWithInputsAndOutputs
6075
}
6176

@@ -74,4 +89,10 @@ object DatabaseQueries {
7489
def dropDatabase(database: Database): Command[Void] =
7590
sql"DROP DATABASE IF EXISTS #${database.value.value}"
7691
.command
92+
93+
def grantAllPrivilegesOnAllTablesInSchemaPublicTo(role: RoleName): Command[Void] =
94+
sql"GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO #${role.value.value}".command
95+
96+
def alterDefaultPrivilegesToGrantAllPrivilegesOnAllTablesInSchemaPublicTo(role: RoleName): Command[Void] =
97+
sql"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO #${role.value.value}".command
7798
}

src/main/scala/com/dwolla/postgres/init/repositories/RoleRepository.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ object RoleRepository {
2929
given Aspect[RoleRepository, TraceableValue, TraceableValue] = Derive.aspect
3030

3131
def roleNameForDatabase(database: Database): RoleName =
32-
RoleName(Refined.unsafeApply(database.value + "_role"))
32+
RoleName(database.value.append(sqlIdentifierTail"_role"))
3333

34-
def apply[F[_] : MonadCancelThrow : Logger : Trace]: RoleRepository[Kleisli[F, Session[F], *]] = new RoleRepository[Kleisli[F, Session[F], *]] {
34+
def apply[F[_] : {MonadCancelThrow, Logger, Trace}]: RoleRepository[Kleisli[F, Session[F], *]] = new RoleRepository[Kleisli[F, Session[F], *]] {
3535
override def createRole(database: Database): Kleisli[F, Session[F], Unit] = {
3636
val role = roleNameForDatabase(database)
3737

src/main/scala/com/dwolla/postgres/init/repositories/UserRepository.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ object UserRepository {
2828
given Aspect[UserRepository, TraceableValue, TraceableValue] = Derive.aspect
2929

3030
def usernameForDatabase(database: Database): Username =
31-
Username(Refined.unsafeApply(database.value.value))
31+
Username(database.value)
3232

3333
def apply[F[_] : {Logger, Temporal, Trace}]: UserRepository[Kleisli[F, Session[F], *]] = new UserRepository[Kleisli[F, Session[F], *]] {
3434
private given [A]: Logger[Kleisli[F, A, *]] = Logger[F].mapK(Kleisli.liftK)

src/test/scala/com/dwolla/postgres/init/ExtractRequestPropertiesSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ class ExtractRequestPropertiesSpec extends munit.FunSuite {
2727
Right(DatabaseMetadata(
2828
host"database-hostname",
2929
port"5432",
30-
Database(SqlIdentifier.unsafeFrom("mydb")),
31-
MasterDatabaseUsername(SqlIdentifier.unsafeFrom("masterdb")),
30+
Database(sqlIdentifier"mydb"),
31+
MasterDatabaseUsername(sqlIdentifier"masterdb"),
3232
MasterDatabasePassword("master-pass"),
3333
List("secret1", "secret2").map(SecretIdType(_)),
3434
))

0 commit comments

Comments
 (0)