├── .gitignore ├── MANIFEST.in ├── README.rst ├── UNLICENSE ├── assets └── box-sketch.png ├── setup.py └── stormpath-export /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.egg-info 3 | build 4 | stormpath-exports 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst setup.py UNLICENSE 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Stormpath is Joining Okta 3 | ========================== 4 | 5 | We are incredibly excited to announce that `Stormpath is joining forces with Okta `_. Please visit `the Migration FAQs `_ for a detailed look at what this means for Stormpath users. 6 | 7 | We're available to answer all questions at `support@stormpath.com `_. 8 | 9 | stormpath-export 10 | ================ 11 | 12 | Easily export your Stormpath user data. 13 | 14 | 15 | .. image:: https://img.shields.io/pypi/v/stormpath-export.svg 16 | :alt: stormpath-export Release 17 | :target: https://pypi.python.org/pypi/stormpath-export 18 | 19 | .. image:: https://img.shields.io/pypi/dm/stormpath-export.svg 20 | :alt: stormpath-export Downloads 21 | :target: https://pypi.python.org/pypi/stormpath-export 22 | 23 | .. image:: https://api.codacy.com/project/badge/grade/d7904abc80dc40a39e8b1850f10000ea 24 | :alt: stormpath-export Code Quality 25 | :target: https://www.codacy.com/app/r/stormpath-export 26 | 27 | .. image:: https://img.shields.io/travis/stormpath/stormpath-export.svg 28 | :alt: stormpath-export Build 29 | :target: https://travis-ci.org/stormpath/stormpath-export 30 | 31 | .. image:: https://github.com/rdegges/stormpath-export/raw/master/assets/box-sketch.png 32 | :alt: Box Sketch 33 | 34 | Stormpath is Joining Okta 35 | ------------------------- 36 | 37 | We are incredibly excited to announce that `Stormpath is joining forces with Okta `_. Please visit `the Migration FAQs `_ for a detailed look at what this means for Stormpath users. 38 | 39 | We're available to answer all questions at `support@stormpath.com `_. 40 | 41 | 42 | Purpose 43 | ------- 44 | 45 | `Stormpath`_ is one of my favorite API services. They provide a scalable, 46 | simple, and secure user management API which makes building scalable systems 47 | simple. 48 | 49 | Whenever I talk to people about using `Stormpath`_, the same question 50 | invariably comes up: "Is it easy to export my user data out of Stormpath? Or 51 | am I locked in?" 52 | 53 | Up until now, the answer has been "Yes! But only if you contact them about it." 54 | 55 | With ``stormpath-export``, however, you can easily back up all your Stormpath 56 | user data instantly! 57 | 58 | ``stormpath-export`` will: 59 | 60 | - Grab all Stormpath data you've stored, and dump it to JSON files locally. 61 | - Dump data into a local directory structure which makes intuitive sense 62 | (*groups are located in the groups directory, etc.*). 63 | - Each object gets it's own JSON file generated. This makes it easy to look at 64 | the file system and extract the information you need. 65 | 66 | ``stormpath-export`` makes it easy to: 67 | 68 | - Download a copy of all your user data. 69 | - Back up your user data (*Stormpath has their own backups of course, but you 70 | can never be too safe*). 71 | - Migrate user data out of Stormpath. 72 | 73 | 74 | Installation 75 | ------------ 76 | 77 | Installing ``stormpath-export`` is simple -- just use `pip`_! 78 | 79 | Once you have pip installed on your computer, you can run the following to 80 | install the latest release of ``stormpath-export``: 81 | 82 | .. code-block:: console 83 | 84 | $ pip install -U stormpath-export 85 | 86 | That's it :) 87 | 88 | 89 | Usage 90 | ----- 91 | 92 | Before you can export all your `Stormpath`_ data, you'll need to configure 93 | ``stormpath-export`` and give it your Stormpath API credentials. To do this, 94 | simply run: 95 | 96 | .. code-block:: console 97 | 98 | $ stormpath-export configure 99 | 100 | This will prompt you for some basic information, then store your credentials 101 | in the local file ``~/.stormy``. 102 | 103 | NOTE: If you are using Stormpath Enterprise, please enter 104 | ``https://enterprise.stormpath.io/v1`` when prompted for the Base URL. This 105 | instructs the export tool to talk to the Stormpath Enterprise environment. 106 | 107 | Next, to initiate a backup job, you can run: 108 | 109 | .. code-block:: console 110 | 111 | $ stormpath-export 112 | 113 | This will export all your Stormpath data, and dump it into a new directory 114 | named ``stormpath-exports``. If you'd like to specify your own backup location, 115 | you can do so by adding a path -- for instance: 116 | 117 | .. code-block:: console 118 | 119 | $ stormpath-export ~/Desktop/stormpath-exports 120 | 121 | When exporting your data, you should see output similar to the following: 122 | 123 | .. code-block:: console 124 | 125 | === Exporting all application data... 126 | - Exporting application: Stormpath 127 | === Done! 128 | 129 | === Exporting all directory data... 130 | - Exporting directory: Stormpath Administrators 131 | - Exporting directory: testdirectory 132 | === Done! 133 | 134 | === Exporting all group data... 135 | - Exporting group: Administrators 136 | === Done! 137 | 138 | === Exporting all account data... 139 | - Exporting account: r@rdegges.com 140 | === Done! 141 | 142 | .. note:: 143 | Depending on how many applications, groups, directories, organizations, and 144 | accounts you have, this process may take a while. 145 | 146 | Once the process is finished, you can navigate the JSON files in the export 147 | directory, which will contain all your Stormpath data. 148 | 149 | For full usage information, run ``stormpath-export -h``: 150 | 151 | .. code-block:: console 152 | 153 | $ stormpath-export -h 154 | stormpath-export 155 | ---------------- 156 | 157 | Easily export your Stormpath (https://stormpath.com/) user data. 158 | 159 | Usage: 160 | stormpath-export configure 161 | stormpath-export [( | -l | --location )] 162 | stormpath-export (-h | --help) 163 | stormpath-export --version 164 | 165 | Options: 166 | -h --help Show this screen. 167 | --version Show version. 168 | 169 | Written by Randall Degges . 170 | 171 | 172 | Help 173 | ---- 174 | 175 | Need help? Can't figure something out? If you think you've found a bug, please 176 | open an issue on the `Github issue tracker`_. 177 | 178 | Otherwise, `shoot us an email`_. 179 | 180 | 181 | Changelog 182 | --------- 183 | 184 | **0.1.2**: 12-27-2016 185 | 186 | - Importing missing dependency. 187 | 188 | **0.1.1**: 10-17-2016 189 | 190 | - Supporting ``--base-url`` argument. 191 | - Making the tool Python 3 compatible. 192 | 193 | **0.1.0**: 03-16-2016 194 | 195 | - Supporting API key exports. 196 | - Supporting Organization exports. 197 | - Fixing documentation. 198 | - Updating Stormpath dependency. 199 | 200 | **0.0.9**: 06-19-2015 201 | 202 | - Fixing version information. 203 | 204 | **0.0.8**: 06-19-2015 205 | 206 | - Supporting private deployments. 207 | 208 | **0.0.7**: 06-18-2015 209 | 210 | - Supporting Account API key backups. 211 | 212 | **0.0.6**: 06-18-2015 213 | 214 | - Completely re-doing export structures -- much more sane now. 215 | - Adding IDs, hrefs, and all fields to all backups. 216 | - Still needs to support Social / ID site / Verification templates. 217 | 218 | **0.0.5**: 05-28-2015 219 | 220 | - Making the application export back up directory mappings. 221 | 222 | **0.0.4**: 05-27-2015 223 | 224 | - Backing up customData for all resource types. 225 | - Upgrading dependencies. 226 | 227 | **0.0.3**: 06-19-2014 228 | 229 | - Making application export include directory name for clarity. 230 | 231 | **0.0.2**: 06-08-2014 232 | 233 | - Fixing bug with groups. 234 | - Adding support for custom data exporting. 235 | - Including new Stormpath SDK. 236 | 237 | **0.0.1**: 12-14-2013 238 | 239 | - First super-beta release of the project. WOO. 240 | 241 | 242 | .. _Stormpath: https://stormpath.com/ "Stormpath" 243 | .. _pip: http://pip.readthedocs.org/en/stable/ "pip" 244 | .. _Github issue tracker: https://github.com/stormpath/stormpath-export/issues "stormpath-export Issue Tracker" 245 | .. _shoot us an email: mailto:support@stormpath.com "Stormpath Support" 246 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /assets/box-sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/stormpath-export/2aeecec0356cb66f277762d3d91b0297054fc48d/assets/box-sketch.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Our python packaging stuff.""" 2 | 3 | 4 | from os.path import abspath, dirname, join, normpath 5 | 6 | from setuptools import setup 7 | 8 | 9 | setup( 10 | 11 | # Basic package information: 12 | name = 'stormpath-export', 13 | version = '0.1.2', 14 | scripts = ('stormpath-export', ), 15 | 16 | # Packaging options: 17 | zip_safe = False, 18 | include_package_data = True, 19 | 20 | # Package dependencies: 21 | install_requires = ['docopt>=0.6.2', 'future>=0.16.0', 'stormpath>=2.2.0'], 22 | 23 | # Metadata for PyPI: 24 | author = 'Randall Degges', 25 | author_email = 'r@rdegges.com', 26 | license = 'UNLICENSE', 27 | url = 'https://github.com/rdegges/stormpath-export', 28 | keywords = 'user authentication auth security api stormpath bcrypt utility', 29 | description = 'Easily export your Stormpath user data.', 30 | long_description = open(normpath(join(dirname(abspath(__file__)), 31 | 'README.rst'))).read() 32 | 33 | ) 34 | -------------------------------------------------------------------------------- /stormpath-export: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | stormpath-export 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | Easily export your Stormpath (https://stormpath.com/) user data. 7 | 8 | Usage: 9 | stormpath-export configure 10 | stormpath-export [( | -l | --location ) (-b | --base-url )] 11 | stormpath-export [(-b | --base-url )] 12 | stormpath-export (-h | --help) 13 | stormpath-export --version 14 | 15 | Options: 16 | -h --help Show this screen. 17 | --version Show version. 18 | 19 | Written by Randall Degges . 20 | """ 21 | 22 | from builtins import input 23 | from json import dumps, loads 24 | from os import chmod, getcwd, makedirs 25 | from os.path import dirname, exists, expanduser 26 | from sys import exit 27 | 28 | from docopt import docopt 29 | from stormpath.client import Client 30 | from stormpath.error import Error 31 | 32 | 33 | ##### GLOBALS 34 | CONFIG_FILE = expanduser('~/.stormy') 35 | VERSION = 'stormpath-export 0.1.2' 36 | 37 | 38 | class StormpathExport(object): 39 | """Our CLI manager.""" 40 | 41 | EXPORTS = ['tenants', 'applications', 'directories', 'groups', 'organizations', 'accounts'] 42 | 43 | def __init__(self, base_url): 44 | """Initialize our Stormpath client, or die tryin' >:)""" 45 | if exists(CONFIG_FILE): 46 | credentials = loads(open(CONFIG_FILE, 'r').read()) 47 | self.client = Client(api_key={ 48 | 'id': credentials.get('stormpath_api_key_id'), 49 | 'secret': credentials.get('stormpath_api_key_secret'), 50 | }, base_url=credentials.get('stormpath_base_url') or base_url or None) 51 | else: 52 | print('No API credentials found! Please run stormpath-export configure to set them up.') 53 | exit(1) 54 | 55 | def get_custom_data(self, resource): 56 | """ 57 | Given a Stormpath resource, we'll extract the custom data in a JSON 58 | compatible format. 59 | 60 | Since all Stormpath resources contain custom data with two properties: 61 | `created_at` and `modified_at`, we'll simply convert these timestamp 62 | fields into ISO 8601 strings. 63 | 64 | :param obj resource: The Stormpath resource object. Account, 65 | Application, Directory, etc. 66 | """ 67 | try: 68 | custom_data = dict(resource.custom_data) 69 | except AttributeError: 70 | custom_data = dict(resource['custom_data']) 71 | 72 | custom_data['createdAt'] = custom_data['created_at'].isoformat() 73 | custom_data['modifiedAt'] = custom_data['modified_at'].isoformat() 74 | 75 | del custom_data['created_at'] 76 | del custom_data['modified_at'] 77 | 78 | return custom_data 79 | 80 | def get_id(self, resource): 81 | """ 82 | Given a Stormpath Resource, we'll extract the resource ID. 83 | 84 | :param obj resource: The Stormpath resource object. Account, 85 | Application, Directory, etc. 86 | """ 87 | try: 88 | return resource.href.split('/')[-1] 89 | except AttributeError: 90 | return resource['href'].split('/')[-1] 91 | 92 | def set_location(self, location): 93 | """ 94 | Return the proper location used to export our JSON data. 95 | 96 | :param str location: The location to backup all Stormpath data (must be 97 | a folder). 98 | """ 99 | if not location: 100 | location = getcwd() + '/stormpath-exports' 101 | 102 | return location 103 | 104 | def write(self, file, data): 105 | """ 106 | Write JSON data to the specified file. 107 | 108 | This is a simple wrapper around our file handling stuff. 109 | 110 | :param str file: The file to write. 111 | :param dict data: The data to write to the file, as a JSON dict. 112 | """ 113 | if not exists(dirname(file)): 114 | makedirs(dirname(file)) 115 | 116 | with open(file + '.json', 'w') as file: 117 | file.write(dumps(data, indent=2, separators=(',', ': '), sort_keys=True)) 118 | 119 | def export_tenants(self): 120 | """Export all tenant data for this Stormpath account.""" 121 | print('\n=== Exporting all tenant data...') 122 | 123 | tenant = dict(self.client.tenant) 124 | 125 | print('- Exporting tenant:', tenant['name']) 126 | 127 | json = { 128 | 'id': self.get_id(tenant), 129 | 'href': tenant['href'], 130 | 'name': tenant['name'], 131 | 'key': tenant['key'], 132 | 'createdAt': tenant['created_at'].isoformat(), 133 | 'modifiedAt': tenant['modified_at'].isoformat(), 134 | 'customData': self.get_custom_data(tenant), 135 | } 136 | 137 | #for application in tenant.applications: 138 | 139 | self.write('%s/%s/meta' % (self.location, json['id']), json) 140 | 141 | print('=== Done!\n') 142 | 143 | def export_applications(self): 144 | """Export all application data for this Stormpath account.""" 145 | print('\n=== Exporting all application data...') 146 | 147 | for application in self.client.applications: 148 | print('- Exporting application:', application.name) 149 | 150 | json = { 151 | 'id': self.get_id(application), 152 | 'href': application.href, 153 | 'name': application.name, 154 | 'description': application.description, 155 | 'status': application.status, 156 | 'createdAt': application.created_at.isoformat(), 157 | 'modifiedAt': application.modified_at.isoformat(), 158 | 'customData': self.get_custom_data(application), 159 | 'default_account_store_mapping': None, 160 | 'default_group_store_mapping': None, 161 | 'account_store_mappings': [], 162 | #'verificationEmails': [], 163 | } 164 | 165 | default_account_store_mapping = application.default_account_store_mapping 166 | default_group_store_mapping = application.default_group_store_mapping 167 | 168 | if default_account_store_mapping: 169 | json['default_account_store_mapping'] = { 170 | 'id': application.default_account_store_mapping.href.split('/')[-1], 171 | 'href': application.default_account_store_mapping.href, 172 | 'type': application.default_account_store_mapping.account_store.__class__.__name__, 173 | 'name': application.default_account_store_mapping.account_store.name, 174 | 'list_index': application.default_account_store_mapping.list_index, 175 | } 176 | 177 | if default_group_store_mapping: 178 | json['default_group_store_mapping'] = { 179 | 'id': application.default_group_store_mapping.href.split('/')[-1], 180 | 'href': application.default_group_store_mapping.href, 181 | 'type': application.default_group_store_mapping.account_store.__class__.__name__, 182 | 'name': application.default_group_store_mapping.account_store.name, 183 | 'list_index': application.default_group_store_mapping.list_index, 184 | } 185 | 186 | for account_store_mapping in application.account_store_mappings: 187 | json['account_store_mappings'].append({ 188 | 'id': self.get_id(account_store_mapping), 189 | 'href': account_store_mapping.href, 190 | 'account_store': { 191 | 'type': account_store_mapping.account_store.__class__.__name__, 192 | 'id': self.get_id(account_store_mapping.account_store), 193 | 'href': account_store_mapping.account_store.href, 194 | 'name': account_store_mapping.account_store.name, 195 | 'description': account_store_mapping.account_store.description, 196 | 'status': account_store_mapping.account_store.status, 197 | }, 198 | 'list_index': account_store_mapping.list_index, 199 | 'is_default_account_store': account_store_mapping.is_default_account_store, 200 | 'is_default_group_store': account_store_mapping.is_default_group_store, 201 | }) 202 | 203 | tenant = self.get_id(application.tenant) 204 | self.write('%s/%s/applications/%s' % (self.location, tenant, json['id']), json) 205 | 206 | print('=== Done!\n') 207 | 208 | def export_directories(self): 209 | """Export all directory data for this Stormpath account.""" 210 | print('=== Exporting all directory data...') 211 | 212 | for directory in self.client.directories: 213 | print('- Exporting directory:', directory.name) 214 | 215 | json = { 216 | 'id': self.get_id(directory), 217 | 'href': directory.href, 218 | 'name': directory.name, 219 | 'description': directory.description, 220 | 'status': directory.status, 221 | 'createdAt': directory.created_at.isoformat(), 222 | 'modifiedAt': directory.modified_at.isoformat(), 223 | 'customData': self.get_custom_data(directory), 224 | 'groups': [], 225 | } 226 | 227 | for group in directory.groups: 228 | json['groups'].append({ 229 | 'id': self.get_id(group), 230 | 'href': group.href, 231 | 'name': group.name, 232 | 'description': group.description, 233 | 'status': group.status, 234 | 'createdAt': group.created_at.isoformat(), 235 | 'modifiedAt': group.modified_at.isoformat(), 236 | }) 237 | 238 | json['provider'] = { 239 | 'href': directory.provider.href, 240 | 'providerId': directory.provider.provider_id, 241 | 'agent': None, 242 | } 243 | 244 | try: 245 | json['provider']['createdAt'] = directory.provider.created_at.isoformat() 246 | json['provider']['modifiedAt'] = directory.provider.modified_at.isoformat() 247 | except AttributeError: 248 | json['provider']['createdAt'] = None 249 | json['provider']['modifiedAt'] = None 250 | 251 | try: 252 | json['provider']['clientId'] = directory.provider.client_id 253 | except AttributeError: 254 | json['provider']['clientId'] = None 255 | 256 | try: 257 | json['provider']['clientSecret'] = directory.provider.client_secret 258 | except AttributeError: 259 | json['provider']['clientSecret'] = None 260 | 261 | try: 262 | json['provider']['redirectUri'] = directory.provider.redirect_uri 263 | except AttributeError: 264 | json['provider']['redirectUri'] = None 265 | 266 | try: 267 | json['provider']['agent'] = { 268 | 'id': self.get_id(directory.provider.agent), 269 | 'href': directory.provider.agent.href, 270 | 'status': directory.provider.agent.status, 271 | 'createdAt': directory.provider.agent.created_at.isoformat(), 272 | 'modifiedAt': directory.provider.agent.modified_at.isoformat(), 273 | 'config': { 274 | 'directoryHost': directory.provider.agent.directory_host, 275 | 'directoryPort': directory.provider.agent.directory_port, 276 | 'sslRequired': directory.provider.agent.ssl_required, 277 | 'agentUserDn': directory.provider.agent.agent_user_dn, 278 | 'agentUserDnPassword': directory.provider.agent.agent_user_dn_password, 279 | 'baseDn': directory.provider.agent.base_dn, 280 | 'pollInterval': directory.provider.agent.poll_interval, 281 | 'referralMode': directory.provider.agent.referral_mode, 282 | 'ignoreReferralIssues': directory.provider.agent.ignore_referral_issues, 283 | 'accountConfig': directory.provider.agent.account_config, 284 | 'groupConfig': directory.provider.agent.group_config, 285 | }, 286 | 'download': { 287 | 288 | }, 289 | } 290 | except AttributeError: 291 | pass 292 | 293 | if directory.password_policy: 294 | json['passwordPolicy'] = { 295 | 'id': self.get_id(directory.password_policy), 296 | 'href': directory.password_policy.href, 297 | #'createdAt': directory.password_policy.created_at.isoformat(), 298 | #'modifiedAt': directory.password_policy.modified_at.isoformat(), 299 | 'resetEmailStatus': directory.password_policy.reset_email_status, 300 | 'resetEmailTemplates': [], 301 | 'resetSuccessEmailStatus': directory.password_policy.reset_success_email_status, 302 | 'resetSuccessEmailTemplates': [], 303 | 'resetTokenTtl': directory.password_policy.reset_token_ttl, 304 | 'strength': { 305 | 'href': directory.password_policy.strength.href, 306 | #'createdAt': directory.password_policy.strength.created_at.isoformat(), 307 | #'modifiedAt': directory.password_policy.strength.modified_at.isoformat(), 308 | 'maxLength': directory.password_policy.strength.max_length, 309 | 'minDiacritic': directory.password_policy.strength.min_diacritic, 310 | 'minLength': directory.password_policy.strength.min_length, 311 | 'minLowerCase': directory.password_policy.strength.min_lower_case, 312 | 'minNumeric': directory.password_policy.strength.min_numeric, 313 | 'minSymbol': directory.password_policy.strength.min_symbol, 314 | 'minUpperCase': directory.password_policy.strength.min_upper_case, 315 | }, 316 | } 317 | 318 | try: 319 | for template in directory.password_policy.reset_email_templates: 320 | json['passwordPolicy']['resetEmailTemplates'].append({ 321 | 'id': self.get_id(template), 322 | 'href': template.href, 323 | 'createdAt': template.created_at.isoformat(), 324 | 'modifiedAt': template.modified_at.isoformat(), 325 | 'fromName': template.from_name, 326 | 'name': template.name, 327 | 'description': template.description, 328 | 'fromEmailAddress': template.from_email_address, 329 | 'textBody': template.text_body, 330 | 'htmlBody': template.html_body, 331 | 'defaultModel': template.default_model, 332 | 'mimeType': template.mime_type, 333 | 'subject': template.subject, 334 | }) 335 | except AttributeError: 336 | pass 337 | 338 | try: 339 | for template in directory.password_policy.reset_success_email_templates: 340 | json['passwordPolicy']['resetSuccessEmailTemplates'].append({ 341 | 'id': self.get_id(template), 342 | 'href': template.href, 343 | 'createdAt': template.created_at.isoformat(), 344 | 'modifiedAt': template.modified_at.isoformat(), 345 | 'fromName': template.from_name, 346 | 'name': template.name, 347 | 'description': template.description, 348 | 'fromEmailAddress': template.from_email_address, 349 | 'textBody': template.text_body, 350 | 'htmlBody': template.html_body, 351 | 'mimeType': template.mime_type, 352 | 'subject': template.subject, 353 | }) 354 | except AttributeError: 355 | pass 356 | 357 | tenant = self.get_id(directory.tenant) 358 | self.write('%s/%s/directories/%s' % (self.location, tenant, json['id']), json) 359 | 360 | print('=== Done!\n') 361 | 362 | def export_organizations(self): 363 | """Export all organization data for this Stormpath account.""" 364 | print('\n=== Exporting all organization data...') 365 | 366 | for organization in self.client.organizations: 367 | print('- Exporting organizations:', organization.name) 368 | 369 | json = { 370 | 'id': self.get_id(organization), 371 | 'href': organization.href, 372 | 'name': organization.name, 373 | 'nameKey': organization.name_key, 374 | 'description': organization.description, 375 | 'status': organization.status, 376 | 'createdAt': organization.created_at.isoformat(), 377 | 'modifiedAt': organization.modified_at.isoformat(), 378 | 'customData': self.get_custom_data(organization), 379 | 'default_account_store_mapping': None, 380 | 'default_group_store_mapping': None, 381 | 'account_store_mappings': [], 382 | } 383 | 384 | default_account_store_mapping = organization.default_account_store_mapping 385 | default_group_store_mapping = organization.default_group_store_mapping 386 | 387 | if default_account_store_mapping: 388 | json['default_account_store_mapping'] = { 389 | 'id': organization.default_account_store_mapping.href.split('/')[-1], 390 | 'href': organization.default_account_store_mapping.href, 391 | 'type': organization.default_account_store_mapping.account_store.__class__.__name__, 392 | 'name': organization.default_account_store_mapping.account_store.name, 393 | 'list_index': organization.default_account_store_mapping.list_index, 394 | } 395 | 396 | if default_group_store_mapping: 397 | json['default_group_store_mapping'] = { 398 | 'id': organization.default_group_store_mapping.href.split('/')[-1], 399 | 'href': organization.default_group_store_mapping.href, 400 | 'type': organization.default_group_store_mapping.account_store.__class__.__name__, 401 | 'name': organization.default_group_store_mapping.account_store.name, 402 | 'list_index': organization.default_group_store_mapping.list_index, 403 | } 404 | 405 | for account_store_mapping in organization.account_store_mappings: 406 | json['account_store_mappings'].append({ 407 | 'id': self.get_id(account_store_mapping), 408 | 'href': account_store_mapping.href, 409 | 'account_store': { 410 | 'type': account_store_mapping.account_store.__class__.__name__, 411 | 'id': self.get_id(account_store_mapping.account_store), 412 | 'href': account_store_mapping.account_store.href, 413 | 'name': account_store_mapping.account_store.name, 414 | 'description': account_store_mapping.account_store.description, 415 | 'status': account_store_mapping.account_store.status, 416 | }, 417 | 'list_index': account_store_mapping.list_index, 418 | 'is_default_account_store': account_store_mapping.is_default_account_store, 419 | 'is_default_group_store': account_store_mapping.is_default_group_store, 420 | }) 421 | 422 | tenant = self.get_id(organization.tenant) 423 | self.write('%s/%s/organizations/%s' % (self.location, tenant, json['id']), json) 424 | 425 | print('=== Done!\n') 426 | 427 | def export_groups(self): 428 | """Export all group data for this Stormpath account.""" 429 | print('=== Exporting all group data...') 430 | 431 | for group in self.client.tenant.groups: 432 | print('- Exporting group:', group.name) 433 | 434 | json = { 435 | 'id': self.get_id(group), 436 | 'href': group.href, 437 | 'name': group.name, 438 | 'description': group.description, 439 | 'status': group.status, 440 | 'createdAt': group.created_at.isoformat(), 441 | 'modifiedAt': group.modified_at.isoformat(), 442 | 'customData': self.get_custom_data(group), 443 | 'directory': { 444 | 'id': self.get_id(group.directory), 445 | 'href': group.directory.href, 446 | 'name': group.directory.name, 447 | 'description': group.directory.description, 448 | 'status': group.directory.status, 449 | 'createdAt': group.directory.created_at.isoformat(), 450 | 'modifiedAt': group.directory.modified_at.isoformat(), 451 | }, 452 | 'accounts': [], 453 | } 454 | 455 | for account in group.accounts: 456 | json['accounts'].append({ 457 | 'id': self.get_id(account), 458 | 'href': account.href, 459 | 'username': account.username, 460 | 'email': account.email, 461 | 'fullName': account.full_name, 462 | 'givenName': account.given_name, 463 | 'middleName': account.middle_name, 464 | 'surname': account.surname, 465 | 'status': account.status, 466 | 'createdAt': account.created_at.isoformat(), 467 | 'modifiedAt': account.modified_at.isoformat(), 468 | }) 469 | 470 | tenant = self.get_id(self.client.tenant) 471 | self.write('%s/%s/groups/%s' % (self.location, tenant, json['id']), json) 472 | 473 | print('=== Done!\n') 474 | 475 | def export_accounts(self): 476 | """Export all account data for this Stormpath account.""" 477 | print('=== Exporting all account data...') 478 | 479 | for account in self.client.tenant.accounts: 480 | print('- Exporting account:', account.email) 481 | 482 | json = { 483 | 'id': self.get_id(account), 484 | 'href': account.href, 485 | 'username': account.username, 486 | 'email': account.email, 487 | 'fullName': account.full_name, 488 | 'givenName': account.given_name, 489 | 'middleName': account.middle_name, 490 | 'surname': account.surname, 491 | 'status': account.status, 492 | 'createdAt': account.created_at.isoformat(), 493 | 'modifiedAt': account.modified_at.isoformat(), 494 | 'customData': self.get_custom_data(account), 495 | 'groups': [], 496 | 'apiKeys': [], 497 | 'directory': { 498 | 'id': self.get_id(account.directory), 499 | 'href': account.directory.href, 500 | 'name': account.directory.name, 501 | 'description': account.directory.description, 502 | 'status': account.directory.status, 503 | 'createdAt': account.directory.created_at.isoformat(), 504 | 'modifiedAt': account.directory.modified_at.isoformat(), 505 | }, 506 | } 507 | 508 | for api_key in account.api_keys: 509 | json['apiKeys'].append({ 510 | 'href': api_key.href, 511 | 'id': api_key.id, 512 | 'secret': api_key.secret, 513 | #'createdAt': api_key.created_at.isoformat(), 514 | #'modifiedAt': api_key.modified_at.isoformat(), 515 | }) 516 | 517 | for group in account.groups: 518 | json['groups'].append({ 519 | 'id': self.get_id(group), 520 | 'href': group.href, 521 | 'name': group.name, 522 | 'description': group.description, 523 | 'status': group.status, 524 | 'createdAt': group.created_at.isoformat(), 525 | 'modifiedAt': group.modified_at.isoformat(), 526 | }) 527 | 528 | tenant = self.get_id(self.client.tenant) 529 | self.write('%s/%s/accounts/%s' % (self.location, tenant, json['id']), json) 530 | 531 | print('=== Done!\n') 532 | 533 | def export(self, location=None): 534 | """ 535 | Export all Stormpath data to the disk, in JSON format. 536 | 537 | Takes an optional argument (the directory to export all data to). 538 | 539 | :param str location: The location to backup all Stormpath data (must be 540 | a folder). 541 | """ 542 | self.location = self.set_location(location) 543 | 544 | # Export all Stormpath data. 545 | for export_type in self.EXPORTS: 546 | getattr(self, 'export_' + export_type)() 547 | 548 | 549 | def configure(): 550 | """ 551 | Initializing stormpath-export. 552 | 553 | This will store the user's API credentials in: ~/.stormy, and ensure the 554 | API credentials specified actually work. 555 | """ 556 | print('Initializing `stormpath-export`...\n') 557 | print("To get started, we'll need to get your Stormpath API credentials. Don't have a Stormpath account? Go get one! https://stormpath.com") 558 | 559 | finished = False 560 | while not finished: 561 | api_key_id = input('Enter your API Key ID: ').strip() 562 | api_key_secret = input('Enter your API Key Secret: ').strip() 563 | base_url = input('Enter your Base URL (optional): ').strip() 564 | if not (api_key_id or api_key_secret): 565 | print('\nNot sure how to find your Stormpath API credentials?') 566 | print('Log into your Stormpath account, then visit your dashboard and use the "Manage Existing Keys" link.\n') 567 | continue 568 | 569 | # Validate the API credentials. 570 | client = Client(api_key={ 571 | 'id': api_key_id, 572 | 'secret': api_key_secret, 573 | }, base_url=base_url or None) 574 | try: 575 | client.applications 576 | print('\nSuccessfully initialized stormy!') 577 | print('Your API credentials are stored in the file:', CONFIG_FILE, '\n') 578 | print('Run stormpath-export for usage information.') 579 | 580 | with open(CONFIG_FILE, 'w') as stormycfg: 581 | stormycfg.write(dumps({ 582 | 'stormpath_api_key_id': api_key_id, 583 | 'stormpath_api_key_secret': api_key_secret, 584 | 'stormpath_base_url': base_url or None 585 | }, indent=2, sort_keys=True)) 586 | 587 | # Make the stormy configuration file only accessible to the current 588 | # user -- this makes the credentials a bit more safe. 589 | chmod(CONFIG_FILE, 0o600) 590 | 591 | finished = True 592 | except Error: 593 | print('\nYour API credentials are not working, please verify they are correct, then try again.\n') 594 | 595 | 596 | def main(): 597 | """Handle user input, and do stuff accordingly.""" 598 | arguments = docopt(__doc__, version=VERSION) 599 | 600 | # Handle the configure as a special case -- this way we won't get invalid 601 | # API credential messages when we're trying to configure stormpath-export. 602 | if arguments['configure']: 603 | configure() 604 | return 605 | 606 | exporter = StormpathExport(arguments['']) 607 | exporter.export(arguments['']) 608 | 609 | 610 | if __name__ == '__main__': 611 | main() 612 | --------------------------------------------------------------------------------