From d4a57a80dc7e8e153cdb38eca72c1c2683e3211d Mon Sep 17 00:00:00 2001
From: Janet Gainer-Dewar <jdewar@broadinstitute.org>
Date: Mon, 26 Sep 2022 14:05:09 -0400
Subject: [PATCH 1/6] Give blob SAS tokens write permission

---
 .../scala/cromwell/filesystems/blob/BlobFileSystemManager.scala  | 1 +
 1 file changed, 1 insertion(+)

diff --git a/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobFileSystemManager.scala b/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobFileSystemManager.scala
index 3b8f5149055..7ed79b8edce 100644
--- a/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobFileSystemManager.scala
+++ b/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobFileSystemManager.scala
@@ -123,6 +123,7 @@ case class NativeBlobTokenGenerator(container: BlobContainerName, endpoint: Endp
     .setReadPermission(true)
     .setCreatePermission(true)
     .setListPermission(true)
+    .setWritePermission(true)
 
 
   def generateAccessToken: Try[AzureSasCredential] = for {

From f41ff6bb13f6bc8b23865e9169db34f57d3b766d Mon Sep 17 00:00:00 2001
From: Janet Gainer-Dewar <jdewar@broadinstitute.org>
Date: Mon, 26 Sep 2022 14:36:35 -0400
Subject: [PATCH 2/6] Case class wrapper for subscription id

---
 .../filesystems/blob/BlobFileSystemManager.scala    | 13 +++++++++----
 .../filesystems/blob/BlobPathBuilderFactory.scala   |  7 ++++---
 2 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobFileSystemManager.scala b/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobFileSystemManager.scala
index 7ed79b8edce..a3e6f33572a 100644
--- a/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobFileSystemManager.scala
+++ b/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobFileSystemManager.scala
@@ -77,10 +77,15 @@ case class BlobFileSystemManager(
 
 sealed trait BlobTokenGenerator {def generateAccessToken: Try[AzureSasCredential]}
 object BlobTokenGenerator {
-  def createBlobTokenGenerator(container: BlobContainerName, endpoint: EndpointURL, subscription: Option[String]): BlobTokenGenerator = {
+  def createBlobTokenGenerator(container: BlobContainerName, endpoint: EndpointURL, subscription: Option[SubscriptionId]): BlobTokenGenerator = {
     createBlobTokenGenerator(container, endpoint, None, None, subscription)
   }
-  def createBlobTokenGenerator(container: BlobContainerName, endpoint: EndpointURL, workspaceId: Option[WorkspaceId], workspaceManagerURL: Option[WorkspaceManagerURL], subscription: Option[String]): BlobTokenGenerator = {
+  def createBlobTokenGenerator(container: BlobContainerName,
+                               endpoint: EndpointURL,
+                               workspaceId: Option[WorkspaceId],
+                               workspaceManagerURL: Option[WorkspaceManagerURL],
+                               subscription: Option[SubscriptionId]
+                              ): BlobTokenGenerator = {
      (container: BlobContainerName, endpoint: EndpointURL, workspaceId, workspaceManagerURL) match {
        case (container, endpoint, None, None) =>
          NativeBlobTokenGenerator(container, endpoint, subscription)
@@ -100,13 +105,13 @@ case class WSMBlobTokenGenerator(container: BlobContainerName, endpoint: Endpoin
   def generateAccessToken: Try[AzureSasCredential] = Failure(new NotImplementedError)
 }
 
-case class NativeBlobTokenGenerator(container: BlobContainerName, endpoint: EndpointURL, subscription: Option[String] = None) extends BlobTokenGenerator {
+case class NativeBlobTokenGenerator(container: BlobContainerName, endpoint: EndpointURL, subscription: Option[SubscriptionId] = None) extends BlobTokenGenerator {
 
   private val azureProfile = new AzureProfile(AzureEnvironment.AZURE)
   private def azureCredentialBuilder = new DefaultAzureCredentialBuilder()
       .authorityHost(azureProfile.getEnvironment.getActiveDirectoryEndpoint)
       .build
-  private def authenticateWithSubscription(sub: String) = AzureResourceManager.authenticate(azureCredentialBuilder, azureProfile).withSubscription(sub)
+  private def authenticateWithSubscription(sub: SubscriptionId) = AzureResourceManager.authenticate(azureCredentialBuilder, azureProfile).withSubscription(sub.value)
   private def authenticateWithDefaultSubscription = AzureResourceManager.authenticate(azureCredentialBuilder, azureProfile).withDefaultSubscription()
   private def azure = subscription.map(authenticateWithSubscription(_)).getOrElse(authenticateWithDefaultSubscription)
 
diff --git a/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilderFactory.scala b/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilderFactory.scala
index c93f751b706..e6d269a07b0 100644
--- a/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilderFactory.scala
+++ b/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilderFactory.scala
@@ -10,18 +10,19 @@ import scala.concurrent.{ExecutionContext, Future}
 
 final case class BlobFileSystemConfig(config: Config)
 
+final case class SubscriptionId(value: String) {override def toString: String = value}
 final case class BlobContainerName(value: String) {override def toString: String = value}
 final case class StorageAccountName(value: String) {override def toString: String = value}
 final case class EndpointURL(value: String) {override def toString: String = value}
 final case class WorkspaceId(value: String) {override def toString: String = value}
 final case class WorkspaceManagerURL(value: String) {override def toString: String = value}
 final case class BlobPathBuilderFactory(globalConfig: Config, instanceConfig: Config, singletonConfig: BlobFileSystemConfig) extends PathBuilderFactory {
-  val subscription: Option[String] = instanceConfig.as[Option[String]]("subscription")
+  val subscription: Option[SubscriptionId] = instanceConfig.as[Option[String]]("subscription").map(SubscriptionId)
   val container: BlobContainerName = BlobContainerName(instanceConfig.as[String]("container"))
   val endpoint: EndpointURL = EndpointURL(instanceConfig.as[String]("endpoint"))
-  val workspaceId: Option[WorkspaceId] = instanceConfig.as[Option[String]]("workspace-id").map(WorkspaceId(_))
+  val workspaceId: Option[WorkspaceId] = instanceConfig.as[Option[String]]("workspace-id").map(WorkspaceId)
   val expiryBufferMinutes: Long = instanceConfig.as[Option[Long]]("expiry-buffer-minutes").getOrElse(10)
-  val workspaceManagerURL: Option[WorkspaceManagerURL] = singletonConfig.config.as[Option[String]]("workspace-manager-url").map(WorkspaceManagerURL(_))
+  val workspaceManagerURL: Option[WorkspaceManagerURL] = singletonConfig.config.as[Option[String]]("workspace-manager-url").map(WorkspaceManagerURL)
 
   val blobTokenGenerator: BlobTokenGenerator = BlobTokenGenerator.createBlobTokenGenerator(
     container, endpoint, workspaceId, workspaceManagerURL, subscription)

From aacfd8132d31b48ae377e007af36d1c6d446d73b Mon Sep 17 00:00:00 2001
From: Janet Gainer-Dewar <jdewar@broadinstitute.org>
Date: Sun, 2 Oct 2022 14:40:20 -0400
Subject: [PATCH 3/6] Resolve duplicate container name in absolute BlobPath

---
 .../filesystems/blob/BlobPathBuilder.scala    | 23 +++++++++++++++----
 1 file changed, 19 insertions(+), 4 deletions(-)

diff --git a/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilder.scala b/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilder.scala
index 3e69ce2a7bd..162b30c9d10 100644
--- a/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilder.scala
+++ b/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilder.scala
@@ -67,19 +67,34 @@ class BlobPathBuilder(container: BlobContainerName, endpoint: EndpointURL)(priva
   override def name: String = "Azure Blob Storage"
 }
 
+object BlobPath {
+  // The Azure NIO library uses `{containerName}:` as the root of the path.
+  // This doesn't work well for our need to easily transfer back and forth
+  // to and from the blob URL format. This method removes anything up to and including
+  // the first colon, to create a path string useful for working with BlobPath.
+  // This is safe because the NIO library enforces no colons except to mark
+  // the root container name.
+  private def nioPathString(nioPath: NioPath): String = {
+    val pathStr = nioPath.toString
+    pathStr.substring(pathStr.indexOf(":")+1)
+  }
+}
+
 case class BlobPath private[blob](pathString: String, endpoint: EndpointURL, container: BlobContainerName)(private val fsm: BlobFileSystemManager) extends Path {
   override def nioPath: NioPath = findNioPath(pathString)
 
-  override protected def newPath(nioPath: NioPath): Path = BlobPath(nioPath.toString, endpoint, container)(fsm)
+  override protected def newPath(nioPath: NioPath): Path =
+    BlobPath(BlobPath.nioPathString(nioPath), endpoint, container)(fsm)
 
-  override def pathAsString: String = List(endpoint, container, nioPath.toString).mkString("/")
+  override def pathAsString: String = List(endpoint, container, pathString.stripPrefix("/")).mkString("/")
 
   //This is purposefully an unprotected get because if the endpoint cannot be parsed this should fail loudly rather than quietly
-  override def pathWithoutScheme: String = parseURI(endpoint.value).map(_.getHost + "/" + container + "/" + nioPath.toString).get
+  override def pathWithoutScheme: String = parseURI(endpoint.value).map(u => List(u.getHost, container, pathString).mkString("/")).get
 
   private def findNioPath(path: String): NioPath = (for {
     fileSystem <- fsm.retrieveFilesystem()
-    nioPath = fileSystem.getPath(path)
+    // The Azure NIO library uses `{container}:` to represent the root of the path
+    nioPath = fileSystem.getPath(s"${container.value}:", path)
   // This is purposefully an unprotected get because the NIO API needing an unwrapped path object.
   // If an error occurs the api expects a thrown exception
   } yield nioPath).get

From f2cba321fce7568a6a41d7403f4740955011010a Mon Sep 17 00:00:00 2001
From: Janet Gainer-Dewar <jdewar@broadinstitute.org>
Date: Sun, 2 Oct 2022 14:40:55 -0400
Subject: [PATCH 4/6] Ignored test demonstrating correct absolute path
 generation

---
 .../filesystems/blob/BlobPathBuilderSpec.scala      | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/filesystems/blob/src/test/scala/cromwell/filesystems/blob/BlobPathBuilderSpec.scala b/filesystems/blob/src/test/scala/cromwell/filesystems/blob/BlobPathBuilderSpec.scala
index 9975065a3e2..e4b7aeb6ef8 100644
--- a/filesystems/blob/src/test/scala/cromwell/filesystems/blob/BlobPathBuilderSpec.scala
+++ b/filesystems/blob/src/test/scala/cromwell/filesystems/blob/BlobPathBuilderSpec.scala
@@ -91,4 +91,17 @@ class BlobPathBuilderSpec extends AnyFlatSpec with Matchers with MockSugar {
     val fileText = (is.readAllBytes.map(_.toChar)).mkString
     fileText should include ("This is my test file!!!! Did it work?")
   }
+
+  ignore should "resolve a path without duplicating container name" in {
+    val endpoint = BlobPathBuilderSpec.buildEndpoint("coaexternalstorage")
+    val store = BlobContainerName("inputs")
+    val blobTokenGenerator = NativeBlobTokenGenerator(store, endpoint)
+    val fsm: BlobFileSystemManager = BlobFileSystemManager(store, endpoint, 10, blobTokenGenerator)
+
+    val rootString = List(endpoint.value, store.value, "cromwell-execution").mkString("/")
+    val blobRoot: BlobPath = new BlobPathBuilder(store, endpoint)(fsm) build rootString getOrElse fail()
+    blobRoot.toAbsolutePath.pathAsString should equal ("https://coaexternalstorage.blob.core.windows.net/inputs/cromwell-execution")
+    val otherFile = blobRoot.resolve("test/inputFile.txt")
+    otherFile.toAbsolutePath.pathAsString should equal ("https://coaexternalstorage.blob.core.windows.net/inputs/cromwell-execution/test/inputFile.txt")
+  }
 }

From a2b10f9d60538f1638b2ff2c0da522bd0ee8e63d Mon Sep 17 00:00:00 2001
From: Janet Gainer-Dewar <jdewar@broadinstitute.org>
Date: Wed, 5 Oct 2022 13:41:00 -0400
Subject: [PATCH 5/6] Update
 filesystems/blob/src/test/scala/cromwell/filesystems/blob/BlobPathBuilderSpec.scala

Co-authored-by: Brian Reilly <breilly@broadinstitute.org>
---
 .../scala/cromwell/filesystems/blob/BlobPathBuilderSpec.scala   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/filesystems/blob/src/test/scala/cromwell/filesystems/blob/BlobPathBuilderSpec.scala b/filesystems/blob/src/test/scala/cromwell/filesystems/blob/BlobPathBuilderSpec.scala
index e4b7aeb6ef8..671e7b2d35b 100644
--- a/filesystems/blob/src/test/scala/cromwell/filesystems/blob/BlobPathBuilderSpec.scala
+++ b/filesystems/blob/src/test/scala/cromwell/filesystems/blob/BlobPathBuilderSpec.scala
@@ -98,7 +98,7 @@ class BlobPathBuilderSpec extends AnyFlatSpec with Matchers with MockSugar {
     val blobTokenGenerator = NativeBlobTokenGenerator(store, endpoint)
     val fsm: BlobFileSystemManager = BlobFileSystemManager(store, endpoint, 10, blobTokenGenerator)
 
-    val rootString = List(endpoint.value, store.value, "cromwell-execution").mkString("/")
+    val rootString = s"${endpoint.value}/${store.value}/cromwell-execution"
     val blobRoot: BlobPath = new BlobPathBuilder(store, endpoint)(fsm) build rootString getOrElse fail()
     blobRoot.toAbsolutePath.pathAsString should equal ("https://coaexternalstorage.blob.core.windows.net/inputs/cromwell-execution")
     val otherFile = blobRoot.resolve("test/inputFile.txt")

From 26eca2f10f184a134f524d80ec6cbc7892a105b5 Mon Sep 17 00:00:00 2001
From: Janet Gainer-Dewar <jdewar@broadinstitute.org>
Date: Wed, 5 Oct 2022 13:54:56 -0400
Subject: [PATCH 6/6] PR feedback

---
 .../cromwell/filesystems/blob/BlobPathBuilder.scala    | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilder.scala b/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilder.scala
index 162b30c9d10..1be6bb8e13b 100644
--- a/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilder.scala
+++ b/filesystems/blob/src/main/scala/cromwell/filesystems/blob/BlobPathBuilder.scala
@@ -78,13 +78,19 @@ object BlobPath {
     val pathStr = nioPath.toString
     pathStr.substring(pathStr.indexOf(":")+1)
   }
+
+  def apply(nioPath: NioPath,
+            endpoint: EndpointURL,
+            container: BlobContainerName,
+            fsm: BlobFileSystemManager): BlobPath = {
+    BlobPath(nioPathString(nioPath), endpoint, container)(fsm)
+  }
 }
 
 case class BlobPath private[blob](pathString: String, endpoint: EndpointURL, container: BlobContainerName)(private val fsm: BlobFileSystemManager) extends Path {
   override def nioPath: NioPath = findNioPath(pathString)
 
-  override protected def newPath(nioPath: NioPath): Path =
-    BlobPath(BlobPath.nioPathString(nioPath), endpoint, container)(fsm)
+  override protected def newPath(nioPath: NioPath): Path = BlobPath(nioPath, endpoint, container, fsm)
 
   override def pathAsString: String = List(endpoint, container, pathString.stripPrefix("/")).mkString("/")