Skip to content

Commit

Permalink
[aws-secrets] Add env spec integration
Browse files Browse the repository at this point in the history
  • Loading branch information
mbj committed Oct 28, 2023
1 parent 39ca4d9 commit 3094998
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 28 deletions.
5 changes: 4 additions & 1 deletion aws-secrets/aws-secrets.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 1.12

-- This file has been generated from package.yaml by hpack version 0.35.2.
-- This file has been generated from package.yaml by hpack version 0.36.0.
--
-- see: https://github.com/sol/hpack

Expand Down Expand Up @@ -78,6 +78,7 @@ library
, optparse-applicative
, stack-deploy
, stratosphere
, stratosphere-iam
, stratosphere-secretsmanager
, text
, unliftio
Expand Down Expand Up @@ -142,6 +143,8 @@ test-suite test
, optparse-applicative
, stack-deploy
, stratosphere
, stratosphere-iam
, stratosphere-lambda
, stratosphere-secretsmanager
, tasty
, tasty-hunit
Expand Down
2 changes: 2 additions & 0 deletions aws-secrets/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies:
- optparse-applicative
- stack-deploy
- stratosphere
- stratosphere-iam
- stratosphere-secretsmanager
- text
- unliftio
Expand All @@ -31,5 +32,6 @@ tests:
- aws-secrets
- devtools
- mio-log
- stratosphere-lambda
- tasty
- tasty-hunit
83 changes: 57 additions & 26 deletions aws-secrets/src/AWS/Secrets.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ module AWS.Secrets
, IsSecret(..)
, SecretConfig(..)
, arnValue
, envSpecEntries
, externalTemplate
, fetchStackSecretArn
, getSecretValuePolicy
, internalComponent
, rdsGeneratePostgresqlPassword
, readEnvSecretValue
, readStackSecretValue
, secretNameValue
, secrets
Expand All @@ -20,14 +23,19 @@ import AWS.Secrets.Prelude
import qualified Amazonka
import qualified Amazonka.CloudFormation.Types as CF
import qualified Amazonka.SecretsManager.GetSecretValue as SecretsManager
import qualified Data.Aeson as JSON
import qualified Data.List as List
import qualified Data.Text as Text
import qualified MIO.Amazonka as AWS
import qualified MIO.Log as Log
import qualified StackDeploy.Component as StackDeploy
import qualified StackDeploy.EnvSpec
import qualified StackDeploy.Template as StackDeploy
import qualified StackDeploy.Utils
import qualified Stratosphere as CFT
import qualified Stratosphere.IAM.Role as IAM.Role
import qualified Stratosphere.SecretsManager.Secret as SecretsManager
import qualified UnliftIO.Environment as Environment
import qualified UnliftIO.Exception as UnliftIO

type Env env = (AWS.Env env, Log.Env env)
Expand All @@ -48,23 +56,17 @@ externalSecretsStackParameter = CFT.mkParameter "ExternalSecretsStack" "String"
internalComponent :: forall a . IsSecret a => StackDeploy.Component
internalComponent = mempty
{ StackDeploy.outputs = CFT.Outputs $ internalOutputs <> externalOutputs
, StackDeploy.resources = CFT.Resources $ mkResource <$> internalSecrets
, StackDeploy.resources = CFT.Resources $ mkSecretResource <$> internalSecrets
, StackDeploy.parameters = CFT.Parameters effectiveParameters
}
where
internalOutputs = mkInternalOutput <$> internalSecrets
externalOutputs = mkExternalOutput <$> externalSecrets @a
internalOutputs = mkInternalOutput <$> internalSecrets
externalOutputs = mkInternalExternalOutput <$> externalSecrets

effectiveParameters =
[externalSecretsStackParameter | not $ List.null externalOutputs]

mkExternalOutput :: a -> CFT.Output
mkExternalOutput secret
= CFT.mkOutput (arnOutputName secret) (arnExternalValue secret)

internalSecrets = filterSecrets @a $ \case
Internal{} -> True
External{} -> False
(externalSecrets, internalSecrets) = partitionSecrets @a

externalTemplate
:: forall a . IsSecret a
Expand All @@ -77,7 +79,7 @@ externalTemplate =
where
stratosphere :: CFT.Template
stratosphere =
(CFT.mkTemplate . CFT.Resources $ mkResource <$> externalSecrets @a)
(CFT.mkTemplate . CFT.Resources $ mkSecretResource <$> externalSecrets)
{ CFT.outputs = pure . CFT.Outputs $ mkExternalOutput <$> externalSecrets }

mkExternalOutput :: a -> CFT.Output
Expand All @@ -88,18 +90,21 @@ externalTemplate =
$ mkOutputExportNameValue CFT.awsStackName secret
}

externalSecrets :: forall a . IsSecret a => [a]
externalSecrets = filterSecrets @a $ \case
Internal{} -> False
External{} -> True
(externalSecrets, _internalSecrets) = partitionSecrets @a

filterSecrets :: forall a . IsSecret a => (SecretConfig -> Bool) -> [a]
filterSecrets filter = List.filter (filter . secretConfig) $ secrets @a
mkInternalExternalOutput :: IsSecret a => a -> CFT.Output
mkInternalExternalOutput secret
= CFT.mkOutput (arnOutputName secret) (arnExternalValue secret)

mkInternalOutput :: IsSecret a => a -> CFT.Output
mkInternalOutput secret = CFT.mkOutput (arnOutputName secret) (arnInternalValue secret)

extractSecretsManagerSecret :: SecretConfig -> SecretsManager.Secret
extractSecretsManagerSecret = \case
(Internal secret) -> secret
(External secret) -> secret
partitionSecrets :: forall a . IsSecret a => ([a], [a])
partitionSecrets = List.partition (filter . secretConfig) $ secrets @a
where
filter = \case
Internal{} -> False
External{} -> True

logicalResourceName :: IsSecret a => a -> Text
logicalResourceName = (<> "Secret") . convert . show
Expand All @@ -122,14 +127,15 @@ mkOutputExportNameValue :: IsSecret a => CFT.Value Text -> a -> CFT.Value Text
mkOutputExportNameValue stackName secret
= CFT.Join "-" [stackName, CFT.Literal $ arnOutputName secret]

mkResource :: IsSecret a => a -> CFT.Resource
mkResource secret
mkSecretResource :: IsSecret a => a -> CFT.Resource
mkSecretResource secret
= CFT.resource (logicalResourceName secret)
. extractSecretsManagerSecret
$ secretConfig secret

mkInternalOutput :: IsSecret a => a -> CFT.Output
mkInternalOutput secret = CFT.mkOutput (arnOutputName secret) (arnInternalValue secret)
where
extractSecretsManagerSecret = \case
(Internal value) -> value
(External value) -> value

secretNameValue :: Text -> CFT.Value Text -> CFT.Value Text
secretNameValue namespace name
Expand All @@ -146,6 +152,9 @@ fetchStackSecretArn stack
readStackSecretValue :: Env env => IsSecret a => CF.Stack -> a -> MIO env Text
readStackSecretValue stack secret = readSecretsManagerSecret =<< fetchStackSecretArn stack secret

readEnvSecretValue :: (Env env, IsSecret a) => a -> MIO env Text
readEnvSecretValue = readSecretsManagerSecret . convert <=< Environment.getEnv . convert . envArnName

readSecretsManagerSecret :: Env env => Text -> MIO env Text
readSecretsManagerSecret arn = do
Log.info $ "Reading secret: " <> arn
Expand All @@ -163,3 +172,25 @@ rdsGeneratePostgresqlPassword
= SecretsManager.mkGenerateSecretStringProperty
& CFT.set @"ExcludeCharacters" "/\"@"
& CFT.set @"PasswordLength" (CFT.Literal 48)
envArnName :: IsSecret a => a -> Text
envArnName = (<> "_ARN") . Text.toUpper . convert . JSON.camelTo2 '_' . show
envSpecEntries :: IsSecret a => [a] -> [StackDeploy.EnvSpec.Entry]
envSpecEntries = fmap $ \secret ->
StackDeploy.EnvSpec.Entry
{ envName = envArnName secret
, envValue = StackDeploy.EnvSpec.StackOutput $ case secretConfig secret of
Internal{} -> mkInternalOutput secret
External{} -> mkInternalExternalOutput secret
}
getSecretValuePolicy :: IsSecret a => [a] -> IAM.Role.PolicyProperty
getSecretValuePolicy values = IAM.Role.mkPolicyProperty [("Statement", statement)] "allow-secrets"
where
statement :: JSON.Value
statement = JSON.Object
[ ("Action", "secretsmanager:GetSecretValue")
, ("Effect", "Allow")
, ("Resource", JSON.toJSON $ arnValue <$> values)
]
28 changes: 27 additions & 1 deletion aws-secrets/test/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import AWS.Secrets.Prelude

import qualified Devtools
import qualified StackDeploy.Component as StackDeploy
import qualified StackDeploy.EnvSpec as StackDeploy
import qualified StackDeploy.Template as StackDeploy
import qualified StackDeploy.Utils as StackDeploy
import qualified Stratosphere as CFT
import qualified Stratosphere.IAM.Role as IAM
import qualified Stratosphere.Lambda.Function as Lambda
import qualified Stratosphere.SecretsManager.Secret as SecretsManager
import qualified Test.Tasty as Tasty

Expand Down Expand Up @@ -50,8 +54,30 @@ main =
where
internalTemplate
= StackDeploy.mkTemplate (fromType @"secrets-internal")
[internalComponent @TestSecret]
[ internalComponent @TestSecret
, lambdaComponent
]

internalTemplateNoExternal
= StackDeploy.mkTemplate (fromType @"secrets-internal-no-external")
[internalComponent @TestSecretNoExternal]

lambdaComponent = mempty
{ StackDeploy.resources = [lambdaFunction, lambdaRole] }
where
lambdaFunction
= CFT.resource "TestLambdaFunction"
$ Lambda.mkFunction lambdaFunctionCode (StackDeploy.getAttArn lambdaRole)
& CFT.set @"Environment"
(StackDeploy.lambdaEnvironment $ envSpecEntries [TestExternal, TestInternal])

lambdaRole
= CFT.resource "TestLambdaRole"
$ IAM.mkRole (StackDeploy.assumeRole "lambda.amazonaws.com")
& CFT.set @"Policies" [getSecretValuePolicy [TestExternal, TestInternal]]

lambdaFunctionCode :: Lambda.CodeProperty
lambdaFunctionCode
= Lambda.mkCodeProperty
& CFT.set @"S3Bucket" "test-bucket"
& CFT.set @"S3Key" "test-key"
79 changes: 79 additions & 0 deletions aws-secrets/test/template/secrets-internal.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,85 @@
}
},
"Type": "AWS::SecretsManager::Secret"
},
"TestLambdaFunction": {
"Properties": {
"Code": {
"S3Bucket": "test-bucket",
"S3Key": "test-key"
},
"Environment": {
"Variables": {
"TEST_EXTERNAL_ARN": {
"Fn::ImportValue": {
"Fn::Join": [
"-",
[
{
"Ref": "ExternalSecretsStack"
},
"TestExternalSecretArn"
]
]
}
},
"TEST_INTERNAL_ARN": {
"Ref": "TestInternalSecret"
}
}
},
"Role": {
"Fn::GetAtt": [
"TestLambdaRole",
"Arn"
]
}
},
"Type": "AWS::Lambda::Function"
},
"TestLambdaRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": {
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
},
"Version": "2012-10-17"
},
"Policies": [
{
"PolicyDocument": {
"Statement": {
"Action": "secretsmanager:GetSecretValue",
"Effect": "Allow",
"Resource": [
{
"Fn::ImportValue": {
"Fn::Join": [
"-",
[
{
"Ref": "ExternalSecretsStack"
},
"TestExternalSecretArn"
]
]
}
},
{
"Ref": "TestInternalSecret"
}
]
}
},
"PolicyName": "allow-secrets"
}
]
},
"Type": "AWS::IAM::Role"
}
}
}

0 comments on commit 3094998

Please sign in to comment.