secretspec 0.16.0 → 0.17.0
raw patch · 3 files changed
+57/−8 lines, 3 files
Files
- secretspec.cabal +1/−1
- src/SecretSpec.hs +20/−7
- test/Main.hs +36/−0
secretspec.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: secretspec-version: 0.16.0+version: 0.17.0 synopsis: Haskell SDK for SecretSpec, a declarative secrets manager description: A thin client over the @secretspec-ffi@ C ABI (linked at build time).
src/SecretSpec.hs view
@@ -23,6 +23,7 @@ , withPath , withProvider , withProfile+ , withScope , withReason , withNoValues -- * Resolve (value-carrying)@@ -125,6 +126,8 @@ data Resolved = Resolved { resolvedProvider :: Text , resolvedProfile :: Text+ -- | Selected manifest scope, or 'Nothing' for a full-profile resolve (0.17+).+ , resolvedScope :: Maybe Text , resolvedSecrets :: Map Text ResolvedSecret , resolvedMissingOptional :: [Text] } deriving (Show, Eq)@@ -157,6 +160,8 @@ data Report = Report { reportProvider :: Text , reportProfile :: Text+ -- | Selected manifest scope, or 'Nothing' for a full-profile report (0.17+).+ , reportScope :: Maybe Text , reportSecrets :: [SecretReport] } deriving (Show, Eq) @@ -166,13 +171,14 @@ { bPath :: Maybe Text , bProvider :: Maybe Text , bProfile :: Maybe Text+ , bScope :: Maybe Text , bReason :: Maybe Text , bNoValues :: Bool } -- | A builder with no options set. builder :: Builder-builder = Builder Nothing Nothing Nothing Nothing False+builder = Builder Nothing Nothing Nothing Nothing Nothing False -- | Resolve from a manifest at this path instead of walking up from the working -- directory.@@ -187,6 +193,10 @@ withProfile :: Text -> Builder -> Builder withProfile v b = b { bProfile = Just v } +-- | Limit resolution to a named manifest scope (SecretSpec 0.17+).+withScope :: Text -> Builder -> Builder+withScope v b = b { bScope = Just v }+ -- | Set a human-readable reason for this access (for audited providers). withReason :: Text -> Builder -> Builder withReason v b = b { bReason = Just v }@@ -249,15 +259,16 @@ load b = do resp <- callNative (requestBytes b Nothing) value <- responseValue resp resolveSchemaVersion "resolve"- (prov, prof, secs, mreq, mopt) <- fromResult (parseEither pResolve value)+ (prov, prof, scope, secs, mreq, mopt) <- fromResult (parseEither pResolve value) case mreq of- [] -> pure (Resolved prov prof secs mopt)+ [] -> pure (Resolved prov prof scope secs mopt) xs -> throwIO (MissingRequiredError xs) where pResolve = withObject "response" $ \o ->- (,,,,)+ (,,,,,) <$> o .: "provider" <*> o .: "profile"+ <*> o .:? "scope" <*> o .:? "secrets" .!= Map.empty <*> o .:? "missing_required" .!= [] <*> o .:? "missing_optional" .!= []@@ -270,13 +281,14 @@ report b = do resp <- callNative (requestBytes b (Just "report")) value <- responseValue resp reportSchemaVersion "report"- (prov, prof, secs) <- fromResult (parseEither pReport value)- pure (Report prov prof secs)+ (prov, prof, scope, secs) <- fromResult (parseEither pReport value)+ pure (Report prov prof scope secs) where pReport = withObject "response" $ \o ->- (,,)+ (,,,) <$> o .: "provider" <*> o .: "profile"+ <*> o .:? "scope" <*> o .:? "secrets" .!= [] -- Build the request JSON for a resolve (@mode = Nothing@) or report@@ -288,6 +300,7 @@ [ ("path" .=) <$> bPath b , ("provider" .=) <$> bProvider b , ("profile" .=) <$> bProfile b+ , ("scope" .=) <$> bScope b , ("reason" .=) <$> bReason b ] ++ ["no_values" .= True | bNoValues b]
test/Main.hs view
@@ -41,6 +41,7 @@ let tests = [ ("abi_version_nonempty", testAbiVersion) , ("missing_required_throws", testMissingRequired)+ , ("scoped_resolution", testScope) , ("codegen", testCodegen) ] ++ concatMap conformanceTests fixtures@@ -92,6 +93,41 @@ case r of Left _ -> pure () Right _ -> ioError (userError "expected MissingRequiredError")++testScope :: IO ()+testScope = do+ tmp <- getTemporaryDirectory+ let dir = tmp </> "secretspec-hs-scope"+ createDirectoryIfMissing True dir+ writeFile (dir </> "secretspec.toml") $+ unlines+ [ "[project]"+ , "name = \"hs-scope\""+ , "revision = \"1.0\""+ , ""+ , "[profiles.default]"+ , "DATABASE_URL = { description = \"DB\", required = true }"+ , "SENTRY_DSN = { description = \"Sentry\", required = false }"+ , ""+ , "[scopes.database]"+ , "secrets = [\"DATABASE_URL\"]"+ ]+ writeFile+ (dir </> ".env")+ "DATABASE_URL=postgres://db\nSENTRY_DSN=https://sentry\n"+ let scoped = fixtureBuilder dir & S.withScope "database"++ resolved <- S.load scoped+ expect (S.resolvedScope resolved == Just "database") "resolve scope was not returned"+ expect+ (Map.keys (S.resolvedSecrets resolved) == ["DATABASE_URL"])+ "resolve exposed an out-of-scope secret"++ rep <- S.report scoped+ expect (S.reportScope rep == Just "database") "report scope was not returned"+ expect+ (map S.srName (S.reportSecrets rep) == ["DATABASE_URL"])+ "report exposed an out-of-scope secret" -- End-to-end codegen: secretspec schema -> quicktype --lang haskell -> compile -- the generated module and decode the SDK's own fieldsJson output with it, so