diff --git a/src/AzureIoTHub.Portal.Domain/ConfigHandler.cs b/src/AzureIoTHub.Portal.Domain/ConfigHandler.cs index 97724719b..db77c3df1 100644 --- a/src/AzureIoTHub.Portal.Domain/ConfigHandler.cs +++ b/src/AzureIoTHub.Portal.Domain/ConfigHandler.cs @@ -81,5 +81,6 @@ public abstract class ConfigHandler public abstract string AWSS3StorageConnectionString { get; } public abstract string AWSBucketName { get; } public abstract string AWSAccountId { get; } + public abstract IEnumerable AWSGreengrassRequiredRoles { get; } } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs b/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs index a87743b92..8500ca865 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs @@ -58,6 +58,7 @@ internal abstract class ConfigHandlerBase : ConfigHandler internal const string AWSS3StorageConnectionStringKey = "AWS:S3Storage:ConnectionString"; internal const string AWSBucketNameKey = "AWS:BucketName"; internal const string AWSAccountIdKey = "AWS:AccountId"; + internal const string AWSGreengrassRequiredRolesKey = "AWS:GreengrassRequiredRoles"; } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs b/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs index 0360bff0c..e859e55cd 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs @@ -3,6 +3,7 @@ namespace AzureIoTHub.Portal.Infrastructure { + using System.Collections.Generic; using AzureIoTHub.Portal.Domain.Shared.Constants; using Microsoft.Extensions.Configuration; @@ -90,5 +91,6 @@ internal DevelopmentConfigHandler(IConfiguration config) public override string AWSS3StorageConnectionString => this.config[AWSS3StorageConnectionStringKey]!; public override string AWSBucketName => this.config[AWSBucketNameKey]!; public override string AWSAccountId => this.config[AWSAccountIdKey]!; + public override IEnumerable AWSGreengrassRequiredRoles => this.config.GetSection(AWSGreengrassRequiredRolesKey).Get()!; } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs b/src/AzureIoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs index 02966e67e..7a9baeb77 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs @@ -90,6 +90,8 @@ internal ProductionAWSConfigHandler(IConfiguration config) public override string AWSS3StorageConnectionString => this.config[AWSS3StorageConnectionStringKey]!; public override string AWSBucketName => this.config[AWSBucketNameKey]!; public override string AWSAccountId => this.config[AWSAccountIdKey]!; + public override IEnumerable AWSGreengrassRequiredRoles => this.config.GetSection(AWSGreengrassRequiredRolesKey).Get()!; + } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs b/src/AzureIoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs index 052d7670a..559217447 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs @@ -3,6 +3,7 @@ namespace AzureIoTHub.Portal.Infrastructure { + using System.Collections.Generic; using AzureIoTHub.Portal.Domain.Shared.Constants; using Microsoft.Extensions.Configuration; @@ -94,5 +95,6 @@ internal ProductionAzureConfigHandler(IConfiguration config) public override string AWSBucketName => throw new NotImplementedException(); public override string AWSAccountId => throw new NotImplementedException(); + public override IEnumerable AWSGreengrassRequiredRoles => throw new NotImplementedException(); } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/Services/AwsExternalDeviceService.cs b/src/AzureIoTHub.Portal.Infrastructure/Services/AwsExternalDeviceService.cs index 314a30e7e..fc81ea169 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Services/AwsExternalDeviceService.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Services/AwsExternalDeviceService.cs @@ -208,17 +208,14 @@ public async Task GetEdgeDeviceCredentials(IoTEdgeDevice devi { var createCertificateTuple = await GenerateCertificate(device.DeviceName); - _ = await this.amazonIoTClient.AttachPolicyAsync(new AttachPolicyRequest + foreach (var item in this.configHandler.AWSGreengrassRequiredRoles) { - PolicyName = "GreengrassV2IoTThingPolicy", - Target = createCertificateTuple.Item2 - }); - - _ = await this.amazonIoTClient.AttachPolicyAsync(new AttachPolicyRequest - { - PolicyName = "GreengrassCoreTokenExchangeRoleAliasPolicy", - Target = createCertificateTuple.Item2 - }); + _ = await this.amazonIoTClient.AttachPolicyAsync(new AttachPolicyRequest + { + PolicyName = item, + Target = createCertificateTuple.Item2 + }); + } return createCertificateTuple.Item1; } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs index dbfea399f..f41047551 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs @@ -54,6 +54,7 @@ private DevelopmentConfigHandler CreateDevelopmentConfigHandler() [TestCase(ConfigHandlerBase.AWSS3StorageConnectionStringKey, nameof(ConfigHandlerBase.AWSS3StorageConnectionString))] [TestCase(ConfigHandlerBase.CloudProviderKey, nameof(ConfigHandlerBase.CloudProvider))] [TestCase(ConfigHandlerBase.AWSBucketNameKey, nameof(ConfigHandlerBase.AWSBucketName))] + [TestCase(ConfigHandlerBase.AWSAccountIdKey, nameof(ConfigHandlerBase.AWSAccountId))] public void SettingsShouldGetValueFromAppSettings(string configKey, string configPropertyName) { // Arrange @@ -74,6 +75,36 @@ public void SettingsShouldGetValueFromAppSettings(string configKey, string confi this.mockRepository.VerifyAll(); } + [TestCase(ConfigHandlerBase.AWSGreengrassRequiredRolesKey, nameof(ConfigHandlerBase.AWSGreengrassRequiredRoles))] + public void SettingsShouldGetSectionFromAppSettings(string configKey, string configPropertyName) + { + // Arrange + var mockSection = this.mockRepository.Create(); + + _ = mockSection.SetupGet(c => c.Value) + .Returns(Guid.NewGuid().ToString()); + + _ = mockSection.SetupGet(c => c.Path) + .Returns(configKey); + + _ = mockSection.Setup(c => c.GetChildren()) + .Returns(Array.Empty()); + + var configHandler = CreateDevelopmentConfigHandler(); + + _ = this.mockConfiguration.Setup(c => c.GetSection(It.Is(x => x == configKey))) + .Returns(mockSection.Object); + + // Act + var result = configHandler + .GetType() + .GetProperty(configPropertyName) + .GetValue(configHandler, null); + + // Assert + this.mockRepository.VerifyAll(); + } + [TestCase(ConfigHandlerBase.OIDCValidateAudienceKey, nameof(ConfigHandlerBase.OIDCValidateAudience))] [TestCase(ConfigHandlerBase.OIDCValidateIssuerKey, nameof(ConfigHandlerBase.OIDCValidateIssuer))] [TestCase(ConfigHandlerBase.OIDCValidateIssuerSigningKeyKey, nameof(ConfigHandlerBase.OIDCValidateIssuerSigningKey))] diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs index 26cbcafc5..092fb507c 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs @@ -54,6 +54,7 @@ private ProductionAWSConfigHandler CreateProductionAWSConfigHandler() [TestCase(ConfigHandlerBase.AWSS3StorageConnectionStringKey, nameof(ConfigHandlerBase.AWSS3StorageConnectionString))] [TestCase(ConfigHandlerBase.CloudProviderKey, nameof(ConfigHandlerBase.CloudProvider))] [TestCase(ConfigHandlerBase.AWSBucketNameKey, nameof(ConfigHandlerBase.AWSBucketName))] + [TestCase(ConfigHandlerBase.AWSAccountIdKey, nameof(ConfigHandlerBase.AWSAccountId))] public void SettingsShouldGetValueFromAppSettings(string configKey, string configPropertyName) { @@ -75,6 +76,36 @@ public void SettingsShouldGetValueFromAppSettings(string configKey, string confi this.mockRepository.VerifyAll(); } + [TestCase(ConfigHandlerBase.AWSGreengrassRequiredRolesKey, nameof(ConfigHandlerBase.AWSGreengrassRequiredRoles))] + public void SettingsShouldGetSectionFromAppSettings(string configKey, string configPropertyName) + { + // Arrange + var mockSection = this.mockRepository.Create(); + + _ = mockSection.SetupGet(c => c.Value) + .Returns(Guid.NewGuid().ToString()); + + _ = mockSection.SetupGet(c => c.Path) + .Returns(configKey); + + _ = mockSection.Setup(c => c.GetChildren()) + .Returns(Array.Empty()); + + var configHandler = CreateProductionAWSConfigHandler(); + + _ = this.mockConfiguration.Setup(c => c.GetSection(It.Is(x => x == configKey))) + .Returns(mockSection.Object); + + // Act + var result = configHandler + .GetType() + .GetProperty(configPropertyName) + .GetValue(configHandler, null); + + // Assert + this.mockRepository.VerifyAll(); + } + [TestCase(nameof(ConfigHandlerBase.StorageAccountConnectionString))] [TestCase(nameof(ConfigHandlerBase.StorageAccountDeviceModelImageMaxAge))] public void SettingsShouldThrowError(string configPropertyName) diff --git a/templates/aws/awsdeploy.yml b/templates/aws/awsdeploy.yml index f9ca108c4..d6374c532 100644 --- a/templates/aws/awsdeploy.yml +++ b/templates/aws/awsdeploy.yml @@ -4,10 +4,6 @@ Description: IoT Hub portal deployment Metadata: AWS::CloudFormation::Interface: ParameterGroups: - - Label: - default: Solution - Parameters: - - UniqueSolutionPrefix - Label: default: Amazon Web Services resources access Parameters: @@ -29,13 +25,6 @@ Metadata: ParameterLabels: ParameterLabel Parameters: - UniqueSolutionPrefix: - Type: String - Description: Prefix used for resource names. Should be unique as this will also be used for bucket and database name. Should not contain uppercase letters - MinLength: "1" - MaxLength: "20" - ConstraintDescription: Should be less than 20 letters - AllowedPattern: "^[a-z]+$" pgsqlAdminLogin: Type: String Description: PostgreSQL user @@ -80,7 +69,7 @@ Resources: BucketName: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "bucket" PublicAccessBlockConfiguration: BlockPublicAcls: false @@ -105,7 +94,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "vpc" PrivateSubnet1: @@ -123,7 +112,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "priv-snet-1" PrivateSubnet2: @@ -141,7 +130,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "priv-snet-2" PrivateSubnet3: @@ -159,7 +148,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "priv-snet-3" PublicSubnet: @@ -173,7 +162,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "pub-snet" InternetGateway: @@ -184,7 +173,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "igw" InternetGatewayAttachment: @@ -205,7 +194,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "pub-rt" InternetRoutePublicSubnet: @@ -227,7 +216,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "natgw-eip" NatGateway: @@ -242,7 +231,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "natgw" PrivateRouteTable: @@ -255,7 +244,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "priv-rt" InternetRoutePrivateSubnet: @@ -307,7 +296,7 @@ Resources: DBInstanceIdentifier: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "pgdb" AllocatedStorage: "20" DBInstanceClass: "db.t2.micro" @@ -321,7 +310,7 @@ Resources: DBName: Fn::Join: - "_" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "db" VPCSecurityGroups: - Ref: PgSQLSecurityGroup @@ -332,7 +321,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "database" PgSQLSecurityGroup: @@ -352,7 +341,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "database-sg" PgSQLSubnetGroup: @@ -361,7 +350,7 @@ Resources: DBSubnetGroupName: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "database-snetgroup" DBSubnetGroupDescription: Database subnet group SubnetIds: @@ -373,7 +362,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "db-snet-group" #======== Secrets ========== @@ -384,7 +373,7 @@ Resources: Name: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "AWSKey" SecretString: Fn::Sub: "${awsAccess}" @@ -395,7 +384,7 @@ Resources: Name: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "AWSSecretKey" SecretString: Fn::Sub: "${awsAccessSecretkey}" @@ -406,14 +395,14 @@ Resources: Name: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "PostgreSQLConnectionString" SecretString: Fn::Join: - "" - - "Server=" - Fn::GetAtt: PostgreSQLDB.Endpoint.Address - - Fn::Sub: ";Database=${UniqueSolutionPrefix}_db;Port=5432;User Id=${pgsqlAdminLogin};Password=${pgsqlAdminPassword};Pooling=true;Connection Lifetime=0;Command Timeout=0;" + - Fn::Sub: ";Database=${AWS::StackName}_db;Port=5432;User Id=${pgsqlAdminLogin};Password=${pgsqlAdminPassword};Pooling=true;Connection Lifetime=0;Command Timeout=0;" SMS3StorageConnectionString: Type: AWS::SecretsManager::Secret @@ -421,13 +410,13 @@ Resources: Name: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "S3StorageConnectionString" SecretString: ! Fn::Join: - "" - - Fn::Sub: "https://s3.${AWS::Region}.amazonaws.com/" - - Ref: UniqueSolutionPrefix + - Ref: "AWS::StackName" - "-bucket" #============= App Runner ============== @@ -438,7 +427,7 @@ Resources: RoleName: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "AppRunner" AssumeRolePolicyDocument: Version: "2012-10-17" @@ -463,7 +452,7 @@ Resources: Fn::Join: - "" - - Fn::Sub: "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:" - - Ref: UniqueSolutionPrefix + - Ref: "AWS::StackName" - "-*" - PolicyName: AmazonElasticContainerRegistryPublicReadOnly PolicyDocument: @@ -526,7 +515,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "apprunner-sg" AppRunnerDatabaseOutboundRule: @@ -562,7 +551,7 @@ Resources: ServiceName: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "portal" InstanceConfiguration: Cpu: 1024 @@ -606,12 +595,21 @@ Resources: - Name: AWS__Region Value: Ref: AWS::Region + - Name: AWS__AccountId + Value: + Ref: AWS::AccountId - Name: AWS__BucketName Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "bucket" + - Name: "AWS__GreengrassRequiredRoles__0" + Value: + Fn::GetAtt: GreengrassCoreTokenExchangeRoleAliasPolicy.Id + - Name: "AWS__GreengrassRequiredRoles__1" + Value: + Fn::GetAtt: GreengrassV2IoTThingPolicy.Id - Name: OIDC__ApiClientId Value: Fn::Sub: "${openIdApiClientId}" @@ -628,7 +626,7 @@ Resources: Value: Fn::Sub: "${openIdScopeName}" - Name: CloudProvider - Value: AWS + Value: AWS ImageIdentifier: Fn::Sub: "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/iot-hub-portal:latest" ImageRepositoryType: ECR @@ -637,7 +635,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "apprunner" AppRunnerServiceVPCConnector: @@ -654,7 +652,7 @@ Resources: Value: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "apprunner-vpc-connector" #============= IoT Greengrass Role ============== @@ -665,7 +663,7 @@ Resources: RoleName: Fn::Join: - "-" - - - Ref: UniqueSolutionPrefix + - - Ref: "AWS::StackName" - "GreenGrass" AssumeRolePolicyDocument: Version: "2012-10-17" @@ -678,7 +676,11 @@ Resources: - arn:aws:iam::aws:policy/service-role/AWSGreengrassResourceAccessRolePolicy - arn:aws:iam::aws:policy/AWSGreengrassFullAccess Policies: - - PolicyName: ECRPermissions + - PolicyName: + Fn::Join: + - "-" + - - Ref: "AWS::StackName" + - "ECRPermissions" PolicyDocument: Version: "2012-10-17" Statement: @@ -692,7 +694,11 @@ Resources: GreengrassV2TokenExchangeRole: Type: AWS::IAM::Role Properties: - RoleName: GreengrassV2TokenExchangeRole + RoleName: + Fn::Join: + - "-" + - - Ref: "AWS::StackName" + - "GreengrassV2TokenExchangeRole" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: @@ -701,7 +707,11 @@ Resources: Service: credentials.iot.amazonaws.com Action: sts:AssumeRole Policies: - - PolicyName: GreengrassV2TokenExchangeRoleAccess + - PolicyName: + Fn::Join: + - "-" + - - Ref: "AWS::StackName" + - "GreengrassV2TokenExchangeRoleAccess" PolicyDocument: Version: "2012-10-17" Statement: @@ -718,14 +728,22 @@ Resources: GreengrassCoreTokenExchangeRoleAlias: Type: AWS::IoT::RoleAlias Properties: - RoleAlias: GreengrassCoreTokenExchangeRoleAlias + RoleAlias: + Fn::Join: + - "-" + - - Ref: "AWS::StackName" + - "GreengrassCoreTokenExchangeRoleAlias" RoleArn: Fn::GetAtt: GreengrassV2TokenExchangeRole.Arn GreengrassCoreTokenExchangeRoleAliasPolicy: Type: AWS::IoT::Policy Properties: - PolicyName: GreengrassCoreTokenExchangeRoleAliasPolicy + PolicyName: + Fn::Join: + - "-" + - - Ref: "AWS::StackName" + - "GreengrassCoreTokenExchangeRoleAliasPolicy" PolicyDocument: Version: '2012-10-17' Statement: @@ -738,7 +756,11 @@ Resources: GreengrassV2IoTThingPolicy: Type: AWS::IoT::Policy Properties: - PolicyName: GreengrassV2IoTThingPolicy + PolicyName: + Fn::Join: + - "-" + - - Ref: "AWS::StackName" + - "GreengrassV2IoTThingPolicy" PolicyDocument: Version: '2012-10-17' Statement: