├── .gitignore ├── .pylintrc ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── images ├── AddUser-01-user-details.png ├── AddUser-02-set-permissions.png ├── AddUser-03-create-policy.png ├── AddUser-04-review-policy.png ├── AddUser-05-select-policy.png ├── AddUser-06-success.png ├── CreateKey-01-alias.png ├── CreateKey-02-review.png ├── CreateKey-03-success.png ├── CreateParam-01-parameter-details.png └── CreatePolicy-01-review-policy.png ├── requirements.test.txt ├── requirements.txt ├── ridi ├── __init__.py └── secret_keeper │ ├── __init__.py │ ├── cmdline.py │ ├── config.py │ └── connect.py ├── setup.py └── test ├── __init__.py └── test_secret_keeper.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # IDE files 99 | .idea/ -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=thrift 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns= 17 | 18 | # Pickle collected data for later comparisons. 19 | persistent=yes 20 | 21 | # List of plugins (as comma separated values of python modules names) to load, 22 | # usually to register additional checkers. 23 | load-plugins= 24 | 25 | # Use multiple processes to speed up Pylint. 26 | jobs=4 27 | 28 | # Allow loading of arbitrary C extensions. Extensions are imported into the 29 | # active Python interpreter and may run arbitrary code. 30 | unsafe-load-any-extension=no 31 | 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code 35 | extension-pkg-whitelist= 36 | 37 | # Allow optimization of some AST trees. This will activate a peephole AST 38 | # optimizer, which will apply various small optimizations. For instance, it can 39 | # be used to obtain the result of joining multiple strings with the addition 40 | # operator. Joining a lot of strings can lead to a maximum recursion error in 41 | # Pylint and this flag can prevent that. It has one side effect, the resulting 42 | # AST will be different than the one from reality. This option is deprecated 43 | # and it will be removed in Pylint 2.0. 44 | optimize-ast=no 45 | 46 | 47 | [MESSAGES CONTROL] 48 | 49 | # Only show warnings with the listed confidence levels. Leave empty to show 50 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 51 | confidence= 52 | 53 | # Enable the message, report, category or checker with the given id(s). You can 54 | # either give multiple identifier separated by comma (,) or put this option 55 | # multiple time (only on the command line, not in the configuration file where 56 | # it should appear only once). See also the "--disable" option for examples. 57 | #enable= 58 | 59 | # Disable the message, report, category or checker with the given id(s). You 60 | # can either give multiple identifiers separated by comma (,) or put this 61 | # option multiple times (only on the command line, not in the configuration 62 | # file where it should appear only once).You can also use "--disable=all" to 63 | # disable everything first and then reenable specific checks. For example, if 64 | # you want to run only the similarities checker, you can use "--disable=all 65 | # --enable=similarities". If you want to run only the classes checker, but have 66 | # no Warning level messages displayed, use"--disable=all --enable=classes 67 | # --disable=W" 68 | 69 | disable=too-many-ancestors,unexpected-keyword-arg,too-many-return-statements,raw_input-builtin,oct-method,invalid-encoded-data,no-absolute-import,invalid-name,unused-import,no-member,too-many-arguments,cmp-method,unicode-builtin,backtick,coerce-method,no-name-in-module,unused-variable,reload-builtin,unpacking-in-except,dict-iter-method,indexing-exception,coerce-builtin,execfile-builtin,map-builtin-not-iterating,intern-builtin,import-star-module-level,xrange-builtin,input-builtin,filter-builtin-not-iterating,getslice-method,print-statement,round-builtin,range-builtin-not-iterating,long-suffix,suppressed-message,next-method-called,apply-builtin,setslice-method,nonzero-method,buffer-builtin,useless-suppression,dict-view-method,reduce-builtin,metaclass-assignment,using-cmp-argument,standarderror-builtin,zip-builtin-not-iterating,long-builtin,parameter-unpacking,unichr-builtin,old-ne-operator,delslice-method,old-division,old-raise-syntax,file-builtin,old-octal-literal,cmp-builtin,hex-method,basestring-builtin,raising-string,import-error,too-few-public-methods,C0111,I0011,I0012,W0142,W0212,W0232,W0613,R0201,W0614,R0914,R0915,R0913,R0904,R0801,C0411 70 | 71 | 72 | # C0111 Missing docstring 73 | # I0011 Warning locally suppressed using disable-msg 74 | # I0012 Warning locally suppressed using disable-msg 75 | # W0704 Except doesn't do anything Used when an except clause does nothing but "pass" and there is no "else" clause 76 | # W0142 Used * or * magic* Used when a function or method is called using *args or **kwargs to dispatch arguments. 77 | # W0212 Access to a protected member %s of a client class 78 | # W0232 Class has no __init__ method Used when a class has no __init__ method, neither its parent classes. 79 | # W0613 Unused argument %r Used when a function or method argument is not used. 80 | # W0614 Unused import XYZ from wildcard import 81 | # R0914 Too many local variables 82 | # R0912 Too many branches 83 | # R0915 Too many statements 84 | # R0913 Too many arguments 85 | # R0904 Too many public methods 86 | 87 | [REPORTS] 88 | 89 | # Set the output format. Available formats are text, parseable, colorized, msvs 90 | # (visual studio) and html. You can also give a reporter class, eg 91 | # mypackage.mymodule.MyReporterClass. 92 | output-format=colorized 93 | 94 | # Put messages in a separate file for each module / package specified on the 95 | # command line instead of printing them on stdout. Reports (if any) will be 96 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 97 | # and it will be removed in Pylint 2.0. 98 | files-output=no 99 | 100 | # Tells whether to display a full report or only the messages 101 | reports=yes 102 | 103 | # Python expression which should return a note less than 10 (10 is the highest 104 | # note). You have access to the variables errors warning, statement which 105 | # respectively contain the number of errors / warnings messages and the total 106 | # number of statements analyzed. This is used by the global evaluation report 107 | # (RP0004). 108 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 109 | 110 | # Template used to display messages. This is a python new-style format string 111 | # used to format the message information. See doc for all details 112 | #msg-template= 113 | 114 | 115 | [BASIC] 116 | 117 | # Good variable names which should always be accepted, separated by a comma 118 | good-names=i,j,k,ex,Run,_ 119 | 120 | # Bad variable names which should always be refused, separated by a comma 121 | bad-names=foo,bar,baz,toto,tutu,tata 122 | 123 | # Colon-delimited sets of names that determine each other's naming style when 124 | # the name regexes allow several styles. 125 | name-group= 126 | 127 | # Include a hint for the correct naming format with invalid-name 128 | include-naming-hint=no 129 | 130 | # List of decorators that produce properties, such as abc.abstractproperty. Add 131 | # to this list to register other decorators that produce valid properties. 132 | property-classes=abc.abstractproperty 133 | 134 | # Regular expression matching correct constant names 135 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 136 | 137 | # Naming hint for constant names 138 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 139 | 140 | # Regular expression matching correct function names 141 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 142 | 143 | # Naming hint for function names 144 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 145 | 146 | # Regular expression matching correct variable names 147 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 148 | 149 | # Naming hint for variable names 150 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 151 | 152 | # Regular expression matching correct class attribute names 153 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 154 | 155 | # Naming hint for class attribute names 156 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 157 | 158 | # Regular expression matching correct inline iteration names 159 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 160 | 161 | # Naming hint for inline iteration names 162 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 163 | 164 | # Regular expression matching correct module names 165 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 166 | 167 | # Naming hint for module names 168 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 169 | 170 | # Regular expression matching correct method names 171 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 172 | 173 | # Naming hint for method names 174 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 175 | 176 | # Regular expression matching correct attribute names 177 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 178 | 179 | # Naming hint for attribute names 180 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 181 | 182 | # Regular expression matching correct class names 183 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 184 | 185 | # Naming hint for class names 186 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 187 | 188 | # Regular expression matching correct argument names 189 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 190 | 191 | # Naming hint for argument names 192 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 193 | 194 | # Regular expression which should only match function or class names that do 195 | # not require a docstring. 196 | no-docstring-rgx=^_ 197 | 198 | # Minimum line length for functions/classes that require docstrings, shorter 199 | # ones are exempt. 200 | docstring-min-length=-1 201 | 202 | 203 | [ELIF] 204 | 205 | # Maximum number of nested blocks for function / method body 206 | max-nested-blocks=5 207 | 208 | 209 | [FORMAT] 210 | 211 | # Maximum number of characters on a single line. 212 | max-line-length=140 213 | 214 | # Regexp for a line that is allowed to be longer than the limit. 215 | ignore-long-lines=^\s*(# )??$ 216 | 217 | # Allow the body of an if to be on the same line as the test if there is no 218 | # else. 219 | single-line-if-stmt=no 220 | 221 | # List of optional constructs for which whitespace checking is disabled. `dict- 222 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 223 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 224 | # `empty-line` allows space-only lines. 225 | no-space-check=trailing-comma,dict-separator 226 | 227 | # Maximum number of lines in a module 228 | max-module-lines=1000 229 | 230 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 231 | # tab). 232 | indent-string=' ' 233 | 234 | # Number of spaces of indent required inside a hanging or continued line. 235 | indent-after-paren=4 236 | 237 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 238 | expected-line-ending-format= 239 | 240 | 241 | [LOGGING] 242 | 243 | # Logging modules to check that the string format arguments are in logging 244 | # function parameter format 245 | logging-modules=logging 246 | 247 | 248 | [MISCELLANEOUS] 249 | 250 | # List of note tags to take in consideration, separated by a comma. 251 | notes=FIXME,XXX 252 | 253 | 254 | [SIMILARITIES] 255 | 256 | # Minimum lines number of a similarity. 257 | min-similarity-lines=4 258 | 259 | # Ignore comments when computing similarities. 260 | ignore-comments=yes 261 | 262 | # Ignore docstrings when computing similarities. 263 | ignore-docstrings=yes 264 | 265 | # Ignore imports when computing similarities. 266 | ignore-imports=no 267 | 268 | 269 | [SPELLING] 270 | 271 | # Spelling dictionary name. Available dictionaries: none. To make it working 272 | # install python-enchant package. 273 | spelling-dict= 274 | 275 | # List of comma separated words that should not be checked. 276 | spelling-ignore-words= 277 | 278 | # A path to a file that contains private dictionary; one word per line. 279 | spelling-private-dict-file= 280 | 281 | # Tells whether to store unknown words to indicated private dictionary in 282 | # --spelling-private-dict-file option instead of raising a message. 283 | spelling-store-unknown-words=no 284 | 285 | 286 | [TYPECHECK] 287 | 288 | # Tells whether missing members accessed in mixin class should be ignored. A 289 | # mixin class is detected if its name ends with "mixin" (case insensitive). 290 | ignore-mixin-members=yes 291 | 292 | # List of module names for which member attributes should not be checked 293 | # (useful for modules/projects where namespaces are manipulated during runtime 294 | # and thus existing member attributes cannot be deduced by static analysis. It 295 | # supports qualified module names, as well as Unix pattern matching. 296 | ignored-modules= 297 | 298 | # List of class names for which member attributes should not be checked (useful 299 | # for classes with dynamically set attributes). This supports the use of 300 | # qualified names. 301 | ignored-classes=optparse.Values,thread._local,_thread._local 302 | 303 | # List of members which are set dynamically and missed by pylint inference 304 | # system, and so shouldn't trigger E1101 when accessed. Python regular 305 | # expressions are accepted. 306 | generated-members=objects, as_view 307 | 308 | # List of decorators that produce context managers, such as 309 | # contextlib.contextmanager. Add to this list to register other decorators that 310 | # produce valid context managers. 311 | contextmanager-decorators=contextlib.contextmanager 312 | 313 | 314 | [VARIABLES] 315 | 316 | # Tells whether we should check for unused import in __init__ files. 317 | init-import=no 318 | 319 | # A regular expression matching the name of dummy variables (i.e. expectedly 320 | # not used). 321 | dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy 322 | 323 | # List of additional names supposed to be defined in builtins. Remember that 324 | # you should avoid to define new builtins when possible. 325 | additional-builtins= 326 | 327 | # List of strings which can identify a callback function by name. A callback 328 | # name must start or end with one of those strings. 329 | callbacks=cb_,_cb 330 | 331 | # List of qualified module names which can have objects that can redefine 332 | # builtins. 333 | redefining-builtins-modules=six.moves,future.builtins 334 | 335 | 336 | [CLASSES] 337 | 338 | # List of method names used to declare (i.e. assign) instance attributes. 339 | defining-attr-methods=__init__,__new__,setUp 340 | 341 | # List of valid names for the first argument in a class method. 342 | valid-classmethod-first-arg=cls 343 | 344 | # List of valid names for the first argument in a metaclass class method. 345 | valid-metaclass-classmethod-first-arg=mcs 346 | 347 | # List of member names, which should be excluded from the protected access 348 | # warning. 349 | exclude-protected=_asdict,_fields,_replace,_source,_make 350 | 351 | 352 | [DESIGN] 353 | 354 | # Maximum number of arguments for function / method 355 | max-args=5 356 | 357 | # Argument names that match this expression will be ignored. Default to name 358 | # with leading underscore 359 | ignored-argument-names=_.* 360 | 361 | # Maximum number of locals for function / method body 362 | max-locals=15 363 | 364 | # Maximum number of return / yield for function / method body 365 | max-returns=6 366 | 367 | # Maximum number of branch for function / method body 368 | max-branches=12 369 | 370 | # Maximum number of statements in function / method body 371 | max-statements=50 372 | 373 | # Maximum number of parents for a class (see R0901). 374 | max-parents=7 375 | 376 | # Maximum number of attributes for a class (see R0902). 377 | max-attributes=12 378 | 379 | # Minimum number of public methods for a class (see R0903). 380 | min-public-methods=2 381 | 382 | # Maximum number of public methods for a class (see R0904). 383 | max-public-methods=20 384 | 385 | # Maximum number of boolean expressions in a if statement 386 | max-bool-expr=5 387 | 388 | 389 | [IMPORTS] 390 | 391 | # Deprecated modules which should not be used, separated by a comma 392 | deprecated-modules=optparse 393 | 394 | # Create a graph of every (i.e. internal and external) dependencies in the 395 | # given file (report RP0402 must not be disabled) 396 | import-graph= 397 | 398 | # Create a graph of external dependencies in the given file (report RP0402 must 399 | # not be disabled) 400 | ext-import-graph= 401 | 402 | # Create a graph of internal dependencies in the given file (report RP0402 must 403 | # not be disabled) 404 | int-import-graph= 405 | 406 | # Force import order to recognize a module as part of the standard 407 | # compatibility libraries. 408 | known-standard-library= 409 | 410 | # Force import order to recognize a module as part of a third party library. 411 | known-third-party=enchant 412 | 413 | # Analyse import fallback blocks. This can be used to support both Python 2 and 414 | # 3 compatible code, which means that the block might have code that exists 415 | # only in one or another interpreter, leading to false positives when analysed. 416 | analyse-fallback-blocks=no 417 | 418 | 419 | [EXCEPTIONS] 420 | 421 | # Exceptions that will emit a warning when being caught. Defaults to 422 | # "Exception" 423 | overgeneral-exceptions=Exception -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | install: 9 | - pip install -r requirements.txt 10 | - pip install -r requirements.test.txt 11 | script: 12 | - python -m pytest test --cov=ridi 13 | after_success: 14 | - coveralls 15 | deploy: 16 | provider: pypi 17 | user: "ridi-data" 18 | password: 19 | secure: "H5Yrnd8bQaXF46lf6iQd23SBBuqX+/v7ViWmqgUzvefva8nasI691eiD+BDDoG6XnAAPK0nMH0qjbhSl3HUc78blWQqR5E5L20nz2318cPOKdQZIGRSkFAVNBQHwESrWgR58vIZn3afKx7PCNNIwflHoijG/eAk1rxNsPN8xCsZcwDZlrixT/05x4jE4L7T3KKWa6q8BNIOGTLzJQ7F8+5y3QHaDB0pgDSLsZidILuSpA7YiIt/a6l48b5ZEVpJVpQHTM2BkBELnDwg8vLqjuqtzDuwuKZCGRoy6o1uCRgfh6OjJsSXDYlrji8oJLsYSKC2QgWouNsQ9s9RYEKy8mWfqGgTp7/I5LSL5Bo89A62xMdE5GVrbTytPoHkyxiVFS0geEbuOs9r3LpuOIyIQhZssmLbFtZ+Wqs/xM8Fbg2AHs95CRX/nYABZILV096AIt9qwIrvPCPfyglLbMz4ZTaBv4tTVWbRCDEdsmpD/p3ROk0sc4uY6RTAAfMlVT+WHv9MTV3Z9MQHUPrV1vMQ2C4VS9H1IOxfd7/S6095ANQUF5pgNuzW/XQIJitwE9PLqpiht7I4d1t3n23F7E5di/zP7UglJfzMMqCosZsZFbRYqJBPGmzIxQxiusnS++VTVsZrJnZmUp3gVeOrtKTXf9rZDaQNkVlEHvaulBcdZ4G8=" 20 | on: 21 | tags: true 22 | python: "3.6" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 RIDI 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build test dist pypi-upload 2 | 3 | all: build 4 | 5 | build: 6 | pip install -r requirements.txt 7 | 8 | test: 9 | pip install pytest pytest-cov coveralls && python -m pytest test --cov=ridi 10 | 11 | lint: 12 | pylint ridi 13 | 14 | dist: 15 | rm -rf ./dist 16 | python setup.py sdist 17 | 18 | pypi-upload: 19 | twine upload dist/* 20 | 21 | release: dist pypi-upload -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secret Keeper - Python 2 | 3 | [![Build Status](https://travis-ci.com/ridi/secret-keeper-python.svg?branch=master)](https://travis-ci.com/ridi/secret-keeper-python) 4 | [![PyPI](https://img.shields.io/pypi/v/secret-keeper.svg)](https://pypi.org/project/secret-keeper/) 5 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/secret-keeper.svg)](https://pypi.org/project/secret-keeper/) 6 | [![Coverage Status](https://coveralls.io/repos/github/ridi/secret-keeper-python/badge.svg?branch=master)](https://coveralls.io/github/ridi/secret-keeper-python?branch=master) 7 | 8 | ## Introduction 9 | Without secret-keeper, you would have: 10 | - hard-coded your secrets in your version-controlled source code (Worst!), or 11 | - created a not-version-controlled config file and manually provide it when you deploy your code, or 12 | - let your deployment system - Jenkins CI, etc - manage your not-version-controlled config file, but you have as many of them as your projects. 13 | 14 | With secret-keeper, you can: 15 | - store your secrets in AWS and let your applications use it safely and conveniently. 16 | - let AWS manage contents of your secrets, keeping them encoded and safe. 17 | - version-control usage of secrets inside your applications, since secrets are referred only with their aliases. 18 | - let your deployment systems use secrets, simply by adding an IAM policy to the IAM user or role that you use in deployment. You don't have to manage per-project config files. 19 | 20 | 21 | ## Install 22 | ```bash 23 | pip install secret-keeper 24 | ``` 25 | 26 | ## Preparation 27 | - [Create a dedicated encryption key in AWS KMS](https://github.com/ridi/secret-keeper-python/wiki/Create-a-dedicated-encryption-key-in-AWS-KMS) 28 | - [Create a dedicated IAM Policy for accessing secrets](https://github.com/ridi/secret-keeper-python/wiki/Create-a-dedicated-IAM-Policy-for-accessing-secrets) 29 | - Add the policy to you IAM User or IAM Role. 30 | - [Create a sample secret in AWS SSM Parameter Store](https://github.com/ridi/secret-keeper-python/wiki/Create-a-sample-secret-in-AWS-SSM-Parameter-Store) 31 | 32 | ## Usage 33 | ### Prepare credentials 34 | If you are running as an IAM user with its security credentials, make sure that your credentials are properly set in either `~/.aws/credentials` file, or `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. 35 | See [`boto3`'s credentials scheme](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#credentials) for details of setting credentials. 36 | 37 | #### Before 0.2.0 38 | If you are using `secret-keeper` of version `0.1.x`, you cannot use `boto3`'s 39 | credentials scheme. You should store credentials as special environment variables, namely `SECRETKEEPER_AWS_ACCESS_KEY`, `SECRETKEEPER_AWS_SECRET_KEY` and `SECRETKEEPER_AWS_REGION`. 40 | 41 | ```bash 42 | $ export SECRETKEEPER_AWS_ACCESS_KEY="YOUR_ACCESS_KEY_ID" 43 | $ export SECRETKEEPER_AWS_SECRET_KEY="YOUR_SECRET_ACCESS_KEY" 44 | $ export SECRETKEEPER_AWS_REGION="us-east-1" 45 | ``` 46 | 47 | ### Commandline Interface 48 | `secret-keeper` commandline interface is supported as of `0.3.0`. 49 | - Write to stdout. 50 | ``` 51 | $ secret-keeper sample.secret 52 | pa$$w@rd! 53 | ``` 54 | - Write to file. 55 | ``` 56 | $ secret-keeper sample.secret --o outfile && cat outfile 57 | pa$$w@rd! 58 | ``` 59 | - Print help. 60 | 61 | ``` 62 | $ secret-keeper -h 63 | usage: secret-keeper [-h] [-o OUTFILE] [-v] alias 64 | 65 | Retrieve and print secrets from `secret-keeper`. You need to configure AWS 66 | credentials by environment variables or files. See https://boto3.amazonaws.com 67 | /v1/documentation/api/latest/guide/configuration.html#credentials for more 68 | detail. 69 | 70 | positional arguments: 71 | alias Alias of the secret 72 | 73 | optional arguments: 74 | -h, --help show this help message and exit 75 | -o OUTFILE, --outfile OUTFILE 76 | Output file name. If not provided, secret is printed 77 | to stdout. 78 | -v, --verbose Gives detailed error message 79 | ``` 80 | 81 | ### Sample application. 82 | - Write a sample application. 83 | ```Python 84 | # sample.py 85 | if __name__ == "__main__": 86 | from ridi import secret_keeper 87 | 88 | secret = secret_keeper.tell("sample.secret") 89 | print("Secret: %s" % secret) 90 | ``` 91 | 92 | - Run the sample application. 93 | 94 | ``` 95 | $ python sample.py 96 | pa$$w@rd! 97 | ``` 98 | 99 | - Rather than using `boto3`'s credentials scheme, you can pass your credentials and region to `configure` function. (as of `0.2.0`) 100 | 101 | ```python 102 | # sample2.py 103 | from ridi import secret_keeper 104 | 105 | secret_keeper.configure( 106 | aws_access_key="YOUR_ACCESS_KEY_ID", 107 | aws_secret_key="YOUR_SECRET_ACCESS_KEY", 108 | aws_region="us-east-1", 109 | ) 110 | ``` 111 | -------------------------------------------------------------------------------- /images/AddUser-01-user-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/AddUser-01-user-details.png -------------------------------------------------------------------------------- /images/AddUser-02-set-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/AddUser-02-set-permissions.png -------------------------------------------------------------------------------- /images/AddUser-03-create-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/AddUser-03-create-policy.png -------------------------------------------------------------------------------- /images/AddUser-04-review-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/AddUser-04-review-policy.png -------------------------------------------------------------------------------- /images/AddUser-05-select-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/AddUser-05-select-policy.png -------------------------------------------------------------------------------- /images/AddUser-06-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/AddUser-06-success.png -------------------------------------------------------------------------------- /images/CreateKey-01-alias.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/CreateKey-01-alias.png -------------------------------------------------------------------------------- /images/CreateKey-02-review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/CreateKey-02-review.png -------------------------------------------------------------------------------- /images/CreateKey-03-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/CreateKey-03-success.png -------------------------------------------------------------------------------- /images/CreateParam-01-parameter-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/CreateParam-01-parameter-details.png -------------------------------------------------------------------------------- /images/CreatePolicy-01-review-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/images/CreatePolicy-01-review-policy.png -------------------------------------------------------------------------------- /requirements.test.txt: -------------------------------------------------------------------------------- 1 | pytest>=3.6 2 | pytest-cov 3 | coveralls 4 | 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.9.16 2 | 3 | -------------------------------------------------------------------------------- /ridi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/ridi/__init__.py -------------------------------------------------------------------------------- /ridi/secret_keeper/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import * 2 | from .connect import * 3 | -------------------------------------------------------------------------------- /ridi/secret_keeper/cmdline.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import argparse 3 | import sys 4 | import traceback 5 | 6 | from botocore.exceptions import ClientError 7 | 8 | from ridi.secret_keeper.connect import tell 9 | 10 | 11 | def run(arguments): 12 | description = """ 13 | Retrieve and print secrets from `secret-keeper`. 14 | You need to configure AWS credentials by environment variables or files. 15 | See https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#credentials for more detail. 16 | """ 17 | parser = argparse.ArgumentParser(description=description) 18 | parser.add_argument("alias", help="Alias of the secret") 19 | parser.add_argument("-o", "--outfile", help="Output file name. If not provided, secret is printed to stdout.") 20 | parser.add_argument("-v", "--verbose", action="store_true", help="Gives detailed error message") 21 | args = parser.parse_args(arguments) 22 | 23 | alias = args.alias 24 | outfile = args.outfile 25 | verbose = args.verbose 26 | 27 | try: 28 | secret = tell(alias).strip() 29 | if outfile: 30 | with open(outfile, "w") as f: 31 | print(secret, file=f) 32 | else: 33 | print(secret) 34 | return 0 35 | except Exception as e: 36 | if verbose: 37 | traceback.print_exc(file=sys.stderr) 38 | if isinstance(e, ClientError): 39 | print("Secret of alias '%s' is not found." % alias, file=sys.stderr) 40 | return 1 41 | 42 | 43 | def main(): 44 | retval = run(sys.argv[1:]) 45 | sys.exit(retval) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /ridi/secret_keeper/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from collections import namedtuple 4 | 5 | ENVNAME_AWS_ACCESS_KEY = "SECRETKEEPER_AWS_ACCESS_KEY" 6 | ENVNAME_AWS_SECRET_KEY = "SECRETKEEPER_AWS_SECRET_KEY" 7 | ENVNAME_AWS_REGION = "SECRETKEEPER_AWS_REGION" 8 | 9 | ConnectArgsType = namedtuple('ConnectArgsType', [ 10 | 'aws_access_key', 'aws_secret_key', 'aws_region' 11 | ]) 12 | 13 | AWS_CONNECT_ARGS = None 14 | 15 | 16 | def configure(aws_access_key=None, aws_secret_key=None, aws_region=None): 17 | global AWS_CONNECT_ARGS 18 | AWS_CONNECT_ARGS = ConnectArgsType( 19 | aws_access_key=aws_access_key, 20 | aws_secret_key=aws_secret_key, 21 | aws_region=aws_region, 22 | ) 23 | 24 | 25 | configure( 26 | aws_access_key=os.environ.get(ENVNAME_AWS_ACCESS_KEY), 27 | aws_secret_key=os.environ.get(ENVNAME_AWS_SECRET_KEY), 28 | aws_region=os.environ.get(ENVNAME_AWS_REGION), 29 | ) 30 | 31 | __all__ = ["configure", "ENVNAME_AWS_ACCESS_KEY", "ENVNAME_AWS_SECRET_KEY", "ENVNAME_AWS_REGION"] 32 | -------------------------------------------------------------------------------- /ridi/secret_keeper/connect.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def _get_client(): 5 | from ridi.secret_keeper.config import AWS_CONNECT_ARGS 6 | 7 | client = boto3.client( 8 | 'ssm', 9 | region_name=AWS_CONNECT_ARGS.aws_region, 10 | aws_access_key_id=AWS_CONNECT_ARGS.aws_access_key, 11 | aws_secret_access_key=AWS_CONNECT_ARGS.aws_secret_key, 12 | ) 13 | return client 14 | 15 | 16 | def tell(alias): 17 | response = _get_client().get_parameter( 18 | Name=alias, WithDecryption=True, 19 | ) 20 | return response['Parameter']['Value'] 21 | 22 | 23 | def tell_safe(alias): 24 | try: 25 | return tell(alias) 26 | except Exception: 27 | return None 28 | 29 | 30 | __all__ = ["tell", "tell_safe"] 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | """A setuptools based setup module. 3 | """ 4 | 5 | from setuptools import setup, find_packages 6 | 7 | setup( 8 | name='secret-keeper', 9 | packages=[ 10 | 'ridi.secret_keeper', 11 | ], 12 | version='0.3.1', 13 | description='Secret Keeper', 14 | url='https://github.com/ridi/secret-keeper-python', 15 | keywords=['secret', 'secret-keeper', 'ridi', 'ridibooks'], 16 | classifiers=[ 17 | 'License :: OSI Approved :: MIT License', 18 | 'Programming Language :: Python', 19 | 'Programming Language :: Python :: 2.7', 20 | 'Programming Language :: Python :: 3.3', 21 | 'Programming Language :: Python :: 3.4', 22 | 'Programming Language :: Python :: 3.5', 23 | 'Programming Language :: Python :: 3.6', 24 | ], 25 | install_requires=[ 26 | 'boto3>=1.9.16', 27 | ], 28 | entry_points={ 29 | 'console_scripts': [ 30 | 'secret-keeper = ridi.secret_keeper.cmdline:main' 31 | ] 32 | }, 33 | ) 34 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridi/secret-keeper-python/152eee167ab108ca975be631c1ef51bbd9b263f1/test/__init__.py -------------------------------------------------------------------------------- /test/test_secret_keeper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import string 4 | import unittest 5 | try: 6 | from StringIO import StringIO # for Python2 support 7 | except ImportError: 8 | from io import StringIO 9 | 10 | from ridi.secret_keeper import tell, tell_safe 11 | from ridi.secret_keeper.cmdline import run 12 | 13 | try: 14 | from unittest import mock 15 | except ImportError: 16 | import mock 17 | 18 | from botocore.exceptions import ClientError 19 | 20 | 21 | class FakeClient(object): 22 | def __init__(self, dict): 23 | self._dict = dict 24 | 25 | def get_parameter(self, Name, WithDecryption): 26 | assert WithDecryption is True 27 | if Name in self._dict: 28 | return { 29 | 'Parameter': { 30 | 'Name': Name, 31 | 'Type': "SecureString", 32 | 'Value': self._dict[Name], 33 | } 34 | } 35 | else: 36 | error_response = { 37 | 'Error': { 38 | 'Code': "ParameterNotFound", 39 | }, 40 | } 41 | raise ClientError(error_response, operation_name="GetParameter") 42 | 43 | 44 | class TestSecretKeeperBase(unittest.TestCase): 45 | def setUp(self): 46 | _client = FakeClient({ 47 | "ones": "11111", 48 | }) 49 | self._patcher = mock.patch('boto3.client', return_value=_client) 50 | self._patcher.start() 51 | 52 | def tearDown(self): 53 | self._patcher.stop() 54 | 55 | 56 | class TestSecretKeeperTell(TestSecretKeeperBase): 57 | def test_existing_keys(self): 58 | self.assertEqual(tell("ones"), "11111") 59 | 60 | def test_non_existing_keys(self): 61 | with self.assertRaises(ClientError): 62 | tell("twos") 63 | 64 | 65 | class TestSecretKeeperTellSafe(TestSecretKeeperBase): 66 | def test_existing_keys(self): 67 | self.assertEqual(tell_safe("ones"), "11111") 68 | 69 | def test_non_existing_keys(self): 70 | self.assertEqual(tell_safe("twos"), None) 71 | 72 | 73 | class TestSecretKeeperCLI(TestSecretKeeperBase): 74 | def test_success_stdout(self): 75 | with mock.patch('sys.stdout', new=StringIO()) as fake_stdout: 76 | retval = run(["ones"]) 77 | output = fake_stdout.getvalue().strip() 78 | self.assertEqual(output, "11111") 79 | self.assertEqual(retval, 0) 80 | 81 | def test_success_file(self): 82 | outfile = "tmp." + "".join(random.choice(string.ascii_letters) for _ in range(10)) 83 | if os.path.exists(outfile): 84 | os.remove(outfile) 85 | 86 | retval = run(["ones", "--outfile", outfile]) 87 | 88 | with open(outfile, "r") as f: 89 | content = f.read().strip() 90 | 91 | if os.path.exists(outfile): 92 | os.remove(outfile) 93 | 94 | self.assertEqual(content, "11111") 95 | self.assertEqual(retval, 0) 96 | 97 | def test_fail_stderr(self): 98 | with mock.patch('sys.stderr', new=StringIO()) as fake_stderr: 99 | retval = run(["twos"]) 100 | output = fake_stderr.getvalue().strip() 101 | self.assertIn("Secret of alias 'twos' is not found.", output) 102 | self.assertEqual(retval, 1) 103 | --------------------------------------------------------------------------------