├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── federator ├── __init__.py └── federator.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info/ 4 | *.egg 5 | *.py[cod] 6 | __pycache__/ 7 | *.so 8 | *~ 9 | *.swp 10 | credentials.dat 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cevo Australia, Pty Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: distclean 2 | 3 | .DEFAULT: usage 4 | 5 | usage: 6 | @echo "Make targets:" 7 | @echo " distclean: clean out distribution package files" 8 | 9 | distclean: 10 | rm -rf dist build *.egg-info 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google AWS Federator 2 | 3 | Tools to manage configuration and maintenance of federating identity between Google Apps (as the 4 | identity provider) and AWS. 5 | 6 | ## Installation 7 | 8 | 1. Clone this repo 9 | 1. In the top-level directory, run `pip install --upgrade .` (if you want to install to just a 10 | user-specific directory, user `pip install --upgrade --user .`) 11 | 1. That's it! 12 | 13 | ## Setup 14 | 15 | ### Prerequisites 16 | 17 | 1. Google Apps set up already. You're going to need to generate secrets for the federator. 18 | 1. You need to know your unique Google 'customer ID'. You can get this from the Google Admin 19 | Console, via `Security -> Set up Single Sign On (SSO)` -- the customer ID is the bit on the 20 | tail end of the Entity ID URL that looks like this: 21 | `https://accounts.google.com/o/saml2?idpid=XXXXXXXXX` (the "XXXXXXXXX" part) 22 | 23 | ### Configuration 24 | 25 | 1. You'll need to register the federator as an external app with Google Apps first: 26 | 1. Log in to your Google Apps Developer Console via https://console.developers.google.com 27 | 1. At the top of the page, there's a pulldown menu; by default, it's marked `Select a 28 | project`. If you click on it, you can choose `Create a Project`, which you should do 29 | 1. Name the project `Federator` (you can call it something else, but you'll have to 30 | translate the rest of this documentation). You can change the App Engine location if it 31 | matters to you under `Advanced Options` but that's not required. You'll also have to 32 | agree to the T&Cs before creating the project, and choose whether to opt-in to the 33 | spam^H^H^H^Hmarketing emails. 34 | 1. Click `Create` to create your new project. 35 | 1. We will have to enable the admin APIs for this project before we create the credentials, 36 | so in the main API Manager screen, click `Admin SDK` under `Google Apps APIs` (you may 37 | have to click on the `More...` button underneath that heading). When the API screen 38 | appears, click `Enable`. 39 | 1. Authentication and authorization for Federator will be via the OAuth2 40 | [Installed Apps](https://developers.google.com/identity/protocols/OAuth2InstalledApp) 41 | pattern, so we will need to create some client identifiers. At the left-hand-side of the screen, 42 | click `Credentials` and then when the popup appears, choose `Create credential` and, from 43 | the pulldown menu, choose `OAuth2 Client ID`. From the selection of client types, choose 44 | `Other`, and set the client name to be `Federator CLI`. Click `Create` 45 | 1. There'll be a popup which displays your OAuth2 Client ID and the client secret. Store 46 | these safely, as they will be used to identify your Federator CLI to Google. 47 | 1. You will need to set up a SAML Identity Provider within your AWS account, and IAM roles which 48 | trust that Identity Provider. You can find an excellent tutorial on doing that at 49 | [Amazon's Security Blog](https://blogs.aws.amazon.com/security/post/TxT8XK9DVM0MGP/How-to-Set-Up-Federated-Single-Sign-On-to-AWS-Using-Google-Apps). Note that the blog post tells you how to 50 | do everything, including setting up the Google schema; when you get to `Step 4: Add the AWS 51 | SAML attributes to your Google Apps user profile`, you can use the Federator to create the 52 | custom Schema (see below for usage). 53 | 1. Step 5 in that document will have to be carried out manually for the moment -- I don't have 54 | code to create the SAML App in Google yet. 55 | 1. Step 6 in that guide can be done using the Federator -- see below for usage on 56 | adding/removing user roles. 57 | 58 | ### First Run 59 | 60 | In order to grant access to the Federator to modify your Google Apps Domain Users, you'll need 61 | to authenticate, via the OAuth2 browser flow, as a user who has the appropriate access. 62 | The Federator will need to have access to modify custom user schemas (to create the schema 63 | for the federated roles) and to modify users (to assign those roles from within the new 64 | custom schema to users). 65 | 66 | ``` 67 | locahost$ federator init -I -C 68 | ``` 69 | 70 | The script will cause a browser to be opened, where you can sign in as an appropriate user and 71 | approve the access. Once this has been done once, the credentials will be stored in a file 72 | under `$HOME/.federator` -- these credentials expire after one hour after they become idle, 73 | but they include a refresh token and will be transparently re-issued on demand, provided your 74 | Google Apps Domain User is still valid. 75 | 76 | Credential files are specific to the scope of access that they need. 77 | 78 | ### Subsequent Runs 79 | 80 | Once the persistent credentials file(s) have been created, you will not have to go through the browser 81 | auth steps again while the credentials are valid. 82 | 83 | ## Usage 84 | 85 | ### Creating AWS SSO Schema 86 | 87 | You can use Federator to add the required custom schema to your Google Apps Domain. This custom 88 | schema is required to define the "shape" of the data that will be used when passing SAML 89 | assertions from Google (as an Identity Provider, or IdP) to Amazon (as the Service Provider, or 90 | SP). 91 | 92 | TL;DR: Federator can make your Google Apps Domain the right shape to use AWS via Google Auth. 93 | 94 | ``` 95 | localhost$ federator schema -C create 96 | ``` 97 | 98 | If the custom schema has already been created, nothing will be done. If the custom schema has 99 | been created, but is the wrong "shape", then nothing will still be done. If you want to update 100 | the custom schema, you will have to delete the existing one, and create a new one (see below for 101 | `delete` functionality) 102 | 103 | ### Deleting the AWS SSO Schema 104 | 105 | Federator can "clean up" by removing the custom AWS SSO schema. Simply: 106 | 107 | ``` 108 | localhost$ federator schema -C delete 109 | ``` 110 | 111 | If the custom schema has already been deleted, nothing will happen. 112 | 113 | It also seems like the schema cannot be deleted if it's in use -- in other words, if any users 114 | have attributes that are described by the custom schema. 115 | 116 | ### Validating whether the custom AWS SSO Schema has been created 117 | 118 | Federator can verify that the custom schema has been created: 119 | 120 | ``` 121 | localhost$ federator schema -C verify 122 | Schema exists 123 | ``` 124 | 125 | Federator will also return exit code 0 if the schema exists, and exit code 1 if it doesn't. 126 | 127 | ### Examining the created custom AWS SSO Schema 128 | 129 | You can dump out the schema quite simply: 130 | 131 | ``` 132 | localhost$ federator schema -C show 133 | { 134 | "etag": "\"XAsypnOPUm9mxokHB31cC07VbXs/hDT2ACjbO2nrT_uVUpNU3VQ_QzU\"", 135 | "fields": [ 136 | { 137 | "etag": "\"XRsypGOPUmlmxokHB51cC07Vb3s/mF8fcTzvlteJZ0DIlUljKGlhlfw\"", 138 | "fieldId": "CfnqfA4pRxqP8i8ueR1wew==", 139 | "fieldName": "role", 140 | "fieldType": "STRING", 141 | "kind": "admin#directory#schema#fieldspec", 142 | "multiValued": true, 143 | "readAccessType": "ADMINS_AND_SELF" 144 | } 145 | ], 146 | "kind": "admin#directory#schema", 147 | "schemaId": "JfLx5all7--VnMs-H39aNQ==", 148 | "schemaName": "SSO" 149 | } 150 | ``` 151 | -------------------------------------------------------------------------------- /federator/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import federator 4 | import sys 5 | import argparse 6 | 7 | def init(args): 8 | args = vars(args) 9 | federator.Federator(clientId=args['clientid'], clientSecret=args['clientsecret']) 10 | 11 | def schema_verify(args): 12 | args = vars(args) 13 | schema = federator.Schema(customerId=args['customerid']) 14 | if schema.exists(): 15 | print("Custom SSO schema exists") 16 | else: 17 | print("Custom SSO schema does not exist") 18 | sys.exit(1) 19 | 20 | def schema_create(args): 21 | args = vars(args) 22 | schema = federator.Schema(customerId=args['customerid']) 23 | if schema.exists(): 24 | return True 25 | 26 | if schema.create(): 27 | print("Created custom SSO schema") 28 | else: 29 | print("Could not create custom SSO schema") 30 | sys.exit(1) 31 | 32 | def schema_delete(args): 33 | args = vars(args) 34 | schema = federator.Schema(customerId=args['customerid']) 35 | if not schema.exists(): 36 | return True 37 | 38 | if schema.delete(): 39 | print("Deleted custom SSO schema") 40 | else: 41 | print("Could not delete custom SSO schema -- is it in use?") 42 | sys.exit(1) 43 | 44 | def schema_show(args): 45 | args = vars(args) 46 | schema = federator.Schema(customerId=args['customerid']) 47 | if not schema.exists(): 48 | sys.exit(1) 49 | 50 | print("%s" % schema.get()) 51 | 52 | def user_add(args): 53 | args = vars(args) 54 | user = federator.User(userKey=args['userkey']) 55 | added = user.add_role(roleArn=args['rolearn'], providerArn=args['providerarn']) 56 | if added: 57 | print("Updated user %s" % args['userkey']) 58 | else: 59 | print("Could not add new role to user %s" % args['userkey']) 60 | sys.exit(1) 61 | 62 | def user_remove(args): 63 | args = vars(args) 64 | print("removing a role from a user: %s" % args) 65 | user = federator.User(userKey=args['userkey']) 66 | if args.has_key('customtype') and args['customtype'] is not None: 67 | removed = user.remove_role(customType=args['customtype']) 68 | elif (args.has_key('rolearn') and args.has_key('providerarn')): 69 | removed = user.remove_role(roleArn=args['rolearn'], providerArn=args['providerarn']) 70 | else: 71 | print("You must specify either the Custom Type, or both Role and Provider ARNs") 72 | sys.exit(1) 73 | 74 | if removed: 75 | print("Removed role %s from user %s" % (removed, args['userkey'])) 76 | else: 77 | print("Could not remove role from user %s" % args['userkey']) 78 | sys.exit(1) 79 | 80 | def user_show(args): 81 | args = vars(args) 82 | user = federator.User(userKey=args['userkey']) 83 | print(user.get()) 84 | 85 | def user_duration(args): 86 | args = vars(args) 87 | user = federator.User(userKey=args['userkey']) 88 | user.set_duration(duration=args['duration']) 89 | 90 | def main(): 91 | parser = argparse.ArgumentParser(prog="federator", description="Manage Google Apps configurations for AWS Single Sign On") 92 | main_subparsers = parser.add_subparsers(help="subcommand help") 93 | 94 | parser_init = main_subparsers.add_parser("init", help="Initial setup of Federator") 95 | parser_schema = main_subparsers.add_parser("schema", help="Operations on custom schema") 96 | parser_user = main_subparsers.add_parser("user", help="User management") 97 | 98 | parser_init.add_argument("-I", "--clientid", required=True) 99 | parser_init.add_argument("-S", "--clientsecret", required=True) 100 | parser_init.set_defaults(func=init) 101 | 102 | parser_schema.add_argument("-C", "--customerid", required=True) 103 | 104 | schema_subparser = parser_schema.add_subparsers(help="Schema subcommand help") 105 | parser_schema_create = schema_subparser.add_parser("create", help="Create the custom schema") 106 | parser_schema_create.set_defaults(func=schema_create) 107 | 108 | parser_schema_delete = schema_subparser.add_parser("delete", help="Delete the custom schema") 109 | parser_schema_delete.set_defaults(func=schema_delete) 110 | 111 | parser_schema_verify = schema_subparser.add_parser("verify", help="Check the custom schema") 112 | parser_schema_verify.set_defaults(func=schema_verify) 113 | 114 | parser_schema_show = schema_subparser.add_parser("show", help="Print the custom schema") 115 | parser_schema_show.set_defaults(func=schema_show) 116 | 117 | parser_user.add_argument("-U", "--userkey", required=True) 118 | user_subparser = parser_user.add_subparsers(help="User subcommand help") 119 | 120 | parser_user_add = user_subparser.add_parser("add", help="Add a role to a user") 121 | parser_user_add.add_argument("-R", "--rolearn", required=True, help="The ARN of the AWS Role") 122 | parser_user_add.add_argument("-P", "--providerarn", required=True, help="The ARN of the AWS Identity Provider") 123 | 124 | parser_user_add.set_defaults(func=user_add) 125 | 126 | parser_user_remove = user_subparser.add_parser("remove", help="Remove a role from a user. You must specify either the custom type name, or both the role/provider ARNs") 127 | parser_user_remove.add_argument("-R", "--rolearn", help="The ARN of the AWS Role to remove") 128 | parser_user_remove.add_argument("-P", "--providerarn", help="The ARN of the AWS Identity Provider associated with the role to remove") 129 | parser_user_remove.add_argument("-T", "--customtype", help="The custom Type name of the role to remove") 130 | 131 | parser_user_remove.set_defaults(func=user_remove) 132 | 133 | parser_user_show = user_subparser.add_parser("show", help="Show the current shape of a user") 134 | parser_user_show.set_defaults(func=user_show) 135 | 136 | parser_user_duration = user_subparser.add_parser("duration", help="Set the duration of sessions for this user") 137 | parser_user_duration.add_argument("-D", "--duration", help="The duration of a user session in seconds") 138 | parser_user_duration.set_defaults(func=user_duration) 139 | 140 | args = parser.parse_args() 141 | # have to clean out our command-line args or they get swallowed twice during init 142 | sys.argv = [''] 143 | args.func(args) 144 | 145 | if __name__ == "__main__": 146 | main() 147 | -------------------------------------------------------------------------------- /federator/federator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from apiclient.discovery import build 4 | import httplib2 5 | from oauth2client import tools 6 | from oauth2client.file import Storage 7 | from oauth2client.client import AccessTokenRefreshError 8 | from oauth2client.client import OAuth2WebServerFlow 9 | import json 10 | from googleapiclient.errors import HttpError 11 | import re 12 | import os 13 | import stat 14 | import sys 15 | from Crypto.Hash import SHA256 16 | 17 | class Federator(object): 18 | # we need multiple scopes, because we need to define a custom schema 19 | # before being able to add roles defined within that schema to a user 20 | 21 | def __init__(self, clientId=None, clientSecret=None, scope=None): 22 | if scope is None: 23 | raise Exception('No scope provided') 24 | 25 | store = os.path.expanduser('~/.federator') 26 | try: 27 | os.mkdir(store, 0700) 28 | except OSError as err: 29 | if err.strerror != 'File exists': 30 | print("Cannot create credential store") 31 | sys.exit(1) 32 | 33 | # Verify the permissions 34 | res = os.stat(store) 35 | if not stat.S_ISDIR(res.st_mode): 36 | print("%s is not a directory" % store) 37 | sys.exit(1) 38 | 39 | if (res.st_mode & stat.S_IRWXG) or (res.st_mode & stat.S_IRWXO): 40 | print("Federator credentials directory %s is not safe; must be mode 0700" % store) 41 | sys.exit(1) 42 | 43 | scope_hash = SHA256.new() 44 | scope_hash.update(scope) 45 | credfile = os.path.join(store, scope_hash.hexdigest()) 46 | 47 | # if the file exists already, make sure its permissions are safe 48 | try: 49 | res = os.stat(credfile) 50 | 51 | if not stat.S_ISREG(res.st_mode): 52 | print("Federator credentials file %s is not a regular file" % credfile) 53 | sys.exit(1) 54 | 55 | if (res.st_mode & stat.S_IRWXG) or (res.st_mode & stat.S_IRWXO) or (res.st_mode & stat.S_IXUSR): 56 | print("Federator credentials file %s is not safe; must be mode 0600" % credfile) 57 | sys.exit(1) 58 | 59 | except OSError as err: 60 | if err.errno != 2: 61 | raise 62 | 63 | storage = Storage(credfile) 64 | credentials = storage.get() 65 | 66 | if credentials is None or credentials.invalid: 67 | if clientId is None or clientSecret is None: 68 | raise Exception("ERROR: No credentials defined. You must supply the CLIENTID and CLIENTSECRET arguments") 69 | 70 | flow = OAuth2WebServerFlow(clientId, clientSecret, scope) 71 | credentials = tools.run_flow(flow, storage, tools.argparser.parse_args()) 72 | 73 | try: 74 | os.chmod(credfile, 0600) 75 | except: 76 | print("Cannot set mode of credentials file") 77 | raise 78 | 79 | self.http = httplib2.Http() 80 | self.http = credentials.authorize(self.http) 81 | self.service = build('admin', 'directory_v1', http=self.http) 82 | 83 | class User(Federator): 84 | roleArnShape = re.compile('^arn:aws:iam::(\d{12}):role/(\w+)') 85 | providerArnShape = re.compile('^arn:aws:iam::\d{12}:saml-provider/\w+') 86 | 87 | patchShape = """ 88 | { 89 | "customSchemas": { 90 | "AWS-SSO": { 91 | "role": [ 92 | { 93 | "value": "%s,%s", 94 | "customType": "%s" 95 | } 96 | ] 97 | } 98 | } 99 | } 100 | """ 101 | 102 | user_scope = "https://www.googleapis.com/auth/admin.directory.user" 103 | 104 | def __init__(self, userKey=None, clientId=None, clientSecret=None): 105 | super(User, self).__init__(scope=self.user_scope, clientId=clientId, clientSecret=clientSecret) 106 | self.userKey = userKey 107 | 108 | def get(self): 109 | request = self.service.users().get(userKey=self.userKey, projection='full') 110 | return json.dumps(request.execute(), sort_keys=True, indent=4, separators=(',', ': ')) 111 | 112 | def get_current(self): 113 | current = json.loads(self.get()) 114 | if current.has_key('customSchemas') and current['customSchemas'].has_key('AWS-SSO'): 115 | current = current['customSchemas']['AWS-SSO'] 116 | else: 117 | current = {'role':[], 'duration':3600} 118 | 119 | return current 120 | 121 | def set_duration(self, duration=3600): 122 | current = self.get_current() 123 | if current.has_key('duration') and current['duration'] == duration: 124 | return 125 | 126 | current['duration'] = duration 127 | patch = {'customSchemas':{'AWS-SSO':current}} 128 | request = self.service.users().patch(userKey=self.userKey, body=patch) 129 | response = request.execute() 130 | 131 | 132 | def add_role(self, roleArn=None, providerArn=None): 133 | # Make sure the ARNs are the right kind of shape 134 | roleMatch = self.roleArnShape.match(roleArn) 135 | if not roleMatch: 136 | print("Role ARN is incorrect; must be 'arn:aws:iam:::role/'") 137 | return False 138 | 139 | providerMatch = self.providerArnShape.match(providerArn) 140 | if not providerMatch: 141 | print("Provider ARN is incorrect; must be 'arn:aws:iam:::saml-provider/'") 142 | return False 143 | 144 | # We have to _add_ the patch to the existing set, 145 | # because the whole custom schema is replaced. We will only add it if the 146 | # , tuple is different and if the customType name 147 | # is different. This means that we can't have two roles 148 | # with the same name in the same account but with different SAML providers 149 | current = self.get_current() 150 | typeName = roleMatch.group(1) + '-' + roleMatch.group(2) 151 | shape = self.patchShape % (roleArn, providerArn, typeName) 152 | patch = json.loads(shape) 153 | 154 | do_add = True 155 | for role in current['role']: 156 | if role['value'] == roleArn + "," + providerArn: 157 | do_add = False 158 | continue 159 | 160 | if role['customType'] == typeName: 161 | do_add = False 162 | continue 163 | 164 | patch['customSchemas']['AWS-SSO']['role'].append(role) 165 | 166 | if not do_add: 167 | print("That user already has access to that role") 168 | return True 169 | 170 | if current.has_key('duration'): 171 | patch['customSchemas']['AWS-SSO']['duration'] = current['duration'] 172 | 173 | request = self.service.users().patch(userKey=self.userKey, body=patch) 174 | response = request.execute() 175 | return roleMatch.group(2) 176 | 177 | def remove_role(self, roleArn=None, providerArn=None, customType=None): 178 | if roleArn is None and providerArn is None and customType is None: 179 | print("ERROR: You must specify the Role ARN and the Provider ARN, or the Custom Type") 180 | return False 181 | 182 | # Removing a role is similar to adding one -- we need to update the current 183 | # custom schema set and re-patch the user. 184 | current = json.loads(self.get()) 185 | if current.has_key('customSchemas') and current['customSchemas'].has_key('AWS-SSO'): 186 | current = current['customSchemas']['AWS-SSO'] 187 | else: 188 | # there are no custom roles associated 189 | return "user has no roles" 190 | 191 | new_shape = { 192 | 'customSchemas': { 193 | 'AWS-SSO': { 194 | 'role': [] 195 | } 196 | } 197 | } 198 | 199 | removed = None 200 | for role in current['role']: 201 | # copy all the roles across to the new list, unless it matches the one 202 | # that we're removing 203 | if customType: 204 | if role['customType'] == customType: 205 | removed = customType 206 | continue 207 | elif roleArn and providerArn: 208 | value = roleArn + "," + providerArn 209 | if role['value'] == value: 210 | removed = value 211 | continue 212 | else: 213 | print("I don't know how I got here") 214 | sys.exit(1) 215 | 216 | new_shape['customSchemas']['AWS-SSO']['role'].append(role) 217 | 218 | if removed: 219 | request = self.service.users().patch(userKey=self.userKey, body=new_shape) 220 | response = request.execute() 221 | 222 | return removed 223 | 224 | 225 | 226 | class Schema(Federator): 227 | rawSchema = """ 228 | { 229 | "fields": 230 | [ 231 | { 232 | "fieldName": "role", 233 | "fieldType": "STRING", 234 | "readAccessType": "ADMINS_AND_SELF", 235 | "multiValued": true 236 | }, 237 | { 238 | "fieldName": "duration", 239 | "fieldType": "INT64", 240 | "readAccessType": "ADMINS_AND_SELF" 241 | } 242 | ], 243 | "schemaName": "AWS-SSO" 244 | } 245 | """ 246 | customSchema = json.loads(rawSchema) 247 | 248 | schema_scope = "https://www.googleapis.com/auth/admin.directory.userschema" 249 | 250 | def __init__(self, customerId=None, clientId=None, clientSecret=None): 251 | super(Schema, self).__init__(scope=self.schema_scope, clientId=clientId, clientSecret=clientSecret) 252 | self.customerId = customerId 253 | 254 | def list(self): 255 | request = self.service.schemas().list(customerId=self.customerId) 256 | response = request.execute() 257 | return response 258 | 259 | def get(self): 260 | key = self.customSchema['schemaName'] 261 | request = self.service.schemas().get(customerId=self.customerId, schemaKey=key) 262 | response = request.execute() 263 | return json.dumps(response, sort_keys=True, indent=4, separators=(',', ': ')) 264 | 265 | def exists(self): 266 | request = self.service.schemas().list(customerId=self.customerId) 267 | response = request.execute() 268 | for schema in response['schemas']: 269 | if schema['schemaName'] == "AWS-SSO": 270 | return True 271 | 272 | return False 273 | 274 | def create(self): 275 | request = self.service.schemas().insert(customerId=self.customerId, body=self.customSchema) 276 | 277 | try: 278 | response = request.execute() 279 | except HttpError as err: 280 | print(err.resp) 281 | if err.resp['status'] == '412': 282 | print("Schema already exists") 283 | else: 284 | raise 285 | 286 | return True 287 | 288 | def delete(self): 289 | key = self.customSchema['schemaName'] 290 | request = self.service.schemas().delete(customerId=self.customerId, schemaKey=key) 291 | 292 | try: 293 | response = request.execute() 294 | except HttpError as err: 295 | if err.resp['status'] == '400': 296 | return False 297 | else: 298 | raise 299 | 300 | return True 301 | 302 | 303 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | 3 | See: 4 | https://packaging.python.org/en/latest/distributing.html 5 | https://github.com/pypa/sampleproject 6 | """ 7 | 8 | # Always prefer setuptools over distutils 9 | from setuptools import setup, find_packages 10 | # To use a consistent encoding 11 | from codecs import open 12 | from os import path 13 | 14 | here = path.abspath(path.dirname(__file__)) 15 | 16 | # Get the long description from the README file 17 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 18 | long_description = f.read() 19 | 20 | setup( 21 | name='google-aws-federator', 22 | 23 | # Versions should comply with PEP440. For a discussion on single-sourcing 24 | # the version across setup.py and the project code, see 25 | # https://packaging.python.org/en/latest/single_source_version.html 26 | version='0.0.1', 27 | 28 | description='Manage SAML Identity federation from Google Apps to AWS', 29 | long_description=long_description, 30 | 31 | # The project's main homepage. 32 | url='https://github.com/cevoaustralia/google-aws-federator', 33 | 34 | # Author details 35 | author='Colin Panisset', 36 | author_email='colin.panisset@cevo.com.au', 37 | 38 | # Choose your license 39 | license='MIT', 40 | 41 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 42 | classifiers=[ 43 | # How mature is this project? Common values are 44 | # 3 - Alpha 45 | # 4 - Beta 46 | # 5 - Production/Stable 47 | 'Development Status :: 3 - Alpha', 48 | 49 | # Indicate who your project is intended for 50 | 'Intended Audience :: Developers', 51 | 'Intended Audience :: System Administrators', 52 | 'Topic :: Security', 53 | 'Topic :: System :: Systems Administration :: Authentication/Directory', 54 | 55 | # Pick your license as you wish (should match "license" above) 56 | 'License :: OSI Approved :: MIT License', 57 | 58 | # Specify the Python versions you support here. In particular, ensure 59 | # that you indicate whether you support Python 2, Python 3 or both. 60 | 'Programming Language :: Python :: 2', 61 | 'Programming Language :: Python :: 2.6', 62 | 'Programming Language :: Python :: 2.7', 63 | ], 64 | 65 | # What does your project relate to? 66 | keywords='saml sso federated identity google aws', 67 | 68 | # You can just specify the packages manually here if your project is 69 | # simple. Or you can use find_packages(). 70 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 71 | 72 | # Alternatively, if you want to distribute just a my_module.py, uncomment 73 | # this: 74 | # py_modules=["my_module"], 75 | 76 | # List run-time dependencies here. These will be installed by pip when 77 | # your project is installed. For an analysis of "install_requires" vs pip's 78 | # requirements files see: 79 | # https://packaging.python.org/en/latest/requirements.html 80 | # install_requires=['peppercorn'], 81 | install_requires=['google-api-python-client', 'oauth2client', 'pycrypto'], 82 | 83 | # List additional groups of dependencies here (e.g. development 84 | # dependencies). You can install these using the following syntax, 85 | # for example: 86 | # $ pip install -e .[dev,test] 87 | # extras_require={ 88 | # 'dev': ['check-manifest'], 89 | # 'test': ['coverage'], 90 | # }, 91 | 92 | # If there are data files included in your packages that need to be 93 | # installed, specify them here. If using Python 2.6 or less, then these 94 | # have to be included in MANIFEST.in as well. 95 | # package_data={ 96 | # 'sample': ['package_data.dat'], 97 | # }, 98 | 99 | # Although 'package_data' is the preferred approach, in some case you may 100 | # need to place data files outside of your packages. See: 101 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 102 | # In this case, 'data_file' will be installed into '/my_data' 103 | # data_files=[('my_data', ['data/data_file'])], 104 | 105 | # To provide executable scripts, use entry points in preference to the 106 | # "scripts" keyword. Entry points provide cross-platform support and allow 107 | # pip to create the appropriate form of executable for the target platform. 108 | entry_points={ 109 | 'console_scripts': [ 110 | 'federator=federator:main', 111 | ], 112 | }, 113 | ) 114 | --------------------------------------------------------------------------------