brendanhay / gogol

A comprehensive Google Services SDK for Haskell.
Other
280 stars 105 forks source link

Generate scope type-aliases and rework scope constraints #178

Closed brendanhay closed 2 years ago

brendanhay commented 2 years ago

OAuth2 scopes are now generated as type aliases. For example, for Gogol.Storage.Types:

-- | View and manage your data across Google Cloud Platform services
type CloudPlatform'FullControl = "https://www.googleapis.com/auth/cloud-platform"

-- | View your data across Google Cloud Platform services
type CloudPlatform'ReadOnly = "https://www.googleapis.com/auth/cloud-platform.read-only"

-- | Manage your data and permissions in Google Cloud Storage
type Devstorage'FullControl = "https://www.googleapis.com/auth/devstorage.full_control"

-- | View your data in Google Cloud Storage
type Devstorage'ReadOnly = "https://www.googleapis.com/auth/devstorage.read_only"

-- | Manage your data in Google Cloud Storage
type Devstorage'ReadWrite = "https://www.googleapis.com/auth/devstorage.read_write"

Where the class instance for storage.objects.insert is generated as:

instance GoogleRequest StorageObjectsInsert where
  type Scopes StorageObjectsInsert = '[CloudPlatform'FullControl, Devstorage'FullControl, Devstorage'ReadWrite]

The various scope-related type families have been reworked to a) explicitly only require one scope from Scopes, above, and b) allow propagating explicit scopes, such as the least privileged scope. You'll still need to make the list of scopes on Env concrete at some point in your program, but hopefully this allows more sensible scope constraints rather than being forced to carry around a concrete type-level list everywhere.

For example, omitting any constraints in the following example:

uploadFile ::
  Env scopes ->
  BucketName ->
  Object ->
  MediaType ->
  FilePath ->
  IO (Either Error Object)
uploadFile env bucket object media path =
  liftIO $ do
    let meta =
          newStorageObjectsInsert bucket.text $
            object
              { bucket = Just bucket.text
              }

    body <- GBody media <$> Network.HTTP.Client.streamFile path

    runResourceT (Gogol.uploadEither env meta body)

downloadFile ::
  Env scopes ->
  BucketName ->
  ObjectName ->
  FilePath ->
  IO (Either Error ())
downloadFile env bucket object path =
  liftIO $ do
    let meta = newStorageObjectsGet bucket.text object.text

    runResourceT $ do
      result <- downloadEither env meta

      for result $ \stream ->
        Conduit.connect stream (Conduit.Combinators.sinkFileCautious path)

Which results in the error(s):

    • One scope from the following list is required:
          '[CloudPlatform'FullControl, Devstorage'FullControl,
            Devstorage'ReadWrite]
      However, none of these scopes are present in the list of scopes you provided:
          scopes
    • In the first argument of ‘runResourceT’, namely
        ‘(uploadEither env meta body)’
      In the expression: runResourceT (uploadEither env meta body)
      In the second argument of ‘($)’, namely
        ‘\ _retryStatus -> runResourceT (uploadEither env meta body)’
   |
00 |       runResourceT (uploadEither env meta body)
   |                     ^^^^^^^^^^^^^^^^^^

    • One scope from the following list is required:
          '[CloudPlatform'FullControl, CloudPlatform'ReadOnly,
            Devstorage'FullControl, Devstorage'ReadOnly, Devstorage'ReadWrite]
      However, none of these scopes are present in the list of scopes you provided:
          scopes
    • In a stmt of a 'do' block:
        result <- downloadEither env meta
      In the second argument of ‘($)’, namely
        ‘do result <- downloadEither env meta
            for result
              $ \ stream
                  -> Conduit.connect
                       stream (Conduit.Combinators.sinkFileCautious path)’
      In the expression:
        runResourceT
          $ do result <- downloadEither env meta
               for result
                 $ \ stream
                     -> Conduit.connect
                          stream (Conduit.Combinators.sinkFileCautious path)
   |
00 |         result <- downloadEither env meta
   | 

The error can be fixed by adding a constraint declaring which scope from required in the error message above can be found in the scopes type variable. For example, to explicitly choose the scope with least privilege for both operations:

uploadFile ::
  HasScope Devstorage'ReadWrite scopes =>
  Env scopes ->
  BucketName ->
  Object ->
  MediaType ->
  FilePath ->
  IO (Either Error Object)
uploadFile = ...

downloadFile ::
  HasScope Devstorage'ReadOnly scopes =>
  Env scopes ->
  BucketName ->
  ObjectName ->
  FilePath ->
  IO (Either Error ())
downloadFile = ...

Alternatively you can just leave it up to the caller to choose by propagating the full set of required scopes (of which only one is required):

uploadFile ::
  HasScopeFor StorageObjectsGet scopes =>
  Env scopes ->
  BucketName ->
  Object ->
  MediaType ->
  FilePath ->
  IO (Either Error Object)
uploadFile = ...

downloadFile ::
  HasScopeFor StorageObjectsInsert scopes =>
  Env scopes ->
  BucketName ->
  ObjectName ->
  FilePath ->
  IO (Either Error ())
downloadFile = ...

See https://github.com/brendanhay/gogol/blob/7789a6a34b14a32d66b080b06c648ae2de6b206c/lib/gogol/src/Gogol/Auth/Scope.hs