Merge pull request #1091 from dod-ccpo/aws-implementation

AWS Implementation of CloudProviderInterface
This commit is contained in:
richard-dds 2019-10-02 16:26:24 -04:00 committed by GitHub
commit b9a17244b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 625 additions and 76 deletions

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$",
"lines": null
},
"generated_at": "2019-10-02T14:52:59Z",
"generated_at": "2019-10-02T14:53:58Z",
"plugins_used": [
{
"base64_limit": 4.5,
@ -26,7 +26,7 @@
"results": {
"Pipfile.lock": [
{
"hashed_secret": "a355e3e3983231194cfc5981d48ea7ff9a7f9cb6",
"hashed_secret": "307e5f510c60e98b8590cfd40e6dcacb0a285e42",
"is_secret": false,
"is_verified": false,
"line_number": 4,
@ -90,21 +90,21 @@
"hashed_secret": "aa419433d95be86df254d499243bee1d5173f1ae",
"is_secret": false,
"is_verified": false,
"line_number": 5,
"line_number": 7,
"type": "Secret Keyword"
},
{
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_secret": false,
"is_verified": false,
"line_number": 18,
"line_number": 20,
"type": "Secret Keyword"
},
{
"hashed_secret": "abcdb568713c255c81376829da20004ba9463fd3",
"is_secret": false,
"is_verified": false,
"line_number": 24,
"line_number": 26,
"type": "Secret Keyword"
}
],
@ -199,5 +199,9 @@
}
]
},
"version": "0.12.6"
"version": "0.12.7",
"word_list": {
"file": null,
"hash": null
}
}

140
Pipfile.lock generated
View File

@ -18,10 +18,10 @@
"default": {
"alembic": {
"hashes": [
"sha256:5609afbb2ab142a991b15ae436347c475f8a517f1610f2fd1b09cdca7c311f3f"
"sha256:9f907d7e8b286a1cfb22db9084f9ce4fde7ad7956bb496dc7c952e10ac90e36a"
],
"index": "pypi",
"version": "==1.2.0"
"version": "==1.2.1"
},
"amqp": {
"hashes": [
@ -85,18 +85,18 @@
},
"boto3": {
"hashes": [
"sha256:0e4d047feb4d7d701e9b2107f10bb8d674952243385cd35d0b413a273c299751",
"sha256:67f957389cf56fb4c24c1093c6d58baebe6cf18139f6dca0f8a177239b0a4f8c"
"sha256:2fc1c407a5ab08cfcf54eb4171d85c523bd27019ab890de257d018af2770f71d",
"sha256:c215cf2c8e5e7b28ae7544b1cbdbc3216bef983d7adb8b701a64f9b893e0320b"
],
"index": "pypi",
"version": "==1.9.232"
"version": "==1.9.238"
},
"botocore": {
"hashes": [
"sha256:724d2349198c6f15f3cee0c0e4d33ecf4435e6d0db311bb79a3a28f6cf5a4090",
"sha256:a57a8fd0145c68e31bb4baab549b27a12f6695068c8dd5f2901d8dc06572dbeb"
"sha256:1ca993f0dc70591e0fca6cf3837ee9be52fd4fbbf1aa96ba1d4a860b41f676b7",
"sha256:6ec3297b87d3e2c4d88b009f91061aaecdb2ceef6d9be9386394571353909adb"
],
"version": "==1.12.232"
"version": "==1.12.238"
},
"celery": {
"hashes": [
@ -214,11 +214,11 @@
},
"flask-sqlalchemy": {
"hashes": [
"sha256:0c9609b0d72871c540a7945ea559c8fdf5455192d2db67219509aed680a3d45a",
"sha256:8631bbea987bc3eb0f72b1f691d47bd37ceb795e73b59ab48586d76d75a7c605"
"sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327",
"sha256:6974785d913666587949f7c2946f7001e4fa2cb2d19f4e69ead02e4b8f50b33d"
],
"index": "pypi",
"version": "==2.4.0"
"version": "==2.4.1"
},
"flask-wtf": {
"hashes": [
@ -265,10 +265,10 @@
},
"kombu": {
"hashes": [
"sha256:55274dc75eb3c3994538b0973a0fadddb236b698a4bc135b8aa4981e0a710b8f",
"sha256:e5f0312dfb9011bebbf528ccaf118a6c2b5c3b8244451f08381fb23e7715809b"
"sha256:31edb84947996fdda065b6560c128d5673bb913ff34aa19e7b84755217a24deb",
"sha256:c9078124ce2616b29cf6607f0ac3db894c59154252dee6392cdbbe15e5c4b566"
],
"version": "==4.6.4"
"version": "==4.6.5"
},
"lockfile": {
"hashes": [
@ -480,11 +480,11 @@
},
"urllib3": {
"hashes": [
"sha256:2f3eadfea5d92bc7899e75b5968410b749a054b492d5a6379c1344a1481bc2cb",
"sha256:9c6c593cb28f52075016307fc26b0a0f8e82bc7d1ff19aaaa959b91710a56c47"
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
],
"markers": "python_version >= '3.4'",
"version": "==1.25.5"
"version": "==1.25.6"
},
"vine": {
"hashes": [
@ -548,10 +548,10 @@
},
"astroid": {
"hashes": [
"sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4",
"sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4"
"sha256:98c665ad84d10b18318c5ab7c3d203fe11714cbad2a4aef4f44651f415392754",
"sha256:b7546ffdedbf7abcfbff93cd1de9e9980b1ef744852689decc5aeada324238c6"
],
"version": "==2.2.5"
"version": "==2.3.1"
},
"atomicwrites": {
"hashes": [
@ -636,35 +636,35 @@
},
"coverage": {
"hashes": [
"sha256:108efa19b676e62590a7a13084098e35183479c0d9608131c20b0921c5a72dc0",
"sha256:16fe3ef881eff27bab287f91dadb4ff0ce4388b9e928d84cbf148a83cc70b3a1",
"sha256:1d0bbc11421827d1100da82ac8dc929532b97ad464038475a0f6505cbf83d6ea",
"sha256:23a8ca5b3c9673f775cc151e85a737f1a967df2ec02b09e8c5a3b606ff2050bf",
"sha256:24b890e51455276762b55cb06fa1c922066e8fc18d1deb1a6399b4d24dfa8ea2",
"sha256:2f0041757ca4801f3c6a74d1660862fdb18a25aea302dd0ce9b067ddbb06b667",
"sha256:3169aba03baddfccdab7cc04cf0878dbf76fc06d300bc35639129a6b794d6484",
"sha256:364fb1bf0f999af2e7f4b1a1e614b2af8c3e0017d11af716aad25f911b7cd0c7",
"sha256:5256856d23f3e45959e7e3a8f9d4cbad3d1613e5660cb8117cd1417798efc395",
"sha256:5b26daa1e1a1147455bf62cd682e504e68f1d1e04235374d50a5248a3c792b1c",
"sha256:60247c8f0c756732e2cfe21f03e6847b923b9a9eaff61f04dc64d3047ec1b669",
"sha256:6463d51507308eb3973340d903537f17ece2ee1e6513aa0c27548fc3a09b0471",
"sha256:64cbadf7a884b299794238bc4391752130e74f71e919993b50c1c431786ef2a2",
"sha256:6de85748ea39ce819ad6d90e660da43964457a1f5cd25262e962a7c7c87945b3",
"sha256:6f95b4794bd84f64aeca25087d8e3abc416aad76842afcac34fa6c3a6f61c62e",
"sha256:778fa184aa3079fa3cbd240e2f5b36771c3382db26bc7bf78aea9d06212c6c66",
"sha256:790a9c5e2dbdf6c41eec9776ed663e99bd36c1604e3bf2e8ae3b123181bfee9f",
"sha256:7d97c1aec0b68b4ea5e3c9edb9fc3f951e8a52360f4bad3aacab9a77defe5b17",
"sha256:93cefddcc0b541d3c52981a232947bf085a38092b0812317f1adb56f02869bcb",
"sha256:95e49867ac616ec63ecd69ea005e65e4b896a48b8db7f9f3ad69f37be29324b7",
"sha256:aca423563eafba66a7c15125391b267befd1e45238de5e1a119ae1fb4ea83b5c",
"sha256:baef7c35e7fce738d9637e9c7a6aa79cb79085e4de49c2ec517ce19239a660f6",
"sha256:c10ccf0797ffce85e93a40aff3a96a3adb63c734f95b59384a7c9522ed25c9e2",
"sha256:ca39704a05bba1886c384a4d7944fda72c53fe5e61979cd933d22084678ad4c1",
"sha256:f6e96d5eee578187f5b7e9266bf646b73de29e2dd7adca8bd83e383680ce1f4c",
"sha256:fc6524511fa664cb4e91401229eedd0dad4ba6ded9c4423fee2f698d78908d9c",
"sha256:fdf2e7e5f074495ad6ea796ca0d245aa6a8b9e4c546ffbf8d30aaaee6601af0f"
"sha256:094378c3a35594335a840ea04d473c019e6d4fe10e343cd0d7fb5e87f8b7e926",
"sha256:10216222f3e4139910b6230d0ca0fe9d10ee98837eb83d29525722d628729d20",
"sha256:147478e21cba12c63b3454df5a2fb77b44df630428cffa3a36a6813e38157eab",
"sha256:230ce08965190c0f69196be34a07a795981b2b02b21419c2e1918a882b3eeab0",
"sha256:2469621d680a4c71cdbd3ea4dbed9d199bba93f21d2be1c107ded907b2db41a8",
"sha256:26526174d11fb2163832628d943edd452e07528b0ecc0c83c88256a59a32287c",
"sha256:2690bf0835f34ef3451860b02471e9560e4b3caf7413abeaa7544af72eb6d9ed",
"sha256:2b6f2d9a60413e75651cebe33c3f2f66d61209db44e8b9cf6d8d66fb0cb01fda",
"sha256:3ce91c6b92160ecefedf95a8c61fbf4fb36b0addef1a40c654acf1ad390653d0",
"sha256:43d16d7e9e9eaace3d9f1828b617b1be248f90d031a4b2dc1b6e1c88f1602dcf",
"sha256:52b6455da5f547cad72fd5cfc57a16678573fda6c695d257b5c266a44dbbd172",
"sha256:533f3036c8f58e6381fcca3306fe988740638c62c7fc86b7fae9c74b85ac3cdc",
"sha256:62d2abe5c733394058cb381d088bcab64a18da3ce9dc9a8ef2a18e122cbe47f1",
"sha256:72c34f99164679e44a5cbf19bf1a13be4e715c680816302b6ceca49b979fde91",
"sha256:81fc07feed4e40a7c0bdd266efa65e5afc83b5e0f1063007acc6759a957322a1",
"sha256:82093e673182c761ce54dfab17f026a06be3c011fee9b653855b9a2649f20232",
"sha256:87947fef728f72860407c446fd9b4a0f98e39e91ad7ae80803c02a85738e63ef",
"sha256:8b18c5a5a6b35b6311d2c356782ce3c7bacf6d987d9dc479178577391bf1c7dd",
"sha256:90e1850e993aa6b81bafaf672c8e508eaa17fbb5eb23aba93f7f4df822f3bd29",
"sha256:99f71e365bcb03a8debe1a75061329c9e45379f244a229442319d64c53c4e844",
"sha256:9b2c559104a90bf0043d6ef262ca205326d1fe6ec572dcf59e34be9289432793",
"sha256:ad22b073d92ea65b063e612154c72d6367dec3dd47ed33c02e3ab339eabe7bf3",
"sha256:bc3648da235fee2113a8cb80154d9fff4e2689d2d4a11ad35c1ecae23454b539",
"sha256:d0e2478bde68c5d853bcd306b5aae8fbe80417e87957a21fa6ee71edb90639f2",
"sha256:d3e6912d2370925222d2bfb3bd2ba02e9698b8da89cf7192ddf80cbb9f2455ee",
"sha256:d4fa98e3e15863568ea89eaec5e0866ca763980bdc56098dd9316865c111a28e",
"sha256:ee924a23457b373241ff39d21570360afd8ccb58520eb1e8e18eb00827b73e2d"
],
"version": "==5.0a6"
"version": "==5.0a7"
},
"decorator": {
"hashes": [
@ -675,11 +675,11 @@
},
"detect-secrets": {
"hashes": [
"sha256:7e1820a3c4ac412a7a2cec13075c274ae4bfc9167b4b831ad3c7f0e6208c9488",
"sha256:bacb5842f149f39799409039fafb1902554ac0c71a9764cc8a8ffab85f99efc1"
"sha256:d6b22e93fa5ccdf11391f87d18c45cba64e11463fdb367e2314cdbeba6963ec0",
"sha256:e2189cd21619fc95a3ee7ec7adfb61adf66e2e4e78d518318a6025ca0f62b364"
],
"index": "pypi",
"version": "==0.12.6"
"version": "==0.12.7"
},
"docopt": {
"hashes": [
@ -712,10 +712,10 @@
},
"gitdb2": {
"hashes": [
"sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2",
"sha256:e3a0141c5f2a3f635c7209d56c496ebe1ad35da82fe4d3ec4aaa36278d70648a"
"sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350",
"sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"
],
"version": "==2.0.5"
"version": "==2.0.6"
},
"gitpython": {
"hashes": [
@ -939,11 +939,11 @@
},
"pylint": {
"hashes": [
"sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09",
"sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1"
"sha256:7edbae11476c2182708063ac387a8f97c760d9cfe36a5ede0ca996f90cf346c8",
"sha256:844ce067788028c1a35086a5c66bc5e599ddd851841c41d6ee1623b36774d9f2"
],
"index": "pypi",
"version": "==2.3.1"
"version": "==2.4.2"
},
"pytest": {
"hashes": [
@ -978,11 +978,11 @@
},
"pytest-mock": {
"hashes": [
"sha256:43ce4e9dd5074993e7c021bb1c22cbb5363e612a2b5a76bc6d956775b10758b7",
"sha256:5bf5771b1db93beac965a7347dc81c675ec4090cb841e49d9d34637a25c30568"
"sha256:3fd5ffb33b7041aea60a77f77b98e05d5acd577d53a01bf2ff0ca9780c6e3d84",
"sha256:a89104018a4083b5c402e23b15b855924b74d9a3511e94f230a33621b72e35e1"
],
"index": "pypi",
"version": "==1.10.4"
"version": "==1.11.0"
},
"pytest-watch": {
"hashes": [
@ -1050,10 +1050,10 @@
},
"soupsieve": {
"hashes": [
"sha256:8662843366b8d8779dec4e2f921bebec9afd856a5ff2e82cd419acc5054a1a92",
"sha256:a5a6166b4767725fd52ae55fee8c8b6137d9a51e9f1edea461a062a759160118"
"sha256:605f89ad5fdbfefe30cdc293303665eff2d188865d4dbe4eb510bba1edfbfce3",
"sha256:b91d676b330a0ebd5b21719cb6e9b57c57d433671f65b9c28dd3461d9a1ed0b6"
],
"version": "==1.9.3"
"version": "==1.9.4"
},
"stevedore": {
"hashes": [
@ -1078,10 +1078,10 @@
},
"traitlets": {
"hashes": [
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
"sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
"sha256:262089114405f22f4833be96b31e143ab906d7764a22c04c71fee0bbda4787ba",
"sha256:6ad5b30dacd5e2424c46cc94a0aeab990d98ae17d181acea2cc4272ac3409fca"
],
"version": "==4.3.2"
"version": "==4.3.3.dev0"
},
"typed-ast": {
"hashes": [
@ -1101,16 +1101,16 @@
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
],
"markers": "implementation_name == 'cpython'",
"markers": "implementation_name == 'cpython' and python_version < '3.8'",
"version": "==1.4.0"
},
"urllib3": {
"hashes": [
"sha256:2f3eadfea5d92bc7899e75b5968410b749a054b492d5a6379c1344a1481bc2cb",
"sha256:9c6c593cb28f52075016307fc26b0a0f8e82bc7d1ff19aaaa959b91710a56c47"
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
],
"markers": "python_version >= '3.4'",
"version": "==1.25.5"
"version": "==1.25.6"
},
"watchdog": {
"hashes": [

View File

@ -1,11 +1,15 @@
from typing import Dict
from uuid import uuid4
import json
from jinja2 import Template
from atst.models.environment_role import CSPRole
from atst.models.user import User
from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole
from botocore.waiter import WaiterModel, create_waiter_with_client, WaiterError
class GeneralCSPException(Exception):
pass
@ -442,3 +446,273 @@ class MockCloudProvider(CloudProviderInterface):
self._delay(1, 5)
if credentials != self._auth_credentials:
raise self.AUTHENTICATION_EXCEPTION
class AWSCloudProvider(CloudProviderInterface):
# These are standins that will be replaced with "real" policies once we know what they are.
BASELINE_POLICIES = [
{
"name": "BillingReadOnly",
"path": "/atat/billing-read-only/",
"document": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"aws-portal:ViewPaymentMethods",
"aws-portal:ViewAccount",
"aws-portal:ViewBilling",
"aws-portal:ViewUsage",
],
"Resource": "*",
}
],
},
"description": "View billing information.",
}
]
MAX_CREATE_ACCOUNT_ATTEMPTS = 10
# Placeholder permission boundary for root user
PERMISSION_BOUNDARY_ARN = "arn:aws:iam::aws:policy/AlexaForBusinessDeviceSetup"
def __init__(self, config, boto3=None):
self.config = config
self.access_key_id = config["AWS_ACCESS_KEY_ID"]
self.secret_key = config["AWS_SECRET_KEY"]
self.region_name = config["AWS_REGION_NAME"]
# TODO: Discuss these values.
self.role_access_org_name = "OrganizationAccountAccessRole"
self.root_account_username = "atat"
self.root_account_policy_name = "OrganizationAccountAccessRole"
if boto3:
self.boto3 = boto3
else:
import boto3
self.boto3 = boto3
def root_creds(self):
return {"AccessKeyId": self.access_key_id, "SecretAccessKey": self.secret_key}
def create_environment(
self, auth_credentials: Dict, user: User, environment: Environment
):
org_client = self._get_client("organizations")
# Create an account. Requires organizations:CreateAccount permission
account_request = org_client.create_account(
Email=user.email, AccountName=uuid4().hex, IamUserAccessToBilling="ALLOW"
)
# Configuration for our CreateAccount Waiter.
# A waiter is a boto3 helper which can be configured to poll a given status
# endpoint until it succeeds or fails. boto3 has many built in waiters, but none
# for the organizations service so we're building our own here.
waiter_config = {
"version": 2,
"waiters": {
"AccountCreated": {
"operation": "DescribeCreateAccountStatus",
"delay": 20,
"maxAttempts": self.MAX_CREATE_ACCOUNT_ATTEMPTS,
"acceptors": [
{
"matcher": "path",
"expected": "SUCCEEDED",
"argument": "CreateAccountStatus.State",
"state": "success",
},
{
"matcher": "path",
"expected": "IN_PROGRESS",
"argument": "CreateAccountStatus.State",
"state": "retry",
},
{
"matcher": "path",
"expected": "FAILED",
"argument": "CreateAccountStatus.State",
"state": "failure",
},
],
}
},
}
waiter_model = WaiterModel(waiter_config)
account_waiter = create_waiter_with_client(
"AccountCreated", waiter_model, org_client
)
try:
# Poll until the CreateAccount request either succeeds or fails.
account_waiter.wait(
CreateAccountRequestId=account_request["CreateAccountStatus"]["Id"]
)
except WaiterError:
# TODO: Possible failure reasons:
# 'ACCOUNT_LIMIT_EXCEEDED'|'EMAIL_ALREADY_EXISTS'|'INVALID_ADDRESS'|'INVALID_EMAIL'|'CONCURRENT_ACCOUNT_MODIFICATION'|'INTERNAL_FAILURE'
raise EnvironmentCreationException(
environment.id, "Failed to create account."
)
# We need to re-fetch this since the Waiter throws away the success response for some reason.
created_account_status = org_client.describe_create_account_status(
CreateAccountRequestId=account_request["CreateAccountStatus"]["Id"]
)
account_id = created_account_status["CreateAccountStatus"]["AccountId"]
return account_id
def create_atat_admin_user(
self, auth_credentials: Dict, csp_environment_id: str
) -> Dict:
"""
Create an IAM user within a given account.
"""
# Create a policy which allows user to assume a role within the account.
iam_client = self._get_client("iam")
iam_client.put_user_policy(
UserName=self.root_account_username,
PolicyName=f"assume-role-{self.root_account_policy_name}-{csp_environment_id}",
PolicyDocument=self._inline_org_management_policy(csp_environment_id),
)
role_arn = (
f"arn:aws:iam::{csp_environment_id}:role/{self.root_account_policy_name}"
)
sts_client = self._get_client("sts", credentials=auth_credentials)
assumed_role_object = sts_client.assume_role(
RoleArn=role_arn, RoleSessionName="AssumeRoleSession1"
)
# From the response that contains the assumed role, get the temporary
# credentials that can be used to make subsequent API calls
credentials = assumed_role_object["Credentials"]
# Use the temporary credentials that AssumeRole returns to make a new connection to IAM
iam_client = self._get_client("iam", credentials=credentials)
# Create the user with a PermissionBoundary
try:
user = iam_client.create_user(
UserName=self.root_account_username,
PermissionsBoundary=self.PERMISSION_BOUNDARY_ARN,
Tags=[{"Key": "foo", "Value": "bar"}],
)["User"]
except iam_client.exceptions.EntityAlreadyExistsException as _exc:
# TODO: Find user, iterate through existing access keys and revoke them.
user = iam_client.get_user(UserName=self.root_account_username)["User"]
access_key = iam_client.create_access_key(UserName=self.root_account_username)[
"AccessKey"
]
credentials = {
"AccessKeyId": access_key["AccessKeyId"],
"SecretAccessKey": access_key["SecretAccessKey"],
}
# TODO: Create real policies in account.
return {
"id": user["UserId"],
"username": user["UserName"],
"resource_id": user["Arn"],
"credentials": credentials,
}
def create_environment_baseline(
self, auth_credentials: Dict, csp_environment_id: str
) -> Dict:
"""Provision the necessary baseline entities (such as roles) in the given environment
Arguments:
auth_credentials -- Object containing CSP account credentials
csp_environment_id -- ID of the CSP Environment to provision roles against.
Returns:
dict: Returns dict that associates the resource identities with their ATAT representations.
Raises:
AuthenticationException: Problem with the credentials
AuthorizationException: Credentials not authorized for current action(s)
ConnectionException: Issue with the CSP API connection
UnknownServerException: Unknown issue on the CSP side
BaselineProvisionException: Specific issue occurred with some aspect of baseline setup
"""
client = self._get_client("iam", credentials=auth_credentials)
created_policies = []
for policy in self.BASELINE_POLICIES:
try:
response = client.create_policy(
PolicyName=policy["name"],
Path=policy["path"],
PolicyDocument=json.dumps(policy["document"]),
Description=policy["description"],
)
created_policies.append({policy["name"]: response["Policy"]["Arn"]})
except client.exceptions.EntityAlreadyExistsException:
# Policy already exists. We can determine its ARN based on the account id and policy path / name.
policy_arn = f"arn:aws:iam:{csp_environment_id}:policy{policy['path']}{policy['name']}"
created_policies.append({policy["name"]: policy_arn})
return {"policies": created_policies}
def _get_client(self, service: str, credentials=None):
"""
A helper for creating a client of a given AWS service.
If `credentials` aren't provided, the configured root credentials will be used.
`credentials` format:
{
"AccessKeyId": "access-key-id",
"SecretAccessKey": "secret-access-key",
"SessionToken": "session-token" # optional
}
"""
credentials = credentials or {}
credential_kwargs = {
"aws_access_key_id": credentials.get("AccessKeyId", self.access_key_id),
"aws_secret_access_key": credentials.get(
"SecretAccessKey", self.secret_key
),
}
if "SessionToken" in credentials:
credential_kwargs["aws_session_token"] = credentials["SessionToken"]
return self.boto3.client(
service, region_name=self.region_name, **credential_kwargs
)
def _inline_org_management_policy(self, account_id: str) -> Dict:
policy_template = Template(
"""
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sts:AssumeRole"
],
"Resource": [
"arn:aws:iam::{{ account_id }}:role/{{ role_name }}"
]
}
]
}
"""
)
return policy_template.render(
account_id=account_id, role_name=self.root_account_policy_name
)

View File

View File

@ -0,0 +1,100 @@
import pytest
from atst.domain.csp.cloud import EnvironmentCreationException
from atst.jobs import (
do_create_environment,
do_create_atat_admin_user,
do_create_environment_baseline,
)
# pylint: disable=unused-import
from tests.mock_boto3 import mock_aws, mock_boto3, AUTH_CREDENTIALS
from tests.factories import EnvironmentFactory
def test_create_environment_succeeds(mock_aws):
environment = EnvironmentFactory.create()
account_id = mock_aws.create_environment(
AUTH_CREDENTIALS, environment.creator, environment
)
assert "account-id" == account_id
@pytest.mark.mock_boto3({"organizations.describe_create_account.failure": True})
def test_create_environment_raises_x_when_account_creation_fails(mock_aws):
environment = EnvironmentFactory.create()
with pytest.raises(EnvironmentCreationException):
mock_aws.create_environment(AUTH_CREDENTIALS, environment.creator, environment)
def test_create_atat_admin_user_succeeds(mock_aws):
root_user_info = mock_aws.create_atat_admin_user(
AUTH_CREDENTIALS, "csp_environment_id"
)
assert {
"id": "user-id",
"username": "user-name",
"resource_id": "user-arn",
"credentials": {
"AccessKeyId": "access-key-id",
"SecretAccessKey": "secret-access-key",
},
} == root_user_info
@pytest.mark.mock_boto3({"iam.create_user.already_exists": True})
def test_create_atat_admin_when_user_already_exists(mock_aws):
root_user_info = mock_aws.create_atat_admin_user(
AUTH_CREDENTIALS, "csp_environment_id"
)
assert {
"id": "user-id",
"username": "user-name",
"resource_id": "user-arn",
"credentials": {
"AccessKeyId": "access-key-id",
"SecretAccessKey": "secret-access-key",
},
} == root_user_info
iam_client = mock_aws.boto3.client("iam")
iam_client.get_user.assert_any_call(UserName="atat")
def test_create_environment_baseline_succeeds(mock_aws):
baseline_info = mock_aws.create_environment_baseline(
AUTH_CREDENTIALS, "csp_environment_id"
)
assert {"policies": [{"BillingReadOnly": "policy-arn"}]} == baseline_info
@pytest.mark.mock_boto3({"iam.create_policy.already_exists": True})
def test_create_environment_baseline_when_policy_already_exists(mock_aws):
baseline_info = mock_aws.create_environment_baseline(
AUTH_CREDENTIALS, "csp_environment_id"
)
assert "policies" in baseline_info
def test_aws_provision_environment(mock_aws, session):
environment = EnvironmentFactory.create()
do_create_environment(mock_aws, environment_id=environment.id)
do_create_atat_admin_user(mock_aws, environment_id=environment.id)
do_create_environment_baseline(mock_aws, environment_id=environment.id)
session.refresh(environment)
assert "account-id" == environment.cloud_id
assert {
"id": "user-id",
"username": "user-name",
"credentials": {
"AccessKeyId": "access-key-id",
"SecretAccessKey": "secret-access-key",
},
"resource_id": "user-arn",
} == environment.root_user_info
assert {
"policies": [{"BillingReadOnly": "policy-arn"}]
} == environment.baseline_info

171
tests/mock_boto3.py Normal file
View File

@ -0,0 +1,171 @@
import pytest
from unittest.mock import Mock
from atst.domain.csp.cloud import AWSCloudProvider
AWS_CONFIG = {
"AWS_ACCESS_KEY_ID": "",
"AWS_SECRET_KEY": "",
"AWS_REGION_NAME": "us-fake-1",
}
AUTH_CREDENTIALS = {
"aws_access_key_id": AWS_CONFIG["AWS_ACCESS_KEY_ID"],
"aws_secret_access_key": AWS_CONFIG["AWS_SECRET_KEY"],
}
def mock_boto_organizations(_config=None, **kwargs):
describe_create_account_status = (
"SUCCEEDED"
if _config.get("organizations.describe_create_account.failure", False) == False
else "FAILED"
)
import boto3
mock = Mock(wraps=boto3.client("organizations", **kwargs))
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/organizations.html#Organizations.Client.create_account
mock.create_account = Mock(
return_value={
"CreateAccountStatus": {
"Id": "create-account-status-id",
"AccountName": "account-name",
"AccountId": "account-id",
"State": "SUCCEEDED",
}
}
)
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/organizations.html#Organizations.Client.describe_create_account_status
mock.describe_create_account_status = Mock(
return_value={
"CreateAccountStatus": {
"Id": "create-account-status-id",
"AccountName": "account-name",
"AccountId": "account-id",
"State": describe_create_account_status,
}
}
)
return mock
def mock_boto_iam(_config=None, **kwargs):
user_already_exists = _config.get("iam.create_user.already_exists", False)
policy_already_exists = _config.get("iam.create_policy.already_exists", False)
def _raise_entity_already_exists(**kwargs):
raise real_iam_client.exceptions.EntityAlreadyExistsException(
{"Error": {}}, "operation-name"
)
import boto3
real_iam_client = boto3.client("iam", **kwargs)
mock = Mock(wraps=real_iam_client)
mock.exceptions.EntityAlreadyExistsException = (
real_iam_client.exceptions.EntityAlreadyExistsException
)
mock.put_user_policy = Mock(return_value={"ResponseMetadata": {}})
if user_already_exists:
mock.create_user = Mock(side_effect=_raise_entity_already_exists)
else:
mock.create_user = Mock(
return_value={
"User": {
"UserId": "user-id",
"Arn": "user-arn",
"UserName": "user-name",
}
}
)
mock.get_user = Mock(
return_value={
"User": {"UserId": "user-id", "Arn": "user-arn", "UserName": "user-name"}
}
)
mock.create_access_key = Mock(
return_value={
"AccessKey": {
"AccessKeyId": "access-key-id",
"SecretAccessKey": "secret-access-key",
}
}
)
if policy_already_exists:
mock.create_policy = Mock(side_effect=_raise_entity_already_exists)
else:
mock.create_policy = Mock(return_value={"Policy": {"Arn": "policy-arn"}})
return mock
def mock_boto_sts(_config=None, **kwargs):
import boto3
mock = Mock(wraps=boto3.client("sts", **kwargs))
mock.assume_role = Mock(
return_value={
"Credentials": {
"AccessKeyId": "access-key-id",
"SecretAccessKey": "secret-access-key",
"SessionToken": "session-token",
}
}
)
return mock
class MockBoto3:
CLIENTS = {
"organizations": mock_boto_organizations,
"iam": mock_boto_iam,
"sts": mock_boto_sts,
}
def __init__(self, config=None):
self.config = config or {}
self.client_instances = {}
def client(self, client_name, **kwargs):
"""
Return a new mock client for the given `client_name`, either by
retrieving it from the `client_instances` cache or by instantiating
it for the first time.
Params should be the same ones you'd pass to `boto3.client`.
"""
if client_name in self.client_instances:
return self.client_instances[client_name]
try:
client_fn = self.CLIENTS[client_name]
client_instance = client_fn(**kwargs, _config=self.config)
self.client_instances[client_name] = client_instance
return client_instance
except KeyError:
raise ValueError(f"MockBoto3: {client_name} client is not yet implemented.")
@pytest.fixture(scope="function")
def mock_boto3(request):
marks = request.node.get_closest_marker("mock_boto3")
if marks:
mock_config = marks.args[0] if len(marks.args) else {}
else:
mock_config = {}
return MockBoto3(mock_config)
@pytest.fixture(scope="function")
def mock_aws(mock_boto3):
return AWSCloudProvider(AWS_CONFIG, boto3=mock_boto3)