- Create a Service Account https://developers.google.com/identity/protocols/oauth2/service-account?hl=en#creatinganaccount That account should be able to read/write to the firestore.
- Download the service account key to the current directory.
- Run
scala-cli main.scala -- <project_id> <key.json>
Created
June 26, 2024 03:26
-
-
Save tanishiking/ef1a390daea2afb9a9c769cb312cf1cf to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//> using scala 3 | |
//> using toolkit latest | |
//> using dep "com.github.jwt-scala::jwt-upickle:9.4.4" | |
//> using dep "org.http4s::http4s-ember-client:0.23.23" | |
//> using dep "io.chrisdavenport::http4s-grpc-google-cloud-firestore-v1:3.15.2+0.0.6" | |
// https://developers.google.com/identity/protocols/oauth2/service-account?hl=en#error-codes | |
import cats.syntax.all._ | |
import cats.effect._ | |
import org.http4s._ | |
import org.http4s.client.Client | |
import org.http4s.ember.client.EmberClientBuilder | |
import org.http4s.client.middleware.Logger | |
import com.google.firestore.v1.firestore.Firestore | |
import com.google.firestore.v1.document.Document | |
import com.google.firestore.v1.firestore.GetDocumentRequest | |
import com.google.firestore.v1.firestore.GetDocumentRequest.ConsistencySelector | |
import com.google.firestore.v1.firestore.CreateDocumentRequest | |
import com.google.firestore.v1.document.Value | |
import com.google.firestore.v1.document.Value.ValueTypeOneof | |
// For issuing access token | |
import java.time.Instant | |
import pdi.jwt.{JwtAlgorithm, JwtUpickle, JwtClaim, JwtHeader} | |
import upickle.default.* | |
import sttp.client4.quick.* | |
case class ServiceAccountCredentials( | |
`type`: String, | |
project_id: String, | |
private_key_id: String, | |
private_key: String, | |
client_email: String, | |
client_id: String, | |
auth_uri: String, | |
token_uri: String, | |
client_x509_cert_url: String, | |
universe_domain: String | |
) derives ReadWriter | |
case class Token( | |
access_token: String, | |
expires_in: Int, | |
token_type: String | |
) derives ReadWriter | |
object Main extends IOApp: | |
override def run(args: List[String]): IO[ExitCode] = | |
val projectId: String = args(0) | |
val serviceAccountKey: String = args(1) | |
getAccessToken(serviceAccountKey) match | |
case Left(value) => | |
IO.println(value) >> ExitCode.Error.pure[IO] | |
case Right(accessToken) => | |
createDocument(projectId, accessToken) >> ExitCode.Success.pure[IO] | |
def createDocument(projectId: String, accessToken: String): IO[Unit] = | |
def createEmberClient(): Resource[IO, Client[IO]] = | |
EmberClientBuilder | |
.default[IO] | |
.withHttp2 | |
.build | |
createEmberClient().use { rawClient => | |
val client = Logger[IO]( | |
logHeaders = true, | |
logBody = true, | |
redactHeadersWhen = _ => false, | |
logAction = Some(msg => IO.println(msg)) | |
)(rawClient) | |
val firestore = Firestore.fromClient( | |
client, | |
Uri | |
.fromString("https://firestore.googleapis.com") | |
.getOrElse(throw new RuntimeException("invalid firestore uri")) | |
) | |
val docId = System.currentTimeMillis().toString | |
firestore | |
.createDocument( | |
CreateDocumentRequest.of( | |
parent = s"projects/$projectId/databases/(default)/documents", | |
collectionId = "jokes", | |
documentId = docId, | |
document = Some(Document.of( | |
name = "", | |
fields = Map( | |
"joke" -> Value.of( | |
ValueTypeOneof.StringValue("joke") | |
) | |
), | |
createTime = None, | |
updateTime = None | |
)), | |
mask = None | |
), | |
Headers.of( | |
headers.Authorization( | |
Credentials.Token(AuthScheme.Bearer, accessToken) | |
), | |
headers.`Content-Type`( | |
new MediaType("application", "grpc") | |
) | |
) | |
) | |
.flatMap { doc => | |
IO.println(doc) | |
} | |
} | |
def getAccessToken(serviceAccountKey: String): Either[RuntimeException, String] = | |
val cred = read[ServiceAccountCredentials](os.read(os.pwd / serviceAccountKey)) | |
// Create a JWT | |
val header = JwtHeader( | |
algorithm = Some(JwtAlgorithm.RS256), | |
typ = Some("JWT"), | |
keyId = Some(cred.private_key_id) | |
) | |
val claim = JwtClaim( | |
expiration = Some(Instant.now.plusSeconds(60 * 60).getEpochSecond), | |
issuedAt = Some(Instant.now.getEpochSecond), | |
issuer = Some(cred.client_email), | |
audience = Some(Set("https://oauth2.googleapis.com/token")) | |
) ++ ("scope" -> "https://www.googleapis.com/auth/datastore") | |
val key = "secretKey" | |
val algo = JwtAlgorithm.HS256 | |
val token = JwtUpickle.encode(header, claim, cred.private_key) | |
// Request an access token from the Google OAuth 2.0 Authorization Server. | |
val request = quickRequest | |
.body(Map("grant_type" -> "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion" -> token)) | |
.post(uri"https://oauth2.googleapis.com/token") | |
val response = request.send(backend) | |
if (response.code == sttp.model.StatusCode.Ok) { | |
val token = read[Token](response.body) | |
Right(token.access_token) | |
} else { | |
Left(new RuntimeException(s"${response.code}: ${response.body}")) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment