Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b391b4e
Tests!
andrewjhumphrey Jul 10, 2025
8edf382
Add in resolver code and integrate into main app
andrewjhumphrey Jul 10, 2025
380ff82
linting
andrewjhumphrey Jul 10, 2025
97feb2f
Add some documentation too
andrewjhumphrey Jul 10, 2025
d0c6d62
PR Feedback fixes
andrewjhumphrey Jul 10, 2025
6de910d
Add tests for the finder class too
andrewjhumphrey Jul 10, 2025
6fd6c5a
Make ruby3.0+ compatible
andrewjhumphrey Jul 10, 2025
eabc066
Handle positional args properly
andrewjhumphrey Jul 10, 2025
8280eb3
Another attempt to fix ruby3 incompatibilities
andrewjhumphrey Jul 10, 2025
47d2b64
Revert "Another attempt to fix ruby3 incompatibilities"
andrewjhumphrey Jul 10, 2025
ba77d21
Revert "Handle positional args properly"
andrewjhumphrey Jul 10, 2025
40d4e7e
Let people know which directory was searched
andrewjhumphrey Jul 10, 2025
f51bee8
Pass hash, not named arguments
andrewjhumphrey Jul 10, 2025
3bf111c
Pass hash, not named arguments (in all the places, not just some)
andrewjhumphrey Jul 10, 2025
6f79b20
Use a before block
andrewjhumphrey Jul 10, 2025
4025580
Umm, itchy tab key finger
andrewjhumphrey Jul 10, 2025
f55d905
Update docs and test for new way of specifying identity-store-id
andrewjhumphrey Jul 10, 2025
f702341
Remove the top level sso_identity_store_id attribute
andrewjhumphrey Jul 10, 2025
af6e6bd
Use the same format as stack_output does to specify the region,identi…
andrewjhumphrey Jul 10, 2025
45d27f0
Use much more efficient method of finding group
andrewjhumphrey Jul 10, 2025
af64e1d
hash not keywords again
andrewjhumphrey Jul 10, 2025
43f67e4
And again
andrewjhumphrey Jul 10, 2025
f8b6f67
Update with PR details
andrewjhumphrey Jul 10, 2025
d6cb2ae
Remove double double quote
andrewjhumphrey Jul 10, 2025
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
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@ The format is based on [Keep a Changelog], and this project adheres to

## [Unreleased]

[Unreleased]: https://github.com/envato/stack_master/compare/v2.16.0...HEAD
[Unreleased]: https://github.com/envato/stack_master/compare/v2.17.0...HEAD

## [2.17.0] - 2025-07-11

### Added

- Add a parameter resolver for AWS SSO/IIC mapping group display name to id ([#390])

```yaml
group_id:
sso_group_id: "us-east-1:d-123456bf8/SSO Group Display Name"
```

## [2.16.0] - 2024-08-01

Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,24 @@ ssh_sg:
- WebAccessSecurityGroup
```

### AWS IIC/SSO Group IDs

Looks up AWS Identity Center group name in the configured Identity Store and returns the ID suitable for use in AWS IIC assignments.
It is likely that account and role will need to be specified to do the lookup, the region specification is optional it defaults to stack region.

```yaml
GroupId:
sso_group_id: '[region:]identity-store-id/SSO Group Name'
```

e.g.
```yaml
GroupIdNotInStackRegion:
sso_group_id: 'us-east-1:d-123456df8:Okta-App-AWS-FooBar'
GroupIdInStackRegion:
sso_group_id: 'd-123456df8:Okta-App-AWS-FooBar'
```

### SNS Topic

Looks up an SNS topic by name and returns the ARN.
Expand Down
3 changes: 3 additions & 0 deletions lib/stack_master.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'aws-sdk-cloudformation'
require 'aws-sdk-ec2'
require 'aws-sdk-ecr'
require 'aws-sdk-identitystore'
require 'aws-sdk-s3'
require 'aws-sdk-sns'
require 'aws-sdk-ssm'
Expand Down Expand Up @@ -33,6 +34,7 @@ module StackMaster
autoload :StackStatus, 'stack_master/stack_status'
autoload :SnsTopicFinder, 'stack_master/sns_topic_finder'
autoload :SecurityGroupFinder, 'stack_master/security_group_finder'
autoload :SsoGroupIdFinder, 'stack_master/sso_group_id_finder'
autoload :ParameterLoader, 'stack_master/parameter_loader'
autoload :ParameterResolver, 'stack_master/parameter_resolver'
autoload :RoleAssumer, 'stack_master/role_assumer'
Expand Down Expand Up @@ -84,6 +86,7 @@ module ParameterResolvers
autoload :Ejson, 'stack_master/parameter_resolvers/ejson'
autoload :SnsTopicName, 'stack_master/parameter_resolvers/sns_topic_name'
autoload :SecurityGroup, 'stack_master/parameter_resolvers/security_group'
autoload :SsoGroupId, 'stack_master/parameter_resolvers/sso_group_id'
autoload :LatestAmiByTags, 'stack_master/parameter_resolvers/latest_ami_by_tags'
autoload :LatestAmi, 'stack_master/parameter_resolvers/latest_ami'
autoload :Env, 'stack_master/parameter_resolvers/env'
Expand Down
21 changes: 21 additions & 0 deletions lib/stack_master/parameter_resolvers/sso_group_id.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module StackMaster
module ParameterResolvers
class SsoGroupId < Resolver
InvalidParameter = Class.new(StandardError)

def initialize(config, stack_definition)
@config = config
@stack_definition = stack_definition
end

def resolve(value)
sso_group_id_finder.find(value)
end

private
def sso_group_id_finder
StackMaster::SsoGroupIdFinder.new()
end
end
end
end
33 changes: 33 additions & 0 deletions lib/stack_master/sso_group_id_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module StackMaster
class SsoGroupIdFinder
SsoGroupNotFound = Class.new(StandardError)

def find(reference)
output_regex = %r{(?:(?<region>[^:]+):)?(?<identity_store_id>[^:/]+)/(?<group_name>.+)}

if !reference.is_a?(String) || !(match = output_regex.match(reference))
raise ArgumentError, 'Sso group lookup parameter must be in the form of [region:]identity-store-id/group_name'
end

region = match[:region] || StackMaster.cloud_formation_driver.region
client = Aws::IdentityStore::Client.new({ region: region })

begin
response = client.get_group_id({
identity_store_id: match[:identity_store_id],
alternate_identifier: {
unique_attribute: {
attribute_path: 'displayName',
attribute_value: match[:group_name],
},
},
})
return response.group_id
rescue Aws::IdentityStore::Errors::ServiceError => e
puts "Error calling GetGroupId: #{e.message}"
end

raise SsoGroupNotFound, "No group with name #{match[:group_name]} found in identity store #{match[:identity_store_id]} in #{region}"
end
end
end
2 changes: 1 addition & 1 deletion lib/stack_master/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module StackMaster
VERSION = "2.16.0"
VERSION = "2.17.0"
end
50 changes: 50 additions & 0 deletions spec/stack_master/parameter_resolvers/sso_group_id_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require 'spec_helper'

RSpec.describe StackMaster::ParameterResolvers::SsoGroupId do
let(:config) { instance_double('Config') }
let(:stack_definition) { instance_double('StackDefinition', region: 'us-east-1') }

subject(:resolver) { described_class.new(config, stack_definition) }

let(:group_reference) { 'us-east-1:d-12345678/AdminGroup' }
let(:resolved_group_id) { 'abc-123-group-id' }
let(:finder) { instance_double(StackMaster::SsoGroupIdFinder) }

before do
allow(StackMaster::SsoGroupIdFinder).to receive(:new).and_return(finder)
end

describe '#resolve' do
context 'when group is found' do
it 'returns the resolved group ID' do
expect(finder).to receive(:find).with(group_reference).and_return(resolved_group_id)

result = resolver.resolve(group_reference)
expect(result).to eq(resolved_group_id)
end
end

context 'when SsoGroupIdFinder raises an error' do
it 'propagates the SsoGroupNotFound error' do
allow(finder).to receive(:find).and_raise(StackMaster::SsoGroupIdFinder::SsoGroupNotFound)

expect {
resolver.resolve(group_reference)
}.to raise_error(StackMaster::SsoGroupIdFinder::SsoGroupNotFound)
end
end

context 'with invalid input' do
let(:invalid_reference) { 'not/a/valid/reference' }

it 'raises ArgumentError from SsoGroupIdFinder' do
allow(finder).to receive(:find).and_raise(ArgumentError)

expect {
resolver.resolve(invalid_reference)
}.to raise_error(ArgumentError)
end
end
end
end

95 changes: 95 additions & 0 deletions spec/stack_master/sso_group_id_finder_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
require 'spec_helper'

RSpec.describe StackMaster::SsoGroupIdFinder do
let(:group_name) { 'AdminGroup' }
let(:identity_store_id) { 'd-12345678' }
let(:region) { 'us-east-1' }
let(:reference) { "#{region}:#{identity_store_id}/#{group_name}" }
let(:aws_client) { instance_double(Aws::IdentityStore::Client) }

subject(:finder) do
allow(Aws::IdentityStore::Client).to receive(:new).with({region: region}).and_return(aws_client)
described_class.new
end

before do
allow(StackMaster).to receive(:cloud_formation_driver).and_return(double(region: region))
end

describe '#find' do
context 'when the group is found successfully' do
it 'returns the group ID' do
group_id = 'abc-123-group-id'

response = double(group_id: group_id)
expect(aws_client).to receive(:get_group_id).with({
identity_store_id: identity_store_id,
alternate_identifier: {
unique_attribute: {
attribute_path: 'displayName',
attribute_value: group_name
}
}
}).and_return(response)

expect(finder.find(reference)).to eq(group_id)
end
end

context 'when the group is not found' do
it 'raises SsoGroupNotFound' do
error = Aws::IdentityStore::Errors::ResourceNotFoundException.new(
Seahorse::Client::RequestContext.new,
"Group not found"
)

expect(aws_client).to receive(:get_group_id).and_raise(error)

expect {
finder.find(reference)
}.to raise_error(StackMaster::SsoGroupIdFinder::SsoGroupNotFound, /No group with name #{group_name} found/)
end
end

context 'when region is not provided in reference' do
let(:reference_without_region) { "#{identity_store_id}/#{group_name}" }

it 'uses the fallback region from cloud_formation_driver' do
allow(Aws::IdentityStore::Client).to receive(:new).with({region: region}).and_return(aws_client)

group_id = 'fallback-region-group-id'
response = double(group_id: group_id)

expect(aws_client).to receive(:get_group_id).with({
identity_store_id: identity_store_id,
alternate_identifier: {
unique_attribute: {
attribute_path: 'displayName',
attribute_value: group_name
}
}
}).and_return(response)

expect(finder.find(reference_without_region)).to eq(group_id)
end
end

context 'when input is not a string' do
it 'raises ArgumentError' do
expect {
finder.find(123)
}.to raise_error(ArgumentError, /Sso group lookup parameter must be in the form/)
end
end

context 'when input is an invalid string' do
it 'raises ArgumentError' do
invalid_reference = 'badformat'

expect {
finder.find(invalid_reference)
}.to raise_error(ArgumentError, /Sso group lookup parameter must be in the form/)
end
end
end
end
1 change: 1 addition & 0 deletions stack_master.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "aws-sdk-acm", "~> 1"
spec.add_dependency "aws-sdk-cloudformation", "~> 1"
spec.add_dependency "aws-sdk-ec2", "~> 1"
spec.add_dependency "aws-sdk-identitystore", "~> 1"
spec.add_dependency "aws-sdk-s3", "~> 1"
spec.add_dependency "aws-sdk-sns", "~> 1"
spec.add_dependency "aws-sdk-ssm", "~> 1"
Expand Down