From c8359ad4a2d5ed55f5a2e14e3faf41dbce4acadd Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 09:33:10 -0400 Subject: [PATCH 01/15] Scaffold out GitHub.App.Token module tree --- .gitignore | 2 + github-app-token/github-app-token.cabal | 55 ++++++++++++++++ github-app-token/package.yaml | 66 +++++++++++++++++++ github-app-token/src/GitHub/App/Token.hs | 8 +++ .../src/GitHub/App/Token/AppCredentials.hs | 24 +++++++ .../src/GitHub/App/Token/Generate.hs | 21 ++++++ .../src/GitHub/App/Token/Prelude.hs | 15 +++++ stack.yaml | 4 ++ stack.yaml.lock | 12 ++++ 9 files changed, 207 insertions(+) create mode 100644 .gitignore create mode 100644 github-app-token/github-app-token.cabal create mode 100644 github-app-token/package.yaml create mode 100644 github-app-token/src/GitHub/App/Token.hs create mode 100644 github-app-token/src/GitHub/App/Token/AppCredentials.hs create mode 100644 github-app-token/src/GitHub/App/Token/Generate.hs create mode 100644 github-app-token/src/GitHub/App/Token/Prelude.hs create mode 100644 stack.yaml create mode 100644 stack.yaml.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa196e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env* +.stack-work diff --git a/github-app-token/github-app-token.cabal b/github-app-token/github-app-token.cabal new file mode 100644 index 0000000..b0549d0 --- /dev/null +++ b/github-app-token/github-app-token.cabal @@ -0,0 +1,55 @@ +cabal-version: 1.18 + +-- This file has been generated from package.yaml by hpack version 0.37.0. +-- +-- see: https://github.com/sol/hpack + +name: github-app-token +version: 0.0.0.0 +synopsis: Library for generating GitHub App installation tokens +description: Please see README.md +category: HTTP +homepage: https://github.com/freckle/freckle-app#readme +bug-reports: https://github.com/freckle/freckle-app/issues +maintainer: Freckle Education +build-type: Simple +extra-doc-files: + README.md + CHANGELOG.md + +source-repository head + type: git + location: https://github.com/freckle/freckle-app + +library + exposed-modules: + GitHub.App.Token + GitHub.App.Token.AppCredentials + GitHub.App.Token.Generate + GitHub.App.Token.Prelude + other-modules: + Paths_github_app_token + hs-source-dirs: + src + default-extensions: + DataKinds + DeriveAnyClass + DerivingVia + DerivingStrategies + DuplicateRecordFields + GADTs + LambdaCase + NoImplicitPrelude + NoMonomorphismRestriction + OverloadedRecordDot + OverloadedStrings + RecordWildCards + TypeFamilies + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-safe -Wno-unsafe + build-depends: + base <5 + , bytestring + , path + default-language: GHC2021 + if impl(ghc >= 9.8) + ghc-options: -Wno-missing-role-annotations -Wno-missing-poly-kind-signatures diff --git a/github-app-token/package.yaml b/github-app-token/package.yaml new file mode 100644 index 0000000..5ffb34a --- /dev/null +++ b/github-app-token/package.yaml @@ -0,0 +1,66 @@ +name: github-app-token +version: 0.0.0.0 +maintainer: Freckle Education +category: HTTP +github: freckle/freckle-app +synopsis: Library for generating GitHub App installation tokens +description: Please see README.md + +extra-doc-files: + - README.md + - CHANGELOG.md + +language: GHC2021 + +ghc-options: + - -fignore-optim-changes + - -fwrite-ide-info + - -Weverything + - -Wno-all-missed-specialisations + - -Wno-missing-exported-signatures # re-enables missing-signatures + - -Wno-missing-import-lists + - -Wno-missing-kind-signatures + - -Wno-missing-local-signatures + - -Wno-missing-safe-haskell-mode + - -Wno-monomorphism-restriction + - -Wno-prepositive-qualified-module + - -Wno-safe + - -Wno-unsafe + +when: + - condition: "impl(ghc >= 9.8)" + ghc-options: + - -Wno-missing-role-annotations + - -Wno-missing-poly-kind-signatures + +dependencies: + - base < 5 + +default-extensions: + - DataKinds + - DeriveAnyClass + - DerivingVia + - DerivingStrategies + - DuplicateRecordFields + - GADTs + - LambdaCase + - NoImplicitPrelude + - NoMonomorphismRestriction + - OverloadedRecordDot + - OverloadedStrings + - RecordWildCards + - TypeFamilies + +library: + source-dirs: src + dependencies: + - bytestring + - path + +# tests: +# spec: +# main: Main.hs +# source-dirs: tests +# ghc-options: -threaded -rtsopts "-with-rtsopts=-N" +# dependencies: +# - github-app-token diff --git a/github-app-token/src/GitHub/App/Token.hs b/github-app-token/src/GitHub/App/Token.hs new file mode 100644 index 0000000..76c87b8 --- /dev/null +++ b/github-app-token/src/GitHub/App/Token.hs @@ -0,0 +1,8 @@ +module GitHub.App.Token + ( AppCredentials (..) + , InstallationId (..) + , generateInstallationToken + ) where + +import GitHub.App.Token.AppCredentials +import GitHub.App.Token.Generate diff --git a/github-app-token/src/GitHub/App/Token/AppCredentials.hs b/github-app-token/src/GitHub/App/Token/AppCredentials.hs new file mode 100644 index 0000000..647f227 --- /dev/null +++ b/github-app-token/src/GitHub/App/Token/AppCredentials.hs @@ -0,0 +1,24 @@ +module GitHub.App.Token.AppCredentials + ( AppCredentials (..) + , AppId (..) + , PrivateKey (..) + , readPrivateKey + ) where + +import GitHub.App.Token.Prelude + +data AppCredentials = AppCredentials + { appId :: AppId + , privateKey :: PrivateKey + } + +newtype AppId = AppId + { unwrap :: Int + } + +newtype PrivateKey = PrivateKey + { unwrap :: ByteString + } + +readPrivateKey :: MonadIO m => Path b File -> m PrivateKey +readPrivateKey = fmap PrivateKey . readBinary diff --git a/github-app-token/src/GitHub/App/Token/Generate.hs b/github-app-token/src/GitHub/App/Token/Generate.hs new file mode 100644 index 0000000..bd46059 --- /dev/null +++ b/github-app-token/src/GitHub/App/Token/Generate.hs @@ -0,0 +1,21 @@ +module GitHub.App.Token.Generate + ( InstallationId (..) + , InstallationToken (..) + , generateInstallationToken + ) where + +import GitHub.App.Token.Prelude + +import GitHub.App.Token.AppCredentials + +newtype InstallationId = InstallationId + { unwrap :: Int + } + +newtype InstallationToken = InstallationToken + { unwrap :: ByteString + } + +generateInstallationToken + :: AppCredentials -> InstallationId -> m InstallationToken +generateInstallationToken = undefined diff --git a/github-app-token/src/GitHub/App/Token/Prelude.hs b/github-app-token/src/GitHub/App/Token/Prelude.hs new file mode 100644 index 0000000..d82b3b3 --- /dev/null +++ b/github-app-token/src/GitHub/App/Token/Prelude.hs @@ -0,0 +1,15 @@ +module GitHub.App.Token.Prelude + ( module X + , module GitHub.App.Token.Prelude + ) where + +import Prelude as X + +import Control.Monad.IO.Class as X (MonadIO (..)) +import Data.ByteString as X (ByteString) +import Path as X (Abs, Dir, File, Path, Rel, toFilePath) + +import Data.ByteString qualified as BS + +readBinary :: MonadIO m => Path b File -> m ByteString +readBinary = liftIO . BS.readFile . toFilePath diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..4c7ab5b --- /dev/null +++ b/stack.yaml @@ -0,0 +1,4 @@ +resolver: lts-22.34 +packages: + - github-app-token + # - github-app-token-cli diff --git a/stack.yaml.lock b/stack.yaml.lock new file mode 100644 index 0000000..401a580 --- /dev/null +++ b/stack.yaml.lock @@ -0,0 +1,12 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/lock_files + +packages: [] +snapshots: +- completed: + sha256: edbd50d7e7c85c13ad5f5835ae2db92fab1e9cf05ecf85340e2622ec0a303df1 + size: 720020 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/34.yaml + original: lts-22.34 From da4a08b213b7fcb4c78415d53f0994944bf0bf9b Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 10:11:40 -0400 Subject: [PATCH 02/15] Initial implementation --- github-app-token/github-app-token.cabal | 10 +- github-app-token/package.yaml | 7 ++ .../src/GitHub/App/Token/AppCredentials.hs | 9 +- .../src/GitHub/App/Token/Generate.hs | 79 +++++++++++++++- github-app-token/src/GitHub/App/Token/JWT.hs | 93 +++++++++++++++++++ .../src/GitHub/App/Token/Prelude.hs | 13 ++- 6 files changed, 190 insertions(+), 21 deletions(-) create mode 100644 github-app-token/src/GitHub/App/Token/JWT.hs diff --git a/github-app-token/github-app-token.cabal b/github-app-token/github-app-token.cabal index b0549d0..90815e3 100644 --- a/github-app-token/github-app-token.cabal +++ b/github-app-token/github-app-token.cabal @@ -26,6 +26,7 @@ library GitHub.App.Token GitHub.App.Token.AppCredentials GitHub.App.Token.Generate + GitHub.App.Token.JWT GitHub.App.Token.Prelude other-modules: Paths_github_app_token @@ -47,9 +48,16 @@ library TypeFamilies ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-safe -Wno-unsafe build-depends: - base <5 + aeson + , base <5 , bytestring + , http-conduit + , http-types + , jwt , path + , text + , time + , unliftio default-language: GHC2021 if impl(ghc >= 9.8) ghc-options: -Wno-missing-role-annotations -Wno-missing-poly-kind-signatures diff --git a/github-app-token/package.yaml b/github-app-token/package.yaml index 5ffb34a..64bab96 100644 --- a/github-app-token/package.yaml +++ b/github-app-token/package.yaml @@ -54,8 +54,15 @@ default-extensions: library: source-dirs: src dependencies: + - aeson - bytestring + - http-conduit + - jwt + - text + - time + - http-types - path + - unliftio # tests: # spec: diff --git a/github-app-token/src/GitHub/App/Token/AppCredentials.hs b/github-app-token/src/GitHub/App/Token/AppCredentials.hs index 647f227..10a3244 100644 --- a/github-app-token/src/GitHub/App/Token/AppCredentials.hs +++ b/github-app-token/src/GitHub/App/Token/AppCredentials.hs @@ -2,9 +2,9 @@ module GitHub.App.Token.AppCredentials ( AppCredentials (..) , AppId (..) , PrivateKey (..) - , readPrivateKey ) where +import GitHub.App.Token.JWT (PrivateKey (..)) import GitHub.App.Token.Prelude data AppCredentials = AppCredentials @@ -15,10 +15,3 @@ data AppCredentials = AppCredentials newtype AppId = AppId { unwrap :: Int } - -newtype PrivateKey = PrivateKey - { unwrap :: ByteString - } - -readPrivateKey :: MonadIO m => Path b File -> m PrivateKey -readPrivateKey = fmap PrivateKey . readBinary diff --git a/github-app-token/src/GitHub/App/Token/Generate.hs b/github-app-token/src/GitHub/App/Token/Generate.hs index bd46059..a40c61c 100644 --- a/github-app-token/src/GitHub/App/Token/Generate.hs +++ b/github-app-token/src/GitHub/App/Token/Generate.hs @@ -1,21 +1,90 @@ module GitHub.App.Token.Generate ( InstallationId (..) - , InstallationToken (..) + , AccessToken (..) , generateInstallationToken + + -- * Errors + , InvalidPrivateKey (..) + , InvalidDate (..) + , InvalidIssuer (..) + , AccessTokenHttpError (..) + , AccessTokenJsonDecodeError (..) ) where import GitHub.App.Token.Prelude +import Data.Aeson (FromJSON, eitherDecode) +import Data.ByteString.Lazy qualified as BSL import GitHub.App.Token.AppCredentials +import GitHub.App.Token.JWT +import Network.HTTP.Simple + ( addRequestHeader + , getResponseBody + , getResponseStatus + , httpLBS + , parseRequest + ) +import Network.HTTP.Types.Header (hAccept, hAuthorization) +import Network.HTTP.Types.Status (Status, statusIsSuccessful) newtype InstallationId = InstallationId { unwrap :: Int } -newtype InstallationToken = InstallationToken - { unwrap :: ByteString +data AccessToken = AccessToken + { token :: Text + , expires_at :: UTCTime + } + deriving stock (Generic) + deriving anyclass (FromJSON) + +data AccessTokenHttpError = AccessTokenHttpError + { status :: Status + , body :: BSL.ByteString + } + deriving stock (Show) + deriving anyclass (Exception) + +data AccessTokenJsonDecodeError = AccessTokenJsonDecodeError + { body :: BSL.ByteString + , message :: String } + deriving stock (Show) + deriving anyclass (Exception) generateInstallationToken - :: AppCredentials -> InstallationId -> m InstallationToken -generateInstallationToken = undefined + :: MonadIO m + => AppCredentials + -> InstallationId + -> m AccessToken +generateInstallationToken creds installationId = do + jwt <- signJWT expiration issuer creds.privateKey + + req <- + liftIO + $ parseRequest + $ "POST https://api.github.com/app/installations/" + <> show installationId.unwrap + <> "/access_tokens" + + -- parse the response body ourselves, to improve error messages + resp <- + httpLBS + $ addRequestHeader hAccept "application/vnd.github+json" + $ addRequestHeader hAuthorization ("Bearer <> " <> jwt) + $ addRequestHeader "X-GitHub-Api-Version" "2022-11-28" req + + let + status = getResponseStatus resp + body = getResponseBody resp + + unless (statusIsSuccessful status) + $ throwIO + $ AccessTokenHttpError {status, body} + + either (throwIO . AccessTokenJsonDecodeError body) pure $ eitherDecode body + where + -- We're going to use it right away and only onces, so 5m should be more than + -- enough + expiration = ExpirationTime $ 5 * 60 + issuer = Issuer $ pack $ show creds.appId.unwrap diff --git a/github-app-token/src/GitHub/App/Token/JWT.hs b/github-app-token/src/GitHub/App/Token/JWT.hs new file mode 100644 index 0000000..db1e973 --- /dev/null +++ b/github-app-token/src/GitHub/App/Token/JWT.hs @@ -0,0 +1,93 @@ +module GitHub.App.Token.JWT + ( signJWT + , ExpirationTime (..) + , Issuer (..) + + -- * Private RSA Key data + , PrivateKey (..) + , readPrivateKey + + -- * Errors + , InvalidPrivateKey (..) + , InvalidDate (..) + , InvalidIssuer (..) + ) where + +import GitHub.App.Token.Prelude + +import Data.Text.Encoding (encodeUtf8) +import Data.Time (NominalDiffTime, addUTCTime, getCurrentTime) +import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) +import Web.JWT qualified as JWT + +newtype ExpirationTime = ExpirationTime + { unwrap :: NominalDiffTime + } + +newtype Issuer = Issuer + { unwrap :: Text + } + deriving stock (Show) + +newtype PrivateKey = PrivateKey + { unwrap :: String + } + deriving stock (Show) + +readPrivateKey :: MonadIO m => Path b File -> m PrivateKey +readPrivateKey = fmap PrivateKey . liftIO . readFile . toFilePath + +newtype InvalidPrivateKey = InvalidPrivateKey PrivateKey + deriving stock (Show) + deriving anyclass (Exception) + +data InvalidDate = InvalidDate + { field :: String + , date :: UTCTime + } + deriving stock (Show) + deriving anyclass (Exception) + +newtype InvalidIssuer = InvalidIssuer Issuer + deriving stock (Show) + deriving anyclass (Exception) + +signJWT + :: MonadIO m + => ExpirationTime + -> Issuer + -> PrivateKey + -> m ByteString +signJWT expirationTime issuer privateKey = liftIO $ do + now <- getCurrentTime + let expiration = addUTCTime expirationTime.unwrap now + + signer <- + maybe (throwIO $ InvalidPrivateKey privateKey) pure + =<< JWT.rsaKeySecret privateKey.unwrap + + iat <- + maybe (throwIO $ InvalidDate "iat" now) pure + $ numericDate now + + exp <- + maybe (throwIO $ InvalidDate "exp" expiration) pure + $ numericDate expiration + + iss <- + maybe (throwIO $ InvalidIssuer issuer) pure + $ JWT.stringOrURI issuer.unwrap + + pure + $ encodeUtf8 + $ JWT.encodeSigned + signer + mempty {JWT.alg = Just JWT.RS256} + mempty + { JWT.iat = Just iat + , JWT.exp = Just exp + , JWT.iss = Just iss + } + +numericDate :: UTCTime -> Maybe JWT.NumericDate +numericDate = JWT.numericDate . utcTimeToPOSIXSeconds diff --git a/github-app-token/src/GitHub/App/Token/Prelude.hs b/github-app-token/src/GitHub/App/Token/Prelude.hs index d82b3b3..9caef3c 100644 --- a/github-app-token/src/GitHub/App/Token/Prelude.hs +++ b/github-app-token/src/GitHub/App/Token/Prelude.hs @@ -1,15 +1,14 @@ module GitHub.App.Token.Prelude ( module X - , module GitHub.App.Token.Prelude ) where -import Prelude as X +import Prelude as X hiding (exp) +import Control.Monad as X (unless, when) import Control.Monad.IO.Class as X (MonadIO (..)) import Data.ByteString as X (ByteString) +import Data.Text as X (Text, pack, unpack) +import Data.Time as X (UTCTime) +import GHC.Generics as X (Generic) import Path as X (Abs, Dir, File, Path, Rel, toFilePath) - -import Data.ByteString qualified as BS - -readBinary :: MonadIO m => Path b File -> m ByteString -readBinary = liftIO . BS.readFile . toFilePath +import UnliftIO.Exception as X (Exception, throwIO) From 5f3025dfb0f85f6cc0c0e6c93e21feb2d7b885c7 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 11:35:50 -0400 Subject: [PATCH 03/15] Add executable README as tests --- .gitignore | 4 ++ github-app-token/.env.example | 6 ++ github-app-token/README.lhs | 62 +++++++++++++++++++ github-app-token/github-app-token.cabal | 36 +++++++++++ github-app-token/package.yaml | 16 ++++- github-app-token/src/GitHub/App/Token.hs | 9 ++- .../src/GitHub/App/Token/Generate.hs | 7 ++- 7 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 github-app-token/.env.example create mode 100644 github-app-token/README.lhs diff --git a/.gitignore b/.gitignore index fa196e3..27d43cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .env* +!.env.example + .stack-work + +key.pem diff --git a/github-app-token/.env.example b/github-app-token/.env.example new file mode 100644 index 0000000..fc7e592 --- /dev/null +++ b/github-app-token/.env.example @@ -0,0 +1,6 @@ +# shellcheck disable=SC2034 +# Fill out and place this file at .env to run the README.lhs test +GITHUB_APP_ID= +GITHUB_INSTALLATION_ID= + +# Additionally, copy the private key to key.pem diff --git a/github-app-token/README.lhs b/github-app-token/README.lhs new file mode 100644 index 0000000..236e850 --- /dev/null +++ b/github-app-token/README.lhs @@ -0,0 +1,62 @@ +# GitHub App Token + +[Generate an installation access token for a GitHub App][docs] + +[docs]: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation + +## Usage + + + +```haskell +import Prelude + +import Control.Lens ((^?)) +import Data.Aeson +import Data.Aeson.Lens +import Data.Text.Encoding (encodeUtf8) +import GitHub.App.Token +import Network.HTTP.Simple +import Network.HTTP.Types.Header (hUserAgent, hAuthorization) +import System.Environment +import Path (mkRelFile) + +example :: IO () +example = do + -- NB: see the separate github-app-token-cli for nicer ways to load these + -- secrets within optparse-applicative and/or envparse. + appId <- AppId . read <$> getEnv "GITHUB_APP_ID" + privateKey <- readPrivateKey $(mkRelFile "key.pem") + installationId <- InstallationId . read <$> getEnv "GITHUB_INSTALLATION_ID" + + -- Generate token + token <- generateInstallationToken AppCredentials {appId, privateKey} installationId + + -- Use token + req <- parseRequest "https://api.github.com/repos/freckle/github-app-token" + resp <- httpJSON @_ @Value + $ addRequestHeader hUserAgent "github-app-token/example" + $ addRequestHeader hAuthorization ("Bearer " <> encodeUtf8 token.token) + $ req + + print $ getResponseBody resp ^? key "description" . _String + -- Just "Generate an installation token for a GitHub App" +``` + + diff --git a/github-app-token/github-app-token.cabal b/github-app-token/github-app-token.cabal index 90815e3..e2018fc 100644 --- a/github-app-token/github-app-token.cabal +++ b/github-app-token/github-app-token.cabal @@ -61,3 +61,39 @@ library default-language: GHC2021 if impl(ghc >= 9.8) ghc-options: -Wno-missing-role-annotations -Wno-missing-poly-kind-signatures + +test-suite readme + type: exitcode-stdio-1.0 + main-is: README.lhs + other-modules: + Paths_github_app_token + default-extensions: + DataKinds + DeriveAnyClass + DerivingVia + DerivingStrategies + DuplicateRecordFields + GADTs + LambdaCase + NoImplicitPrelude + NoMonomorphismRestriction + OverloadedRecordDot + OverloadedStrings + RecordWildCards + TypeFamilies + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-safe -Wno-unsafe -pgmL markdown-unlit + build-depends: + aeson + , base <5 + , dotenv + , github-app-token + , http-conduit + , http-types + , lens + , lens-aeson + , markdown-unlit + , path + , text + default-language: GHC2021 + if impl(ghc >= 9.8) + ghc-options: -Wno-missing-role-annotations -Wno-missing-poly-kind-signatures diff --git a/github-app-token/package.yaml b/github-app-token/package.yaml index 64bab96..6545d8c 100644 --- a/github-app-token/package.yaml +++ b/github-app-token/package.yaml @@ -64,10 +64,24 @@ library: - path - unliftio -# tests: +tests: # spec: # main: Main.hs # source-dirs: tests # ghc-options: -threaded -rtsopts "-with-rtsopts=-N" # dependencies: # - github-app-token + readme: + main: README.lhs + ghc-options: -pgmL markdown-unlit + dependencies: + - aeson + - dotenv + - github-app-token + - http-conduit + - http-types + - lens + - lens-aeson + - markdown-unlit + - path + - text diff --git a/github-app-token/src/GitHub/App/Token.hs b/github-app-token/src/GitHub/App/Token.hs index 76c87b8..6c135ef 100644 --- a/github-app-token/src/GitHub/App/Token.hs +++ b/github-app-token/src/GitHub/App/Token.hs @@ -1,8 +1,13 @@ module GitHub.App.Token - ( AppCredentials (..) + ( generateInstallationToken + , AppCredentials (..) + , AppId (..) + , PrivateKey (..) + , readPrivateKey , InstallationId (..) - , generateInstallationToken + , AccessToken (..) ) where import GitHub.App.Token.AppCredentials import GitHub.App.Token.Generate +import GitHub.App.Token.JWT diff --git a/github-app-token/src/GitHub/App/Token/Generate.hs b/github-app-token/src/GitHub/App/Token/Generate.hs index a40c61c..20f4f06 100644 --- a/github-app-token/src/GitHub/App/Token/Generate.hs +++ b/github-app-token/src/GitHub/App/Token/Generate.hs @@ -24,7 +24,7 @@ import Network.HTTP.Simple , httpLBS , parseRequest ) -import Network.HTTP.Types.Header (hAccept, hAuthorization) +import Network.HTTP.Types.Header (hAccept, hAuthorization, hUserAgent) import Network.HTTP.Types.Status (Status, statusIsSuccessful) newtype InstallationId = InstallationId @@ -35,7 +35,7 @@ data AccessToken = AccessToken { token :: Text , expires_at :: UTCTime } - deriving stock (Generic) + deriving stock (Show, Generic) deriving anyclass (FromJSON) data AccessTokenHttpError = AccessTokenHttpError @@ -71,7 +71,8 @@ generateInstallationToken creds installationId = do resp <- httpLBS $ addRequestHeader hAccept "application/vnd.github+json" - $ addRequestHeader hAuthorization ("Bearer <> " <> jwt) + $ addRequestHeader hAuthorization ("Bearer " <> jwt) + $ addRequestHeader hUserAgent "github-app-token" $ addRequestHeader "X-GitHub-Api-Version" "2022-11-28" req let From 4d3b855de1a86e9eff919fc856e318aa328c2204 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 11:37:30 -0400 Subject: [PATCH 04/15] More packaging --- github-app-token/CHANGELOG.md | 1 + github-app-token/README.md | 1 + github-app-token/github-app-token.cabal | 8 ++++---- github-app-token/package.yaml | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 github-app-token/CHANGELOG.md create mode 120000 github-app-token/README.md diff --git a/github-app-token/CHANGELOG.md b/github-app-token/CHANGELOG.md new file mode 100644 index 0000000..4640904 --- /dev/null +++ b/github-app-token/CHANGELOG.md @@ -0,0 +1 @@ +# TODO diff --git a/github-app-token/README.md b/github-app-token/README.md new file mode 120000 index 0000000..5e04f79 --- /dev/null +++ b/github-app-token/README.md @@ -0,0 +1 @@ +./README.lhs \ No newline at end of file diff --git a/github-app-token/github-app-token.cabal b/github-app-token/github-app-token.cabal index e2018fc..535f9f1 100644 --- a/github-app-token/github-app-token.cabal +++ b/github-app-token/github-app-token.cabal @@ -6,11 +6,11 @@ cabal-version: 1.18 name: github-app-token version: 0.0.0.0 -synopsis: Library for generating GitHub App installation tokens +synopsis: Generate an installation access token for a GitHub App description: Please see README.md category: HTTP -homepage: https://github.com/freckle/freckle-app#readme -bug-reports: https://github.com/freckle/freckle-app/issues +homepage: https://github.com/freckle/github-app-token#readme +bug-reports: https://github.com/freckle/github-app-token/issues maintainer: Freckle Education build-type: Simple extra-doc-files: @@ -19,7 +19,7 @@ extra-doc-files: source-repository head type: git - location: https://github.com/freckle/freckle-app + location: https://github.com/freckle/github-app-token library exposed-modules: diff --git a/github-app-token/package.yaml b/github-app-token/package.yaml index 6545d8c..c0024a4 100644 --- a/github-app-token/package.yaml +++ b/github-app-token/package.yaml @@ -2,8 +2,8 @@ name: github-app-token version: 0.0.0.0 maintainer: Freckle Education category: HTTP -github: freckle/freckle-app -synopsis: Library for generating GitHub App installation tokens +github: freckle/github-app-token +synopsis: Generate an installation access token for a GitHub App description: Please see README.md extra-doc-files: From d1e19b1687385de6596bf762164cc99b3b93b1e4 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 11:38:52 -0400 Subject: [PATCH 05/15] Add CI --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cb729d4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + pull_request: + push: + branches: main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: generate + uses: freckle/stack-action/generate-matrix@v5 + outputs: + stack-yamls: ${{ steps.generate.outputs.stack-yamls }} + + test: + runs-on: ubuntu-latest + needs: generate + + strategy: + matrix: + stack-yaml: ${{ fromJSON(needs.generate.outputs.stack-yamls) }} + fail-fast: false + + steps: + - uses: actions/checkout@v4 + - uses: freckle/stack-action@v5 + env: + STACK_YAML: ${{ matrix.stack-yaml }} + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: haskell-actions/hlint-setup@v2 + - uses: haskell-actions/hlint-run@v2 + with: + fail-on: warning From 5499e97ee4195fe87901a0a1773b6de572e01eb3 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 11:49:19 -0400 Subject: [PATCH 06/15] Add CI secrets setup --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb729d4..a211f52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,9 +30,16 @@ jobs: steps: - uses: actions/checkout@v4 + - run: | + cat > github-app-token/key.pem <<'EOM' + ${{ secrets.FRECKLE_AUTOMATION_PRIVATE_KEY }} + EOM + - uses: freckle/stack-action@v5 env: STACK_YAML: ${{ matrix.stack-yaml }} + GITHUB_APP_ID: ${{ vars.FRECKLE_AUTOMATION_APP_ID }} + GITHUB_INSTALLATION_ID: ${{ vars.FRECKLE_AUTOMATION_INSTALLATION_ID }} lint: runs-on: ubuntu-latest From 55841697c4afbc19ba44e439363ef9f3a0265280 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 12:12:55 -0400 Subject: [PATCH 07/15] Make .env loading conditional --- github-app-token/README.lhs | 5 ++++- github-app-token/github-app-token.cabal | 1 + github-app-token/package.yaml | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/github-app-token/README.lhs b/github-app-token/README.lhs index 236e850..a581608 100644 --- a/github-app-token/README.lhs +++ b/github-app-token/README.lhs @@ -13,6 +13,8 @@ module Main (module Main) where import Configuration.Dotenv qualified as Dotenv +import Control.Monad (when) +import System.Directory (doesFileExist) import Text.Markdown.Unlit () ``` --> @@ -56,7 +58,8 @@ example = do ```haskell main :: IO () main = do - Dotenv.loadFile Dotenv.defaultConfig + isLocal <- doesFileExist ".env" + when isLocal $ Dotenv.loadFile Dotenv.defaultConfig example ``` --> diff --git a/github-app-token/github-app-token.cabal b/github-app-token/github-app-token.cabal index 535f9f1..08c9d5e 100644 --- a/github-app-token/github-app-token.cabal +++ b/github-app-token/github-app-token.cabal @@ -85,6 +85,7 @@ test-suite readme build-depends: aeson , base <5 + , directory , dotenv , github-app-token , http-conduit diff --git a/github-app-token/package.yaml b/github-app-token/package.yaml index c0024a4..8c4167f 100644 --- a/github-app-token/package.yaml +++ b/github-app-token/package.yaml @@ -76,6 +76,7 @@ tests: ghc-options: -pgmL markdown-unlit dependencies: - aeson + - directory - dotenv - github-app-token - http-conduit From 9a6497e848286445f98d6dfa1a91a722779c09b2 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 12:19:10 -0400 Subject: [PATCH 08/15] Configure Restyled --- .restyled.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .restyled.yaml diff --git a/.restyled.yaml b/.restyled.yaml new file mode 100644 index 0000000..9e208ea --- /dev/null +++ b/.restyled.yaml @@ -0,0 +1,4 @@ +restylers: + - "fourmolu" + - "!stylish-haskell" + - "*" From 8b3a7081e76a2ba4c727870877fc501b2a62b98d Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 12:19:57 -0400 Subject: [PATCH 09/15] Configure Fourmolu --- fourmolu.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 fourmolu.yaml diff --git a/fourmolu.yaml b/fourmolu.yaml new file mode 100644 index 0000000..ec1f505 --- /dev/null +++ b/fourmolu.yaml @@ -0,0 +1,17 @@ +indentation: 2 +column-limit: 80 # ignored until v12 / ghc-9.6 +function-arrows: leading +comma-style: leading # default +import-export-style: leading +indent-wheres: false # default +record-brace-space: true +newlines-between-decls: 1 # default +haddock-style: single-line +let-style: mixed +in-style: left-align +single-constraint-parens: never # ignored until v12 / ghc-9.6 +unicode: never # default +respectful: true # default +fixities: + - "infix 4 `stringEqual`" + - "infixl 1 &" From 9affe44fb353a3c49efe22101cb37cb2860fa3be Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 12:20:19 -0400 Subject: [PATCH 10/15] Tweak comment in package.yaml --- github-app-token/package.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/github-app-token/package.yaml b/github-app-token/package.yaml index 8c4167f..17a883a 100644 --- a/github-app-token/package.yaml +++ b/github-app-token/package.yaml @@ -65,12 +65,12 @@ library: - unliftio tests: -# spec: -# main: Main.hs -# source-dirs: tests -# ghc-options: -threaded -rtsopts "-with-rtsopts=-N" -# dependencies: -# - github-app-token + # spec: + # main: Main.hs + # source-dirs: tests + # ghc-options: -threaded -rtsopts "-with-rtsopts=-N" + # dependencies: + # - github-app-token readme: main: README.lhs ghc-options: -pgmL markdown-unlit From ee5d9848ad66f9d65aa382da465a63e8101fb455 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 12:22:34 -0400 Subject: [PATCH 11/15] fixup! Configure Fourmolu --- fourmolu.yaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/fourmolu.yaml b/fourmolu.yaml index ec1f505..3ad2fbd 100644 --- a/fourmolu.yaml +++ b/fourmolu.yaml @@ -1,5 +1,5 @@ indentation: 2 -column-limit: 80 # ignored until v12 / ghc-9.6 +column-limit: 80 function-arrows: leading comma-style: leading # default import-export-style: leading @@ -9,9 +9,7 @@ newlines-between-decls: 1 # default haddock-style: single-line let-style: mixed in-style: left-align -single-constraint-parens: never # ignored until v12 / ghc-9.6 +single-constraint-parens: never unicode: never # default respectful: true # default -fixities: - - "infix 4 `stringEqual`" - - "infixl 1 &" +fixities: [] # default From 4fac52cf00555449b63afeaaaa97dfbc6233da6c Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Wed, 11 Sep 2024 13:08:16 -0400 Subject: [PATCH 12/15] Update github-app-token/src/GitHub/App/Token/Generate.hs --- github-app-token/src/GitHub/App/Token/Generate.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github-app-token/src/GitHub/App/Token/Generate.hs b/github-app-token/src/GitHub/App/Token/Generate.hs index 20f4f06..143dcbe 100644 --- a/github-app-token/src/GitHub/App/Token/Generate.hs +++ b/github-app-token/src/GitHub/App/Token/Generate.hs @@ -85,7 +85,7 @@ generateInstallationToken creds installationId = do either (throwIO . AccessTokenJsonDecodeError body) pure $ eitherDecode body where - -- We're going to use it right away and only onces, so 5m should be more than + -- We're going to use it right away and only once, so 5m should be more than -- enough expiration = ExpirationTime $ 5 * 60 issuer = Issuer $ pack $ show creds.appId.unwrap From 287cfe56b94a81158712f3c2ac70be9a99a4dd5e Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 16:24:08 -0400 Subject: [PATCH 13/15] Simplify README.lhs --- .github/workflows/ci.yml | 6 +----- .gitignore | 3 --- github-app-token/.env.example | 9 ++++++--- github-app-token/README.lhs | 20 ++++++++------------ github-app-token/github-app-token.cabal | 4 +--- github-app-token/package.yaml | 2 -- 6 files changed, 16 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a211f52..f7c2a3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,15 +30,11 @@ jobs: steps: - uses: actions/checkout@v4 - - run: | - cat > github-app-token/key.pem <<'EOM' - ${{ secrets.FRECKLE_AUTOMATION_PRIVATE_KEY }} - EOM - - uses: freckle/stack-action@v5 env: STACK_YAML: ${{ matrix.stack-yaml }} GITHUB_APP_ID: ${{ vars.FRECKLE_AUTOMATION_APP_ID }} + GITHUB_PRIVATE_KEY: ${{ secrets.FRECKLE_AUTOMATION_PRIVATE_KEY }} GITHUB_INSTALLATION_ID: ${{ vars.FRECKLE_AUTOMATION_INSTALLATION_ID }} lint: diff --git a/.gitignore b/.gitignore index 27d43cc..5f4d542 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ .env* !.env.example - .stack-work - -key.pem diff --git a/github-app-token/.env.example b/github-app-token/.env.example index fc7e592..a478b1a 100644 --- a/github-app-token/.env.example +++ b/github-app-token/.env.example @@ -1,6 +1,9 @@ # shellcheck disable=SC2034 -# Fill out and place this file at .env to run the README.lhs test +# vim: ft=sh GITHUB_APP_ID= +GITHUB_PRIVATE_KEY=" +-----BEGIN RSA PRIVATE KEY----- +... +-----END RSA PRIVATE KEY----- +" GITHUB_INSTALLATION_ID= - -# Additionally, copy the private key to key.pem diff --git a/github-app-token/README.lhs b/github-app-token/README.lhs index a581608..a8e9c27 100644 --- a/github-app-token/README.lhs +++ b/github-app-token/README.lhs @@ -8,8 +8,6 @@