Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/Garnix.hs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ withEnv testFeatures buildLogsDir buildLogsReportingPort action = do
nixConfig <-
lookupOptionalSecret "GITHUB_ACCESS_TOKEN" (secretFile "github_access_token")
<&> maybe defaultNixConfig (\token -> githubAccessTokenNixConfig (GhToken token) <> defaultNixConfig)
allowedBuildOwners <- parseAllowedOwners . fmap cs <$> lookupEnv "GARNIX_ALLOWED_OWNERS"
s3CacheEnabled <-
lookupEnv "S3_CACHE_ENABLED" <&> \case
Just v | T.toLower (cs v) == "false" -> False
Expand Down Expand Up @@ -464,6 +465,7 @@ withEnv testFeatures buildLogsDir buildLogsReportingPort action = do
githubClientSecret = ghClientSecret,
githubClientId = ghClientId,
adminGithubLogin = adminGhLogin,
allowedBuildOwners = allowedBuildOwners,
buildLogsReportingPort = buildLogsReportingPort,
workingDir = curDir,
nixXdgCacheDir = Nothing,
Expand Down
18 changes: 18 additions & 0 deletions backend/src/Garnix/Monad.hs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ data Env = Env
githubClientSecret :: Text,
githubClientId :: Text,
adminGithubLogin :: Maybe GhLogin,
-- | Self-hosted owner allowlist. Nothing = allow all (upstream default);
-- Just os = only these GitHub owners may build (GARNIX_ALLOWED_OWNERS).
allowedBuildOwners :: Maybe [GhLogin],
buildLogsReportingPort :: Maybe Int,
githubInterface :: GithubInterface,
-- | A thread-safe version of `CWD`
Expand Down Expand Up @@ -852,3 +855,18 @@ safeGetAbsoluteFlakeDir (FlakeDir flakeDir) = do
then pure result
else do
throw $ OtherError $ "'" <> cs flakeDir <> "' is not a path within the repo"

-- | The @GARNIX_ALLOWED_OWNERS@ allowlist: a comma-separated list of GitHub
-- owners. 'Nothing' — unset, empty, or only separators — keeps upstream's
-- allow-all. Logins are normalized, since what an operator types here is not
-- necessarily the casing GitHub sends.
parseAllowedOwners :: Maybe Text -> Maybe [GhLogin]
parseAllowedOwners raw =
case maybe [] (filter (not . T.null) . map T.strip . T.splitOn ",") raw of
[] -> Nothing
owners -> Just $ map (normalizeGhLogin . GhLogin) owners

-- | 'Nothing' means no allowlist is configured, so every owner may build.
isAllowedOwner :: Maybe [GhLogin] -> GhRepoOwner -> Bool
isAllowedOwner Nothing _ = True
isAllowedOwner (Just owners) owner = normalizeGhLogin (getGhRepoOwner owner) `elem` owners
4 changes: 4 additions & 0 deletions backend/src/Garnix/Orchestrator.hs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ handleRerun ev = do

assertIsAllowedToBuild :: GhRepoOwner -> GhRepoName -> M ()
assertIsAllowedToBuild owner repo = do
-- Self-hosted owner allowlist, checked before the denylist: when
-- GARNIX_ALLOWED_OWNERS is set only those owners may build.
allowed <- view #allowedBuildOwners
unless (isAllowedOwner allowed owner) $ throw IsDeniedAccess
isDenied <- DB.isDenylisted owner repo
when isDenied $ do
throw IsDeniedAccess
6 changes: 6 additions & 0 deletions backend/src/Garnix/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,12 @@ newtype GhLogin = GhLogin {getGhLogin :: Text}
IsString
)

-- | GitHub logins are case-insensitive, but 'GhLogin' compares as plain 'Text'.
-- Fold both sides through this before matching a login against operator-supplied
-- configuration, where the casing is whatever someone happened to type.
normalizeGhLogin :: GhLogin -> GhLogin
normalizeGhLogin = GhLogin . T.toLower . getGhLogin

newtype GhRepoName = GhRepoName {getGhRepoName :: Text}
deriving stock (Eq, Show, Generic)
deriving newtype
Expand Down
30 changes: 30 additions & 0 deletions backend/test/spec/Garnix/MonadSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,36 @@ import Test.Hspec.QuickCheck

spec :: Spec
spec = around_ silence $ do
describe "the build owner allowlist" $ do
describe "parseAllowedOwners" $ do
it "treats an unset, empty or separator-only variable as no allowlist" $ do
parseAllowedOwners Nothing `shouldBe` Nothing
parseAllowedOwners (Just "") `shouldBe` Nothing
parseAllowedOwners (Just " , ") `shouldBe` Nothing

it "splits on commas and trims each entry"
$ parseAllowedOwners (Just "one, two ,three")
`shouldBe` Just ["one", "two", "three"]

-- GitHub logins are case-insensitive, but what an operator types into the
-- deployment is not necessarily the casing GitHub sends on the webhook.
it "normalizes the casing an operator happened to type"
$ parseAllowedOwners (Just "KraDalby")
`shouldBe` Just ["kradalby"]

describe "isAllowedOwner" $ do
it "allows every owner when no allowlist is configured"
$ isAllowedOwner Nothing "anyone"
`shouldBe` True

it "allows a listed owner and denies an unlisted one" $ do
isAllowedOwner (parseAllowedOwners (Just "kradalby,myorg")) "myorg" `shouldBe` True
isAllowedOwner (parseAllowedOwners (Just "kradalby,myorg")) "someone-else" `shouldBe` False

it "matches regardless of the casing on either side" $ do
isAllowedOwner (parseAllowedOwners (Just "KraDalby")) "kradalby" `shouldBe` True
isAllowedOwner (parseAllowedOwners (Just "kradalby")) "KraDalby" `shouldBe` True

describe "<?>" $ do
modifyMaxSuccess (const 5) $ do
prop "doesn't change the result of a success" $ \(value :: Int) -> runTestM $ do
Expand Down
1 change: 1 addition & 0 deletions backend/test/spec/Garnix/TestHelpers/Monad.hs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ withTestEnvironment tempDir action = do
githubClientSecret = "github-client-secret",
githubClientId = "github-client-id",
adminGithubLogin = Nothing,
allowedBuildOwners = Nothing,
buildLogsReportingPort = Nothing,
workingDir = tempDir,
nixXdgCacheDir = Nothing,
Expand Down