Create user complete, added impl-specific types.
This commit is contained in:
parent
5c7c33d1b5
commit
5481686e08
8 changed files with 419 additions and 46 deletions
|
|
@ -2,6 +2,7 @@ package gs.smolban.auth
|
||||||
|
|
||||||
import cats.effect.Sync
|
import cats.effect.Sync
|
||||||
import cats.syntax.all.*
|
import cats.syntax.all.*
|
||||||
|
import java.nio.charset.Charset
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import org.bouncycastle.crypto.generators.Argon2BytesGenerator
|
import org.bouncycastle.crypto.generators.Argon2BytesGenerator
|
||||||
import org.bouncycastle.crypto.params.Argon2Parameters
|
import org.bouncycastle.crypto.params.Argon2Parameters
|
||||||
|
|
@ -28,13 +29,19 @@ final class Argon2[F[_]: Sync](
|
||||||
* @return
|
* @return
|
||||||
* The calculated hash.
|
* The calculated hash.
|
||||||
*/
|
*/
|
||||||
def calculateHash(input: String): F[Argon2Hash] =
|
def calculateHash(
|
||||||
|
input: String,
|
||||||
|
charset: Charset = StandardCharsets.UTF_8
|
||||||
|
): F[Argon2Hash] =
|
||||||
|
calculateHash(input.getBytes(charset))
|
||||||
|
|
||||||
|
def calculateHash(input: Array[Byte]): F[Argon2Hash] =
|
||||||
randomSalt().map { salt =>
|
randomSalt().map { salt =>
|
||||||
val ap = buildAlgorithmParams(salt)
|
val ap = buildAlgorithmParams(salt)
|
||||||
val a2 = new Argon2BytesGenerator()
|
val a2 = new Argon2BytesGenerator()
|
||||||
val _ = a2.init(ap)
|
val _ = a2.init(ap)
|
||||||
val bytes = new Array[Byte](config.hashLengthInBytes)
|
val bytes = new Array[Byte](config.hashLengthInBytes)
|
||||||
val _ = a2.generateBytes(input.getBytes(StandardCharsets.UTF_8), bytes)
|
val _ = a2.generateBytes(input, bytes)
|
||||||
new Argon2Hash(
|
new Argon2Hash(
|
||||||
algorithmVersion = config.algorithmVersion,
|
algorithmVersion = config.algorithmVersion,
|
||||||
algorithmType = config.algorithmType,
|
algorithmType = config.algorithmType,
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@ import gs.smolban.model.account.AiAgent
|
||||||
import gs.smolban.model.account.PermissionSet
|
import gs.smolban.model.account.PermissionSet
|
||||||
import gs.smolban.model.account.ServiceAccount
|
import gs.smolban.model.account.ServiceAccount
|
||||||
import gs.smolban.model.account.User
|
import gs.smolban.model.account.User
|
||||||
|
import gs.smolban.model.metadata.CreatedAt
|
||||||
|
|
||||||
/** Database interface for [[Account]] and related management activities.
|
/** Database interface for [[Account]] and related management activities.
|
||||||
*/
|
*/
|
||||||
trait AuthDb[F[_]]:
|
trait AuthDb[DBIO[_], F[_]]:
|
||||||
|
|
||||||
/** Get the user account with the given name.
|
/** Get the user account with the given name.
|
||||||
*
|
*
|
||||||
|
|
@ -28,7 +29,7 @@ trait AuthDb[F[_]]:
|
||||||
*/
|
*/
|
||||||
def getUser(
|
def getUser(
|
||||||
name: AccountName
|
name: AccountName
|
||||||
): F[Option[User]]
|
): DBIO[Option[User]]
|
||||||
|
|
||||||
/** Create a new [[User]].
|
/** Create a new [[User]].
|
||||||
*
|
*
|
||||||
|
|
@ -44,8 +45,9 @@ trait AuthDb[F[_]]:
|
||||||
def createUser(
|
def createUser(
|
||||||
name: AccountName,
|
name: AccountName,
|
||||||
initialPassword: Password,
|
initialPassword: Password,
|
||||||
initialPermissions: PermissionSet
|
initialPermissions: PermissionSet,
|
||||||
): EitherT[F, DbError, User]
|
createdAt: CreatedAt
|
||||||
|
): F[DBIO[Either[DbError, User]]]
|
||||||
|
|
||||||
/** Update the password for an existing [[User]].
|
/** Update the password for an existing [[User]].
|
||||||
*
|
*
|
||||||
|
|
@ -59,12 +61,12 @@ trait AuthDb[F[_]]:
|
||||||
def setUserPassword(
|
def setUserPassword(
|
||||||
id: AccountId,
|
id: AccountId,
|
||||||
newPassword: Password
|
newPassword: Password
|
||||||
): EitherT[F, DbError, User]
|
): EitherT[DBIO, DbError, User]
|
||||||
|
|
||||||
/** @return
|
/** @return
|
||||||
* List of all active [[User]].
|
* List of all active [[User]].
|
||||||
*/
|
*/
|
||||||
def listActiveUsers(): F[List[User]]
|
def listActiveUsers(): fs2.Stream[DBIO, User]
|
||||||
|
|
||||||
/** Get the [[ServiceAccount]] with the given name.
|
/** Get the [[ServiceAccount]] with the given name.
|
||||||
*
|
*
|
||||||
|
|
@ -76,7 +78,7 @@ trait AuthDb[F[_]]:
|
||||||
*/
|
*/
|
||||||
def getServiceAccount(
|
def getServiceAccount(
|
||||||
name: AccountName
|
name: AccountName
|
||||||
): F[Option[ServiceAccount]]
|
): DBIO[Option[ServiceAccount]]
|
||||||
|
|
||||||
/** Create a new [[ServiceAccount]].
|
/** Create a new [[ServiceAccount]].
|
||||||
*
|
*
|
||||||
|
|
@ -93,12 +95,12 @@ trait AuthDb[F[_]]:
|
||||||
name: AccountName,
|
name: AccountName,
|
||||||
owner: AccountId,
|
owner: AccountId,
|
||||||
initialPermissions: PermissionSet
|
initialPermissions: PermissionSet
|
||||||
): EitherT[F, DbError, NewServiceAccount]
|
): EitherT[DBIO, DbError, NewServiceAccount]
|
||||||
|
|
||||||
/** @return
|
/** @return
|
||||||
* List of all active [[ServiceAccount]].
|
* List of all active [[ServiceAccount]].
|
||||||
*/
|
*/
|
||||||
def listActiveServiceAccounts(): F[List[ServiceAccount]]
|
def listActiveServiceAccounts(): DBIO[List[ServiceAccount]]
|
||||||
|
|
||||||
/** Get the [[AiAgent]] with the given name.
|
/** Get the [[AiAgent]] with the given name.
|
||||||
*
|
*
|
||||||
|
|
@ -109,7 +111,7 @@ trait AuthDb[F[_]]:
|
||||||
*/
|
*/
|
||||||
def getAgentAccount(
|
def getAgentAccount(
|
||||||
name: AccountName
|
name: AccountName
|
||||||
): F[Option[AiAgent]]
|
): DBIO[Option[AiAgent]]
|
||||||
|
|
||||||
/** Create a new [[AiAgent]].
|
/** Create a new [[AiAgent]].
|
||||||
*
|
*
|
||||||
|
|
@ -126,12 +128,12 @@ trait AuthDb[F[_]]:
|
||||||
name: AccountName,
|
name: AccountName,
|
||||||
owner: AccountId,
|
owner: AccountId,
|
||||||
initialPermissions: PermissionSet
|
initialPermissions: PermissionSet
|
||||||
): EitherT[F, DbError, NewAiAgentAccount]
|
): EitherT[DBIO, DbError, NewAiAgentAccount]
|
||||||
|
|
||||||
/** @return
|
/** @return
|
||||||
* List of all active [[AiAgent]].
|
* List of all active [[AiAgent]].
|
||||||
*/
|
*/
|
||||||
def listActiveAgentAccounts(): F[List[AiAgent]]
|
def listActiveAgentAccounts(): DBIO[List[AiAgent]]
|
||||||
|
|
||||||
/** Rotate the specified secret and return the new secret. This is the only
|
/** Rotate the specified secret and return the new secret. This is the only
|
||||||
* time the new value will ever be exposed.
|
* time the new value will ever be exposed.
|
||||||
|
|
@ -153,7 +155,7 @@ trait AuthDb[F[_]]:
|
||||||
accountId: AccountId,
|
accountId: AccountId,
|
||||||
credentialId: CredentialId,
|
credentialId: CredentialId,
|
||||||
overlapHours: Int
|
overlapHours: Int
|
||||||
): EitherT[F, DbError, Base64]
|
): EitherT[DBIO, DbError, Base64]
|
||||||
|
|
||||||
/** Replace the permissions assigned to some [[Account]].
|
/** Replace the permissions assigned to some [[Account]].
|
||||||
*
|
*
|
||||||
|
|
@ -167,7 +169,7 @@ trait AuthDb[F[_]]:
|
||||||
def setAccountPermissions(
|
def setAccountPermissions(
|
||||||
id: AccountId,
|
id: AccountId,
|
||||||
newPermissions: PermissionSet
|
newPermissions: PermissionSet
|
||||||
): EitherT[F, DbError, Account]
|
): EitherT[DBIO, DbError, Account]
|
||||||
|
|
||||||
/** List all credentials that exist for the given [[AccountId]].
|
/** List all credentials that exist for the given [[AccountId]].
|
||||||
*
|
*
|
||||||
|
|
@ -178,7 +180,7 @@ trait AuthDb[F[_]]:
|
||||||
*/
|
*/
|
||||||
def listAccountCredentials(
|
def listAccountCredentials(
|
||||||
id: AccountId
|
id: AccountId
|
||||||
): F[List[Credential]]
|
): DBIO[List[Credential]]
|
||||||
|
|
||||||
/** Revoke a credential, making it unusable.
|
/** Revoke a credential, making it unusable.
|
||||||
*
|
*
|
||||||
|
|
@ -189,7 +191,7 @@ trait AuthDb[F[_]]:
|
||||||
*/
|
*/
|
||||||
def revokeCredential(
|
def revokeCredential(
|
||||||
id: CredentialId
|
id: CredentialId
|
||||||
): EitherT[F, DbError, Unit]
|
): EitherT[DBIO, DbError, Unit]
|
||||||
|
|
||||||
/** Expire a credential, making it unusable.
|
/** Expire a credential, making it unusable.
|
||||||
*
|
*
|
||||||
|
|
@ -200,4 +202,4 @@ trait AuthDb[F[_]]:
|
||||||
*/
|
*/
|
||||||
def expireCredential(
|
def expireCredential(
|
||||||
id: CredentialId
|
id: CredentialId
|
||||||
): EitherT[F, DbError, Unit]
|
): EitherT[DBIO, DbError, Unit]
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,161 @@
|
||||||
package gs.smolban.db.doobie
|
package gs.smolban.db.doobie
|
||||||
|
|
||||||
import cats.data.EitherT
|
import cats.data.EitherT
|
||||||
|
import cats.effect.Sync
|
||||||
|
import cats.syntax.all.*
|
||||||
import doobie.*
|
import doobie.*
|
||||||
|
import doobie.implicits.*
|
||||||
|
import gs.smolban.auth.Argon2
|
||||||
|
import gs.smolban.auth.Argon2Hash
|
||||||
import gs.smolban.auth.Base64
|
import gs.smolban.auth.Base64
|
||||||
import gs.smolban.auth.Credential
|
import gs.smolban.auth.Credential
|
||||||
|
import gs.smolban.auth.CredentialEffectivity
|
||||||
import gs.smolban.auth.CredentialId
|
import gs.smolban.auth.CredentialId
|
||||||
|
import gs.smolban.auth.CredentialStatus
|
||||||
|
import gs.smolban.auth.CredentialType
|
||||||
import gs.smolban.auth.NewAiAgentAccount
|
import gs.smolban.auth.NewAiAgentAccount
|
||||||
import gs.smolban.auth.NewServiceAccount
|
import gs.smolban.auth.NewServiceAccount
|
||||||
import gs.smolban.auth.Password
|
import gs.smolban.auth.Password
|
||||||
|
import gs.smolban.auth.RsaDecryption
|
||||||
|
import gs.smolban.auth.RsaEncryptedBytes
|
||||||
import gs.smolban.db.AuthDb
|
import gs.smolban.db.AuthDb
|
||||||
import gs.smolban.db.DbError
|
import gs.smolban.db.DbError
|
||||||
|
import gs.smolban.db.doobie.DoobieTypes.*
|
||||||
import gs.smolban.model.account.Account
|
import gs.smolban.model.account.Account
|
||||||
import gs.smolban.model.account.AccountId
|
import gs.smolban.model.account.AccountId
|
||||||
import gs.smolban.model.account.AccountName
|
import gs.smolban.model.account.AccountName
|
||||||
|
import gs.smolban.model.account.AccountStatus
|
||||||
|
import gs.smolban.model.account.AccountType
|
||||||
import gs.smolban.model.account.AiAgent
|
import gs.smolban.model.account.AiAgent
|
||||||
|
import gs.smolban.model.account.Permission
|
||||||
import gs.smolban.model.account.PermissionSet
|
import gs.smolban.model.account.PermissionSet
|
||||||
import gs.smolban.model.account.ServiceAccount
|
import gs.smolban.model.account.ServiceAccount
|
||||||
import gs.smolban.model.account.User
|
import gs.smolban.model.account.User
|
||||||
|
import gs.smolban.model.group.GroupName
|
||||||
|
import gs.smolban.model.metadata.CreatedAt
|
||||||
|
|
||||||
final class DoobieAuthDb extends AuthDb[ConnectionIO] {
|
final class DoobieAuthDb[F[_]: Sync](
|
||||||
|
val argon2: Argon2[F],
|
||||||
|
val rsaDecryption: RsaDecryption[F],
|
||||||
|
val sqlStates: CuratedSqlStates,
|
||||||
|
val implTypes: ImplTypes
|
||||||
|
) extends AuthDb[ConnectionIO, F]:
|
||||||
|
private val Sql = new DoobieAuthDb.Sql(implTypes)
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
*/
|
*/
|
||||||
override def getUser(name: AccountName): ConnectionIO[Option[User]] = ???
|
override def getUser(name: AccountName): ConnectionIO[Option[User]] =
|
||||||
|
Sql.getUser(name).option.flatMap {
|
||||||
|
case Some((id, status, createdAt)) =>
|
||||||
|
getUserPermissions(id).map { permissions =>
|
||||||
|
Some(
|
||||||
|
new User(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
status = status,
|
||||||
|
permissions = permissions,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case None => None.pure[ConnectionIO]
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getUserPermissions(id: AccountId): ConnectionIO[PermissionSet] =
|
||||||
|
for
|
||||||
|
global <- Sql.getUserGlobalPermissions(id).to[Set]
|
||||||
|
group <- Sql.getUserGroupPermissions(id).to[Set]
|
||||||
|
yield PermissionSet(
|
||||||
|
global = global,
|
||||||
|
group = group.groupMap(_._1)(_._2)
|
||||||
|
)
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
*/
|
*/
|
||||||
override def createUser(
|
override def createUser(
|
||||||
name: AccountName,
|
name: AccountName,
|
||||||
initialPassword: Password,
|
initialPassword: Password,
|
||||||
initialPermissions: PermissionSet
|
initialPermissions: PermissionSet,
|
||||||
): EitherT[[A] =>> ConnectionIO[A], DbError, User] = ???
|
createdAt: CreatedAt
|
||||||
|
): F[ConnectionIO[Either[DbError, User]]] =
|
||||||
|
// Prepare Password
|
||||||
|
exchangeEncryptedForHash(initialPassword.unwrap()).map { passwordHash =>
|
||||||
|
// Insert the base user record.
|
||||||
|
val accountId = AccountId.generate()
|
||||||
|
Sql
|
||||||
|
.insertUser(accountId, name, AccountStatus.Active, createdAt)
|
||||||
|
.run
|
||||||
|
.attemptSqlState
|
||||||
|
.flatMap {
|
||||||
|
case Left(sqlState) =>
|
||||||
|
if sqlState.value == sqlStates.uniqueViolation.value then
|
||||||
|
Left(DbError.AccountAlreadyExists(name, AccountType.User))
|
||||||
|
.pure[ConnectionIO]
|
||||||
|
else Left(DbError.GenericDatabaseError(sqlState)).pure[ConnectionIO]
|
||||||
|
case Right(_) =>
|
||||||
|
for
|
||||||
|
// Insert the user's credential.
|
||||||
|
_ <- Sql
|
||||||
|
.insertCredential(
|
||||||
|
id = CredentialId.generate(),
|
||||||
|
hash = passwordHash,
|
||||||
|
accountId = accountId,
|
||||||
|
accountType = AccountType.User,
|
||||||
|
credentialType = CredentialType.Password,
|
||||||
|
status = CredentialStatus.Active,
|
||||||
|
effective = None,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
.run
|
||||||
|
// Insert the user's global permissions.
|
||||||
|
_ <- Sql.insertUserGlobalPermissions(
|
||||||
|
accountId,
|
||||||
|
initialPermissions.global
|
||||||
|
)
|
||||||
|
// Insert the user's group permissions.
|
||||||
|
_ <- Sql.insertUserGroupPermissions(
|
||||||
|
accountId,
|
||||||
|
initialPermissions.group
|
||||||
|
)
|
||||||
|
yield Right(
|
||||||
|
new User(
|
||||||
|
id = accountId,
|
||||||
|
name = name,
|
||||||
|
status = AccountStatus.Active,
|
||||||
|
permissions = initialPermissions,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def exchangeEncryptedForHash(rsa: RsaEncryptedBytes): F[Argon2Hash] =
|
||||||
|
rsaDecryption.decrypt(rsa).flatMap(argon2.calculateHash)
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
*/
|
*/
|
||||||
override def setUserPassword(
|
override def setUserPassword(
|
||||||
id: AccountId,
|
id: AccountId,
|
||||||
newPassword: Password
|
newPassword: Password
|
||||||
): EitherT[[A] =>> ConnectionIO[A], DbError, User] = ???
|
): EitherT[ConnectionIO, DbError, User] = ???
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
*/
|
*/
|
||||||
override def listActiveUsers(): ConnectionIO[List[User]] = ???
|
override def listActiveUsers(): fs2.Stream[ConnectionIO, User] =
|
||||||
|
Sql
|
||||||
|
.listUsersForStatus(AccountStatus.Active)
|
||||||
|
.stream
|
||||||
|
.evalMap { case (id, name, createdAt) =>
|
||||||
|
getUserPermissions(id).map { permissions =>
|
||||||
|
new User(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
status = AccountStatus.Active,
|
||||||
|
permissions = permissions,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
*/
|
*/
|
||||||
|
|
@ -54,7 +168,7 @@ final class DoobieAuthDb extends AuthDb[ConnectionIO] {
|
||||||
name: AccountName,
|
name: AccountName,
|
||||||
owner: AccountId,
|
owner: AccountId,
|
||||||
initialPermissions: PermissionSet
|
initialPermissions: PermissionSet
|
||||||
): EitherT[[A] =>> ConnectionIO[A], DbError, NewServiceAccount] = ???
|
): EitherT[ConnectionIO, DbError, NewServiceAccount] = ???
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
*/
|
*/
|
||||||
|
|
@ -108,4 +222,94 @@ final class DoobieAuthDb extends AuthDb[ConnectionIO] {
|
||||||
override def expireCredential(id: CredentialId)
|
override def expireCredential(id: CredentialId)
|
||||||
: EitherT[[A] =>> ConnectionIO[A], DbError, Unit] = ???
|
: EitherT[[A] =>> ConnectionIO[A], DbError, Unit] = ???
|
||||||
|
|
||||||
}
|
object DoobieAuthDb:
|
||||||
|
|
||||||
|
class Sql(implTypes: ImplTypes):
|
||||||
|
import implTypes.given
|
||||||
|
|
||||||
|
def getUser(name: AccountName)
|
||||||
|
: Query0[(AccountId, AccountStatus, CreatedAt)] =
|
||||||
|
sql"""
|
||||||
|
SELECT id, status, created_at
|
||||||
|
FROM user
|
||||||
|
WHERE name = $name
|
||||||
|
""".query[(AccountId, AccountStatus, CreatedAt)]
|
||||||
|
|
||||||
|
def getUserGlobalPermissions(id: AccountId): Query0[Permission] =
|
||||||
|
sql"""
|
||||||
|
SELECT permission
|
||||||
|
FROM user_permission_global
|
||||||
|
WHERE user_id = $id
|
||||||
|
""".query[Permission]
|
||||||
|
|
||||||
|
def getUserGroupPermissions(id: AccountId)
|
||||||
|
: Query0[(GroupName, Permission)] =
|
||||||
|
sql"""
|
||||||
|
SELECT group_name, permission
|
||||||
|
FROM user_permission_group
|
||||||
|
WHERE user_id = $id
|
||||||
|
""".query[(GroupName, Permission)]
|
||||||
|
|
||||||
|
def listUsersForStatus(status: AccountStatus)
|
||||||
|
: Query0[(AccountId, AccountName, CreatedAt)] =
|
||||||
|
sql"""
|
||||||
|
SELECT id, name, created_at
|
||||||
|
FROM user
|
||||||
|
WHERE status = $status
|
||||||
|
""".query[(AccountId, AccountName, CreatedAt)]
|
||||||
|
|
||||||
|
def insertUser(
|
||||||
|
id: AccountId,
|
||||||
|
name: AccountName,
|
||||||
|
status: AccountStatus,
|
||||||
|
createdAt: CreatedAt
|
||||||
|
): Update0 =
|
||||||
|
sql"""
|
||||||
|
INSERT INTO user(id, name, status, created_at)
|
||||||
|
VALUES ($id, $name, $status, $createdAt)
|
||||||
|
""".update
|
||||||
|
|
||||||
|
def insertCredential(
|
||||||
|
id: CredentialId,
|
||||||
|
hash: Argon2Hash,
|
||||||
|
accountId: AccountId,
|
||||||
|
accountType: AccountType,
|
||||||
|
credentialType: CredentialType,
|
||||||
|
status: CredentialStatus,
|
||||||
|
effective: Option[CredentialEffectivity],
|
||||||
|
createdAt: CreatedAt
|
||||||
|
): Update0 =
|
||||||
|
sql"""
|
||||||
|
INSERT INTO credential(credential_id, credential_hash, account_id,
|
||||||
|
account_type, credential_type, status, effective_at, effective_through,
|
||||||
|
created_at)
|
||||||
|
VALUES ($id, $hash, $accountId, $accountType, $credentialType, $status,
|
||||||
|
${effective.map(_.startDate)}, ${effective.flatMap(_.endDate)},
|
||||||
|
$createdAt)
|
||||||
|
""".update
|
||||||
|
|
||||||
|
def insertUserGlobalPermissions(
|
||||||
|
id: AccountId,
|
||||||
|
permissions: Set[Permission]
|
||||||
|
): ConnectionIO[Int] =
|
||||||
|
val command = """
|
||||||
|
INSERT INTO user_permission_global (user_id, permission) VALUES (?, ?)
|
||||||
|
""".stripMargin
|
||||||
|
Update[(AccountId, Permission)](command)
|
||||||
|
.updateMany(permissions.map(p => id -> p).toList)
|
||||||
|
|
||||||
|
def insertUserGroupPermissions(
|
||||||
|
id: AccountId,
|
||||||
|
permissions: Map[GroupName, Set[Permission]]
|
||||||
|
): ConnectionIO[Int] =
|
||||||
|
val command = """
|
||||||
|
INSERT INTO user_permission_group (user_id, group_name, permission) VALUES (?, ?, ?)
|
||||||
|
""".stripMargin
|
||||||
|
Update[(AccountId, GroupName, Permission)](command)
|
||||||
|
.updateMany(permissions.flatMap { case (gn, ps) =>
|
||||||
|
ps.map(p => (id, gn, p))
|
||||||
|
}.toList)
|
||||||
|
|
||||||
|
end Sql
|
||||||
|
|
||||||
|
end DoobieAuthDb
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@ import gs.smolban.model.metadata.Tag
|
||||||
import gs.smolban.model.metadata.TagValue
|
import gs.smolban.model.metadata.TagValue
|
||||||
|
|
||||||
final class DoobieTagDb(
|
final class DoobieTagDb(
|
||||||
sqlStates: CuratedSqlStates
|
sqlStates: CuratedSqlStates,
|
||||||
|
implTypes: ImplTypes
|
||||||
) extends TagDb[ConnectionIO]:
|
) extends TagDb[ConnectionIO]:
|
||||||
|
private val Sql = new DoobieTagDb.Sql(implTypes)
|
||||||
import DoobieTagDb.Sql
|
|
||||||
|
|
||||||
/** @inheritdoc
|
/** @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
|
@ -46,7 +46,8 @@ final class DoobieTagDb(
|
||||||
|
|
||||||
object DoobieTagDb:
|
object DoobieTagDb:
|
||||||
|
|
||||||
private object Sql:
|
private class Sql(implTypes: ImplTypes):
|
||||||
|
import implTypes.given
|
||||||
|
|
||||||
def createTag(
|
def createTag(
|
||||||
tag: TagValue,
|
tag: TagValue,
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,80 @@ package gs.smolban.db.doobie
|
||||||
|
|
||||||
import DoobieTypes.ErrorMessages
|
import DoobieTypes.ErrorMessages
|
||||||
import doobie.*
|
import doobie.*
|
||||||
import gs.smolban.model.metadata.CreatedAt
|
import gs.smolban.auth.Argon2Hash
|
||||||
|
import gs.smolban.auth.CredentialId
|
||||||
|
import gs.smolban.auth.CredentialStatus
|
||||||
|
import gs.smolban.auth.CredentialType
|
||||||
|
import gs.smolban.model.account.AccountId
|
||||||
|
import gs.smolban.model.account.AccountName
|
||||||
|
import gs.smolban.model.account.AccountStatus
|
||||||
|
import gs.smolban.model.account.AccountType
|
||||||
|
import gs.smolban.model.account.Permission
|
||||||
|
import gs.smolban.model.group.GroupName
|
||||||
import gs.smolban.model.metadata.TagValue
|
import gs.smolban.model.metadata.TagValue
|
||||||
import gs.smolban.model.ticket.CommentId
|
import gs.smolban.model.ticket.CommentId
|
||||||
import gs.smolban.model.ticket.TicketStatus
|
import gs.smolban.model.ticket.TicketStatus
|
||||||
import gs.uuid.v0.UUID
|
import gs.uuid.v0.UUID
|
||||||
import gs.uuid.v0.UUIDFormat
|
import gs.uuid.v0.UUIDFormat
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
trait DoobieTypes:
|
trait DoobieTypes:
|
||||||
|
|
||||||
|
implicit val accountIdGet: Get[AccountId] =
|
||||||
|
Get[Array[Byte]].tmap(bytes => AccountId(UUID(UUIDFormat.fromBytes(bytes))))
|
||||||
|
|
||||||
|
implicit val accountIdPut: Put[AccountId] =
|
||||||
|
Put[Array[Byte]].tcontramap(id => UUIDFormat.toBytes(id.toUUID().toUUID()))
|
||||||
|
|
||||||
|
implicit val accountNameGet: Get[AccountName] =
|
||||||
|
Get[String].temap { value =>
|
||||||
|
AccountName.validate(value) match
|
||||||
|
case None => Left(ErrorMessages.invalidAccountName(value))
|
||||||
|
case Some(s) => Right(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit val accountNamePut: Put[AccountName] =
|
||||||
|
Put[String].tcontramap(_.toString())
|
||||||
|
|
||||||
|
implicit val accountStatusGet: Get[AccountStatus] =
|
||||||
|
Get[String].temap(dbValue =>
|
||||||
|
AccountStatus.parse(dbValue) match
|
||||||
|
case None => Left(ErrorMessages.invalidAccountStatus(dbValue))
|
||||||
|
case Some(status) => Right(status)
|
||||||
|
)
|
||||||
|
|
||||||
|
implicit val accountStatusPut: Put[AccountStatus] =
|
||||||
|
Put[String].tcontramap(_.name)
|
||||||
|
|
||||||
|
implicit val accountTypeGet: Get[AccountType] =
|
||||||
|
Get[String].temap(dbValue =>
|
||||||
|
AccountType.parse(dbValue) match
|
||||||
|
case None => Left(ErrorMessages.invalidAccountType(dbValue))
|
||||||
|
case Some(status) => Right(status)
|
||||||
|
)
|
||||||
|
|
||||||
|
implicit val accountTypePut: Put[AccountType] =
|
||||||
|
Put[String].tcontramap(_.name)
|
||||||
|
|
||||||
|
implicit val permissionGet: Get[Permission] =
|
||||||
|
Get[String].temap(dbValue =>
|
||||||
|
Permission.parse(dbValue) match
|
||||||
|
case None => Left(ErrorMessages.invalidPermission(dbValue))
|
||||||
|
case Some(status) => Right(status)
|
||||||
|
)
|
||||||
|
|
||||||
|
implicit val permissionPut: Put[Permission] =
|
||||||
|
Put[String].tcontramap(_.name)
|
||||||
|
|
||||||
|
implicit val groupNameGet: Get[GroupName] =
|
||||||
|
Get[String].temap { value =>
|
||||||
|
GroupName.validate(value) match
|
||||||
|
case None => Left(ErrorMessages.invalidGroupName(value))
|
||||||
|
case Some(s) => Right(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit val groupNamePut: Put[GroupName] =
|
||||||
|
Put[String].tcontramap(_.toString())
|
||||||
|
|
||||||
implicit val ticketStatusGet: Get[TicketStatus] =
|
implicit val ticketStatusGet: Get[TicketStatus] =
|
||||||
Get[String].temap(dbValue =>
|
Get[String].temap(dbValue =>
|
||||||
TicketStatus.parse(dbValue) match
|
TicketStatus.parse(dbValue) match
|
||||||
|
|
@ -28,18 +92,6 @@ trait DoobieTypes:
|
||||||
implicit val commentIdPut: Put[CommentId] =
|
implicit val commentIdPut: Put[CommentId] =
|
||||||
Put[Array[Byte]].tcontramap(id => UUIDFormat.toBytes(id.toUUID().toUUID()))
|
Put[Array[Byte]].tcontramap(id => UUIDFormat.toBytes(id.toUUID().toUUID()))
|
||||||
|
|
||||||
implicit val createdAtGet: Get[CreatedAt] =
|
|
||||||
Get[Long].tmap(CreatedAt.fromMilliseconds)
|
|
||||||
|
|
||||||
implicit val createdAtPut: Put[CreatedAt] =
|
|
||||||
Put[Long].tcontramap(_.toMilliseconds())
|
|
||||||
|
|
||||||
implicit val instantGet: Get[Instant] =
|
|
||||||
Get[Long].tmap(Instant.ofEpochMilli)
|
|
||||||
|
|
||||||
implicit val instantPut: Put[Instant] =
|
|
||||||
Put[Long].tcontramap(_.toEpochMilli())
|
|
||||||
|
|
||||||
implicit val tagValueGet: Get[TagValue] =
|
implicit val tagValueGet: Get[TagValue] =
|
||||||
Get[String].temap { value =>
|
Get[String].temap { value =>
|
||||||
TagValue.validate(value) match
|
TagValue.validate(value) match
|
||||||
|
|
@ -50,16 +102,78 @@ trait DoobieTypes:
|
||||||
implicit val tagValuePut: Put[TagValue] =
|
implicit val tagValuePut: Put[TagValue] =
|
||||||
Put[String].tcontramap(_.toString())
|
Put[String].tcontramap(_.toString())
|
||||||
|
|
||||||
|
implicit val credentialIdGet: Get[CredentialId] =
|
||||||
|
Get[Array[Byte]].tmap(bytes =>
|
||||||
|
CredentialId(UUID(UUIDFormat.fromBytes(bytes)))
|
||||||
|
)
|
||||||
|
|
||||||
|
implicit val credentialIdPut: Put[CredentialId] =
|
||||||
|
Put[Array[Byte]].tcontramap(id => UUIDFormat.toBytes(id.toUUID().toUUID()))
|
||||||
|
|
||||||
|
implicit val argon2HashPut: Put[Argon2Hash] =
|
||||||
|
Put[String].tcontramap(_.encode())
|
||||||
|
|
||||||
|
implicit val argon2HashGet: Get[Argon2Hash] =
|
||||||
|
Get[String].temap(dbValue =>
|
||||||
|
Argon2Hash.decode(dbValue) match
|
||||||
|
case None => Left(ErrorMessages.invalidAccountStatus(dbValue))
|
||||||
|
case Some(hash) => Right(hash)
|
||||||
|
)
|
||||||
|
|
||||||
|
implicit val credentialTypeGet: Get[CredentialType] =
|
||||||
|
Get[String].temap(dbValue =>
|
||||||
|
CredentialType.parse(dbValue) match
|
||||||
|
case None => Left(ErrorMessages.invalidCredentialType(dbValue))
|
||||||
|
case Some(status) => Right(status)
|
||||||
|
)
|
||||||
|
|
||||||
|
implicit val credentialTypePut: Put[CredentialType] =
|
||||||
|
Put[String].tcontramap(_.name)
|
||||||
|
|
||||||
|
implicit val credentialStatusGet: Get[CredentialStatus] =
|
||||||
|
Get[String].temap(dbValue =>
|
||||||
|
CredentialStatus.parse(dbValue) match
|
||||||
|
case None => Left(ErrorMessages.invalidCredentialStatus(dbValue))
|
||||||
|
case Some(status) => Right(status)
|
||||||
|
)
|
||||||
|
|
||||||
|
implicit val credentialStatusPut: Put[CredentialStatus] =
|
||||||
|
Put[String].tcontramap(_.name)
|
||||||
|
|
||||||
object DoobieTypes extends DoobieTypes:
|
object DoobieTypes extends DoobieTypes:
|
||||||
|
|
||||||
object ErrorMessages:
|
object ErrorMessages:
|
||||||
|
|
||||||
|
def invalidAccountName(candidate: String): String =
|
||||||
|
s"'$candidate' is not a valid account name."
|
||||||
|
|
||||||
|
def invalidAccountStatus(candidate: String): String =
|
||||||
|
s"'$candidate' is not a valid account status."
|
||||||
|
|
||||||
|
def invalidAccountType(candidate: String): String =
|
||||||
|
s"'$candidate' is not a valid account type."
|
||||||
|
|
||||||
|
def invalidPermission(candidate: String): String =
|
||||||
|
s"'$candidate' is not a valid permission."
|
||||||
|
|
||||||
|
def invalidGroupName(candidate: String): String =
|
||||||
|
s"'$candidate' is not a valid group name."
|
||||||
|
|
||||||
def invalidTagValue(candidate: String): String =
|
def invalidTagValue(candidate: String): String =
|
||||||
s"'$candidate' is not a valid tag value."
|
s"'$candidate' is not a valid tag value."
|
||||||
|
|
||||||
def invalidTicketStatus(candidate: String): String =
|
def invalidTicketStatus(candidate: String): String =
|
||||||
s"'$candidate' is not a valid ticket status."
|
s"'$candidate' is not a valid ticket status."
|
||||||
|
|
||||||
|
def invalidArgon2Hash(candidate: String): String =
|
||||||
|
s"'$candidate' is not a valid Argon2 hash."
|
||||||
|
|
||||||
|
def invalidCredentialType(candidate: String): String =
|
||||||
|
s"'$candidate' is not a valid credential type."
|
||||||
|
|
||||||
|
def invalidCredentialStatus(candidate: String): String =
|
||||||
|
s"'$candidate' is not a valid credential status."
|
||||||
|
|
||||||
end ErrorMessages
|
end ErrorMessages
|
||||||
|
|
||||||
end DoobieTypes
|
end DoobieTypes
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package gs.smolban.db.doobie
|
||||||
|
|
||||||
|
import doobie.*
|
||||||
|
import gs.smolban.model.metadata.CreatedAt
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
/** Abstraction for supporting implementation-specific types.
|
||||||
|
*/
|
||||||
|
trait ImplTypes:
|
||||||
|
given createdAtGet: Get[CreatedAt]
|
||||||
|
given createdAtPut: Put[CreatedAt]
|
||||||
|
given instantGet: Get[Instant]
|
||||||
|
given instantPut: Put[Instant]
|
||||||
|
given localDateGet: Get[LocalDate]
|
||||||
|
given localDatePut: Put[LocalDate]
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package gs.smolban.db.doobie
|
||||||
|
|
||||||
|
import doobie.*
|
||||||
|
import gs.smolban.model.metadata.CreatedAt
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
/** SQLite implementation of specific types.
|
||||||
|
*/
|
||||||
|
final class SqliteImplTypes extends ImplTypes:
|
||||||
|
|
||||||
|
given createdAtGet: Get[CreatedAt] =
|
||||||
|
Get[Long].tmap(CreatedAt.fromMilliseconds)
|
||||||
|
|
||||||
|
given createdAtPut: Put[CreatedAt] =
|
||||||
|
Put[Long].tcontramap(_.toMilliseconds())
|
||||||
|
|
||||||
|
given instantGet: Get[Instant] =
|
||||||
|
Get[Long].tmap(Instant.ofEpochMilli)
|
||||||
|
|
||||||
|
given instantPut: Put[Instant] =
|
||||||
|
Put[Long].tcontramap(_.toEpochMilli())
|
||||||
|
|
||||||
|
given localDateGet: Get[LocalDate] =
|
||||||
|
Get[String].tmap(LocalDate.parse)
|
||||||
|
|
||||||
|
given localDatePut: Put[LocalDate] =
|
||||||
|
Put[String].tcontramap(_.toString())
|
||||||
|
|
@ -29,7 +29,8 @@ class DoobieTagDbTests extends munit.FunSuite:
|
||||||
test(name)(f.unsafeRunSync())
|
test(name)(f.unsafeRunSync())
|
||||||
|
|
||||||
private val tagDb: TagDb[ConnectionIO] = new DoobieTagDb(
|
private val tagDb: TagDb[ConnectionIO] = new DoobieTagDb(
|
||||||
CuratedSqlStates.sqlite
|
CuratedSqlStates.sqlite,
|
||||||
|
new SqliteImplTypes
|
||||||
)
|
)
|
||||||
|
|
||||||
private val clock = Clock.systemDefaultZone()
|
private val clock = Clock.systemDefaultZone()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue