|
60 | 60 |
|
61 | 61 | import requests
|
62 | 62 | import yaml
|
| 63 | +from requests.auth import AuthBase |
63 | 64 | from requests_file import FileAdapter
|
64 | 65 |
|
65 | 66 | from awsrun.cache import PersistentExpiringValue
|
@@ -916,6 +917,174 @@ def __init__(self, name_regexp=None):
|
916 | 917 | super().__init__(accts)
|
917 | 918 |
|
918 | 919 |
|
| 920 | +class CSVParser: |
| 921 | + """Returns a list of dicts from a buffer of CSV text. |
| 922 | +
|
| 923 | + To override options passed to `csv.DictReader`, specify them as keyword |
| 924 | + arguments in the constructor. By default, the `delimiter` is `","` and |
| 925 | + `skipinitialspace` is `True`. |
| 926 | + """ |
| 927 | + |
| 928 | + def __init__(self, **kwargs): |
| 929 | + self.kwargs = kwargs |
| 930 | + self.kwargs.setdefault("delimiter", ",") |
| 931 | + self.kwargs.setdefault("skipinitialspace", True) |
| 932 | + |
| 933 | + def __call__(self, text): |
| 934 | + buf = io.StringIO(text.strip()) |
| 935 | + return list(csv.DictReader(buf, **self.kwargs)) |
| 936 | + |
| 937 | + |
| 938 | +class JSONParser: |
| 939 | + """Returns a list or dict from a buffer of JSON-formatted text. |
| 940 | +
|
| 941 | + To override options passed to `json.loads`, specify them as keyword |
| 942 | + arguments in the constructor. |
| 943 | + """ |
| 944 | + |
| 945 | + def __init__(self, **kwargs): |
| 946 | + self.kwargs = kwargs |
| 947 | + |
| 948 | + def __call__(self, text): |
| 949 | + return json.loads(text, **self.kwargs) |
| 950 | + |
| 951 | + |
| 952 | +class YAMLParser: |
| 953 | + """Returns a list or dict from a buffer of YAML-formatted text. |
| 954 | +
|
| 955 | + To override options passed to `yaml.safe_load`, specify them as keyword |
| 956 | + arguments in the constructor. |
| 957 | + """ |
| 958 | + |
| 959 | + def __init__(self, **kwargs): |
| 960 | + self.kwargs = kwargs |
| 961 | + |
| 962 | + def __call__(self, text): |
| 963 | + return yaml.safe_load(text, **self.kwargs) |
| 964 | + |
| 965 | + |
| 966 | +class HTTPOAuth2(AuthBase): |
| 967 | + """Attaches an OAuth2 bearer token to the given `requests.Request` object. |
| 968 | +
|
| 969 | + Use `token_url` to specify the token provider's URL. The `client_id` and |
| 970 | + `client_secret` specify the credentials used to authenticate with the |
| 971 | + token provider. Three additional keyword parameters are accepted: |
| 972 | +
|
| 973 | + `scope` |
| 974 | + : Default is "AppIdClaimsTrust". |
| 975 | +
|
| 976 | + `grant_type` |
| 977 | + : Default is "client_credentials". |
| 978 | +
|
| 979 | + `intent` |
| 980 | + : Default is "awsrun account loader plugin" |
| 981 | + """ |
| 982 | + |
| 983 | + def __init__( |
| 984 | + self, |
| 985 | + token_url, |
| 986 | + username, |
| 987 | + password, |
| 988 | + scope="AppIdClaimsTrust", |
| 989 | + grant_type="client_credentials", |
| 990 | + intent="awsrun account loader plugin", |
| 991 | + ): |
| 992 | + self.url = token_url |
| 993 | + self.data = {} |
| 994 | + self.data["client_id"] = username |
| 995 | + self.data["client_secret"] = password |
| 996 | + self.data["scope"] = scope |
| 997 | + self.data["grant_type"] = grant_type |
| 998 | + self.data["intent"] = intent |
| 999 | + |
| 1000 | + def _get_token(self): |
| 1001 | + resp = requests.post(self.url, data=self.data) |
| 1002 | + resp.raise_for_status() |
| 1003 | + return resp.json()["access_token"] |
| 1004 | + |
| 1005 | + def __call__(self, req): |
| 1006 | + req.headers["Authorization"] = f"Bearer {self._get_token()}" |
| 1007 | + return req |
| 1008 | + |
| 1009 | + |
| 1010 | +class URLAccountLoader(MetaAccountLoader): |
| 1011 | + """Returns an `AccountLoader` with accounts loaded from a URL. |
| 1012 | +
|
| 1013 | + Loaded accounts will include metadata associated with each account in the |
| 1014 | + document retrieved from the `url`. File based URLs can be used to load |
| 1015 | + data from a local file. This data will be parsed as JSON by default. To |
| 1016 | + override, use `parser` to specify a callable that accepts the text and |
| 1017 | + returns a list or dict of accounts (see `MetaAccountLoader`). To cache the |
| 1018 | + results, specify a non-zere number of seconds in `max_age`. The default |
| 1019 | + location on disk is the system temp directory in a file called |
| 1020 | + `awsrun.dat`, which can be overrided via `cache_path`. |
| 1021 | +
|
| 1022 | + Given the following JSON: |
| 1023 | +
|
| 1024 | + { |
| 1025 | + "Accounts": [ |
| 1026 | + {"id": "100200300400", "env": "prod", "status": "active"}, |
| 1027 | + {"id": "200300400100", "env": "non-prod", "status": "active"}, |
| 1028 | + {"id": "300400100200", "env": "non-prod", "status": "suspended"} |
| 1029 | + ] |
| 1030 | + } |
| 1031 | +
|
| 1032 | + The account loader will build account objects with the following attribute |
| 1033 | + names: `id`, `env`, `status`. Assume the above JSON is returned from |
| 1034 | + http://example.com/accts.json: |
| 1035 | +
|
| 1036 | + loader = URLAccountLoader('http://example.com/accts.json', path=['Accounts']) |
| 1037 | + accts = loader.accounts() |
| 1038 | +
|
| 1039 | + # Let's inspect the 1st account object and its metadata |
| 1040 | + assert accts[0].id == '100200300400' |
| 1041 | + assert accts[0].env == 'prod' |
| 1042 | + assert accts[0].status == 'active' |
| 1043 | +
|
| 1044 | + URLAccountLoader is a subclass of the `MetaAccountLoader`, which loads |
| 1045 | + accounts from a set of dicts. As such, the remainder of the parameters in |
| 1046 | + the constructor -- `id_attr`, `path`, `str_template`, `include_attrs`, and |
| 1047 | + `exclude_attrs` -- are defined in the constructor of `MetaAccountLoader`. |
| 1048 | + """ |
| 1049 | + |
| 1050 | + def __init__( |
| 1051 | + self, |
| 1052 | + url, |
| 1053 | + parser=JSONParser(), |
| 1054 | + auth=None, |
| 1055 | + max_age=0, |
| 1056 | + id_attr="id", |
| 1057 | + path=None, |
| 1058 | + str_template=None, |
| 1059 | + include_attrs=None, |
| 1060 | + exclude_attrs=None, |
| 1061 | + no_verify=False, |
| 1062 | + cache_path=None, |
| 1063 | + ): |
| 1064 | + |
| 1065 | + session = requests.Session() |
| 1066 | + session.mount("file://", FileAdapter()) |
| 1067 | + |
| 1068 | + def load_cache(): |
| 1069 | + r = session.get(url, auth=auth, verify=not no_verify) |
| 1070 | + r.raise_for_status() |
| 1071 | + return parser(r.text) |
| 1072 | + |
| 1073 | + if not cache_path: |
| 1074 | + cache_path = Path(tempfile.gettempdir(), "awsrun.dat") |
| 1075 | + |
| 1076 | + accts = PersistentExpiringValue(load_cache, cache_path, max_age=max_age) |
| 1077 | + |
| 1078 | + super().__init__( |
| 1079 | + accts.value(), |
| 1080 | + id_attr=id_attr, |
| 1081 | + path=[] if path is None else path, |
| 1082 | + str_template=str_template, |
| 1083 | + include_attrs=[] if include_attrs is None else include_attrs, |
| 1084 | + exclude_attrs=[] if exclude_attrs is None else exclude_attrs, |
| 1085 | + ) |
| 1086 | + |
| 1087 | + |
919 | 1088 | class AbstractAccount:
|
920 | 1089 | """Abstract base class used by `MetaAccountLoader` to represent an account and its metadata.
|
921 | 1090 |
|
|
0 commit comments