packages feed

github-actions-0.1.0.0: src/Language/Github/Actions/Permissions.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

-- |
-- Module      : Language.Github.Actions.Permissions
-- Description : GitHub Actions permissions and access control
-- Copyright   : (c) 2025 Bellroy Pty Ltd
-- License     : BSD-3-Clause
-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>
--
-- This module provides types for managing GitHub Actions permissions and access control.
-- Permissions control what GitHub APIs and resources workflows and jobs can access.
--
-- You can set permissions at the workflow level (affecting all jobs) or at individual
-- job levels. This follows the principle of least privilege by allowing you to grant
-- only the specific permissions needed.
--
-- For more information about GitHub Actions permissions, see:
-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#permissions>
module Language.Github.Actions.Permissions
  ( Permissions (..),
    PermissionType (..),
    Permission (..),
    gen,
  )
where

import Control.Monad.Fail.Hoist (hoistFail')
import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey)
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
import Data.Map (Map)
import Data.String.Interpolate (i)
import Data.Text (Text)
import GHC.Generics (Generic)
import Hedgehog (MonadGen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Language.Github.Actions.Internal (inverseMap)

-- | Types of permissions that can be granted to GitHub Actions workflows.
--
-- Each permission type corresponds to a specific area of GitHub functionality
-- that workflows might need to access.
--
-- For more details about each permission type, see: <https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token>
data PermissionType
  = -- | Manage GitHub Actions (e.g., cancel workflow runs)
    Actions
  | -- | Create and verify attestations
    Attestations
  | -- | Create and update check runs and suites
    Checks
  | -- | Read and write repository contents
    Contents
  | -- | Create and manage deployments
    Deployments
  | -- | Request OIDC JWT ID tokens
    IdToken
  | -- | Create and manage issues
    Issues
  | -- | Create and manage discussions
    Discussions
  | -- | Publish and manage packages
    Packages
  | -- | Deploy to GitHub Pages
    Pages
  | -- | Create and manage pull requests
    PullRequests
  | -- | Manage repository projects
    RepositoryProjects
  | -- | Read and write security events
    SecurityEvents
  | -- | Create commit status checks
    Statuses
  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)

instance FromJSON PermissionType where
  parseJSON =
    Aeson.withText "PermissionType" $
      hoistFail' . parsePermissionType

instance FromJSONKey PermissionType where
  fromJSONKey = Aeson.FromJSONKeyTextParser (Aeson.parseJSON . Aeson.String)

instance ToJSON PermissionType where
  toJSON = Aeson.String . renderPermissionType

instance ToJSONKey PermissionType where
  toJSONKey = Aeson.toJSONKeyText renderPermissionType

renderPermissionType :: PermissionType -> Text
renderPermissionType = \case
  Actions -> "actions"
  Attestations -> "attestations"
  Checks -> "checks"
  Contents -> "contents"
  Deployments -> "deployments"
  IdToken -> "id-token"
  Issues -> "issues"
  Discussions -> "discussions"
  Packages -> "packages"
  Pages -> "pages"
  PullRequests -> "pull-requests"
  RepositoryProjects -> "repository-projects"
  SecurityEvents -> "security-events"
  Statuses -> "statuses"

parsePermissionType :: Text -> Either String PermissionType
parsePermissionType t =
  maybe (Left [i|Unknown PermissionType: #{t}|]) Right $
    inverseMap renderPermissionType t

-- | Permission levels that can be granted for each permission type.
data Permission
  = -- | No access granted
    None
  | -- | Read-only access
    Read
  | -- | Read and write access
    Write
  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)

instance FromJSON Permission where
  parseJSON =
    Aeson.withText "Permission" $
      hoistFail' . parsePermission

instance ToJSON Permission where
  toJSON = Aeson.String . renderPermission

renderPermission :: Permission -> Text
renderPermission = \case
  None -> "none"
  Read -> "read"
  Write -> "write"

parsePermission :: Text -> Either String Permission
parsePermission t =
  maybe (Left [i|Unknown Permission: #{t}|]) Right $
    inverseMap renderPermission t

-- | Overall permissions configuration for a workflow or job.
--
-- Permissions can be set globally (affecting all permission types) or
-- individually for specific permission types.
--
-- Example usage:
--
-- @
-- import Language.Github.Actions.Permissions
--
-- -- Grant read access to everything
-- readOnlyPerms :: Permissions
-- readOnlyPerms = ReadAll
--
-- -- Grant specific permissions only
-- customPerms :: Permissions
-- customPerms = Custom $ Map.fromList
--  [ (Contents, Read)
--  , (PullRequests, Write)
--  ]
-- @
data Permissions
  = -- | No permissions granted (empty object)
    NoPermissions
  | -- | Read access to all permission types
    ReadAll
  | -- | Write access to all permission types
    WriteAll
  | -- | Custom permission mapping
    Custom (Map PermissionType Permission)
  deriving stock (Eq, Generic, Ord, Show)

instance FromJSON Permissions where
  parseJSON v = case v of
    Aeson.String "{}" -> pure NoPermissions
    Aeson.String "read-all" -> pure ReadAll
    Aeson.String "write-all" -> pure WriteAll
    Aeson.String s -> fail [i|Invalid Permissions: #{s}|]
    Aeson.Object o -> Custom <$> Aeson.parseJSON (Aeson.Object o)
    _ -> fail "Permissions must be a string or an object"

instance ToJSON Permissions where
  toJSON = \case
    NoPermissions -> Aeson.String "{}"
    ReadAll -> Aeson.String "read-all"
    WriteAll -> Aeson.String "write-all"
    Custom m -> Aeson.toJSON m

gen :: (MonadGen m) => m Permissions
gen =
  Gen.choice
    [ pure NoPermissions,
      pure ReadAll,
      pure WriteAll,
      fmap Custom . Gen.map (Range.linear 1 5) $
        liftA2 (,) Gen.enumBounded Gen.enumBounded
    ]