Skip to content

Commit

Permalink
feat!: using SecretValue for sensitive fields
Browse files Browse the repository at this point in the history
  • Loading branch information
Rafał Pawłaszek committed Nov 26, 2023
1 parent 4a71ab5 commit b1c09ff
Show file tree
Hide file tree
Showing 34 changed files with 279 additions and 303 deletions.
4 changes: 4 additions & 0 deletions .projenrc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { CdklabsConstructLibrary } from 'cdklabs-projen-project-types';
import { Stability } from 'projen/lib/cdk';

Expand Down
230 changes: 108 additions & 122 deletions API.md

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ For more information, see the [Amazon AppFlow User Guide](https://docs.aws.amazo
## Example

```ts
import { SecretValue } from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { ISecret } from 'aws-cdk-lib/aws-secretsmanager';
import {
Expand All @@ -30,8 +31,8 @@ import {
} from '@cdklabs/cdk-appflow';

declare const clientSecret: ISecret;
declare const accessToken: string;
declare const refreshToken: string;
declare const accessToken: SecretValue;
declare const refreshToken: SecretValue;
declare const instanceUrl: string;

const profile = new SalesforceConnectorProfile(this, 'MyConnectorProfile', {
Expand Down Expand Up @@ -188,7 +189,7 @@ It is *recommended* to follow [data protection mechanisms for Amazon AppFlow](ht

## Confidential information

Amazon AppFlow application integration is done using `ConnectionProfiles`. A `ConnectionProfile` requires providing sensitive information in the form of e.g. access and refresh tokens. It is *recommended* that such information is stored securely and passed to AWS CDK securely. All sensitive fields are effectively `IResolvable` and this means they can be resolved at deploy time. With that one should follow the [best practices for credentials with CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/security-best-practices.html#creds).
Amazon AppFlow application integration is done using `ConnectionProfiles`. A `ConnectionProfile` requires providing sensitive information in the form of e.g. access and refresh tokens. It is *recommended* that such information is stored securely and passed to AWS CDK securely. All sensitive fields are effectively `IResolvable` and this means they can be resolved at deploy time. With that one should follow the [best practices for credentials with CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/security-best-practices.html#creds). In this library, the sensitive fields are typed as `SecretValue` to emphasize these should not be plain strings.

An example of using a predefined AWS Secrets Manager secret for storing sensitive information can be found below:

Expand All @@ -204,9 +205,9 @@ const profile = new GoogleAnalytics4ConnectorProfile(stack, 'GA4Connector', {
oAuth: {
flow: {
refreshTokenGrant: {
refreshToken: secret.secretValueFromJson('refreshToken').toString(),
clientId: secret.secretValueFromJson('clientId').toString(),
clientSecret: secret.secretValueFromJson('clientSecret').toString(),
refreshToken: secret.secretValueFromJson('refreshToken'),
clientId: secret.secretValueFromJson('clientId'),
clientSecret: secret.secretValueFromJson('clientSecret'),
},
},
},
Expand Down
17 changes: 9 additions & 8 deletions src/googleanalytics4/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { SecretValue } from 'aws-cdk-lib';
import { CfnConnectorProfile } from 'aws-cdk-lib/aws-appflow';
import { Construct } from 'constructs';
import { GoogleAnalytics4ConnectorType } from './type';
Expand Down Expand Up @@ -35,17 +36,17 @@ export interface GoogleAnalytics4RefreshTokenGrantFlow {
/**
* A non-expired refresh token.
*/
readonly refreshToken?: string;
readonly refreshToken?: SecretValue;

/**
* The secret of the client app.
*/
readonly clientSecret?: string;
readonly clientSecret?: SecretValue;

/**
* The id of the client app.
*/
readonly clientId?: string;
readonly clientId?: SecretValue;
}

/**
Expand All @@ -67,7 +68,7 @@ export interface GoogleAnalytics4OAuthSettings {
*
* @default Retrieves a fresh accessToken with the information in the [flow property]{@link GoogleAnalytics4OAuthSettings#flow}
*/
readonly accessToken?: string;
readonly accessToken?: SecretValue;

/**
* The OAuth flow used for obtaining a new accessToken when the old is not present or expired.
Expand Down Expand Up @@ -124,10 +125,10 @@ export class GoogleAnalytics4ConnectorProfile extends ConnectorProfileBase {
customConnector: {
oauth2: {
// INFO: when using Refresh Token Grant Flow - access token property is required
accessToken: properties.oAuth.accessToken ?? 'dummyAccessToken',
refreshToken: properties.oAuth.flow?.refreshTokenGrant.refreshToken,
clientId: properties.oAuth.flow?.refreshTokenGrant.clientId,
clientSecret: properties.oAuth.flow?.refreshTokenGrant.clientSecret,
accessToken: properties.oAuth.accessToken?.unsafeUnwrap() ?? 'dummyAccessToken',
refreshToken: properties.oAuth.flow?.refreshTokenGrant.refreshToken?.unsafeUnwrap(),
clientId: properties.oAuth.flow?.refreshTokenGrant.clientId?.unsafeUnwrap(),
clientSecret: properties.oAuth.flow?.refreshTokenGrant.clientSecret?.unsafeUnwrap(),
},
authenticationType: ConnectorAuthenticationType.OAUTH2,
},
Expand Down
13 changes: 7 additions & 6 deletions src/marketo/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { SecretValue } from 'aws-cdk-lib';
import { CfnConnectorProfile } from 'aws-cdk-lib/aws-appflow';
import { Construct } from 'constructs';
import { MarketoConnectorType } from './type';
Expand All @@ -13,16 +14,16 @@ export interface MarketoConnectorProfileProps extends ConnectorProfileProps {
}

export interface MarketoOAuthClientCredentialsFlow {
readonly clientId: string;
readonly clientSecret: string;
readonly clientId: SecretValue;
readonly clientSecret: SecretValue;
}

export interface MarketoOAuthFlow {
readonly clientCredentials: MarketoOAuthClientCredentialsFlow;
}

export interface MarketoOAuthSettings {
readonly accessToken?: string;
readonly accessToken?: SecretValue;
readonly flow: MarketoOAuthFlow;
}

Expand Down Expand Up @@ -53,9 +54,9 @@ export class MarketoConnectorProfile extends ConnectorProfileBase {
const properties = (props as MarketoConnectorProfileProps);
return {
marketo: {
accessToken: properties.oAuth.accessToken,
clientId: properties.oAuth.flow.clientCredentials.clientId,
clientSecret: properties.oAuth.flow.clientCredentials.clientSecret,
accessToken: properties.oAuth.accessToken?.unsafeUnwrap(),
clientId: properties.oAuth.flow.clientCredentials.clientId.unsafeUnwrap(),
clientSecret: properties.oAuth.flow.clientCredentials.clientSecret.unsafeUnwrap(),
},
};
}
Expand Down
17 changes: 9 additions & 8 deletions src/microsoftdynamics365/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { SecretValue } from 'aws-cdk-lib';
import { CfnConnectorProfile } from 'aws-cdk-lib/aws-appflow';
import { Construct } from 'constructs';
import { MicrosoftDynamics365ConnectorType } from './type';
Expand All @@ -20,9 +21,9 @@ export interface MicrosoftDynamics365OAuthEndpointsSettings {
}

export interface MicrosoftDynamics365RefreshTokenGrantFlow {
readonly refreshToken?: string;
readonly clientSecret?: string;
readonly clientId?: string;
readonly refreshToken?: SecretValue;
readonly clientSecret?: SecretValue;
readonly clientId?: SecretValue;
}

export interface MicrosoftDynamics365OAuthFlow {
Expand All @@ -36,7 +37,7 @@ export interface MicrosoftDynamics365OAuthSettings {
*
* Note that if only the access token is provided AppFlow is not able to retrieve a fresh access token when the current one is expired
*/
readonly accessToken?: string;
readonly accessToken?: SecretValue;

readonly flow?: MicrosoftDynamics365OAuthFlow;

Expand Down Expand Up @@ -93,10 +94,10 @@ export class MicrosoftDynamics365ConnectorProfile extends ConnectorProfileBase {
customConnector: {
oauth2: {
// INFO: when using Refresh Token Grant Flow - access token property is required
accessToken: properties.oAuth.accessToken ?? 'dummyAccessToken',
refreshToken: properties.oAuth.flow?.refreshTokenGrant.refreshToken ?? 'dummyRefreshToken',
clientId: properties.oAuth.flow?.refreshTokenGrant.clientId,
clientSecret: properties.oAuth.flow?.refreshTokenGrant.clientSecret,
accessToken: properties.oAuth.accessToken?.unsafeUnwrap() ?? 'dummyAccessToken',
refreshToken: properties.oAuth.flow?.refreshTokenGrant.refreshToken?.unsafeUnwrap() ?? 'dummyRefreshToken',
clientId: properties.oAuth.flow?.refreshTokenGrant.clientId?.unsafeUnwrap(),
clientSecret: properties.oAuth.flow?.refreshTokenGrant.clientSecret?.unsafeUnwrap(),
},
authenticationType: ConnectorAuthenticationType.OAUTH2,
},
Expand Down
17 changes: 9 additions & 8 deletions src/microsoftsharepointonline/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { SecretValue } from 'aws-cdk-lib';
import { CfnConnectorProfile } from 'aws-cdk-lib/aws-appflow';
import { Construct } from 'constructs';
import { MicrosoftSharepointOnlineConnectorType } from './type';
Expand All @@ -19,9 +20,9 @@ export interface MicrosoftSharepointOnlineOAuthEndpointsSettings {
}

export interface MicrosoftSharepointOnlineRefreshTokenGrantFlow {
readonly refreshToken?: string;
readonly clientSecret?: string;
readonly clientId?: string;
readonly refreshToken?: SecretValue;
readonly clientSecret?: SecretValue;
readonly clientId?: SecretValue;
}

export interface MicrosoftSharepointOnlineOAuthFlow {
Expand All @@ -35,7 +36,7 @@ export interface MicrosoftSharepointOnlineOAuthSettings {
*
* Note that if only the access token is provided AppFlow is not able to retrieve a fresh access token when the current one is expired
*/
readonly accessToken?: string;
readonly accessToken?: SecretValue;

readonly flow?: MicrosoftSharepointOnlineOAuthFlow;

Expand Down Expand Up @@ -89,10 +90,10 @@ export class MicrosoftSharepointOnlineConnectorProfile extends ConnectorProfileB
customConnector: {
oauth2: {
// INFO: when using Refresh Token Grant Flow - access token property is required
accessToken: properties.oAuth.accessToken ?? 'dummyAccessToken',
refreshToken: properties.oAuth.flow?.refreshTokenGrant.refreshToken,
clientId: properties.oAuth.flow?.refreshTokenGrant.clientId,
clientSecret: properties.oAuth.flow?.refreshTokenGrant.clientSecret,
accessToken: properties.oAuth.accessToken?.unsafeUnwrap() ?? 'dummyAccessToken',
refreshToken: properties.oAuth.flow?.refreshTokenGrant.refreshToken?.unsafeUnwrap(),
clientId: properties.oAuth.flow?.refreshTokenGrant.clientId?.unsafeUnwrap(),
clientSecret: properties.oAuth.flow?.refreshTokenGrant.clientSecret?.unsafeUnwrap(),
},
authenticationType: ConnectorAuthenticationType.OAUTH2,
},
Expand Down
6 changes: 3 additions & 3 deletions src/redshift/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { ICluster } from '@aws-cdk/aws-redshift-alpha';
import { Aws } from 'aws-cdk-lib';
import { Aws, SecretValue } from 'aws-cdk-lib';
import { CfnConnectorProfile } from 'aws-cdk-lib/aws-appflow';
import { Effect, IRole, Policy, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from 'aws-cdk-lib/custom-resources';
Expand All @@ -15,7 +15,7 @@ import { ConnectorProfileBase, ConnectorProfileProps } from '../core/connectors/

export interface RedshiftConnectorBasicCredentials {
readonly username?: string;
readonly password?: string;
readonly password?: SecretValue;
}

export interface RedshiftConnectorProfileProps extends ConnectorProfileProps {
Expand Down Expand Up @@ -105,7 +105,7 @@ export class RedshiftConnectorProfile extends ConnectorProfileBase {
return {
redshift: properties && {
username: properties.basicAuth.username,
password: properties.basicAuth.password,
password: properties.basicAuth.password?.unsafeUnwrap(),
},
};
}
Expand Down
13 changes: 7 additions & 6 deletions src/salesforce-marketing-cloud/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { SecretValue } from 'aws-cdk-lib';
import { CfnConnectorProfile } from 'aws-cdk-lib/aws-appflow';
import { Construct } from 'constructs';
import { SalesforceMarketingCloudConnectorType } from './type';
Expand All @@ -19,16 +20,16 @@ export interface SalesforceMarketingCloudOAuthEndpoints {
}

export interface SalesforceMarketingCloudOAuthClientSettings {
readonly clientId: string;
readonly clientSecret: string;
readonly clientId: SecretValue;
readonly clientSecret: SecretValue;
}

export interface SalesforceMarketingCloudFlowSettings {
readonly clientCredentials: SalesforceMarketingCloudOAuthClientSettings;
}

export interface SalesforceMarketingCloudOAuthSettings {
readonly accessToken?: string;
readonly accessToken?: SecretValue;
readonly flow?: SalesforceMarketingCloudFlowSettings;
readonly endpoints: SalesforceMarketingCloudOAuthEndpoints;
}
Expand Down Expand Up @@ -71,9 +72,9 @@ export class SalesforceMarketingCloudConnectorProfile extends ConnectorProfileBa
return {
customConnector: {
oauth2: {
accessToken: properties.oAuth.accessToken,
clientId: properties.oAuth.flow?.clientCredentials.clientId,
clientSecret: properties.oAuth.flow?.clientCredentials.clientSecret,
accessToken: properties.oAuth.accessToken?.unsafeUnwrap(),
clientId: properties.oAuth.flow?.clientCredentials.clientId?.unsafeUnwrap(),
clientSecret: properties.oAuth.flow?.clientCredentials.clientSecret?.unsafeUnwrap(),
},
authenticationType: ConnectorAuthenticationType.OAUTH2,
},
Expand Down
39 changes: 7 additions & 32 deletions src/salesforce/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { SecretValue } from 'aws-cdk-lib';
import { CfnConnectorProfile } from 'aws-cdk-lib/aws-appflow';
import { ISecret } from 'aws-cdk-lib/aws-secretsmanager';
import { Construct } from 'constructs';
Expand All @@ -25,27 +26,20 @@ export interface SalesforceConnectorProfileProps extends ConnectorProfileProps {
// For now only 1) and 2) are implemented, but 3) is not possible due to the requirement of a user's creds

export interface SalesforceOAuthRefreshTokenGrantFlow {
readonly refreshToken?: string;
readonly refreshToken?: SecretValue;
readonly client?: ISecret;
}

export interface SalesforceOAuthFlow {

/**
* The parameters required for the refresh token grant OAuth flow
*
* @deprecated - this property will be removed in the future releases. Use refreshTokenGrant property instead.
*/
readonly refresTokenGrant?: SalesforceOAuthRefreshTokenGrantFlow;

/**
* The parameters required for the refresh token grant OAuth flow
*/
readonly refreshTokenGrant?: SalesforceOAuthRefreshTokenGrantFlow;
}

export interface SalesforceOAuthSettings {
readonly accessToken?: string;
readonly accessToken?: SecretValue;
readonly flow?: SalesforceOAuthFlow;
}

Expand All @@ -61,7 +55,7 @@ export class SalesforceConnectorProfile extends ConnectorProfileBase {

constructor(scope: Construct, id: string, props: SalesforceConnectorProfileProps) {
super(scope, id, props, SalesforceConnectorType.instance);
this.tryAddNodeDependency(this, this.getRefreshTokenGrantFlowProperty(props.oAuth.flow)?.client);
this.tryAddNodeDependency(this, props.oAuth.flow?.refreshTokenGrant?.client);
}

protected buildConnectorProfileProperties(properties: ConnectorProfileProps): CfnConnectorProfile.ConnectorProfilePropertiesProperty {
Expand All @@ -74,35 +68,16 @@ export class SalesforceConnectorProfile extends ConnectorProfileBase {
};
}

/**
* This is a auxiliary method for obtaining a refreshTokeNGrandFlow. It's a temporary solution due to the typo in the properties
* as we don't want to abruptly fail customer solutions depending on the library.
* @param flow a SalesforceOAuthFlow object
* @returns a SalesforceOAuthRefreshTokenGrantFlow object or undefined if the flow is undefined.
* @throws an error if both refreshTokenGrant and refresTokenGrant are specified. This is a temporary solution due to the typo in the properties.
* @deprecated - this method will be removed in the future releases.
*/
private getRefreshTokenGrantFlowProperty(flow?: SalesforceOAuthFlow): SalesforceOAuthRefreshTokenGrantFlow | undefined {

if (flow) {
if (flow.refresTokenGrant && flow.refreshTokenGrant) {
throw new Error('Only one of the properties refreshTokenGrant or refresTokenGrant should be specified');
}
return flow.refresTokenGrant ?? flow.refreshTokenGrant;
}
return undefined;
}

protected buildConnectorProfileCredentials(properties: ConnectorProfileProps): CfnConnectorProfile.ConnectorProfileCredentialsProperty {
const props = properties as SalesforceConnectorProfileProps;

let salesforce: { [key: string]: any } = {};


salesforce.accessToken = props.oAuth.accessToken;
salesforce.accessToken = props.oAuth.accessToken?.unsafeUnwrap();

const refreshTokenGrant = this.getRefreshTokenGrantFlowProperty(props.oAuth.flow);
salesforce.refreshToken = refreshTokenGrant?.refreshToken ?? 'dummyRefreshToken';
const refreshTokenGrant = props.oAuth.flow?.refreshTokenGrant;
salesforce.refreshToken = refreshTokenGrant?.refreshToken?.unsafeUnwrap() ?? 'dummyRefreshToken';

if (refreshTokenGrant?.client) {
salesforce.clientCredentialsArn = refreshTokenGrant.client.secretArn;
Expand Down
Loading

0 comments on commit b1c09ff

Please sign in to comment.