From 4330ac9c05ae49a415799ea8a422bd9b4b8347b4 Mon Sep 17 00:00:00 2001 From: Kevin Glisson Date: Mon, 22 Jun 2015 13:47:27 -0700 Subject: [PATCH] initial commit --- .bowerrc | 3 + .gitattributes | 1 + .gitignore | 28 + .jshintignore | 4 + .jshintrc | 24 + .travis.yml | 7 + AUTHORS | 1 + CHANGES | 0 LICENSE | 202 ++++++ README.rst | 19 + bower.json | 49 ++ config-default.py | 2 + docs/Makefile | 177 ++++++ docs/administration/index.rst | 540 ++++++++++++++++ docs/changelog.rst | 2 + docs/conf.py | 262 ++++++++ docs/developer/index.rst | 197 ++++++ docs/developer/internals/lemur.accounts.rst | 20 + docs/developer/internals/lemur.analyze.rst | 11 + docs/developer/internals/lemur.auth.rst | 20 + .../developer/internals/lemur.authorities.rst | 20 + .../internals/lemur.certificates.rst | 43 ++ docs/developer/internals/lemur.common.rst | 34 ++ .../internals/lemur.common.services.aws.rst | 35 ++ ...ommon.services.issuers.plugins.cloudca.rst | 19 + .../lemur.common.services.issuers.plugins.rst | 11 + ...mmon.services.issuers.plugins.verisign.rst | 19 + .../lemur.common.services.issuers.rst | 26 + .../internals/lemur.common.services.rst | 11 + docs/developer/internals/lemur.domains.rst | 19 + docs/developer/internals/lemur.elbs.rst | 35 ++ docs/developer/internals/lemur.listeners.rst | 27 + docs/developer/internals/lemur.roles.rst | 20 + docs/developer/internals/lemur.rst | 87 +++ docs/developer/internals/lemur.status.rst | 11 + docs/developer/internals/lemur.users.rst | 19 + docs/developer/plugins/index.rst | 151 +++++ docs/developer/rest.rst | 60 ++ docs/faq.rst | 30 + docs/guide/index.rst | 10 + docs/index.rst | 67 ++ docs/license/index.rst | 20 + docs/plugins/index.rst | 20 + docs/production/index.rst | 277 +++++++++ docs/quickstart/index.rst | 237 ++++++++ gulp/build.js | 231 +++++++ gulp/server.js | 56 ++ gulp/watch.js | 12 + gulpfile.js | 31 + hooks/pre-commit | 54 ++ lemur/__init__.py | 69 +++ lemur/accounts/__init__.py | 0 lemur/accounts/models.py | 29 + lemur/accounts/service.py | 112 ++++ lemur/accounts/views.py | 300 +++++++++ lemur/analyze/__init__.py | 0 lemur/analyze/service.py | 62 ++ lemur/auth/__init__.py | 0 lemur/auth/permissions.py | 62 ++ lemur/auth/service.py | 188 ++++++ lemur/auth/views.py | 257 ++++++++ lemur/authorities/__init__.py | 0 lemur/authorities/models.py | 58 ++ lemur/authorities/service.py | 173 ++++++ lemur/authorities/views.py | 372 +++++++++++ lemur/certificates/__init__.py | 0 lemur/certificates/exceptions.py | 87 +++ lemur/certificates/models.py | 307 ++++++++++ lemur/certificates/service.py | 446 ++++++++++++++ lemur/certificates/sync.py | 169 +++++ lemur/certificates/verify.py | 135 ++++ lemur/certificates/views.py | 575 ++++++++++++++++++ lemur/common/__init__.py | 0 lemur/common/crypto.py | 185 ++++++ lemur/common/health.py | 15 + lemur/common/services/__init__.py | 0 lemur/common/services/aws/__init__.py | 0 lemur/common/services/aws/elb.py | 140 +++++ lemur/common/services/aws/iam.py | 104 ++++ lemur/common/services/aws/ses.py | 29 + lemur/common/services/aws/sts.py | 41 ++ lemur/common/services/issuers/__init__.py | 0 lemur/common/services/issuers/issuer.py | 32 + lemur/common/services/issuers/manager.py | 37 ++ .../services/issuers/plugins/__init__.py | 0 .../issuers/plugins/cloudca/__init__.py | 5 + .../issuers/plugins/cloudca/cloudca.py | 346 +++++++++++ .../issuers/plugins/cloudca/constants.py | 27 + .../issuers/plugins/verisign/__init__.py | 5 + .../issuers/plugins/verisign/constants.py | 159 +++++ .../issuers/plugins/verisign/verisign.py | 194 ++++++ lemur/common/utils.py | 63 ++ lemur/constants.py | 10 + lemur/database.py | 278 +++++++++ lemur/decorators.py | 55 ++ lemur/domains/__init__.py | 0 lemur/domains/models.py | 27 + lemur/domains/service.py | 64 ++ lemur/domains/views.py | 182 ++++++ lemur/elbs/__init__.py | 0 lemur/elbs/models.py | 44 ++ lemur/elbs/service.py | 125 ++++ lemur/elbs/sync.py | 72 +++ lemur/elbs/views.py | 78 +++ lemur/exceptions.py | 61 ++ lemur/extensions.py | 18 + lemur/factory.py | 138 +++++ lemur/listeners/__init__.py | 0 lemur/listeners/models.py | 43 ++ lemur/listeners/service.py | 162 +++++ lemur/listeners/views.py | 128 ++++ lemur/manage.py | 507 +++++++++++++++ lemur/migrations/README | 1 + lemur/migrations/alembic.ini | 45 ++ lemur/migrations/env.py | 73 +++ lemur/migrations/script.py.mako | 22 + lemur/models.py | 30 + lemur/notifications.py | 184 ++++++ lemur/roles/__init__.py | 0 lemur/roles/models.py | 39 ++ lemur/roles/service.py | 125 ++++ lemur/roles/views.py | 445 ++++++++++++++ lemur/static/app/.buildignore | 1 + lemur/static/app/404.html | 157 +++++ .../app/angular/accounts/account/account.js | 27 + .../angular/accounts/account/account.tpl.html | 45 ++ lemur/static/app/angular/accounts/services.js | 53 ++ .../static/app/angular/accounts/view/view.js | 52 ++ .../app/angular/accounts/view/view.tpl.html | 44 ++ lemur/static/app/angular/app.js | 113 ++++ .../app/angular/authentication/login/login.js | 28 + .../authentication/login/login.tpl.html | 27 + .../angular/authentication/logout/logout.js | 12 + .../authentication/logout/logout.tpl.html | 10 + .../app/angular/authentication/services.js | 62 ++ .../angular/authentication/unlock/unlock.js | 18 + .../authentication/unlock/unlock.tpl.html | 16 + .../authorities/authority/authority.js | 55 ++ .../authority/authorityEdit.tpl.html | 44 ++ .../authority/authorityWizard.tpl.html | 17 + .../authority/distinguishedName.tpl.html | 55 ++ .../authorities/authority/extensions.tpl.html | 219 +++++++ .../authorities/authority/options.tpl.html | 78 +++ .../authority/permissions.tpl.html | 28 + .../authorities/authority/select.tpl.html | 3 + .../authorities/authority/tracking.tpl.html | 71 +++ .../app/angular/authorities/services.js | 130 ++++ .../app/angular/authorities/view/view.js | 46 ++ .../angular/authorities/view/view.tpl.html | 50 ++ .../certificates/certificate/certificate.js | 94 +++ .../certificate/certificate.tpl.html | 20 + .../certificate/certificateWizard.tpl.html | 17 + .../certificate/destinations.tpl.html | 62 ++ .../certificate/distinguishedName.tpl.html | 55 ++ .../certificates/certificate/options.tpl.html | 224 +++++++ .../certificate/tracking.tpl.html | 103 ++++ .../certificates/certificate/upload.js | 26 + .../certificates/certificate/upload.tpl.html | 136 +++++ .../app/angular/certificates/services.js | 238 ++++++++ .../app/angular/certificates/view/view.js | 63 ++ .../angular/certificates/view/view.tpl.html | 142 +++++ .../static/app/angular/components/filters.js | 9 + .../static/app/angular/dashboard/dashboard.js | 93 +++ .../app/angular/dashboard/dashboard.tpl.html | 41 ++ lemur/static/app/angular/domains/services.js | 13 + lemur/static/app/angular/domains/view/view.js | 35 ++ .../app/angular/domains/view/view.tpl.html | 25 + lemur/static/app/angular/elbs/elb/elb.js | 3 + .../static/app/angular/elbs/elb/elb.tpl.html | 10 + lemur/static/app/angular/elbs/services.js | 57 ++ lemur/static/app/angular/elbs/view/view.js | 34 ++ .../app/angular/elbs/view/view.tpl.html | 128 ++++ .../static/app/angular/listeners/services.js | 35 ++ lemur/static/app/angular/loadingModal.html | 6 + lemur/static/app/angular/pager.html | 19 + lemur/static/app/angular/roles/role/role.js | 30 + .../app/angular/roles/role/role.tpl.html | 85 +++ lemur/static/app/angular/roles/services.js | 119 ++++ lemur/static/app/angular/roles/view/view.js | 42 ++ .../app/angular/roles/view/view.tpl.html | 41 ++ lemur/static/app/angular/users/services.js | 89 +++ lemur/static/app/angular/users/user/user.js | 38 ++ .../app/angular/users/user/user.tpl.html | 89 +++ lemur/static/app/angular/users/view/view.js | 43 ++ .../app/angular/users/view/view.tpl.html | 43 ++ lemur/static/app/angular/welcome/welcome.html | 19 + lemur/static/app/angular/wizard.html | 23 + lemur/static/app/favicon.ico | Bin 0 -> 11153 bytes lemur/static/app/index.html | 86 +++ lemur/static/app/robots.txt | 3 + lemur/static/app/styles/lemur.css | 161 +++++ lemur/status/__init__.py | 0 lemur/status/views.py | 33 + lemur/templates/__init__.py | 0 lemur/templates/config.py | 4 + lemur/templates/event.html | 144 +++++ lemur/tests/__init__.py | 16 + lemur/tests/certificates/__init__.py | 0 lemur/tests/certificates/test_certificates.py | 87 +++ lemur/tests/certificates/test_csr.py | 38 ++ .../certificates/test_pack/challenge.txt | 1 + .../certificates/test_pack/csr_config.txt | 38 ++ .../tests/certificates/test_pack/private.key | 27 + .../tests/certificates/test_pack/request.csr | 17 + lemur/tests/certificates/test_pack/server.crt | 21 + lemur/tests/constants.py | 51 ++ lemur/tests/elbs/__init__.py | 0 lemur/tests/elbs/test_elbs.py | 5 + lemur/tests/js/.jshintrc | 36 ++ lemur/tests/js/runner.html | 10 + .../js/spec/controllers/certificatecreate.js | 22 + .../tests/js/spec/controllers/certificates.js | 22 + lemur/tests/js/spec/controllers/dashboard.js | 22 + lemur/tests/js/spec/controllers/elbs.js | 22 + lemur/tests/js/spec/controllers/main.js | 22 + lemur/tests/listeners/__init__.py | 0 lemur/tests/services/__init__.py | 0 lemur/tests/services/test_elb.py | 51 ++ lemur/tests/services/test_iam.py | 37 ++ lemur/tests/services/test_issuer_manager.py | 23 + lemur/tests/services/test_s3.py | 27 + lemur/users/__init__.py | 0 lemur/users/models.py | 88 +++ lemur/users/service.py | 150 +++++ lemur/users/views.py | 401 ++++++++++++ package.json | 67 ++ setup.py | 114 ++++ trustores/__init__.py | 0 228 files changed, 16656 insertions(+) create mode 100644 .bowerrc create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .jshintignore create mode 100644 .jshintrc create mode 100644 .travis.yml create mode 100644 AUTHORS create mode 100644 CHANGES create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 bower.json create mode 100644 config-default.py create mode 100644 docs/Makefile create mode 100644 docs/administration/index.rst create mode 100644 docs/changelog.rst create mode 100644 docs/conf.py create mode 100644 docs/developer/index.rst create mode 100644 docs/developer/internals/lemur.accounts.rst create mode 100644 docs/developer/internals/lemur.analyze.rst create mode 100644 docs/developer/internals/lemur.auth.rst create mode 100644 docs/developer/internals/lemur.authorities.rst create mode 100644 docs/developer/internals/lemur.certificates.rst create mode 100644 docs/developer/internals/lemur.common.rst create mode 100644 docs/developer/internals/lemur.common.services.aws.rst create mode 100644 docs/developer/internals/lemur.common.services.issuers.plugins.cloudca.rst create mode 100644 docs/developer/internals/lemur.common.services.issuers.plugins.rst create mode 100644 docs/developer/internals/lemur.common.services.issuers.plugins.verisign.rst create mode 100644 docs/developer/internals/lemur.common.services.issuers.rst create mode 100644 docs/developer/internals/lemur.common.services.rst create mode 100644 docs/developer/internals/lemur.domains.rst create mode 100644 docs/developer/internals/lemur.elbs.rst create mode 100644 docs/developer/internals/lemur.listeners.rst create mode 100644 docs/developer/internals/lemur.roles.rst create mode 100644 docs/developer/internals/lemur.rst create mode 100644 docs/developer/internals/lemur.status.rst create mode 100644 docs/developer/internals/lemur.users.rst create mode 100644 docs/developer/plugins/index.rst create mode 100644 docs/developer/rest.rst create mode 100644 docs/faq.rst create mode 100644 docs/guide/index.rst create mode 100644 docs/index.rst create mode 100644 docs/license/index.rst create mode 100644 docs/plugins/index.rst create mode 100644 docs/production/index.rst create mode 100644 docs/quickstart/index.rst create mode 100644 gulp/build.js create mode 100644 gulp/server.js create mode 100644 gulp/watch.js create mode 100644 gulpfile.js create mode 100644 hooks/pre-commit create mode 100644 lemur/__init__.py create mode 100644 lemur/accounts/__init__.py create mode 100644 lemur/accounts/models.py create mode 100644 lemur/accounts/service.py create mode 100644 lemur/accounts/views.py create mode 100644 lemur/analyze/__init__.py create mode 100644 lemur/analyze/service.py create mode 100644 lemur/auth/__init__.py create mode 100644 lemur/auth/permissions.py create mode 100644 lemur/auth/service.py create mode 100644 lemur/auth/views.py create mode 100644 lemur/authorities/__init__.py create mode 100644 lemur/authorities/models.py create mode 100644 lemur/authorities/service.py create mode 100644 lemur/authorities/views.py create mode 100644 lemur/certificates/__init__.py create mode 100644 lemur/certificates/exceptions.py create mode 100644 lemur/certificates/models.py create mode 100644 lemur/certificates/service.py create mode 100644 lemur/certificates/sync.py create mode 100644 lemur/certificates/verify.py create mode 100644 lemur/certificates/views.py create mode 100644 lemur/common/__init__.py create mode 100644 lemur/common/crypto.py create mode 100644 lemur/common/health.py create mode 100644 lemur/common/services/__init__.py create mode 100644 lemur/common/services/aws/__init__.py create mode 100644 lemur/common/services/aws/elb.py create mode 100644 lemur/common/services/aws/iam.py create mode 100644 lemur/common/services/aws/ses.py create mode 100644 lemur/common/services/aws/sts.py create mode 100644 lemur/common/services/issuers/__init__.py create mode 100644 lemur/common/services/issuers/issuer.py create mode 100644 lemur/common/services/issuers/manager.py create mode 100644 lemur/common/services/issuers/plugins/__init__.py create mode 100644 lemur/common/services/issuers/plugins/cloudca/__init__.py create mode 100644 lemur/common/services/issuers/plugins/cloudca/cloudca.py create mode 100644 lemur/common/services/issuers/plugins/cloudca/constants.py create mode 100644 lemur/common/services/issuers/plugins/verisign/__init__.py create mode 100644 lemur/common/services/issuers/plugins/verisign/constants.py create mode 100644 lemur/common/services/issuers/plugins/verisign/verisign.py create mode 100644 lemur/common/utils.py create mode 100644 lemur/constants.py create mode 100644 lemur/database.py create mode 100644 lemur/decorators.py create mode 100644 lemur/domains/__init__.py create mode 100644 lemur/domains/models.py create mode 100644 lemur/domains/service.py create mode 100644 lemur/domains/views.py create mode 100644 lemur/elbs/__init__.py create mode 100644 lemur/elbs/models.py create mode 100644 lemur/elbs/service.py create mode 100644 lemur/elbs/sync.py create mode 100644 lemur/elbs/views.py create mode 100644 lemur/exceptions.py create mode 100644 lemur/extensions.py create mode 100644 lemur/factory.py create mode 100644 lemur/listeners/__init__.py create mode 100644 lemur/listeners/models.py create mode 100644 lemur/listeners/service.py create mode 100644 lemur/listeners/views.py create mode 100644 lemur/manage.py create mode 100644 lemur/migrations/README create mode 100644 lemur/migrations/alembic.ini create mode 100644 lemur/migrations/env.py create mode 100644 lemur/migrations/script.py.mako create mode 100644 lemur/models.py create mode 100644 lemur/notifications.py create mode 100644 lemur/roles/__init__.py create mode 100644 lemur/roles/models.py create mode 100644 lemur/roles/service.py create mode 100644 lemur/roles/views.py create mode 100644 lemur/static/app/.buildignore create mode 100644 lemur/static/app/404.html create mode 100644 lemur/static/app/angular/accounts/account/account.js create mode 100644 lemur/static/app/angular/accounts/account/account.tpl.html create mode 100644 lemur/static/app/angular/accounts/services.js create mode 100644 lemur/static/app/angular/accounts/view/view.js create mode 100644 lemur/static/app/angular/accounts/view/view.tpl.html create mode 100644 lemur/static/app/angular/app.js create mode 100644 lemur/static/app/angular/authentication/login/login.js create mode 100644 lemur/static/app/angular/authentication/login/login.tpl.html create mode 100644 lemur/static/app/angular/authentication/logout/logout.js create mode 100644 lemur/static/app/angular/authentication/logout/logout.tpl.html create mode 100644 lemur/static/app/angular/authentication/services.js create mode 100644 lemur/static/app/angular/authentication/unlock/unlock.js create mode 100644 lemur/static/app/angular/authentication/unlock/unlock.tpl.html create mode 100644 lemur/static/app/angular/authorities/authority/authority.js create mode 100644 lemur/static/app/angular/authorities/authority/authorityEdit.tpl.html create mode 100644 lemur/static/app/angular/authorities/authority/authorityWizard.tpl.html create mode 100644 lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html create mode 100644 lemur/static/app/angular/authorities/authority/extensions.tpl.html create mode 100644 lemur/static/app/angular/authorities/authority/options.tpl.html create mode 100644 lemur/static/app/angular/authorities/authority/permissions.tpl.html create mode 100644 lemur/static/app/angular/authorities/authority/select.tpl.html create mode 100644 lemur/static/app/angular/authorities/authority/tracking.tpl.html create mode 100644 lemur/static/app/angular/authorities/services.js create mode 100644 lemur/static/app/angular/authorities/view/view.js create mode 100644 lemur/static/app/angular/authorities/view/view.tpl.html create mode 100644 lemur/static/app/angular/certificates/certificate/certificate.js create mode 100644 lemur/static/app/angular/certificates/certificate/certificate.tpl.html create mode 100644 lemur/static/app/angular/certificates/certificate/certificateWizard.tpl.html create mode 100644 lemur/static/app/angular/certificates/certificate/destinations.tpl.html create mode 100644 lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html create mode 100644 lemur/static/app/angular/certificates/certificate/options.tpl.html create mode 100644 lemur/static/app/angular/certificates/certificate/tracking.tpl.html create mode 100644 lemur/static/app/angular/certificates/certificate/upload.js create mode 100644 lemur/static/app/angular/certificates/certificate/upload.tpl.html create mode 100644 lemur/static/app/angular/certificates/services.js create mode 100644 lemur/static/app/angular/certificates/view/view.js create mode 100644 lemur/static/app/angular/certificates/view/view.tpl.html create mode 100644 lemur/static/app/angular/components/filters.js create mode 100644 lemur/static/app/angular/dashboard/dashboard.js create mode 100644 lemur/static/app/angular/dashboard/dashboard.tpl.html create mode 100644 lemur/static/app/angular/domains/services.js create mode 100644 lemur/static/app/angular/domains/view/view.js create mode 100644 lemur/static/app/angular/domains/view/view.tpl.html create mode 100644 lemur/static/app/angular/elbs/elb/elb.js create mode 100644 lemur/static/app/angular/elbs/elb/elb.tpl.html create mode 100644 lemur/static/app/angular/elbs/services.js create mode 100644 lemur/static/app/angular/elbs/view/view.js create mode 100644 lemur/static/app/angular/elbs/view/view.tpl.html create mode 100644 lemur/static/app/angular/listeners/services.js create mode 100644 lemur/static/app/angular/loadingModal.html create mode 100644 lemur/static/app/angular/pager.html create mode 100644 lemur/static/app/angular/roles/role/role.js create mode 100644 lemur/static/app/angular/roles/role/role.tpl.html create mode 100644 lemur/static/app/angular/roles/services.js create mode 100644 lemur/static/app/angular/roles/view/view.js create mode 100644 lemur/static/app/angular/roles/view/view.tpl.html create mode 100644 lemur/static/app/angular/users/services.js create mode 100644 lemur/static/app/angular/users/user/user.js create mode 100644 lemur/static/app/angular/users/user/user.tpl.html create mode 100644 lemur/static/app/angular/users/view/view.js create mode 100644 lemur/static/app/angular/users/view/view.tpl.html create mode 100644 lemur/static/app/angular/welcome/welcome.html create mode 100644 lemur/static/app/angular/wizard.html create mode 100644 lemur/static/app/favicon.ico create mode 100644 lemur/static/app/index.html create mode 100644 lemur/static/app/robots.txt create mode 100644 lemur/static/app/styles/lemur.css create mode 100644 lemur/status/__init__.py create mode 100644 lemur/status/views.py create mode 100644 lemur/templates/__init__.py create mode 100644 lemur/templates/config.py create mode 100644 lemur/templates/event.html create mode 100644 lemur/tests/__init__.py create mode 100644 lemur/tests/certificates/__init__.py create mode 100644 lemur/tests/certificates/test_certificates.py create mode 100644 lemur/tests/certificates/test_csr.py create mode 100644 lemur/tests/certificates/test_pack/challenge.txt create mode 100644 lemur/tests/certificates/test_pack/csr_config.txt create mode 100644 lemur/tests/certificates/test_pack/private.key create mode 100644 lemur/tests/certificates/test_pack/request.csr create mode 100644 lemur/tests/certificates/test_pack/server.crt create mode 100644 lemur/tests/constants.py create mode 100644 lemur/tests/elbs/__init__.py create mode 100644 lemur/tests/elbs/test_elbs.py create mode 100644 lemur/tests/js/.jshintrc create mode 100644 lemur/tests/js/runner.html create mode 100644 lemur/tests/js/spec/controllers/certificatecreate.js create mode 100644 lemur/tests/js/spec/controllers/certificates.js create mode 100644 lemur/tests/js/spec/controllers/dashboard.js create mode 100644 lemur/tests/js/spec/controllers/elbs.js create mode 100644 lemur/tests/js/spec/controllers/main.js create mode 100644 lemur/tests/listeners/__init__.py create mode 100644 lemur/tests/services/__init__.py create mode 100644 lemur/tests/services/test_elb.py create mode 100644 lemur/tests/services/test_iam.py create mode 100644 lemur/tests/services/test_issuer_manager.py create mode 100644 lemur/tests/services/test_s3.py create mode 100644 lemur/users/__init__.py create mode 100644 lemur/users/models.py create mode 100644 lemur/users/service.py create mode 100644 lemur/users/views.py create mode 100644 package.json create mode 100644 setup.py create mode 100644 trustores/__init__.py diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 00000000..6a408552 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "lemur/static/app/vendor/bower_components" +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..21256661 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3235fc47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +.coverage +.tox +.DS_Store +/.tmp/ +*.egg-info +*.pyc +*.log +*.egg +*.db +*.pid +*.enc +MANIFEST +test.conf +pip-log.txt +/htmlcov +/cover +/build +/dist +/node_modules/ +/bower_components/ +/docs/html +/docs/doctrees +/lemur/static/dist/ +/lemur/static/app/vendor/ +/wheelhouse +docs/_build +.editorconfig +.idea diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 00000000..82755159 --- /dev/null +++ b/.jshintignore @@ -0,0 +1,4 @@ +tests/ +lemur/static/lemur/scripts/lib/ +lemur/static/lemur/dist/ +lemur/static/lemur/vendor/ \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..40377ba2 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,24 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "smarttabs": true, + "globals": { + "angular": false + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..83f4e22f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - '0.8' + - '0.10' +before_script: + - 'npm install -g bower grunt-cli' + - 'bower install' diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..f186b025 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +- Kevin Glisson (kglisson@netflix.com) \ No newline at end of file diff --git a/CHANGES b/CHANGES new file mode 100644 index 00000000..e69de29b diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..85c20990 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2014 Netflix, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..0fc4e635 --- /dev/null +++ b/README.rst @@ -0,0 +1,19 @@ +Lemur +***** + +Lemur manages SSL certificate creation. It provides a central portal for developers to issuer their own SSL certificates with 'sane' defaults. + +It works on CPython 2.7. It is known +to work on Ubuntu Linux and OS X. + +Project resources +================= + +- `Documentation `_ +- `Source code `_ +- `Issue tracker `_ + + +.. image:: https://badges.gitter.im/Join%20Chat.svg + :alt: Join the chat at https://gitter.im/Netflix/lemur + :target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 00000000..3d36949e --- /dev/null +++ b/bower.json @@ -0,0 +1,49 @@ +{ + "name": "lemur", + "repository": { + "type": "git", + "url": "git://github.com/netflix/lemur.git" + }, + "private": true, + "dependencies": { + "angular": "1.3", + "json3": "~3.3", + "es5-shim": "~4.0", + "jquery": "~2.1", + "angular-resource": "1.2.15", + "angular-cookies": "1.2.15", + "angular-sanitize": "1.2.15", + "angular-route": "1.2.15", + "angular-strap": "~2.0.2", + "restangular": "~1.4.0", + "ng-table": "~0.5.4", + "ngAnimate": "*", + "moment": "~2.6.0", + "angular-animate": "~1.4.0", + "angular-loading-bar": "~0.6.0", + "fontawesome": "~4.2.0", + "angular-wizard": "~0.4.0", + "bootswatch": "3.3.1+2", + "angular-spinkit": "~0.3.3", + "angular-bootstrap": "~0.12.0", + "angular-ui-switch": "~0.1.0", + "angular-chart.js": "~0.7.1", + "satellizer": "~0.9.4", + "angularjs-toaster": "~0.4.14" + }, + "devDependencies": { + "angular-mocks": "~1.3", + "angular-scenario": "~1.3" + }, + "resolutions": { + "bootstrap": "~3.3.1", + "angular": "1.3" + }, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/config-default.py b/config-default.py new file mode 100644 index 00000000..8f5b6577 --- /dev/null +++ b/config-default.py @@ -0,0 +1,2 @@ +import os +_basedir = os.path.abspath(os.path.dirname(__file__)) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..8f670bd9 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/lemur.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/lemur.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/lemur" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/lemur" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/administration/index.rst b/docs/administration/index.rst new file mode 100644 index 00000000..c8c31dea --- /dev/null +++ b/docs/administration/index.rst @@ -0,0 +1,540 @@ +Configuration +============= + +.. warning:: + There are many secrets that Lemur uses that must be protected. All of these options are set via the Lemur configruation + file. It is highly advised that you do not store your secrets in this file! Lemur provides functions + that allow you to encrypt files at rest and decrypt them when it's time for deployment. See :ref:`Credential Management ` + for more information. + +Basic Configuration +------------------- + +.. data:: LOG_LEVEL + :noindex: + + :: + + LOG_LEVEL = "DEBUG" + +.. data:: LOG_FILE + :noindex: + + :: + + LOG_FILE = "/logs/lemur/lemur-test.log" + + +.. data:: debug + :noindex: + + Sets the flask debug flag to true (if supported by the webserver) + + :: + + debug = False + + + .. warning:: + This should never be used in a production environment as it exposes Lemur to + remote code execution through the debug console. + + +.. data:: CORS + :noindex: + + Allows for cross domain requests, this is most commonly used for development but could + be use in production if you decided to host the webUI on a different domain than the server. + + Use this cautiously, if you're not sure. Set it to `False` + + :: + + CORS = False + + +.. data:: SQLACHEMY_DATABASE_URI + :noindex: + + If you have ever used sqlalchemy before this is the standard connection string used. Lemur uses a postgres database and the connection string would look something like: + + :: + + SQLALCHEMY_DATABASE_URI = 'postgresql://:@:5432/lemur' + + +.. data:: LEMUR_MAIL + :noindex: + + Lemur mail service + + :: + + LEMUR_MAIL = 'lemur.example.com' + + +.. data:: LEMUR_SECURITY_TEAM_EMAIL + :noindex: + + This is an email or list of emails that should be notified when a certificate is expiring. It is also the contact email address for any discovered certificate. + + :: + + LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com'] + + +.. data:: LEMUR_RESTRICTED_DOMAINS + :noindex: + + This allows the administrator to mark a subset of domains or domains matching a particular regex as + *restricted*. This means that only an administrator is allows to issue the domains in question. + +.. data:: LEMUR_TOKEN_SECRET + :noindex: + + The TOKEN_SECRET is the secret used to create JWT tokens that are given out to users. This should be securely generated and be kept private. + + See `SECRET_KEY` for methods on secure secret generation. + + :: + + LEMUR_TOKEN_SECRET = 'supersecret' + + An example of how you might generate a random string: + + >>> import random + >>> secret_key = ''.join(random.choice(string.ascii_uppercase) for x in range(6)) + >>> secret_key = secret_key + ''.join(random.choice("~!@#$%^&*()_+") for x in range(6)) + >>> secret_key = secret_key + ''.join(random.choice(string.ascii_lowercase) for x in range(6)) + >>> secret_key = secret_key + ''.join(random.choice(string.digits) for x in range(6)) + + +.. data:: LEMUR_ENCRYPTION_KEY + :noindex: + + The LEMUR_ENCRYPTION_KEY is used to encrypt data at rest within Lemur's database. Without this key Lemur will refuse + to start. + + See `LEMUR_TOKEN_SECRET` for methods of secure secret generation. + + :: + + LEMUR_ENCRYPTION_KEY = 'supersupersecret' + + +Authority Options +----------------- + +Authorities will each have their own configuration options. There are currently two plugins bundled with Lemur, +Verisign/Symantec and CloudCA + +.. data:: VERISIGN_URL + :noindex: + + This is the url for the verisign API + + +.. data:: VERISIGN_PEM_PATH + :noindex: + + This is the path to the mutual SSL certificate used for communicating with Verisign + + +.. data:: CLOUDCA_URL + :noindex: + + This is the URL for CLoudCA API + + +.. data:: CLOUDCA_PEM_PATH + :noindex: + + This is the path to the mutual SSL Certificate use for communicating with CLOUDCA + +.. data:: CLOUDCA_BUNDLE + :noindex: + + This is the path to the CLOUDCA certificate bundle + +Authentication +-------------- +Lemur currently supports Basic Authentication and Ping OAuth2, additional flows can be added relatively easily +If you are not using PING you do not need to configure any of these options + +.. data:: PING_SECRET + :noindex: + + :: + + PING_SECRET = 'somethingsecret' + +.. data:: PING_ACCESS_TOKEN_URL + :noindex: + + :: + + PING_ACCESS_TOKEN_URL = "https:///as/token.oauth2" + + +.. data:: PING_USER_API_URL + :noindex: + + :: + + PING_USER_API_URL = "https:///idp/userinfo.openid" + +.. data:: PING_JWKS_URL + :noindex: + + :: + + PING_JWKS_URL = "https:///pf/JWKS" + + +Notifications +============= + +Lemur currently has very basic support for notifications. Notifications are send to the certificate creator, owner and +security team as specified by the `SECURITY_TEAM_EMAIL` configuration parameter. + +The template for all of these notifications lives under lemur/template/event.html and can be easily modified to fit your +needs. + +Certificates marked as in-active will **not** be notified of upcoming expiration. This enables a user to essentially +silence the expiration. If a certificate is active and is expiring the above will be notified at 30, 15, 5, 2 days +respectively. Lemur will not attempt to notify about certificate that have already expired. + + +AWS Configuration +================= + +In order for Lemur to manage it's own account and other accounts we must ensure it has the correct AWS permissions. + +.. note:: AWS usage is completely optional. Lemur can upload, find and manage SSL certificates in AWS. But is not required to do so. + +AWS Configuration Options +------------------------- + +.. data:: AWS_ACCOUNT_MAPPINGS + :noindex: + + Lemur maintains it's own internal table of AWS accounts with their alias and account numbers, this variable is used during setup to bootstrap + your particular enviroment. + + Defaults to ``{}``. + + :: + + AWS_ACCOUNT_MAPPINGS = { + 'awsaccountalias': 111111111111 + } + + +Setting up IAM roles +-------------------- + +Lemur uses boto heavily to talk to all the AWS resources it manages. By default it uses the on-instance credentials to make the necessary calls. + +In order to limit the permissions we will create a new two IAM roles for Lemur. You can name them whatever you would like but for example sake we will be calling them LemurInstanceProfile and Lemur. + +Lemur uses to STS to talk to different accounts. For managing one account this isn't necessary but we will still use it so that we can easily add new accounts. + +LemurInstanceProfile is the IAM role you will launch your instance with. It actually has almost no rights. In fact it should really only be able to use STS to assume role to the Lemur role. + +Here is are example polices for the LemurInstanceProfile: + +SES-SendEmail + +.. code-block:: python + + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ses:SendEmail" + ], + "Resource": "*" + } + ] + } + + +STS-AssumeRole + +.. code-block:: python + + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": + "sts:AssumeRole", + "Resource": "*" + } + ] + } + + + +Next we will create the the Lemur IAM role. Lemur + +Here is an example policy for Lemur: + +IAM-ServerCertificate + +.. code-block:: python + + { + "Statement": [ + { + "Action": [ + "iam:ListServerCertificates", + "iam:UpdateServerCertificate", + "iam:GetServerCertificate", + "iam:UploadServerCertificate" + ], + "Resource": [ + "*" + ], + "Effect": "Allow", + "Sid": "Stmt1404836868000" + } + ] + } + + +.. code-block:: python + + { + "Statement": [ + { + "Action": [ + "elasticloadbalancing:DescribeInstanceHealth", + "elasticloadbalancing:DescribeLoadBalancerAttributes", + "elasticloadbalancing:DescribeLoadBalancerPolicyTypes", + "elasticloadbalancing:DescribeLoadBalancerPolicies", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DeleteLoadBalancerListeners", + "elasticloadbalancing:CreateLoadBalancerListeners" + ], + "Resource": [ + "*" + ], + "Effect": "Allow", + "Sid": "Stmt1404841912000" + } + ] + } + + +Setting up STS access +--------------------- +Once we have setup our accounts we need to ensure that we create a trust relationship so that LemurInstanceProfile can assume the Lemur role. + +In the AWS console select the Lemur IAM role and select the Trust Relationships tab and click Edit Trust Relationship + +Below is an example policy: + +.. code-block:: python + + { + "Version": "2008-10-17", + "Statement": [ + { + "Sid": "", + "Effect": "Allow", + "Principal": { + "AWS": [ + "arn:aws:iam:::role/LemurInstanceProfile", + ] + }, + "Action": "sts:AssumeRole" + } + ] + } + + +Adding N+1 accounts +------------------- + +To add another account we go to the new account and create a new Lemur IAM role with the same policy as above. + +Then we would go to the account that Lemur is running is and edit the trust relationship policy. + +An example policy: + +.. code-block:: python + + { + "Version": "2008-10-17", + "Statement": [ + { + "Sid": "", + "Effect": "Allow", + "Principal": { + "AWS": [ + "arn:aws:iam:::role/LemurInstanceProfile", + "arn:aws:iam:::role/LemurInstanceProfile", + ] + }, + "Action": "sts:AssumeRole" + } + ] + } + +Setting up SES +-------------- + +Lemur has built in support for sending it's certificate notifications via Amazon's simple email service (SES). To force +Lemur to use SES ensure you are the running as the IAM role defined above and that you have followed the steps outlined +in Amazon's documentation `Setting up Amazon SES `_ + +The configuration:: + + LEMUR_MAIL = 'lemur.example.com' + +Will be sender of all notifications, so ensure that it is verified with AWS. + +SES if the default notification gateway and will be used unless SMTP settings are configured in the application configuration +settings. + +Upgrading Lemur +=============== + +Lemur provides an easy way to upgrade between versions. Simply download the newest +version of Lemur from pypi and then apply any schema cahnges with the following command. + +.. code-block:: bash + + $ lemur db upgrade + +.. note:: Internally, this uses `Alembic `_ to manage database migrations. + +.. _CommandLineInterface: + +Command Line Interface +====================== + +Lemur installs a command line script under the name ``lemur``. This will allow you to +perform most required operations that are unachievable within the web UI. + +If you're using a non-standard configuration location, you'll need to prefix every command with +--config (excluding create_config, which is a special case). For example:: + + lemur --config=/etc/lemur.conf.py help + +For a list of commands, you can also use ``lemur help``, or ``lemur [command] --help`` +for help on a specific command. + +.. note:: The script is powered by a library called `Flask-Script `_ + +Builtin Commands +---------------- + +All commands default to `~/.lemur/lemur.conf.py` if a configuration is not specified. + +.. data:: create_config + + Creates a default configuration file for Lemur. + + Path defaults to ``~/.lemur/lemur.config.py`` + + :: + + lemur create_config . + + .. note:: + This command is a special case and does not depend on the configuration file + being set. + + +.. data:: init + + Initializes the configuration file for Lemur. + + :: + + lemur -c /etc/lemur.conf.py init + + +.. data:: start + + Starts a Lemur service. You can also pass any flag that Gunicorn uses to specify the webserver configuration. + + :: + + lemur start -w 6 -b 127.0.0.1:8080 + + +.. data:: db upgrade + + Performs any needed database migrations. + + :: + + lemur db upgrade + + +.. data:: create_user + + Creates new users within Lemur. + + :: + + lemur create_user -u jim -e jim@example.com + + +.. data:: create_role + + Creates new roles within Lemur. + + :: + + lemur create_role -n example -d "a new role" + + +.. data:: check_revoked + + Traverses every certificate that Lemur is aware of and attempts to understand it's validity. + It utilizes both OCSP and CRL. If Lemur is unable to come to a conclusion about a certificates + validity it's status is marked 'unknown' + + +.. data:: sync + + Sync attempts to discover certificates in the environment that were not created by Lemur. There + + :: + + lemur sync --all + + +Identity and Access Management +============================== + +Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a +user is first created in Lemur the can be assigned one or more roles. These roles are typically dynamically created +depending on a external identity provider (Google, LDAP, etc.,) or are hardcoded within Lemur and associated with special +meaning. + +Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such +as ViewPrivateKeyPermission are compositions of these three main Permissions. + +Lets take a look at how these permissions used: + +Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles +that the `Authority` is associated with it Lemur allows that user to user/view/update that `Authority`. + +This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission +structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that +private key. + +These permissions are applied to the user upon login and refreshed on every request. + +.. seealso:: + `Flask-Principal `_ + + diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 00000000..98acfde0 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,2 @@ +Change Log +========== \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..7cbbc653 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +# +# security_monkey documentation build configuration file, created by +# sphinx-quickstart on Sat Jun 7 18:43:48 2014. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('..')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinxcontrib.autohttp.flask', + 'sphinx.ext.todo', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'lemur' +copyright = u'2015, Kevin Glisson' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'lemurdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'lemur.tex', u'Lemur Documentation', + u'Kevin Glisson', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'Lemur', u'Lemur Documentation', + [u'Kevin Glisson'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'Lemur', u'Lemur Documentation', + u'Kevin Glisson', 'Lemur', 'SSL Certificate Management', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/developer/index.rst b/docs/developer/index.rst new file mode 100644 index 00000000..201d1d18 --- /dev/null +++ b/docs/developer/index.rst @@ -0,0 +1,197 @@ +Contributing +============ + +Want to contribute back to Lemur? This page describes the general development flow, +our philosophy, the test suite, and issue tracking. + + +Documentation +------------- + +If you're looking to help document Lemur, you can get set up with Sphinx, our documentation tool, +but first you will want to make sure you have a few things on your local system: + +* python-dev (if you're on OS X, you already have this) +* pip +* virtualenvwrapper + +Once you've got all that, the rest is simple: + +:: + + # If you have a fork, you'll want to clone it instead + git clone git://github.com/netflix/lemur.git + + # Create a python virtualenv + mkvirtualenv lemur + + # Make the magic happen + make dev-docs + +Running ``make dev-docs`` will install the basic requirements to get Sphinx running. + + +Building Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +Inside the ``docs`` directory, you can run ``make`` to build the documentation. +See ``make help`` for available options and the `Sphinx Documentation `_ for more information. + + +Developing Against HEAD +----------------------- + +We try to make it easy to get up and running in a development environment using a git checkout +of Lemur. You'll want to make sure you have a few things on your local system first: + +* python-dev (if you're on OS X, you already have this) +* pip +* virtualenv (ideally virtualenvwrapper) +* node.js (for npm and building css/javascript) +* (Optional) Potgresql + +Once you've got all that, the rest is simple: + +:: + + # If you have a fork, you'll want to clone it instead + git clone git://github.com/lemur/lemur.git + + # Create a python virtualenv + mkvirtualenv lemur + + # Make the magic happen + make + +Running ``make`` will do several things, including: + +* Setting up any submodules (including Bootstrap) +* Installing Python requirements +* Installing NPM requirements + +.. note:: + You will want to store your virtualenv out of the ``lemur`` directory you cloned above, + otherwise ``make`` will fail. + +Create a default Lemur configuration just as if this were a production instance: + +:: + + lemur init + +You'll likely want to make some changes to the default configuration (we recommend developing against Postgres, for example). Once done, migrate your database using the following command: + +:: + + lemur upgrade + + +.. note:: The ``upgrade`` shortcut is simply a shorcut to Alembic's upgrade command. + + +Coding Standards +---------------- + +Lemur follows the guidelines laid out in `pep8 `_ with a little bit +of flexibility on things like line length. We always give way for the `Zen of Python `_. We also use strict mode for JavaScript, enforced by jshint. + +You can run all linters with ``make lint``, or respectively ``lint-python`` or ``lint-js``. + +Spacing +~~~~~~~ + +Python: + 4 Spaces + +JavaScript: + 2 Spaces + +CSS: + 2 Spaces + +HTML: + 2 Spaces + + +Running the Test Suite +---------------------- + +The test suite consists of multiple parts, testing both the Python and JavaScript components in Lemur. If you've setup your environment correctly, you can run the entire suite with the following command: + +:: + + make test + +If you only need to run the Python tests, you can do so with ``make test-python``, as well as ``test-js`` for the JavaScript tests. + + +You'll notice that the test suite is structured based on where the code lives, and strongly encourages using the mock library to drive more accurate individual tests. + +.. note:: We use py.test for the Python test suite, and a combination of phantomjs and jasmine for the JavaScript tests. + + +Static Media +------------ + +Lemur uses a library that compiles it's static media assets (LESS and JS files) automatically. If you're developing using +runserver you'll see changes happen not only in the original files, but also the minified or processed versions of the file. + +If you've made changes and need to compile them by hand for any reason, you can do so by running: + +:: + + lemur compilestatic + +The minified and processed files should be committed alongside the unprocessed changes. + +Developing with Flask +---------------------- + +Because Lemur is just Flask, you can use all of the standard Flask functionality. The only difference is you'll be accessing commands that would normally go through manage.py using the ``lemur`` CLI helper instead. + +For example, you probably don't want to use ``lemur start`` for development, as it doesn't support anything like +automatic reloading on code changes. For that you'd want to use the standard builtin ``runserver`` command: + +:: + + lemur runserver + + +DDL (Schema Changes) +-------------------- + +Schema changes should always introduce the new schema in a commit, and then introduce code relying on that schema in a followup commit. This also means that new columns must be NULLable. + +Removing columns and tables requires a slightly more painful flow, and should resemble the follow multi-commit flow: + +- Remove all references to the column or table (but dont remove the Model itself) +- Remove the model code +- Remove the table or column + + +Contributing Back Code +---------------------- + +All patches should be sent as a pull request on GitHub, include tests, and documentation where needed. If you're fixing a bug or making a large change the patch **must** include test coverage. + +Uncertain about how to write tests? Take a look at some existing tests that are similar to the code you're changing, and go from there. + +You can see a list of open pull requests (pending changes) by visiting https://github.com/netflix/lemur/pulls + + +Plugins +======= + +.. toctree:: + :maxdepth: 1 + + plugins/index + +Internals +========= + +.. toctree:: + :maxdepth: 1 + + internals/lemur + diff --git a/docs/developer/internals/lemur.accounts.rst b/docs/developer/internals/lemur.accounts.rst new file mode 100644 index 00000000..6191c687 --- /dev/null +++ b/docs/developer/internals/lemur.accounts.rst @@ -0,0 +1,20 @@ +accounts Package +================ + +:mod:`models` Module +-------------------- + +.. automodule:: lemur.accounts.models + :members: + :undoc-members: + :show-inheritance: + +:mod:`service` Module +--------------------- + +.. automodule:: lemur.accounts.service + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/developer/internals/lemur.analyze.rst b/docs/developer/internals/lemur.analyze.rst new file mode 100644 index 00000000..6f8dd07f --- /dev/null +++ b/docs/developer/internals/lemur.analyze.rst @@ -0,0 +1,11 @@ +analyze Package +=============== + +:mod:`service` Module +--------------------- + +.. automodule:: lemur.analyze.service + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/developer/internals/lemur.auth.rst b/docs/developer/internals/lemur.auth.rst new file mode 100644 index 00000000..3538758c --- /dev/null +++ b/docs/developer/internals/lemur.auth.rst @@ -0,0 +1,20 @@ +auth Package +============ + +:mod:`permissions` Module +------------------------- + +.. automodule:: lemur.auth.permissions + :members: + :undoc-members: + :show-inheritance: + +:mod:`service` Module +--------------------- + +.. automodule:: lemur.auth.service + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/developer/internals/lemur.authorities.rst b/docs/developer/internals/lemur.authorities.rst new file mode 100644 index 00000000..925d33c3 --- /dev/null +++ b/docs/developer/internals/lemur.authorities.rst @@ -0,0 +1,20 @@ +authorities Package +=================== + +:mod:`models` Module +-------------------- + +.. automodule:: lemur.authorities.models + :members: + :undoc-members: + :show-inheritance: + +:mod:`service` Module +--------------------- + +.. automodule:: lemur.authorities.service + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/developer/internals/lemur.certificates.rst b/docs/developer/internals/lemur.certificates.rst new file mode 100644 index 00000000..a7b9f5c0 --- /dev/null +++ b/docs/developer/internals/lemur.certificates.rst @@ -0,0 +1,43 @@ +certificates Package +==================== + +:mod:`exceptions` Module +------------------------ + +.. automodule:: lemur.certificates.exceptions + :members: + :undoc-members: + :show-inheritance: + +:mod:`models` Module +-------------------- + +.. automodule:: lemur.certificates.models + :members: + :undoc-members: + :show-inheritance: + +:mod:`service` Module +--------------------- + +.. automodule:: lemur.certificates.service + :members: + :undoc-members: + :show-inheritance: + +:mod:`sync` Module +------------------ + +.. automodule:: lemur.certificates.sync + :members: + :undoc-members: + :show-inheritance: + +:mod:`verify` Module +-------------------- + +.. automodule:: lemur.certificates.verify + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/developer/internals/lemur.common.rst b/docs/developer/internals/lemur.common.rst new file mode 100644 index 00000000..ec03c39d --- /dev/null +++ b/docs/developer/internals/lemur.common.rst @@ -0,0 +1,34 @@ +common Package +============== + +:mod:`crypto` Module +-------------------- + +.. automodule:: lemur.common.crypto + :members: + :undoc-members: + :show-inheritance: + +:mod:`health` Module +-------------------- + +.. automodule:: lemur.common.health + :members: + :undoc-members: + :show-inheritance: + +:mod:`utils` Module +------------------- + +.. automodule:: lemur.common.utils + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + + lemur.common.services + diff --git a/docs/developer/internals/lemur.common.services.aws.rst b/docs/developer/internals/lemur.common.services.aws.rst new file mode 100644 index 00000000..9d95dd1d --- /dev/null +++ b/docs/developer/internals/lemur.common.services.aws.rst @@ -0,0 +1,35 @@ +aws Package +=========== + +:mod:`elb` Module +----------------- + +.. automodule:: lemur.common.services.aws.elb + :members: + :undoc-members: + :show-inheritance: + +:mod:`iam` Module +----------------- + +.. automodule:: lemur.common.services.aws.iam + :members: + :undoc-members: + :show-inheritance: + +:mod:`ses` Module +----------------- + +.. automodule:: lemur.common.services.aws.ses + :members: + :undoc-members: + :show-inheritance: + +:mod:`sts` Module +----------------- + +.. automodule:: lemur.common.services.aws.sts + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/developer/internals/lemur.common.services.issuers.plugins.cloudca.rst b/docs/developer/internals/lemur.common.services.issuers.plugins.cloudca.rst new file mode 100644 index 00000000..44ced49c --- /dev/null +++ b/docs/developer/internals/lemur.common.services.issuers.plugins.cloudca.rst @@ -0,0 +1,19 @@ +cloudca Package +=============== + +:mod:`cloudca` Module +--------------------- + +.. automodule:: lemur.common.services.issuers.plugins.cloudca.cloudca + :members: + :undoc-members: + :show-inheritance: + +:mod:`constants` Module +----------------------- + +.. automodule:: lemur.common.services.issuers.plugins.cloudca.constants + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/developer/internals/lemur.common.services.issuers.plugins.rst b/docs/developer/internals/lemur.common.services.issuers.plugins.rst new file mode 100644 index 00000000..18010965 --- /dev/null +++ b/docs/developer/internals/lemur.common.services.issuers.plugins.rst @@ -0,0 +1,11 @@ +plugins Package +=============== + +Subpackages +----------- + +.. toctree:: + + lemur.common.services.issuers.plugins.cloudca + lemur.common.services.issuers.plugins.verisign + diff --git a/docs/developer/internals/lemur.common.services.issuers.plugins.verisign.rst b/docs/developer/internals/lemur.common.services.issuers.plugins.verisign.rst new file mode 100644 index 00000000..23e56719 --- /dev/null +++ b/docs/developer/internals/lemur.common.services.issuers.plugins.verisign.rst @@ -0,0 +1,19 @@ +verisign Package +================ + +:mod:`constants` Module +----------------------- + +.. automodule:: lemur.common.services.issuers.plugins.verisign.constants + :members: + :undoc-members: + :show-inheritance: + +:mod:`verisign` Module +---------------------- + +.. automodule:: lemur.common.services.issuers.plugins.verisign.verisign + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/developer/internals/lemur.common.services.issuers.rst b/docs/developer/internals/lemur.common.services.issuers.rst new file mode 100644 index 00000000..331c5a95 --- /dev/null +++ b/docs/developer/internals/lemur.common.services.issuers.rst @@ -0,0 +1,26 @@ +issuers Package +=============== + +:mod:`issuer` Module +-------------------- + +.. automodule:: lemur.common.services.issuers.issuer + :members: + :undoc-members: + :show-inheritance: + +:mod:`manager` Module +--------------------- + +.. automodule:: lemur.common.services.issuers.manager + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + + lemur.common.services.issuers.plugins + diff --git a/docs/developer/internals/lemur.common.services.rst b/docs/developer/internals/lemur.common.services.rst new file mode 100644 index 00000000..6ef59523 --- /dev/null +++ b/docs/developer/internals/lemur.common.services.rst @@ -0,0 +1,11 @@ +services Package +================ + +Subpackages +----------- + +.. toctree:: + + lemur.common.services.aws + lemur.common.services.issuers + diff --git a/docs/developer/internals/lemur.domains.rst b/docs/developer/internals/lemur.domains.rst new file mode 100644 index 00000000..e728a053 --- /dev/null +++ b/docs/developer/internals/lemur.domains.rst @@ -0,0 +1,19 @@ +domains Package +=============== + +:mod:`models` Module +-------------------- + +.. automodule:: lemur.domains.models + :members: + :undoc-members: + :show-inheritance: + +:mod:`service` Module +--------------------- + +.. automodule:: lemur.domains.service + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/developer/internals/lemur.elbs.rst b/docs/developer/internals/lemur.elbs.rst new file mode 100644 index 00000000..74786ed6 --- /dev/null +++ b/docs/developer/internals/lemur.elbs.rst @@ -0,0 +1,35 @@ +elbs Package +============ + +:mod:`models` Module +-------------------- + +.. automodule:: lemur.elbs.models + :members: + :undoc-members: + :show-inheritance: + +:mod:`service` Module +--------------------- + +.. automodule:: lemur.elbs.service + :members: + :undoc-members: + :show-inheritance: + +:mod:`sync` Module +------------------ + +.. automodule:: lemur.elbs.sync + :members: + :undoc-members: + :show-inheritance: + +:mod:`views` Module +------------------- + +.. automodule:: lemur.elbs.views + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/developer/internals/lemur.listeners.rst b/docs/developer/internals/lemur.listeners.rst new file mode 100644 index 00000000..4ef98588 --- /dev/null +++ b/docs/developer/internals/lemur.listeners.rst @@ -0,0 +1,27 @@ +listeners Package +================= + +:mod:`models` Module +-------------------- + +.. automodule:: lemur.listeners.models + :members: + :undoc-members: + :show-inheritance: + +:mod:`service` Module +--------------------- + +.. automodule:: lemur.listeners.service + :members: + :undoc-members: + :show-inheritance: + +:mod:`views` Module +------------------- + +.. automodule:: lemur.listeners.views + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/developer/internals/lemur.roles.rst b/docs/developer/internals/lemur.roles.rst new file mode 100644 index 00000000..bb60e23c --- /dev/null +++ b/docs/developer/internals/lemur.roles.rst @@ -0,0 +1,20 @@ +roles Package +============= + +:mod:`models` Module +-------------------- + +.. automodule:: lemur.roles.models + :members: + :undoc-members: + :show-inheritance: + +:mod:`service` Module +--------------------- + +.. automodule:: lemur.roles.service + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/developer/internals/lemur.rst b/docs/developer/internals/lemur.rst new file mode 100644 index 00000000..6d7e64bf --- /dev/null +++ b/docs/developer/internals/lemur.rst @@ -0,0 +1,87 @@ +:mod:`constants` Module +----------------------- + +.. automodule:: lemur.constants + :members: + :undoc-members: + :show-inheritance: + +:mod:`database` Module +---------------------- + +.. automodule:: lemur.database + :members: + :undoc-members: + :show-inheritance: + +:mod:`decorators` Module +------------------------ + +.. automodule:: lemur.decorators + :members: + :undoc-members: + :show-inheritance: + +:mod:`exceptions` Module +------------------------ + +.. automodule:: lemur.exceptions + :members: + :undoc-members: + :show-inheritance: + +:mod:`extensions` Module +------------------------ + +.. automodule:: lemur.extensions + :members: + :undoc-members: + :show-inheritance: + +:mod:`factory` Module +--------------------- + +.. automodule:: lemur.factory + :members: + :undoc-members: + :show-inheritance: + +:mod:`manage` Module +-------------------- + +.. automodule:: lemur.manage + :members: + :undoc-members: + :show-inheritance: + +:mod:`models` Module +-------------------- + +.. automodule:: lemur.models + :members: + :undoc-members: + :show-inheritance: + +:mod:`notifications` Module +--------------------------- + +.. automodule:: lemur.notifications + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + + lemur.accounts + lemur.auth + lemur.authorities + lemur.certificates + lemur.common + lemur.domains + lemur.roles + lemur.status + lemur.users + diff --git a/docs/developer/internals/lemur.status.rst b/docs/developer/internals/lemur.status.rst new file mode 100644 index 00000000..cdd4e8cc --- /dev/null +++ b/docs/developer/internals/lemur.status.rst @@ -0,0 +1,11 @@ +status Package +============== + +:mod:`views` Module +------------------- + +.. automodule:: lemur.status.views + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/developer/internals/lemur.users.rst b/docs/developer/internals/lemur.users.rst new file mode 100644 index 00000000..3039ef00 --- /dev/null +++ b/docs/developer/internals/lemur.users.rst @@ -0,0 +1,19 @@ +users Package +============= + +:mod:`models` Module +-------------------- + +.. automodule:: lemur.users.models + :members: + :undoc-members: + :show-inheritance: + +:mod:`service` Module +--------------------- + +.. automodule:: lemur.users.service + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/developer/plugins/index.rst b/docs/developer/plugins/index.rst new file mode 100644 index 00000000..b7cdcd2a --- /dev/null +++ b/docs/developer/plugins/index.rst @@ -0,0 +1,151 @@ +Writing a Plugin +================ + +**The plugin interface is a work in progress.** + +Several interfaces exist for extending Lemur: + +* Issuers (lemur.issuers) + +Structure +--------- + +A plugins layout generally looks like the following:: + + setup.py + lemur_pluginname/ + lemur_pluginname/__init__.py + lemur_pluginname/plugin.py + +The ``__init__.py`` file should contain no plugin logic, and at most, a VERSION = 'x.x.x' line. For example, +if you want to pull the version using pkg_resources (which is what we recommend), your file might contain:: + + try: + VERSION = __import__('pkg_resources') \ + .get_distribution(__name__).version + except Exception, e: + VERSION = 'unknown' + +Inside of ``plugin.py``, you'll declare your Plugin class:: + + import lemur_pluginname + from lemur.common.services.issuers.plugins import Issuer + + class PluginName(Plugin): + title = 'Plugin Name' + slug = 'pluginname' + description = 'My awesome plugin!' + version = lemur_pluginname.VERSION + + author = 'Your Name' + author_url = 'https://github.com/yourname/lemur_pluginname' + + def widget(self, request, group, **kwargs): + return "

Absolutely useless widget

" + +And you'll register it via ``entry_points`` in your ``setup.py``:: + + setup( + # ... + entry_points={ + 'lemur.plugins': [ + 'pluginname = lemur_pluginname.issuers:PluginName' + ], + }, + ) + + +That's it! Users will be able to install your plugin via ``pip install `` and configure it +via the web interface based on the hooks you enabled. + + +Permissions +=========== + +As described in the plugin interface, Lemur provides a suite of permissions. + +In most cases, a admin (that is, if User.is_admin is ``True``), will be granted implicit permissions +on everything. + +This page attempts to describe those permissions, and the contextual objects along with them. + +.. data:: add_project + + Controls whether a user can create a new project. + + :: + + >>> has_perm('add_project', user) + + +Testing +======= + +Lemur provides a basic py.test-based testing framework for extensions. + +In a simple project, you'll need to do a few things to get it working: + +setup.py +-------- + +Augment your setup.py to ensure at least the following: + +.. code-block:: python + + setup( + # ... + install_requires=[ + 'lemur', + ] + ) + + +conftest.py +----------- + +The ``conftest.py`` file is our main entry-point for py.test. We need to configure it to load the Lemur pytest configuration: + +.. code-block:: python + + from __future__ import absolute_import + + pytest_plugins = [ + 'lemur.utils.pytest' + ] + + +Test Cases +---------- + +You can now inherit from Lemur's core test classes. These are Django-based and ensure the database and other basic utilities are in a clean state: + +.. code-block:: python + + # test_myextension.py + from __future__ import absolute_import + + from lemur.testutils import TestCase + + class MyExtensionTest(TestCase): + def test_simple(self): + assert 1 != 2 + + +Running Tests +------------- + +Running tests follows the py.test standard. As long as your test files and methods are named appropriately (``test_filename.py`` and ``test_function()``) you can simply call out to py.test: + +:: + + $ py.test -v + ============================== test session starts ============================== + platform darwin -- Python 2.7.9 -- py-1.4.26 -- pytest-2.6.4/python2.7 + plugins: django + collected 1 items + + tests/test_myextension.py::MyExtensionTest::test_simple PASSED + + =========================== 1 passed in 0.35 seconds ============================ + + diff --git a/docs/developer/rest.rst b/docs/developer/rest.rst new file mode 100644 index 00000000..63b1cf0d --- /dev/null +++ b/docs/developer/rest.rst @@ -0,0 +1,60 @@ +Lemur's front end is entirely API driven. Any action that you can accomplish via the UI can also be accomplished by the +UI. The following is documents and provides examples on how to make requests to the Lemur API. + +Authentication +-------------- + +.. automodule:: lemur.auth.views + :members: + :undoc-members: + :show-inheritance: + +Accounts +-------- + +.. automodule:: lemur.accounts.views + :members: + :undoc-members: + :show-inheritance: + +Users +----- + +.. automodule:: lemur.users.views + :members: + :undoc-members: + :show-inheritance: + +Roles +----- + +.. automodule:: lemur.roles.views + :members: + :undoc-members: + :show-inheritance: + +Certificates +------------ + +.. automodule:: lemur.certificates.views + :members: + :undoc-members: + :show-inheritance: + +Authorities +----------- + +.. automodule:: lemur.authorities.views + :members: + :undoc-members: + :show-inheritance: + +Domains +------- + +.. automodule:: lemur.domains.views + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 00000000..115d319f --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,30 @@ +Frequently Asked Questions +========================== + +Common Problems +--------------- + +In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is ENCRYPTION_KEY set?'* + You likely have not correctly configured **ENCRYPTION_KEY**. See + :doc:`administration/configuration` for more information. + + +How do I +-------- + +... script the Lemur installation to bootstrap things like roles and users? + Lemur is a simple Flask (Python) application that runs using a utility + runner. A script that creates a project and default user might look something + like this: + + .. code-block:: python + + # Bootstrap the Flask environment + from flask import current_app + + from lemur.users.service import create as create_user + from lemur.roles.service import create as create_role + from lemur.accounts.service import create as create_account + + role = create_role('aRole', 'this is a new role') + create_user('admin', 'password', 'lemur@nobody', True, [role] diff --git a/docs/guide/index.rst b/docs/guide/index.rst new file mode 100644 index 00000000..1564897b --- /dev/null +++ b/docs/guide/index.rst @@ -0,0 +1,10 @@ +Creating Certificates +===================== + + +Creating Users +============== + + +Creating Roles +============== \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..13adf6c5 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,67 @@ +Lemur +===== + +Lemur is a SSL management service. It attempts to help track and create certificates. By removing common issues with +CSR creation it gives normal developers 'sane' SSL defaults and helps security teams push SSL usage throughout an organization. + +Installation +------------ + +.. toctree:: + :maxdepth: 2 + + quickstart/index + production/index + +User Guide +---------- + +.. toctree:: + :maxdepth: 2 + + guide/index + +Administration +-------------- + +.. toctree:: + :maxdepth: 2 + + administration/index + plugins/index + +Developers +---------- + +.. toctree:: + :maxdepth: 2 + + developer/index + + +REST API +-------- + +.. toctree:: + :maxdepth: 2 + + developer/rest + +FAQ +---- + +.. toctree:: + :maxdepth: 1 + + faq + +Reference +--------- + +.. toctree:: + :maxdepth: 1 + + changelog + license/index + + diff --git a/docs/license/index.rst b/docs/license/index.rst new file mode 100644 index 00000000..4df00576 --- /dev/null +++ b/docs/license/index.rst @@ -0,0 +1,20 @@ +License +======= + +Lemur is licensed under a three clause APACHE License. + +The full license text can be found below (:ref:`lemur-license`). + +Authors +------- + +Lemur was originally written and is maintained by Kevin Glisson. + +A list of additional contributors can be seen on `GitHub `_. + +.. _lemur-license: + +Lemur License +------------- + +.. include:: ../../LICENSE \ No newline at end of file diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst new file mode 100644 index 00000000..ee6959ac --- /dev/null +++ b/docs/plugins/index.rst @@ -0,0 +1,20 @@ +Plugins +======= + +There are several interfaces currently available to extend Lemur. These are a work in +progress and the API is not frozen. + +Bundled Plugins +--------------- + +Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec and CloudCA services. + +3rd Party Extensions +-------------------- + +The following extensions are available and maintained by members of the Lemur community: + +Have an extension that should be listed here? Submit a `pull request `_ and we'll +get it added. + +Want to create your own extension? See :doc:`../developer/plugins/index` to get started. \ No newline at end of file diff --git a/docs/production/index.rst b/docs/production/index.rst new file mode 100644 index 00000000..2f788cfa --- /dev/null +++ b/docs/production/index.rst @@ -0,0 +1,277 @@ +Production +********** + +There are several steps needed to make Lemur production ready. Here we focus on making Lemur more reliable and secure. + +Basics +====== + +Because of the sensitivity of the information stored and maintain by Lemur it is important that you follow standard host hardening practices: + +- Run Lemur with a limited user +- Disabled any unneeded service +- Enable remote logging + +.. _CredentialManagement: + +Credential Management +--------------------- + +Lemur often contains credentials such as mutual SSL keys that are used to communicate with third party resources and for encrypting stored secrets. Lemur comes with the ability +to automatically encrypt these keys such that your keys not be in clear text. + +The keys are located within lemur/keys and broken down by environment + +To utilize this ability use the following commands: + + ``lemur lock`` + +and + + ``lemur unlock`` + +If you choose to use this feature ensure that the KEY are decrypted before Lemur starts as it will have trouble communicating with the database otherwise. + +SSL +==== + +Nginx +----- + +Nginx is a very popular choice to serve a Python project: + +- It's fast. +- It's lightweight. +- Configuration files are simple. + +Nginx doesn't run any Python process, it only serves requests from outside to +the Python server. + +Therefor there are two steps: + +- Run the Python process. +- Run Nginx. + +You will benefit from having: + +- the possibility to have several projects listening to the port 80; +- your web site processes won't run with admin rights, even if --user doesn't + work on your OS; +- the ability to manage a Python process without touching Nginx or the other + processes. It's very handy for updates. + + +You must create a Nginx configuration file for Lemur. On GNU/Linux, they usually +go into /etc/nginx/conf.d/. Name it lemur.conf. + +The minimal configuration file to run the site is:: + + server { + listen 80; + server_name www.yourwebsite.com; + + location / { + proxy_pass http://127.0.0.1:5000; + } + } + +`proxy_pass` just passes the external request to the Python process. +The port much match the one used by the 0bin process of course. + +You can make some adjustments to get a better user experience:: + + server_tokens off; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + + server { + listen 80; + return 301 https://$host$request_uri; + } + + server { + listen 443; + access_log /var/log/nginx/log/lemur.access.log; + error_log /var/log/nginx/log/lemur.error.log; + + location /api { + proxy_pass http://127.0.0.1:5000; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; + proxy_redirect off; + proxy_buffering off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location / { + root /apps/lemur/lemur/static/dist; + index index.html; + } + + + } + +This makes Nginx serve the favicon and static files which is is much better at than python. + +It is highly recommended that you deploy SSL when deploying Lemur. This may be obvious given Lemur's purpose but the +sensitive nature of Lemur and what it controls makes this essential. This is a sample config for Lemur that also terminates SSL:: + + server_tokens off; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + + server { + listen 80; + return 301 https://$host$request_uri; + } + + server { + listen 443; + access_log /var/log/nginx/log/lemur.access.log; + error_log /var/log/nginx/log/lemur.error.log; + + # certs sent to the client in SERVER HELLO are concatenated in ssl_certificate + ssl_certificate /path/to/signed_cert_plus_intermediates; + ssl_certificate_key /path/to/private_key; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:50m; + + # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits + ssl_dhparam /path/to/dhparam.pem; + + # modern configuration. tweak to your needs. + ssl_protocols TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK'; + ssl_prefer_server_ciphers on; + + # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) + add_header Strict-Transport-Security max-age=15768000; + + # OCSP Stapling --- + # fetch OCSP records from URL in ssl_certificate and cache them + ssl_stapling on; + ssl_stapling_verify on; + + ## verify chain of trust of OCSP response using Root CA and Intermediate certs + ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates; + + resolver ; + + location /api { + proxy_pass http://127.0.0.1:5000; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; + proxy_redirect off; + proxy_buffering off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location / { + root /apps/lemur/lemur/static/dist; + index index.html; + } + + + } + +Apache +------ + +An example apache config:: + + + ... + SSLEngine on + SSLCertificateFile /path/to/signed_certificate + SSLCertificateChainFile /path/to/intermediate_certificate + SSLCertificateKeyFile /path/to/private/key + SSLCACertificateFile /path/to/all_ca_certs + + # intermediate configuration, tweak to your needs + SSLProtocol all -SSLv2 -SSLv3 + SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA + SSLHonorCipherOrder on + + # HSTS (mod_headers is required) (15768000 seconds = 6 months) + Header always set Strict-Transport-Security "max-age=15768000" + ... + + +Also included in the configurations above are several best practices when it comes to deploying SSL. Things like enabling +HSTS, disabling vulnerable ciphers are all good ideas when it comes to deploying Lemur into a production environment. + +.. seealso:: + `Mozilla SSL Configuration Generator `_ + +.. _UsingSupervisor: + +Supervisor +========== + +Supervisor is a very nice way to manage you Python processes. We won't cover +the setup (which is just apt-get install supervisor or pip install supervisor +most of the time), but here is a quick overview on how to use it. + +Create a configuration file named supervisor.ini:: + + [unix_http_server] + file=/tmp/supervisor.sock; + + [supervisorctl] + serverurl=unix:///tmp/supervisor.sock; + + [rpcinterface:supervisor] + supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface + + [supervisord] + logfile=/tmp/lemur.log + logfile_maxbytes=50MB + logfile_backups=2 + loglevel=trace + pidfile=/tmp/supervisord.pid + nodaemon=false + minfds=1024 + minprocs=200 + user=lemur + + [program:lemur] + command=python /path/to/lemur/manage.py manage.py start + + directory=/path/to/lemur/ + environment=PYTHONPATH='/path/to/lemur/' + user=lemur + autostart=true + autorestart=true + +The 4 first entries are just boiler plate to get you started, you can copy +them verbatim. + +The last one define one (you can have many) process supervisor should manage. + +It means it will run the command:: + + python manage.py start + + +In the directory, with the environment and the user you defined. + +This command will be ran as a daemon, in the background. + +`autostart` and `autorestart` just make it fire and forget: the site will always be +running, even it crashes temporarily or if you restart the machine. + +The first time you run supervisor, pass it the configuration file:: + + supervisord -c /path/to/supervisor.ini + +Then you can manage the process by running:: + + supervisorctl -c /path/to/supervisor.ini + +It will start a shell from were you can start/stop/restart the service + +You can read all errors that might occurs from /tmp/lemur.log. diff --git a/docs/quickstart/index.rst b/docs/quickstart/index.rst new file mode 100644 index 00000000..f291ba8c --- /dev/null +++ b/docs/quickstart/index.rst @@ -0,0 +1,237 @@ +Quickstart +********** + +This guide will step you through setting up a Python-based virtualenv, installing the required packages, and configuring the basic web service. + +Dependencies +------------ + +Some basic prerequisites which you'll need in order to run Lemur: + +* A UNIX-based operating system. We test on Ubuntu, develop on OS X +* Python 2.7 +* PostgreSQL +* Ngnix + +.. note:: Lemur was built with in AWS in mind. This means that things such as databases (RDS), mail (SES), and SSL (ELB), + are largely handled for us. Lemur does **not** require AWS to function. Our guides and documentation try to be + be as generic as possible and are not intended to document every step of launching Lemur into a given environment. + +Setting up an Environment +------------------------- + +The first thing you'll need is the Python ``virtualenv`` package. You probably already +have this, but if not, you can install it with:: + + pip install -U virtualenv + +Once that's done, choose a location for the environment, and create it with the ``virtualenv`` +command. For our guide, we're going to choose ``/www/lemur/``:: + + virtualenv /www/lemur/ + +Finally, activate your virtualenv:: + + source /www/lemur/bin/activate + +.. note:: Activating the environment adjusts your PATH, so that things like pip now + install into the virtualenv by default. + + +Installing Lemur +---------------- + +Once you've got the environment setup, you can install Lemur and all its dependencies with +the same command you used to grab virtualenv:: + + pip install -U lemur + +Once everything is installed, you should be able to execute the Lemur CLI, via ``lemur``, and get something +like the following: + +.. code-block:: bash + + $ lemur + usage: lemur [--config=/path/to/settings.py] [command] [options] + + +Installing from Source +~~~~~~~~~~~~~~~~~~~~~~ + +If you're installing the Lemur source (e.g. from git), you'll also need to install **npm**. + +Once your system is prepared, symlink your source into the virtualenv: + +.. code-block:: bash + + $ python setup.py develop + +.. Note:: This command will install npm dependencies as well as compile static assets. + + +Creating a configuration +------------------------ + +Before we run Lemur we must create a valid configuration file for it. + +The Lemur cli comes with a simple command to get you up and running quickly. + +Simply run: + +.. code-block:: bash + + $ lemur create_config + +.. Note:: This command will create a default configuration under `~/.lemur/lemur.conf.py` you + can specify this location by passing the `config_path` parameter to the `create_config` command. + +You can specify `-c` or `--config` to any Lemur command to specify the current environment +you are working in. Lemur will also look under the environmental variable `LEMUR_CONF` should +that be easier to setup in your environment. + +Once created you will need to update the configuration file with information about your environment, +such as which database to talk to, where keys are stores etc.. + +Initializing Lemur +------------------ + +Lemur provides a helpful command that will initialize your database for you. It creates a default user (lemur) that is +used by Lemur to help associate certificates that do not currently have an owner. This is most commonly the case when +Lemur has discovered certificates from a third party resource. This is also a default user that can be used to +administer Lemur. + +.. code-block:: bash + + $ lemur init + +.. note:: It is recommended that once the 'lemur' user is created that you create individual users for every day access. + There is currently no way for a user to self enroll for Lemur access, they must have an administrator create an account + for them or be enrolled automatically through SSO. This can be done through the CLI or UI. + See :ref:`Creating Users ` and :ref:`Command Line Interface ` for details + +.. note:: + This assumes you have already created a postgres database and have specified the right postgres URI in the + lemur configuration. See the `Postgres Documentation `_ + for details. + + +Starting the Web Service +------------------------ + +Lemur provides a built-in webserver (powered by gunicorn and eventlet) to get you off the ground quickly. + +To start the webserver, you simply use ``lemur start``. If you opted to use an alternative configuration path +you can pass that via the --config option. + +:: + + # Lemur's server runs on port 5000 by default. Make sure your client reflects + # the correct host and port! + lemur --config=/etc/lemur.conf.py start + +You should now be able to test the web service by visiting `http://localhost:5000/`. + +Setup a Reverse Proxy +--------------------- + +By default, Lemur runs on port 5000. Even if you change this, under normal conditions you won't be able to bind to +port 80. To get around this (and to avoid running Lemur as a privileged user, which you shouldn't), we recommend +you setup a simple web proxy. + +Proxying with Nginx +~~~~~~~~~~~~~~~~~~~ + +You'll use the builtin HttpProxyModule within Nginx to handle proxying:: + + location / { + proxy_pass http://localhost:5000; + proxy_redirect off; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + +See :doc:`../production/index` for more details on using Nginx. + +Proxying with Apache +~~~~~~~~~~~~~~~~~~~~ + +Apache requires the use of mod_proxy for forwarding requests:: + + ProxyPass / http://localhost:5000/ + ProxyPassReverse / http://localhost:5000/ + ProxyPreserveHost On + RequestHeader set X-Forwarded-Proto "https" env=HTTPS + +You will need to enable ``headers``, ``proxy``, and ``proxy_http`` apache modules to use these settings. + +See :doc:`../production/index` for more details on using Apache. + + +Running Lemur as a Service +--------------------------- + +We recommend using whatever software you are most familiar with for managing Lemur processes. One option is +`Supervisor `_. + +Configure ``supervisord`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configuring Supervisor couldn't be more simple. Just point it to the ``lemur`` executable in your virtualenv's bin/ +folder and you're good to go. + +:: + + [program:lemur-web] + directory=/www/lemur/ + command=/www/lemur/bin/lemur start + autostart=true + autorestart=true + redirect_stderr=true + stdout_logfile syslog + stderr_logfile syslog + +See :ref:`Using Supervisor ` for more details on using Supervisor. + +Syncing +------- + +Lemur uses periodic sync tasks to make sure it is up-to-date with it's environment. As always things can change outside +of Lemur, but we do our best to reconcile those changes. + +.. code-block:: bash + + $ crontab -e + * 3 * * * lemur sync + * 3 * * * lemur check_revoked + +Additional Utilities +-------------------- + +If you're familiar with Python you'll quickly find yourself at home, and even more so if you've used Flask. The +``lemur`` command is just a simple wrapper around Flask's ``manage.py``, which means you get all of the +power and flexibility that goes with it. + +Some of those which you'll likely find useful are: + +lock +~~~~ + +Encrypts sensitive key material - This is most useful for storing encrypted secrets in source code. + +unlock +~~~~~~ + +Decrypts sensitive key material - Used to decrypt the secrets stored in source during deployment. + + +What's Next? +------------ + +The above gets you going, but for production there are several different security considerations to take into account, +remember Lemur is handling sensitive data and security is imperative. + +See :doc:`../production/index` for more details on how to configure Lemur for production. + diff --git a/gulp/build.js b/gulp/build.js new file mode 100644 index 00000000..9326d6be --- /dev/null +++ b/gulp/build.js @@ -0,0 +1,231 @@ +var gulp = require('gulp'), + minifycss = require('gulp-minify-css'), + concat = require('gulp-concat'), + less = require('gulp-less'), + gulpif = require('gulp-if'), + order = require('gulp-order'), + gutil = require('gulp-util'), + rename = require('gulp-rename'), + foreach = require('gulp-foreach'), + debug = require('gulp-debug'), + path =require('path'), + merge = require('merge-stream'), + del = require('del'), + size = require('gulp-size'), + plumber = require('gulp-plumber'), + autoprefixer = require('gulp-autoprefixer'), + jshint = require('gulp-jshint'), + inject = require('gulp-inject'), + cache = require('gulp-cache'), + ngAnnotate = require('gulp-ng-annotate'), + csso = require('gulp-csso'), + useref = require('gulp-useref'), + filter = require('gulp-filter'), + rev = require('gulp-rev'), + revReplace = require('gulp-rev-replace'), + imagemin = require('gulp-imagemin'), + minifyHtml = require('gulp-minify-html'), + bowerFiles = require('main-bower-files'), + replace = require('gulp-replace-task'); + + +gulp.task('default', ['clean'], function () { + gulp.start('fonts', 'styles'); +}); + +gulp.task('clean', function (cb) { + del(['.tmp', 'lemur/static/dist'], cb); +}); + +gulp.task('dev:fonts', function () { + var fileList = [ + 'lemur/static/app/vendor/bower_components/bootstrap/dist/fonts/*', + 'lemur/static/app/vendor/bower_components/fontawesome/fonts/*' + ]; + + return gulp.src(fileList) + .pipe(gulp.dest('.tmp/fonts')); +}); + +gulp.task('dev:styles', function () { + var baseContent = '@import "lemur/static/app/vendor/bower_components/bootstrap/less/bootstrap.less";@import "lemur/static/app/vendor/bower_components/bootswatch/$theme$/variables.less";@import "lemur/static/app/vendor/bower_components/bootswatch/$theme$/bootswatch.less";@import "lemur/static/app/vendor/bower_components/bootstrap/less/utilities.less";'; + var isBootswatchFile = function (file) { + + var suffix = 'bootswatch.less'; + return file.path.indexOf(suffix, file.path.length - suffix.length) !== -1; + }; + + var isBootstrapFile = function (file) { + var suffix = 'bootstrap-', + fileName = path.basename(file.path); + + return fileName.indexOf(suffix) === 0; + }; + + var fileList = [ + 'lemur/static/app/styles/lemur.css', + 'lemur/static/app/vendor/bower_components/bootswatch/sandstone/bootswatch.less', + 'lemur/static/app/vendor/bower_components/fontawesome/css/font-awesome.css', + 'lemur/static/app/vendor/bower_components/angular-spinkit/src/angular-spinkit.css', + 'lemur/static/app/vendor/bower_components/angular-chart.js/dist/angular-chart.css', + 'lemur/static/app/vendor/bower_components/angular-loading-bar/src/loading-bar.css', + 'lemur/static/app/vendor/bower_components/angular-ui-switch/angular-ui-switch.css', + 'lemur/static/app/vendor/bower_components/angular-wizard/dist/angular-wizard.css', + 'lemur/static/app/vendor/bower_components/ng-table/ng-table.css', + 'lemur/static/app/vendor/bower_components/angularjs-toaster/toaster.css' + ]; + + return gulp.src(fileList) + .pipe(gulpif(isBootswatchFile, foreach(function (stream, file) { + var themeName = path.basename(path.dirname(file.path)), + content = replaceAll(baseContent, '$theme$', themeName), + file = string_src('bootstrap-' + themeName + '.less', content); + + return file; + }))) + .pipe(less()) + .pipe(gulpif(isBootstrapFile, foreach(function (stream, file) { + var fileName = path.basename(file.path), + themeName = fileName.substring(fileName.indexOf('-') + 1, fileName.indexOf('.')); + + // http://stackoverflow.com/questions/21719833/gulp-how-to-add-src-files-in-the-middle-of-a-pipe + // https://github.com/gulpjs/gulp/blob/master/docs/recipes/using-multiple-sources-in-one-task.md + return merge(stream, gulp.src(['.tmp/styles/font-awesome.css', '.tmp/styles/lemur.css'])) + .pipe(concat('style-' + themeName + ".css")); + }))) + .pipe(plumber()) + .pipe(concat('styles.css')) + .pipe(minifycss()) + .pipe(autoprefixer('last 1 version')) + .pipe(gulp.dest('.tmp/styles')) + .pipe(size()); +}); + +// http://stackoverflow.com/questions/1144783/replacing-all-occurrences-of-a-string-in-javascript +function escapeRegExp(string) { + return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); +} + +function replaceAll(string, find, replace) { + return string.replace(new RegExp(escapeRegExp(find), 'g'), replace); +} + +function string_src(filename, string) { + var src = require('stream').Readable({ objectMode: true }); + src._read = function () { + this.push(new gutil.File({ cwd: "", base: "", path: filename, contents: new Buffer(string) })); + this.push(null); + }; + return src; +} + +gulp.task('dev:scripts', function () { + return gulp.src(['lemur/static/app/angular/**/*.js']) + .pipe(jshint()) + .pipe(jshint.reporter('jshint-stylish')) + .pipe(size()); +}); + +gulp.task('build:extras', function () { + return gulp.src(['lemur/static/app/*.*', '!lemur/static/app/*.html']) + .pipe(gulp.dest('lemur/static/dist')); +}); + +function injectHtml(isDev) { + return gulp.src('lemur/static/app/index.html') + .pipe( + inject(gulp.src(bowerFiles({ base: 'app' }), { + read: false + }), { + starttag: '', + addRootSlash: false, + ignorePath: isDev ? ['lemur/static/app/', '.tmp/'] : null + }) + ) + .pipe(inject(gulp.src(['lemur/static/app/angular/**/*.js'], { + read: false + }), { + read: false, + starttag: '', + addRootSlash: false, + ignorePath: isDev ? ['lemur/static/app/', '.tmp/'] : null + })) + .pipe(inject(gulp.src(['.tmp/styles/**/*.css'], { + read: false + }), { + read: false, + starttag: '', + addRootSlash: false, + ignorePath: isDev ? ['lemur/static/app/', '.tmp/'] : null + })) + .pipe( + gulpif(!isDev, + inject(gulp.src('lemur/static/dist/ngviews/ngviews.min.js'), { + read: false, + starttag: '', + addRootSlash: false + }) + ) + ) + .pipe(gulp.dest('.tmp/')); +} + +gulp.task('dev:inject', ['dev:styles', 'dev:scripts'], function () { + return injectHtml(true); +}); + +gulp.task('build:inject', ['dev:styles', 'dev:scripts', 'build:ngviews'], function () { + return injectHtml(false); +}); + +gulp.task('build:ngviews', function () { + return gulp.src(['lemur/static/app/angular/**/*.html']) + .pipe(minifyHtml({ + empty: true, + spare: true, + quotes: true + })) + .pipe(gulp.dest('lemur/static/dist/angular')) + .pipe(size()); +}); + +gulp.task('build:html', ['dev:styles', 'dev:scripts', 'build:ngviews', 'build:inject'], function () { + var jsFilter = filter('**/*.js'); + var cssFilter = filter('**/*.css'); + + var assets = useref.assets(); + + return gulp.src('.tmp/index.html') + .pipe(assets) + .pipe(rev()) + .pipe(jsFilter) + .pipe(ngAnnotate()) + .pipe(jsFilter.restore()) + .pipe(cssFilter) + .pipe(csso()) + .pipe(cssFilter.restore()) + .pipe(assets.restore()) + .pipe(useref()) + .pipe(revReplace()) + .pipe(gulp.dest('lemur/static/dist')) + .pipe(size()); +}); + +gulp.task('build:fonts', ['dev:fonts'], function () { + return gulp.src('.tmp/fonts/**/*') + .pipe(gulp.dest('lemur/static/dist/fonts')); +}); + +gulp.task('build:images', function () { + return gulp.src('lemur/static/app/images/**/*') + .pipe(cache(imagemin({ + optimizationLevel: 3, + progressive: true, + interlaced: true + }))) + .pipe(gulp.dest('lemur/static/dist/images')) + .pipe(size()); +}); + + +gulp.task('build', ['build:ngviews', 'build:inject', 'build:images', 'build:fonts', 'build:html', 'build:extras']); diff --git a/gulp/server.js b/gulp/server.js new file mode 100644 index 00000000..1f00c599 --- /dev/null +++ b/gulp/server.js @@ -0,0 +1,56 @@ +'use strict'; + +var gulp = require('gulp'); + +var browserSync = require('browser-sync'); +var httpProxy = require('http-proxy'); + +/* This configuration allow you to configure browser sync to proxy your backend */ +/* + var proxyTarget = 'http://localhost/context/'; // The location of your backend + var proxyApiPrefix = 'api'; // The element in the URL which differentiate between API request and static file request + var proxy = httpProxy.createProxyServer({ + target: proxyTarget + }); + function proxyMiddleware(req, res, next) { + if (req.url.indexOf(proxyApiPrefix) !== -1) { + proxy.web(req, res); + } else { + next(); + } + } + */ + +function browserSyncInit(baseDir, files, browser) { + browser = browser === undefined ? 'default' : browser; + + browserSync.instance = browserSync.init(files, { + startPath: '/index.html', + server: { + baseDir: baseDir + }, + browser: browser, + ghostMode: false + }); + +} + +gulp.task('serve', ['watch'], function () { + browserSyncInit([ + '.tmp', + 'app' + ], [ + '.tmp/*.html', + '.tmp/styles/**/*.css', + 'lemur/static/app/angular/**/*.js', + 'lemur/static/app/partials/**/*.html', + 'lemur/static/app/images/**/*', + 'lemur/static/app/angular/**/*', + 'lemur/static/app/index.html' + ]); +}); + + +gulp.task('serve:dist', ['build'], function () { + browserSyncInit('lemur/static/dist'); +}); diff --git a/gulp/watch.js b/gulp/watch.js new file mode 100644 index 00000000..9d6f894c --- /dev/null +++ b/gulp/watch.js @@ -0,0 +1,12 @@ +'use strict'; + +var gulp = require('gulp'); + + +gulp.task('watch', ['dev:styles', 'dev:scripts', 'dev:inject'] ,function () { + gulp.watch('app/styles/**/*.less', ['dev:styles']); + gulp.watch('app/styles/**/*.css', ['dev:styles']); + gulp.watch('app/**/*.js', ['dev:scripts']); + gulp.watch('app/images/**/*', ['build:images']); + gulp.watch('bower.json', ['dev:inject']); +}); diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..a9da3d3c --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,31 @@ +/** + * Created by kglisson on 1/19/15. + */ +'use strict'; + +var gulp = require('gulp'); + +require('require-dir')('./gulp'); + +gulp.task('default', function () { + var c = { + reset: '\x1b[0m', + bold: '\x1b[1m', + green: '\x1b[32m', + magenta: '\x1b[35m' + }; + + console.log(''); + console.log(c.green + c.bold + 'Main Commands' + c.reset); + console.log(c.green + '-------------------------------------------' + c.reset); + console.log(c.green + 'clean' + c.reset + ' - delete the .tmp/ and dist/ folders.'); + console.log(c.green + 'build' + c.reset + ' - execute the release build and output into the dist/ folder.'); + console.log(c.green + 'serve:dist' + c.reset + ' - execute the release build and output into the dist/ folder then run a local server for the files.'); + console.log(c.green + 'serve' + c.reset + ' - run JShint and LESS compiler to produce .tmp/ folder. Then serve up the app on a local server.'); + console.log(''); + console.log(c.green + c.bold + 'All Commands' + c.reset); + console.log(c.green + '-------------------------------------------' + c.reset); + console.log(Object.keys(gulp.tasks).sort().join('\n')); + console.log(''); + return; +}); diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100644 index 00000000..b41d5f35 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +import glob +import os +import sys + +os.environ['PYFLAKES_NODOCTEST'] = '1' + +# pep8.py uses sys.argv to find setup.cfg +sys.argv = [os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)] + +# git usurbs your bin path for hooks and will always run system python +if 'VIRTUAL_ENV' in os.environ: + site_packages = glob.glob( + '%s/lib/*/site-packages' % os.environ['VIRTUAL_ENV'])[0] + sys.path.insert(0, site_packages) + + +def py_lint(files_modified): + from flake8.main import DEFAULT_CONFIG + from flake8.engine import get_style_guide + + # remove non-py files and files which no longer exist + files_modified = filter(lambda x: x.endswith('.py'), files_modified) + + flake8_style = get_style_guide(parse_argv=True, config_file=DEFAULT_CONFIG) + report = flake8_style.check_files(files_modified) + + return report.total_errors != 0 + + +def js_lint(files_modified): + has_errors = False + if os.system('node_modules/.bin/jshint src/sentry'): + has_errors = True + + return has_errors + + +def main(): + from flake8.hooks import run + + gitcmd = "git diff-index --cached --name-only HEAD" + + _, files_modified, _ = run(gitcmd) + + files_modified = filter(lambda x: os.path.exists(x), files_modified) + + if any((py_lint(files_modified), js_lint(files_modified))): + return 1 + return 0 + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/lemur/__init__.py b/lemur/__init__.py new file mode 100644 index 00000000..388914bd --- /dev/null +++ b/lemur/__init__.py @@ -0,0 +1,69 @@ +""" +.. module: lemur + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + + +""" +from flask import jsonify + +from lemur import factory + +from lemur.users.views import mod as users +from lemur.roles.views import mod as roles +from lemur.auth.views import mod as auth +from lemur.domains.views import mod as domains +from lemur.elbs.views import mod as elbs +from lemur.accounts.views import mod as accounts +from lemur.authorities.views import mod as authorities +from lemur.listeners.views import mod as listeners +from lemur.certificates.views import mod as certificates +from lemur.status.views import mod as status + +LEMUR_BLUEPRINTS = ( + users, + roles, + auth, + domains, + elbs, + accounts, + authorities, + listeners, + certificates, + status +) + +def create_app(config=None): + app = factory.create_app(app_name=__name__, blueprints=LEMUR_BLUEPRINTS, config=config) + configure_hook(app) + return app + + +def configure_hook(app): + """ + + :param app: + :return: + """ + from flask.ext.principal import PermissionDenied + from lemur.decorators import crossdomain + if app.config.get('CORS'): + @app.after_request + @crossdomain(origin="http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE']) + def after(response): + return response + + @app.errorhandler(PermissionDenied) + def handle_invalid_usage(error): + response = {'message': 'You are not allow to access this resource'} + response.status_code = 403 + return response + + + + + + diff --git a/lemur/accounts/__init__.py b/lemur/accounts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/accounts/models.py b/lemur/accounts/models.py new file mode 100644 index 00000000..7adba330 --- /dev/null +++ b/lemur/accounts/models.py @@ -0,0 +1,29 @@ +""" +.. module: lemur.accounts.models + :platform: unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from sqlalchemy import Column, Integer, String, Text +from sqlalchemy.orm import relationship + +from lemur.database import db + + +class Account(db.Model): + __tablename__ = 'accounts' + id = Column(Integer, primary_key=True) + account_number = Column(String(32), unique=True) + label = Column(String(32)) + notes = Column(Text()) + elbs = relationship("ELB", backref='account', cascade="all, delete, delete-orphan") + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + def serialize(self): + blob = self.as_dict() + blob['elbs'] = [x.id for x in self.elbs] + return blob + diff --git a/lemur/accounts/service.py b/lemur/accounts/service.py new file mode 100644 index 00000000..61fe0633 --- /dev/null +++ b/lemur/accounts/service.py @@ -0,0 +1,112 @@ +""" +.. module: lemur.accounts.views + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from lemur import database +from lemur.accounts.models import Account +from lemur.certificates.models import Certificate + + +def create(account_number, label=None, comments=None): + """ + Creates a new account, that can then be used as a destination for certificates. + + :param account_number: AWS assigned ID + :param label: Account common name + :param comments: + :rtype : Account + :return: New account + """ + acct = Account(account_number=account_number, label=label, notes=comments) + return database.create(acct) + + +def update(account_id, account_number, label, comments=None): + """ + Updates an existing account. + + :param account_id: Lemur assigned ID + :param account_number: AWS assigned ID + :param label: Account common name + :param comments: + :rtype : Account + :return: + """ + account = get(account_id) + + account.account_number = account_number + account.label = label + account.notes = comments + + return database.update(account) + + +def delete(account_id): + """ + Deletes an account. + + :param account_id: Lemur assigned ID + """ + database.delete(get(account_id)) + + +def get(account_id): + """ + Retrieves an account by it's lemur assigned ID. + + :param account_id: Lemur assigned ID + :rtype : Account + :return: + """ + return database.get(Account, account_id) + + +def get_by_account_number(account_number): + """ + Retrieves an account by it's amazon assigned ID. + + :rtype : Account + :param account_number: AWS assigned ID + :return: + """ + return database.get(Account, account_number, field='account_number') + + +def get_all(): + """ + Retrieves all account currently known by Lemur. + + :return: + """ + query = database.session_query(Account) + return database.find_all(query, Account, {}).all() + + +def render(args): + sort_by = args.pop('sort_by') + sort_dir = args.pop('sort_dir') + page = args.pop('page') + count = args.pop('count') + filt = args.pop('filter') + certificate_id = args.pop('certificate_id', None) + + if certificate_id: + query = database.session_query(Account).join(Certificate, Account.certificate) + query = query.filter(Certificate.id == certificate_id) + else: + query = database.session_query(Account) + + if filt: + terms = filt.split(';') + query = database.filter(query, Account, terms) + + query = database.find_all(query, Account, args) + + if sort_by and sort_dir: + query = database.sort(query, Account, sort_by, sort_dir) + + return database.paginate(query, page, count) + diff --git a/lemur/accounts/views.py b/lemur/accounts/views.py new file mode 100644 index 00000000..b3304af4 --- /dev/null +++ b/lemur/accounts/views.py @@ -0,0 +1,300 @@ +""" +.. module: lemur.accounts.views + :platform: Unix + :synopsis: This module contains all of the accounts view code. + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from flask import Blueprint +from flask.ext.restful import Api, reqparse, fields +from lemur.accounts import service + +from lemur.auth.service import AuthenticatedResource +from lemur.auth.permissions import admin_permission +from lemur.common.utils import paginated_parser, marshal_items + + +mod = Blueprint('accounts', __name__) +api = Api(mod) + + +FIELDS = { + 'accountNumber': fields.Integer(attribute='account_number'), + 'label': fields.String, + 'comments': fields.String(attribute='notes'), + 'id': fields.Integer, +} + + +class AccountsList(AuthenticatedResource): + """ Defines the 'accounts' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(AccountsList, self).__init__() + + @marshal_items(FIELDS) + def get(self): + """ + .. http:get:: /accounts + + The current account list + + **Example request**: + + .. sourcecode:: http + + GET /accounts HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "items": [ + { + "id": 2, + "accountNumber": 222222222, + "label": "account2", + "comments": "this is a thing" + }, + { + "id": 1, + "accountNumber": 11111111111, + "label": "account1", + "comments": "this is a thing" + }, + ] + "total": 2 + } + + :query sortBy: field to sort on + :query sortDir: acs or desc + :query page: int. default is 1 + :query filter: key value pair. format is k=v; + :query limit: limit number. default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + parser = paginated_parser.copy() + args = parser.parse_args() + return service.render(args) + + @admin_permission.require(http_exception=403) + @marshal_items(FIELDS) + def post(self): + """ + .. http:post:: /accounts + + Creates a new account + + **Example request**: + + .. sourcecode:: http + + POST /accounts HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "accountNumber": 11111111111, + "label": "account1, + "comments": "this is a thing" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "accountNumber": 11111111111, + "label": "account1", + "comments": "this is a thing" + } + + :arg accountNumber: aws account number + :arg label: human readable account label + :arg comments: some description about the account + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + self.reqparse.add_argument('accountNumber', type=int, dest="account_number", location='json', required=True) + self.reqparse.add_argument('label', type=str, location='json', required=True) + self.reqparse.add_argument('comments', type=str, location='json') + + args = self.reqparse.parse_args() + return service.create(args['account_number'], args['label'], args['comments']) + + +class Accounts(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Accounts, self).__init__() + + @marshal_items(FIELDS) + def get(self, account_id): + """ + .. http:get:: /accounts/1 + + Get a specific account + + **Example request**: + + .. sourcecode:: http + + GET /accounts/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "accountNumber": 11111111111, + "label": "account1", + "comments": "this is a thing" + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + return service.get(account_id) + + @admin_permission.require(http_exception=403) + @marshal_items(FIELDS) + def put(self, account_id): + """ + .. http:post:: /accounts/1 + + Updates an account + + **Example request**: + + .. sourcecode:: http + + POST /accounts/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "accountNumber": 11111111111, + "label": "labelChanged, + "comments": "this is a thing" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "accountNumber": 11111111111, + "label": "labelChanged", + "comments": "this is a thing" + } + + :arg accountNumber: aws account number + :arg label: human readable account label + :arg comments: some description about the account + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + self.reqparse.add_argument('accountNumber', type=int, dest="account_number", location='json', required=True) + self.reqparse.add_argument('label', type=str, location='json', required=True) + self.reqparse.add_argument('comments', type=str, location='json') + + args = self.reqparse.parse_args() + return service.update(account_id, args['account_number'], args['label'], args['comments']) + + @admin_permission.require(http_exception=403) + def delete(self, account_id): + service.delete(account_id) + return {'result': True} + + + +class CertificateAccounts(AuthenticatedResource): + """ Defines the 'certificate/', endpoint='account') +api.add_resource(CertificateAccounts, '/certificates//accounts', endpoint='certificateAccounts') + diff --git a/lemur/analyze/__init__.py b/lemur/analyze/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/analyze/service.py b/lemur/analyze/service.py new file mode 100644 index 00000000..7b17d3db --- /dev/null +++ b/lemur/analyze/service.py @@ -0,0 +1,62 @@ +""" +.. module: lemur.analyze.service + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +#def analyze(endpoints, truststores): +# results = {"headings": ["Endpoint"], +# "results": [], +# "time": datetime.now().strftime("#Y%m%d %H:%M:%S")} +# +# for store in truststores: +# results['headings'].append(os.path.basename(store)) +# +# for endpoint in endpoints: +# result_row = [endpoint] +# for store in truststores: +# result = {'details': []} +# +# tests = [] +# for region, ip in REGIONS.items(): +# try: +# domain = dns.name.from_text(endpoint) +# if not domain.is_absolute(): +# domain = domain.concatenate(dns.name.root) +# +# my_resolver = dns.resolver.Resolver() +# my_resolver.nameservers = [ip] +# answer = my_resolver.query(domain) +# +# #force the testing of regional enpoints by changing the dns server +# response = requests.get('https://' + str(answer[0]), verify=store) +# tests.append('pass') +# result['details'].append("{}: SSL testing completed without errors".format(region)) +# +# except SSLError as e: +# log.debug(e) +# if 'hostname' in str(e): +# tests.append('pass') +# result['details'].append("{}: This test passed ssl negotiation but failed hostname verification becuase the hostname is not included in the certificate".format(region)) +# elif 'certificate verify failed' in str(e): +# tests.append('fail') +# result['details'].append("{}: This test failed to verify the SSL certificate".format(region)) +# else: +# tests.append('fail') +# result['details'].append("{}: {}".format(region, str(e))) +# +# except Exception as e: +# log.debug(e) +# tests.append('fail') +# result['details'].append("{}: {}".format(region, str(e))) +# +# #any failing tests fails the whole endpoint +# if 'fail' in tests: +# result['test'] = 'fail' +# else: +# result['test'] = 'pass' +# +# result_row.append(result) +# results['results'].append(result_row) +# return results +# diff --git a/lemur/auth/__init__.py b/lemur/auth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/auth/permissions.py b/lemur/auth/permissions.py new file mode 100644 index 00000000..16d686b7 --- /dev/null +++ b/lemur/auth/permissions.py @@ -0,0 +1,62 @@ +""" +.. module: permissions + :platform: Unix + :synopsis: This module defines all the permission used within Lemur + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from functools import partial +from collections import namedtuple + +from flask.ext.principal import Permission, RoleNeed + +# Permissions +operator_permission = Permission(RoleNeed('operator')) +admin_permission = Permission(RoleNeed('secops@netflix.com')) + +CertificateCreator = namedtuple('certificate', ['method', 'value']) +CertificateCreatorNeed = partial(CertificateCreator, 'certificateView') + +CertificateOwner = namedtuple('certificate', ['method', 'value']) +CertificateOwnerNeed = partial(CertificateOwner, 'certificateView') + + +class ViewKeyPermission(Permission): + def __init__(self, role_id, certificate_id): + c_need = CertificateCreatorNeed(unicode(certificate_id)) + o_need = CertificateOwnerNeed(unicode(role_id)) + super(ViewKeyPermission, self).__init__(o_need, c_need, RoleNeed('admin')) + + +class UpdateCertificatePermission(Permission): + def __init__(self, role_id, certificate_id): + c_need = CertificateCreatorNeed(unicode(certificate_id)) + o_need = CertificateOwnerNeed(unicode(role_id)) + super(UpdateCertificatePermission, self).__init__(o_need, c_need, RoleNeed('admin')) + + +RoleUser = namedtuple('role', ['method', 'value']) +ViewRoleCredentialsNeed = partial(RoleUser, 'roleView') + + +class ViewRoleCredentialsPermission(Permission): + def __init__(self, role_id): + need = ViewRoleCredentialsNeed(unicode(role_id)) + super(ViewRoleCredentialsPermission, self).__init__(need, RoleNeed('admin')) + + +AuthorityCreator = namedtuple('authority', ['method', 'value']) +AuthorityCreatorNeed = partial(AuthorityCreator, 'authorityUse') + +AuthorityOwner = namedtuple('authority', ['method', 'value']) +AuthorityOwnerNeed = partial(AuthorityOwner, 'role') + + +class AuthorityPermission(Permission): + def __init__(self, authority_id, roles): + needs = [RoleNeed('admin'), AuthorityCreatorNeed(unicode(authority_id))] + for r in roles: + needs.append(AuthorityOwnerNeed(unicode(r))) + + super(AuthorityPermission, self).__init__(*needs) diff --git a/lemur/auth/service.py b/lemur/auth/service.py new file mode 100644 index 00000000..0675f640 --- /dev/null +++ b/lemur/auth/service.py @@ -0,0 +1,188 @@ +""" +.. module: lemur.auth.service + :platform: Unix + :synopsis: This module contains all of the authentication duties for + lemur + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson + +""" +import jwt +import json +import base64 +import binascii +from functools import wraps +from datetime import datetime, timedelta + +from flask import g, current_app, jsonify, request + +from flask.ext.restful import Resource +from flask.ext.principal import identity_loaded, RoleNeed, UserNeed + +from flask.ext.principal import Identity, identity_changed + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers + +from lemur.users import service as user_service +from lemur.auth.permissions import CertificateOwnerNeed, CertificateCreatorNeed, \ + AuthorityCreatorNeed, AuthorityOwnerNeed, ViewRoleCredentialsNeed + + +def base64url_decode(data): + if isinstance(data, unicode): + data = str(data) + + rem = len(data) % 4 + + if rem > 0: + data += b'=' * (4 - rem) + + return base64.urlsafe_b64decode(data) + + +def base64url_encode(data): + return base64.urlsafe_b64encode(data).replace(b'=', b'') + + +def get_rsa_public_key(n, e): + """ + Retrieve an RSA public key based on a module and exponent as provided by the JWKS format. + + :param n: + :param e: + :return: a RSA Public Key in PEM format + """ + n = int(binascii.hexlify(base64url_decode(n)), 16) + e = int(binascii.hexlify(base64url_decode(e)), 16) + pub = RSAPublicNumbers(e, n).public_key(default_backend()) + return pub.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + +def create_token(user): + """ + Create a valid JWT for a given user, this token is then used to authenticate + sessions until the token expires. + + :param user: + :return: + """ + expiration_delta = timedelta(days=int(current_app.config.get('TOKEN_EXPIRATION', 1))) + payload = { + 'sub': user.id, + 'iat': datetime.now(), + 'exp': datetime.now() + expiration_delta + } + token = jwt.encode(payload, current_app.config['TOKEN_SECRET']) + return token.decode('unicode_escape') + + +def login_required(f): + """ + Validates the JWT and ensures that is has not expired. + + :param f: + :return: + """ + @wraps(f) + def decorated_function(*args, **kwargs): + if not request.headers.get('Authorization'): + response = jsonify(message='Missing authorization header') + response.status_code = 401 + return response + + token = request.headers.get('Authorization').split()[1] + + try: + payload = jwt.decode(token, current_app.config['TOKEN_SECRET']) + except jwt.DecodeError: + return dict(message='Token is invalid'), 403 + except jwt.ExpiredSignatureError: + return dict(message='Token has expired'), 403 + except jwt.InvalidTokenError: + return dict(message='Token is invalid'), 403 + + g.current_user = user_service.get(payload['sub']) + + if not g.current_user.id: + return dict(message='You are not logged in'), 403 + + # Tell Flask-Principal the identity changed + identity_changed.send(current_app._get_current_object(), identity=Identity(g.current_user.id)) + + return f(*args, **kwargs) + + return decorated_function + + +def fetch_token_header(token): + """ + Fetch the header out of the JWT token. + + :param token: + :return: :raise jwt.DecodeError: + """ + token = token.encode('utf-8') + try: + signing_input, crypto_segment = token.rsplit(b'.', 1) + header_segment, payload_segment = signing_input.split(b'.', 1) + except ValueError: + raise jwt.DecodeError('Not enough segments') + + try: + return json.loads(base64url_decode(header_segment)) + except TypeError, binascii.Error: + raise jwt.DecodeError('Invalid header padding') + + + +@identity_loaded.connect +def on_identity_loaded(sender, identity): + """ + Sets the identity of a given option, assigns additional permissions based on + the role that the user is a part of. + + :param sender: + :param identity: + """ + # load the user + user = user_service.get(identity.id) + + # add the UserNeed to the identity + identity.provides.add(UserNeed(identity.id)) + + # identity with the roles that the user provides + if hasattr(user, 'roles'): + for role in user.roles: + identity.provides.add(CertificateOwnerNeed(unicode(role.id))) + identity.provides.add(ViewRoleCredentialsNeed(unicode(role.id))) + identity.provides.add(RoleNeed(role.name)) + + # apply ownership for authorities + if hasattr(user, 'authorities'): + for authority in user.authorities: + identity.provides.add(AuthorityCreatorNeed(unicode(authority.id))) + + # apply ownership of certificates + if hasattr(user, 'certificates'): + for certificate in user.certificates: + identity.provides.add(CertificateCreatorNeed(unicode(certificate.id))) + + g.user = user + + +class AuthenticatedResource(Resource): + """ + Inherited by all resources that need to be protected by authentication. + """ + method_decorators = [login_required] + + def __init__(self): + super(AuthenticatedResource, self).__init__() + + diff --git a/lemur/auth/views.py b/lemur/auth/views.py new file mode 100644 index 00000000..06e77f77 --- /dev/null +++ b/lemur/auth/views.py @@ -0,0 +1,257 @@ +""" +.. module: lemur.auth.views + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +import jwt +import base64 +import requests + +from flask import g, Blueprint, current_app, abort + +from flask.ext.restful import reqparse, Resource, Api +from flask.ext.principal import Identity, identity_changed + +from lemur.common.crypto import unlock + +from lemur.auth.permissions import admin_permission +from lemur.users import service as user_service +from lemur.roles import service as role_service +from lemur.certificates import service as cert_service +from lemur.auth.service import AuthenticatedResource, create_token, fetch_token_header, get_rsa_public_key + + +mod = Blueprint('auth', __name__) +api = Api(mod) + + +class Login(Resource): + """ + Provides an endpoint for Lemur's basic authentication. It takes a username and password + combination and returns a JWT token. + + This token token is required for each API request and must be provided in the Authorization Header for the request. + :: + + Authorization:Bearer + + Tokens have a set expiration date. You can inspect the token expiration be base64 decoding the token and inspecting + it's contents. + + .. note:: It is recommended that the token expiration is fairly short lived (hours not days). This will largely depend \ + on your uses cases but. It is important to not that there is currently no build in method to revoke a users token \ + and force re-authentication. + """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Login, self).__init__() + + def post(self): + """ + .. http:post:: /auth/login + + Login with username:password + + **Example request**: + + .. sourcecode:: http + + POST /auth/login HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "username": "test", + "password": "test" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "token": "12343243243" + } + + :arg username: username + :arg password: password + :statuscode 401: invalid credentials + :statuscode 200: no error + """ + self.reqparse.add_argument('username', type=str, required=True, location='json') + self.reqparse.add_argument('password', type=str, required=True, location='json') + + args = self.reqparse.parse_args() + + if '@' in args['username']: + user = user_service.get_by_email(args['username']) + else: + user = user_service.get_by_username(args['username']) + + if user and user.check_password(args['password']): + # Tell Flask-Principal the identity changed + identity_changed.send(current_app._get_current_object(), + identity=Identity(user.id)) + return dict(token=create_token(user)) + + return dict(message='The supplied credentials are invalid'), 401 + + def get(self): + return {'username': g.current_user.username, 'roles': [r.name for r in g.current_user.roles]} + + +class Ping(Resource): + """ + This class serves as an example of how one might implement an SSO provider for use with Lemur. In + this example we use a OpenIDConnect authentication flow, that is essentially OAuth2 underneath. If you have an + OAuth2 provider you want to use Lemur there would be two steps: + + 1. Define your own class that inherits from :class:`flask.ext.restful.Resource` and create the HTTP methods the \ + provider uses for it's callbacks. + 2. Add or change the Lemur AngularJS Configuration to point to your new provider + """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Ping, self).__init__() + + def post(self): + self.reqparse.add_argument('clientId', type=str, required=True, location='json') + self.reqparse.add_argument('redirectUri', type=str, required=True, location='json') + self.reqparse.add_argument('code', type=str, required=True, location='json') + + args = self.reqparse.parse_args() + + # take the information we have received from Meechum to create a new request + params = { + 'client_id': args['clientId'], + 'grant_type': 'authorization_code', + 'scope': 'openid email profile address', + 'redirect_uri': args['redirectUri'], + 'code': args['code'] + } + + # you can either discover these dynamically or simply configure them + access_token_url = current_app.config.get('PING_ACCESS_TOKEN_URL') + user_api_url = current_app.config.get('PING_USER_API_URL') + + # the secret and cliendId will be given to you when you signup for meechum + basic = base64.b64encode('{0}:{1}'.format(args['clientId'], current_app.config.get("PING_SECRET"))) + headers = {'Authorization': 'Basic {0}'.format(basic)} + + # exchange authorization code for access token. + + r = requests.post(access_token_url, headers=headers, params=params) + id_token = r.json()['id_token'] + access_token = r.json()['access_token'] + + # fetch token public key + header_data = fetch_token_header(id_token) + jwks_url = current_app.config.get('PING_JWKS_URL') + + # retrieve the key material as specified by the token header + r = requests.get(jwks_url) + for key in r.json()['keys']: + if key['kid'] == header_data['kid']: + secret = get_rsa_public_key(key['n'], key['e']) + algo = header_data['alg'] + break + else: + return dict(message='Key not found'), 403 + + # validate your token based on the key it was signed with + try: + jwt.decode(id_token, secret, algorithms=[algo], audience=args['clientId']) + except jwt.DecodeError: + return dict(message='Token is invalid'), 403 + except jwt.ExpiredSignatureError: + return dict(message='Token has expired'), 403 + except jwt.InvalidTokenError: + return dict(message='Token is invalid'), 403 + + user_params = dict(access_token=access_token, schema='profile') + + # retrieve information about the current user. + r = requests.get(user_api_url, params=user_params) + profile = r.json() + + user = user_service.get_by_email(profile['email']) + + # update their google 'roles' + roles = [] + + # Legacy edge case - 'admin' has some special privileges associated with it + if 'secops@netflix.com' in profile['googleGroups']: + roles.append(role_service.get_by_name('admin')) + + for group in profile['googleGroups']: + role = role_service.get_by_name(group) + if not role: + role = role_service.create(group, description='This is a google group based role created by Lemur') + roles.append(role) + + # if we get an sso user create them an account + # we still pick a random password in case sso is down + if not user: + # every user is an operator (tied to the verisignCA) + v = role_service.get_by_name('verisign') + if v: + roles.append(v) + + user = user_service.create( + profile['email'], + cert_service.create_challenge(), + profile['email'], + True, + profile.get('thumbnailPhotoUrl'), + roles + ) + + else: + # we add 'lemur' specific roles, so they do not get marked as removed + for ur in user.roles: + if ur.authority_id: + roles.append(ur) + + # update any changes to the user + user_service.update( + user.id, + profile['email'], + profile['email'], + True, + profile.get('thumbnailPhotoUrl'), # incase profile isn't google+ enabled + roles + ) + + # Tell Flask-Principal the identity changed + identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) + + return dict(token=create_token(user)) + + +class Unlock(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Unlock, self).__init__() + + @admin_permission.require(http_exception=403) + def post(self): + self.reqparse.add_argument('password', type=str, required=True, location='json') + args = self.reqparse.parse_args() + unlock(args['password']) + return { + "message": "You have successfully unlocked this Lemur instance", + "type": "success" + } + + +api.add_resource(Login, '/auth/login', endpoint='login') +api.add_resource(Ping, '/auth/ping', endpoint='ping') +api.add_resource(Unlock, '/auth/unlock', endpoint='unlock') + + diff --git a/lemur/authorities/__init__.py b/lemur/authorities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/authorities/models.py b/lemur/authorities/models.py new file mode 100644 index 00000000..011acf2d --- /dev/null +++ b/lemur/authorities/models.py @@ -0,0 +1,58 @@ +""" +.. module: lemur.authorities.models + :platform: unix + :synopsis: This module contains all of the models need to create a authority within Lemur. + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from cryptography import x509 +from cryptography.hazmat.backends import default_backend + +from sqlalchemy.orm import relationship +from sqlalchemy import Column, Integer, String, Text, func, ForeignKey, DateTime, PassiveDefault, Boolean +from sqlalchemy.dialects.postgresql import JSON + +from lemur.database import db +from lemur.certificates.models import cert_get_cn, cert_get_not_after, cert_get_not_before + + +class Authority(db.Model): + __tablename__ = 'authorities' + id = Column(Integer, primary_key=True) + owner = Column(String(128)) + name = Column(String(128), unique=True) + body = Column(Text()) + chain = Column(Text()) + bits = Column(Integer()) + cn = Column(String(128)) + not_before = Column(DateTime) + not_after = Column(DateTime) + active = Column(Boolean, default=True) + date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) + plugin_name = Column(String(64)) + description = Column(Text) + options = Column(JSON) + roles = relationship('Role', backref=db.backref('authority'), lazy='dynamic') + user_id = Column(Integer, ForeignKey('users.id')) + certificates = relationship("Certificate", backref='authority') + + def __init__(self, name, owner, plugin_name, body, roles=None, chain=None, description=None): + self.name = name + self.body = body + self.chain = chain + self.owner = owner + self.plugin_name = plugin_name + cert = x509.load_pem_x509_certificate(str(body), default_backend()) + self.cn = cert_get_cn(cert) + self.not_before = cert_get_not_before(cert) + self.not_after = cert_get_not_after(cert) + self.roles = roles + self.description = description + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + def serialize(self): + blob = self.as_dict() + return blob diff --git a/lemur/authorities/service.py b/lemur/authorities/service.py new file mode 100644 index 00000000..d7d82a48 --- /dev/null +++ b/lemur/authorities/service.py @@ -0,0 +1,173 @@ +""" +.. module: lemur.authorities.service + :platform: Unix + :synopsis: This module contains all of the services level functions used to + administer authorities in Lemur + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson + +""" +from flask import g + +from lemur import database +from lemur.authorities.models import Authority +from lemur.roles import service as role_service + +from lemur.roles.models import Role +import lemur.certificates.service as cert_service + +from lemur.common.services.issuers.manager import get_plugin_by_name + +def update(authority_id, active=None, roles=None): + """ + Update a an authority with new values. + + :param authority_id: + :param roles: roles that are allowed to use this authority + :rtype : Authority + :return: + """ + authority = get(authority_id) + if roles: + authority = database.update_list(authority, 'roles', Role, roles) + + if active: + authority.active = active + return database.update(authority) + + +def create(kwargs): + """ + Create a new authority. + + :param name: name of the authority + :param roles: roles that are allowed to use this authority + :param options: available options for authority + :param description: + :rtype : Authority + :return: + """ + + issuer = get_plugin_by_name(kwargs.get('pluginName')) + + kwargs['creator'] = g.current_user.email + cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs) + + cert = cert_service.save_cert(cert_body, None, intermediate, None, None, None) + cert.user = g.current_user + + # we create and attach any roles that cloudCA gives us + role_objs = [] + for r in issuer_roles: + role = role_service.create(r['name'], password=r['password'], description="CloudCA auto generated role", + username=r['username']) + # the user creating the authority should be able to administer it + if role.username == 'admin': + g.current_user.roles.append(role) + role_objs.append(role) + + authority = Authority( + kwargs.get('caName'), + kwargs['ownerEmail'], + kwargs['pluginName'], + cert_body, + description=kwargs['caDescription'], + chain=intermediate, + roles=role_objs + ) + + # do this last encase we need to roll back/abort + database.update(cert) + authority = database.create(authority) + + g.current_user.authorities.append(authority) + + return authority + + +def get_all(): + """ + Get all authorities that are currently in Lemur. + + :rtype : List + :return: + """ + query = database.session_query(Authority) + return database.find_all(query, Authority, {}).all() + + +def get(authority_id): + """ + Retrieves an authority given it's ID + + :rtype : Authority + :param authority_id: + :return: + """ + return database.get(Authority, authority_id) + + +def get_by_name(authority_name): + """ + Retrieves an authority given it's name. + + :param authority_name: + :rtype : Authority + :return: + """ + return database.get(Authority, authority_name, field='name') + + +def get_authority_role(ca_name): + """ + Attempts to get the authority role for a given ca uses current_user + as a basis for accomplishing that. + + :param ca_name: + """ + if g.current_user.is_admin: + authority = get_by_name(ca_name) + #TODO we should pick admin ca roles for admin + return authority.roles[0] + else: + for role in g.current_user.roles: + if role.authority: + if role.authority.name == ca_name: + return role + + +def render(args): + """ + Helper that helps us render the REST Api responses. + :param args: + :return: + """ + query = database.session_query(Authority) + sort_by = args.pop('sort_by') + sort_dir = args.pop('sort_dir') + page = args.pop('page') + count = args.pop('count') + filt = args.pop('filter') + + if filt: + terms = filt.split(';') + if 'active' in filt: # this is really weird but strcmp seems to not work here?? + query = query.filter(Authority.active == terms[1]) + else: + query = database.filter(query, Authority, terms) + + # we make sure that a user can only use an authority they either own are are a member of - admins can see all + if not g.current_user.is_admin: + authority_ids = [] + for role in g.current_user.roles: + if role.authority: + authority_ids.append(role.authority.id) + query = query.filter(Authority.id.in_(authority_ids)) + + query = database.find_all(query, Authority, args) + + if sort_by and sort_dir: + query = database.sort(query, Authority, sort_by, sort_dir) + + return database.paginate(query, page, count) diff --git a/lemur/authorities/views.py b/lemur/authorities/views.py new file mode 100644 index 00000000..e973a10c --- /dev/null +++ b/lemur/authorities/views.py @@ -0,0 +1,372 @@ +""" +.. module: lemur.authorities.views + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from flask import Blueprint, g +from flask.ext.restful import reqparse, fields, Api + +from lemur.authorities import service +from lemur.roles import service as role_service +from lemur.certificates import service as certificate_service +from lemur.auth.service import AuthenticatedResource + +from lemur.auth.permissions import AuthorityPermission + +from lemur.common.utils import paginated_parser, marshal_items + + +FIELDS = { + 'name': fields.String, + 'description': fields.String, + 'options': fields.Raw, + 'pluginName': fields.String, + 'body': fields.String, + 'chain': fields.String, + 'active': fields.Boolean, + 'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'), + 'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'), + 'id': fields.Integer, +} + +mod = Blueprint('authorities', __name__) +api = Api(mod) + + +class AuthoritiesList(AuthenticatedResource): + """ Defines the 'authorities' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(AuthoritiesList, self).__init__() + + @marshal_items(FIELDS) + def get(self): + """ + .. http:get:: /authorities + + The current list of authorities + + **Example request**: + + .. sourcecode:: http + + GET /authorities HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "items": [ + { + "id": 1, + "name": "authority1", + "description": "this is authority1", + "pluginName": null, + "chain": "-----Begin ...", + "body": "-----Begin ...", + "active": true, + "notBefore": "2015-06-05T17:09:39", + "notAfter": "2015-06-10T17:09:39" + "options": null + } + ] + "total": 1 + } + + :query sortBy: field to sort on + :query sortDir: acs or desc + :query page: int. default is 1 + :query filter: key value pair. format is k=v; + :query limit: limit number. default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + + :note: this will only show certificates that the current user is authorized to use + """ + parser = paginated_parser.copy() + args = parser.parse_args() + return service.render(args) + + @marshal_items(FIELDS) + def post(self): + """ + .. http:post:: /authorities + + Create an authority + + **Example request**: + + .. sourcecode:: http + + POST /authorities HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "caDN": { + "country": "US", + "state": "CA", + "location": "A Location", + "organization": "ExampleInc", + "organizationalUnit": "Operations", + "commonName": "a common name" + }, + "caType": "root", + "caSigningAlgo": "sha256WithRSA", + "caSensitivity": "medium", + "keyType": "RSA2048", + "pluginName": "cloudca", + "validityStart": "2015-06-11T07:00:00.000Z", + "validityEnd": "2015-06-13T07:00:00.000Z", + "caName": "DoctestCA", + "ownerEmail": "jimbob@example.com", + "caDescription": "Example CA", + "extensions": { + "subAltNames": { + "names": [] + } + }, + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "authority1", + "description": "this is authority1", + "pluginName": null, + "chain": "-----Begin ...", + "body": "-----Begin ...", + "active": true, + "notBefore": "2015-06-05T17:09:39", + "notAfter": "2015-06-10T17:09:39" + "options": null + } + + :arg caName: authority's name + :arg caDescription: a sensible description about what the CA with be used for + :arg ownerEmail: the team or person who 'owns' this authority + :arg validityStart: when this authority should start issuing certificates + :arg validityEnd: when this authority should stop issuing certificates + :arg extensions: certificate extensions + :arg pluginName: name of the plugin to create the authority + :arg caType: the type of authority (root/subca) + :arg caParent: the parent authority if this is to be a subca + :arg caSigningAlgo: algorithm used to sign the authority + :arg keyType: key type + :arg caSensitivity: the sensitivity of the root key, for CloudCA this determines if the root keys are stored + in an HSM + :arg caKeyName: name of the key to store in the HSM (CloudCA) + :arg caSerialNumber: serial number of the authority + :arg caFirstSerial: specifies the starting serial number for certificates issued off of this authority + :reqheader Authorization: OAuth token to authenticate + :statuscode 403: unauthenticated + :statuscode 200: no error + """ + self.reqparse.add_argument('caName', type=str, location='json', required=True) + self.reqparse.add_argument('caDescription', type=str, location='json', required=False) + self.reqparse.add_argument('ownerEmail', type=str, location='json', required=True) + self.reqparse.add_argument('caDN', type=dict, location='json', required=False) + self.reqparse.add_argument('validityStart', type=str, location='json', required=False) # TODO validate + self.reqparse.add_argument('validityEnd', type=str, location='json', required=False) # TODO validate + self.reqparse.add_argument('extensions', type=dict, location='json', required=False) + self.reqparse.add_argument('pluginName', type=str, location='json', required=True) + self.reqparse.add_argument('caType', type=str, location='json', required=False) + self.reqparse.add_argument('caParent', type=str, location='json', required=False) + self.reqparse.add_argument('caSigningAlgo', type=str, location='json', required=False) + self.reqparse.add_argument('keyType', type=str, location='json', required=False) + self.reqparse.add_argument('caSensitivity', type=str, location='json', required=False) + self.reqparse.add_argument('caKeyName', type=str, location='json', required=False) + self.reqparse.add_argument('caSerialNumber', type=int, location='json', required=False) + self.reqparse.add_argument('caFirstSerial', type=int, location='json', required=False) + + args = self.reqparse.parse_args() + return service.create(args) + + +class Authorities(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Authorities, self).__init__() + + @marshal_items(FIELDS) + def get(self, authority_id): + """ + .. http:get:: /authorities/1 + + One authority + + **Example request**: + + .. sourcecode:: http + + GET /authorities/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "authority1", + "description": "this is authority1", + "pluginName": null, + "chain": "-----Begin ...", + "body": "-----Begin ...", + "active": true, + "notBefore": "2015-06-05T17:09:39", + "notAfter": "2015-06-10T17:09:39" + "options": null + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + return service.get(authority_id) + + @marshal_items(FIELDS) + def put(self, authority_id): + """ + .. http:put:: /authorities/1 + + Update a authority + + **Example request**: + + .. sourcecode:: http + + PUT /authorities/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "roles": [], + "active": false + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "authority1", + "description": "this is authority1", + "pluginname": null, + "chain": "-----begin ...", + "body": "-----begin ...", + "active": false, + "notbefore": "2015-06-05t17:09:39", + "notafter": "2015-06-10t17:09:39" + "options": null + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + self.reqparse.add_argument('roles', type=list, location='json') + self.reqparse.add_argument('active', type=str, location='json') + args = self.reqparse.parse_args() + + authority = service.get(authority_id) + role = role_service.get_by_name(authority.owner) + + # all the authority role members should be allowed + roles = [x.name for x in authority.roles] + + # allow "owner" roles by team DL + roles.append(role) + permission = AuthorityPermission(authority_id, roles) + + # we want to make sure that we cannot add roles that we are not members of + if not g.current_user.is_admin: + role_ids = set([r['id'] for r in args['roles']]) + user_role_ids = set([r.id for r in g.current_user.roles]) + + if not role_ids.issubset(user_role_ids): + return dict(message="You are not allowed to associate a role which you are not a member of"), 400 + + if permission.can(): + return service.update(authority_id, active=args['active'], roles=args['roles']) + + return dict(message="You are not authorized to update this authority"), 403 + + +class CertificateAuthority(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(CertificateAuthority, self).__init__() + + @marshal_items(FIELDS) + def get(self, certificate_id): + """ + .. http:get:: /certificates/1/authority + + One authority for given certificate + + **Example request**: + + .. sourcecode:: http + + GET /certificates/1/authority HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "authority1", + "description": "this is authority1", + "pluginName": null, + "chain": "-----Begin ...", + "body": "-----Begin ...", + "active": true, + "notBefore": "2015-06-05T17:09:39", + "notAfter": "2015-06-10T17:09:39" + "options": null + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + return certificate_service.get(certificate_id).authority + +api.add_resource(AuthoritiesList, '/authorities', endpoint='authorities') +api.add_resource(Authorities, '/authorities/', endpoint='authority') +api.add_resource(CertificateAuthority, '/certificates//authority', endpoint='certificateAuthority') diff --git a/lemur/certificates/__init__.py b/lemur/certificates/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/certificates/exceptions.py b/lemur/certificates/exceptions.py new file mode 100644 index 00000000..a9ed6e0a --- /dev/null +++ b/lemur/certificates/exceptions.py @@ -0,0 +1,87 @@ +""" +.. module: lemur.certificates.exceptions + :synopsis: Defines all monterey specific exceptions + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from flask import current_app +from lemur.exceptions import LemurException + + +class UnknownAuthority(LemurException): + def __init__(self, authority): + self.code = 404 + self.authority = authority + self.data = {"message": "The authority specified '{}' is not a valid authority".format(self.authority)} + + current_app.logger.warning(self) + + def __str__(self): + return repr(self.data['message']) + + +class InsufficientDomains(LemurException): + def __init__(self): + self.code = 400 + self.data = {"message": "Need at least one domain specified in order create a certificate"} + + current_app.logger.warning(self) + + def __str__(self): + return repr(self.data['message']) + + +class InvalidCertificate(LemurException): + def __init__(self): + self.code = 400 + self.data = {"message": "Need at least one domain specified in order create a certificate"} + + current_app.logger.warning(self) + + def __str__(self): + return repr(self.data['message']) + + +class UnableToCreateCSR(LemurException): + def __init__(self): + self.code = 500 + self.data = {"message": "Unable to generate CSR"} + + current_app.logger.error(self) + + def __str__(self): + return repr(self.data['message']) + +class UnableToCreatePrivateKey(LemurException): + def __init__(self): + self.code = 500 + self.data = {"message": "Unable to generate Private Key"} + + current_app.logger.error(self) + + def __str__(self): + return repr(self.data['message']) + +class MissingFiles(LemurException): + def __init__(self, path): + self.code = 500 + self.path = path + self.data = {"path": self.path, "message": "Expecting missing files"} + + current_app.logger.error(self) + + def __str__(self): + return repr(self.data['message']) + + +class NoPersistanceFound(LemurException): + def __init__(self): + self.code = 500 + self.data = {"code": 500, "message": "No peristence method found, Lemur cannot persist sensitive information"} + + current_app.logger.error(self) + + def __str__(self): + return repr(self.data['message']) + diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py new file mode 100644 index 00000000..81947bfc --- /dev/null +++ b/lemur/certificates/models.py @@ -0,0 +1,307 @@ +""" +.. module: lemur.certificates.models + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +import os +import datetime +from cryptography import x509 +from cryptography.hazmat.backends import default_backend + +from flask import current_app + +from sqlalchemy.orm import relationship +from sqlalchemy import Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean + +from sqlalchemy_utils import EncryptedType + +from lemur.database import db + +from lemur.domains.models import Domain +from lemur.users import service as user_service + +from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE, NONSTANDARD_NAMING_TEMPLATE +from lemur.models import certificate_associations, certificate_account_associations + + +def cert_get_cn(cert): + """ + Attempts to get a sane common name from a given certificate. + + :param cert: + :return: Common name or None + """ + try: + return cert.subject.get_attributes_for_oid( + x509.OID_COMMON_NAME + )[0].value.strip() + except Exception as e: + current_app.logger.error("Unable to get CN! {0}".format(e)) + + +def cert_get_domains(cert): + """ + Attempts to get an domains listed in a certificate. + If 'subjectAltName' extension is not available we simply + return the common name. + + :param cert: + :return: List of domains + """ + domains = [] + try: + ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME) + entries = ext.get_values_for(x509.DNSName) + for entry in entries: + domains.append(entry.split(":")[1].strip(", ")) + except Exception as e: + current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e)) + domains.append(cert_get_cn(cert)) + return domains + + +def cert_get_serial(cert): + """ + Fetch the serial number from the certificate. + + :param cert: + :return: serial number + """ + return cert.serial + + +def cert_is_san(cert): + """ + Determines if a given certificate is a SAN certificate. + SAN certificates are simply certificates that cover multiple domains. + + :param cert: + :return: Bool + """ + domains = cert_get_domains(cert) + if len(domains) > 1: + return True + return False + + +def cert_is_wildcard(cert): + """ + Determines if certificate is a wildcard certificate. + + :param cert: + :return: Bool + """ + domains = cert_get_domains(cert) + if len(domains) == 1 and domains[0][0:1] == "*": + return True + return False + + +def cert_get_bitstrength(cert): + """ + Calculates a certificates public key bit length. + + :param cert: + :return: Integer + """ + return cert.public_key().key_size * 8 + + +def cert_get_issuer(cert): + """ + Gets a sane issuer from a given certificate. + + :param cert: + :return: Issuer + """ + try: + return cert.subject.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value + except Exception as e: + current_app.logger.error("Unable to get issuer! {0}".format(e)) + + +def cert_is_internal(cert): + """ + Uses an internal resource in order to determine if + a given certificate was issued by an 'internal' certificate + authority. + + :param cert: + :return: Bool + """ + if cert_get_issuer(cert) in current_app.config.get('INTERNAL_CA', []): + return True + return False + + +def cert_get_not_before(cert): + """ + Gets the naive datetime of the certificates 'not_before' field. + This field denotes the first date in time which the given certificate + is valid. + + :param cert: + :return: Datetime + """ + return cert.not_valid_before + + +def cert_get_not_after(cert): + """ + Gets the naive datetime of the certificates 'not_after' field. + This field denotes the last date in time which the given certificate + is valid. + + :param cert: + :return: Datetime + """ + return cert.not_valid_after + + +def get_name_from_arn(arn): + """ + Extract the certificate name from an arn. + + :param arn: IAM SSL arn + :return: name of the certificate as uploaded to AWS + """ + return arn.split("/", 1)[1] + + +def get_account_number(arn): + """ + Extract the account number from an arn. + + :param arn: IAM SSL arn + :return: account number associated with ARN + """ + return arn.split(":")[4] + + +class Certificate(db.Model): + __tablename__ = 'certificates' + id = Column(Integer, primary_key=True) + owner = Column(String(128)) + body = Column(Text()) + private_key = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY'))) + challenge = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY'))) + csr_config = Column(Text()) + status = Column(String(128)) + deleted = Column(Boolean, index=True) + name = Column(String(128)) + chain = Column(Text()) + bits = Column(Integer()) + issuer = Column(String(128)) + serial = Column(String(128)) + cn = Column(String(128)) + description = Column(String(1024)) + active = Column(Boolean, default=True) + san = Column(String(1024)) + not_before = Column(DateTime) + not_after = Column(DateTime) + date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) + user_id = Column(Integer, ForeignKey('users.id')) + authority_id = Column(Integer, ForeignKey('authorities.id')) + accounts = relationship("Account", secondary=certificate_account_associations, backref='certificate') + domains = relationship("Domain", secondary=certificate_associations, backref="certificate") + elb_listeners = relationship("Listener", lazy='dynamic', backref='certificate') + + def __init__(self, body, private_key=None, challenge=None, chain=None, csr_config=None): + self.body = body + # We encrypt the private_key on creation + self.private_key = private_key + self.chain = chain + self.csr_config = csr_config + self.challenge = challenge + cert = x509.load_pem_x509_certificate(str(self.body), default_backend()) + self.bits = cert_get_bitstrength(cert) + self.issuer = cert_get_issuer(cert) + self.serial = cert_get_serial(cert) + self.cn = cert_get_cn(cert) + self.san = cert_is_san(cert) + self.not_before = cert_get_not_before(cert) + self.not_after = cert_get_not_after(cert) + self.name = self.create_name + + for domain in cert_get_domains(cert): + self.domains.append(Domain(name=domain)) + + @property + def create_name(self): + """ + Create a name for our certificate. A naming standard + is based on a series of templates. The name includes + useful information such as Common Name, Validation dates, + and Issuer. + + :rtype : str + :return: + """ + # aws doesn't allow special chars + if self.cn: + subject = self.cn.replace('*', "WILDCARD") + + if self.san: + t = SAN_NAMING_TEMPLATE + else: + t = DEFAULT_NAMING_TEMPLATE + + temp = t.format( + subject=subject, + issuer=self.issuer, + not_before=self.not_before.strftime('%Y%m%d'), + not_after=self.not_after.strftime('%Y%m%d') + ) + + else: + t = NONSTANDARD_NAMING_TEMPLATE + + temp = t.format( + issuer=self.issuer, + not_before=self.not_before.strftime('%Y%m%d'), + not_after=self.not_after.strftime('%Y%m%d') + ) + + return temp + + @property + def is_expired(self): + if self.not_after < datetime.datetime.now(): + return True + + @property + def is_unused(self): + if self.elb_listeners.count() == 0: + return True + + @property + def is_revoked(self): + # we might not yet know the condition of the cert + if self.status: + if 'revoked' in self.status: + return True + + def get_arn(self, account_number): + """ + Generate a valid AWS IAM arn + + :rtype : str + :param account_number: + :return: + """ + return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + def serialize(self): + blob = self.as_dict() + # TODO this should be done with relationships + user = user_service.get(self.user_id) + if user: + blob['creator'] = user.username + + return blob + diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py new file mode 100644 index 00000000..354c9868 --- /dev/null +++ b/lemur/certificates/service.py @@ -0,0 +1,446 @@ +""" +.. module: service + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +import os +import arrow +import string +import random +import hashlib +import datetime +import subprocess + +from sqlalchemy import func, or_ +from flask import g, current_app + +from lemur import database +from lemur.common.services.aws import iam +from lemur.common.services.issuers.manager import get_plugin_by_name + +from lemur.certificates.models import Certificate +from lemur.certificates.exceptions import UnableToCreateCSR, \ + UnableToCreatePrivateKey, MissingFiles + +from lemur.accounts.models import Account +from lemur.accounts import service as account_service +from lemur.authorities.models import Authority + +from lemur.roles.models import Role + + +def get(cert_id): + """ + Retrieves certificate by it's ID. + + :param cert_id: + :return: + """ + return database.get(Certificate, cert_id) + + +def get_by_name(name): + """ + Retrieves certificate by it's Name. + + :param name: + :return: + """ + return database.get(Certificate, name, field='name') + + +def delete(cert_id): + """ + Delete's a certificate. + + :param cert_id: + """ + database.delete(get(cert_id)) + + +def disassociate_aws_account(certs, account): + """ + Removes the account association from a certificate. We treat AWS as a completely + external service. Certificates are added and removed from this service but a record + of that certificate is always kept and tracked by Lemur. This allows us to migrate + certificates to different accounts with ease. + + :param certs: + :param account: + """ + account_certs = Certificate.query.filter(Certificate.accounts.any(Account.id == 1)).\ + filter(~Certificate.body.in_(certs)).all() + + for a_cert in account_certs: + try: + a_cert.accounts.remove(account) + except Exception as e: + current_app.logger.debug("Skipping {0} account {1} is already disassociated".format(a_cert.name, account.label)) + continue + database.update(a_cert) + + +def get_all_certs(): + """ + Retrieves all certificates within Lemur. + + :return: + """ + return Certificate.query.all() + + +def find_duplicates(cert_body): + """ + Finds certificates that already exist within Lemur. We do this by looking for + certificate bodies that are the same. This is the most reliable way to determine + if a certificate is already being tracked by Lemur. + + :param cert_body: + :return: + """ + return Certificate.query.filter_by(body=cert_body).all() + + +def update(cert_id, owner, active): + """ + Updates a certificate. + + :param cert_id: + :param owner: + :param active: + :return: + """ + cert = get(cert_id) + cert.owner = owner + cert.active = active + return database.update(cert) + + +def mint(issuer_options): + """ + Minting is slightly different for each authority. + Support for multiple authorities is handled by individual plugins. + + :param issuer_options: + """ + authority = issuer_options['authority'] + + issuer = get_plugin_by_name(authority.plugin_name) + # NOTE if we wanted to support more issuers it might make sense to + # push CSR creation down to the plugin + path = create_csr(issuer.get_csr_config(issuer_options)) + challenge, csr, csr_config, private_key = load_ssl_pack(path) + + issuer_options['challenge'] = challenge + issuer_options['creator'] = g.user.email + cert_body, cert_chain = issuer.create_certificate(csr, issuer_options) + + cert = save_cert(cert_body, private_key, cert_chain, challenge, csr_config, issuer_options.get('accounts')) + cert.user = g.user + cert.authority = authority + database.update(cert) + + # securely delete pack after saving it to RDS and IAM (if applicable) + delete_ssl_pack(path) + + return cert, private_key, cert_chain, + + +def import_certificate(**kwargs): + """ + Uploads already minted certificates and pulls the required information into Lemur. + + This is to be used for certificates that are reated outside of Lemur but + should still be tracked. + + Internally this is used to bootstrap Lemur with external + certificates, and used when certificates are 'discovered' through various discovery + techniques. was still in aws. + + :param kwargs: + """ + cert = Certificate(kwargs['public_certificate']) + cert.owner = kwargs.get('owner', ) + cert.creator = kwargs.get('creator', 'Lemur') + + # NOTE existing certs may not follow our naming standard we will + # overwrite the generated name with the actual cert name + if kwargs.get('name'): + cert.name = kwargs.get('name') + + if kwargs.get('user'): + cert.user = kwargs.get('user') + + if kwargs.get('account'): + cert.accounts.append(kwargs.get('account')) + + cert = database.create(cert) + return cert + + +def save_cert(cert_body, private_key, cert_chain, challenge, csr_config, accounts): + """ + Determines if the certificate needs to be uploaded to AWS or other services. + + :param cert_body: + :param private_key: + :param cert_chain: + :param challenge: + :param csr_config: + :param account_ids: + """ + cert = Certificate(cert_body, private_key, challenge, cert_chain, csr_config) + # if we have an AWS accounts lets upload them + if accounts: + for account in accounts: + account = account_service.get(account['id']) + iam.upload_cert(account.account_number, cert, private_key, cert_chain) + cert.accounts.append(account) + return cert + + +def upload(**kwargs): + """ + Allows for pre-made certificates to be imported into Lemur. + """ + # save this cert the same way we save all of our certs, including uploading + # to aws if necessary + cert = save_cert( + kwargs.get('public_cert'), + kwargs.get('private_key'), + kwargs.get('intermediate_cert'), + None, + None, + kwargs.get('accounts') + ) + + cert.owner = kwargs['owner'] + cert = database.create(cert) + g.user.certificates.append(cert) + return cert + + +def create(**kwargs): + """ + Creates a new certificate. + """ + cert, private_key, cert_chain = mint(kwargs) + + cert.owner = kwargs['owner'] + database.create(cert) + g.user.certificates.append(cert) + database.update(g.user) + return cert + + +def render(args): + """ + Helper function that allows use to render our REST Api. + + :param args: + :return: + """ + query = database.session_query(Certificate) + + time_range = args.pop('time_range') + account_id = args.pop('account_id') + show = args.pop('show') + owner = args.pop('owner') + creator = args.pop('creator') # TODO we should enabling filtering by owner + + filt = args.pop('filter') + + if filt: + terms = filt.split(';') + if 'issuer' in terms: + # we can't rely on issuer being correct in the cert directly so we combine queries + sub_query = database.session_query(Authority.id)\ + .filter(Authority.name.ilike('%{0}%'.format(terms[1])))\ + .subquery() + + query = query.filter( + or_( + Certificate.issuer.ilike('%{0}%'.format(terms[1])), + Certificate.authority_id.in_(sub_query) + ) + ) + return database.sort_and_page(query, Certificate, args) + + if 'account' in terms: + query = query.filter(Certificate.accounts.any(Account.id == terms[1])) + elif 'active' in filt: # this is really weird but strcmp seems to not work here?? + query = query.filter(Certificate.active == terms[1]) + else: + query = database.filter(query, Certificate, terms) + + if show: + sub_query = database.session_query(Role.name).filter(Role.user_id == g.user.id).subquery() + query = query.filter( + or_( + Certificate.user_id == g.user.id, + Certificate.owner.in_(sub_query) + ) + ) + + if account_id: + query = query.filter(Certificate.accounts.any(Account.id == account_id)) + + if time_range: + to = arrow.now().replace(weeks=+time_range).format('YYYY-MM-DD') + now = arrow.now().format('YYYY-MM-DD') + query = query.filter(Certificate.not_after <= to).filter(Certificate.not_after >= now) + + return database.sort_and_page(query, Certificate, args) + + +def create_csr(csr_config): + """ + Given a list of domains create the appropriate csr + for those domains + + :param csr_config: + """ + + # we create a no colliding file name + path = create_path(hashlib.md5(csr_config).hexdigest()) + + challenge = create_challenge() + challenge_path = os.path.join(path, 'challenge.txt') + + with open(challenge_path, 'w') as c: + c.write(challenge) + + csr_path = os.path.join(path, 'csr_config.txt') + + with open(csr_path, 'w') as f: + f.write(csr_config) + + #TODO use cloudCA to seed a -rand file for each call + #TODO replace openssl shell calls with cryptograph + with open('/dev/null', 'w') as devnull: + code = subprocess.call(['openssl', 'genrsa', + '-out', os.path.join(path, 'private.key'), '2048'], + stdout=devnull, stderr=devnull) + + if code != 0: + raise UnableToCreatePrivateKey(code) + + with open('/dev/null', 'w') as devnull: + code = subprocess.call(['openssl', 'req', '-new', '-sha256', '-nodes', + '-config', csr_path, "-key", os.path.join(path, 'private.key'), + "-out", os.path.join(path, 'request.csr')], stdout=devnull, stderr=devnull) + + if code != 0: + raise UnableToCreateCSR(code) + + return path + + +def create_path(domain_hash): + """ + + :param domain_hash: + :return: + """ + path = os.path.join('/tmp', domain_hash) + + try: + os.mkdir(path) + except OSError as e: + now = datetime.datetime.now() + path = os.path.join('/tmp', "{}.{}".format(domain_hash, now.strftime('%s'))) + os.mkdir(path) + current_app.logger.warning(e) + + current_app.logger.debug("Writing ssl files to: {}".format(path)) + return path + + +def load_ssl_pack(path): + """ + Loads the information created by openssl to be used by other functions. + + :param path: + """ + if len(os.listdir(path)) != 4: + raise MissingFiles(path) + + with open(os.path.join(path, 'challenge.txt')) as c: + challenge = c.read() + + with open(os.path.join(path, 'request.csr')) as r: + csr = r.read() + + with open(os.path.join(path, 'csr_config.txt')) as config: + csr_config = config.read() + + with open(os.path.join(path, 'private.key')) as key: + private_key = key.read() + + return (challenge, csr, csr_config, private_key,) + + +def delete_ssl_pack(path): + """ + Removes the temporary files associated with CSR creation. + + :param path: + """ + subprocess.check_call(['srm', '-r', path]) + + +def create_challenge(): + """ + Create a random and strongish csr challenge. + """ + challenge = ''.join(random.choice(string.ascii_uppercase) for x in range(6)) + challenge += ''.join(random.choice("~!@#$%^&*()_+") for x in range(6)) + challenge += ''.join(random.choice(string.ascii_lowercase) for x in range(6)) + challenge += ''.join(random.choice(string.digits) for x in range(6)) + return challenge + + +def stats(**kwargs): + """ + Helper that defines some useful statistics about certifications. + + :param kwargs: + :return: + """ + query = database.session_query(Certificate) + + if kwargs.get('active') == 'true': + query = query.filter(Certificate.elb_listeners.any()) + + if kwargs.get('account_id'): + query = query.filter(Certificate.accounts.any(Account.id == kwargs.get('account_id'))) + + if kwargs.get('metric') == 'not_after': + start = arrow.utcnow() + end = start.replace(weeks=+32) + items = database.db.session.query(Certificate.issuer, func.count(Certificate.id))\ + .group_by(Certificate.issuer)\ + .filter(Certificate.not_after <= end.format('YYYY-MM-DD')) \ + .filter(Certificate.not_after >= start.format('YYYY-MM-DD')).all() + + else: + attr = getattr(Certificate, kwargs.get('metric')) + query = database.db.session.query(attr, func.count(attr)) + + # TODO this could be cleaned up + if kwargs.get('active') == 'true': + query = query.filter(Certificate.elb_listeners.any()) + + items = query.group_by(attr).all() + + keys = [] + values = [] + for key, count in items: + keys.append(key) + values.append(count) + + return {'labels': keys, 'values': values} + + diff --git a/lemur/certificates/sync.py b/lemur/certificates/sync.py new file mode 100644 index 00000000..3de59982 --- /dev/null +++ b/lemur/certificates/sync.py @@ -0,0 +1,169 @@ +""" +.. module: sync + :platform: Unix + :synopsis: This module contains various certificate syncing operations. + Because of the nature of the SSL environment there are multiple ways + a certificate could be created without Lemur's knowledge. Lemur attempts + to 'sync' with as many different datasources as possible to try and track + any certificate that may be in use. + + This include querying AWS for certificates attached to ELBs, querying our own + internal CA for certificates issued. As well as some rudimentary source code + scraping that attempts to find certificates checked into source code. + + These operations are typically run on a periodic basis from either the command + line or a cron job. + + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +import requests +from bs4 import BeautifulSoup + +from flask import current_app + +from lemur.users import service as user_service +from lemur.accounts import service as account_service +from lemur.certificates import service as cert_service +from lemur.certificates.models import Certificate, get_name_from_arn +from lemur.common.services.aws.iam import get_all_server_certs +from lemur.common.services.aws.iam import get_cert_from_arn + +from lemur.common.services.issuers.manager import get_plugin_by_name + + +def aws(): + """ + Attempts to retrieve all certificates located in known AWS accounts + :raise e: + """ + new = 0 + updated = 0 + + # all certificates 'discovered' by lemur are tracked by the lemur + # user + user = user_service.get_by_email('lemur@nobody') + + # we don't need to check regions as IAM is a global service + for account in account_service.get_all(): + certificate_bodies = [] + try: + cert_arns = get_all_server_certs(account.account_number) + except Exception as e: + current_app.logger.error("Failed to to get Certificates from '{}/{}' reason {}".format( + account.label, account.account_number, e.message) + ) + raise e + + current_app.logger.info("found {} certs from '{}/{}' ... ".format( + len(cert_arns), account.account_number, account.label) + ) + + for cert in cert_arns: + cert_body = get_cert_from_arn(cert.arn)[0] + certificate_bodies.append(cert_body) + existing = cert_service.find_duplicates(cert_body) + + if not existing: + cert_service.import_certificate( + **{'owner': 'secops@netflix.com', + 'creator': 'Lemur', + 'name': get_name_from_arn(cert.arn), + 'account': account, + 'user': user, + 'public_certificate': cert_body + } + ) + new += 1 + + elif len(existing) == 1: # we check to make sure we know about the current account for this certificate + for e_account in existing[0].accounts: + if e_account.account_number == account.account_number: + break + else: # we have a new account + existing[0].accounts.append(account) + updated += 1 + + else: + current_app.logger.error( + "Multiple certificates with the same body found, unable to correctly determine which entry to update" + ) + + # make sure we remove any certs that have been removed from AWS + cert_service.disassociate_aws_account(certificate_bodies, account) + current_app.logger.info("found {} new certificates in aws {}".format(new, account.label)) + + +def cloudca(): + """ + Attempts to retrieve all certificates that are stored in CloudCA + """ + user = user_service.get_by_email('lemur@nobody') + # sync all new certificates/authorities not created through lemur + issuer = get_plugin_by_name('cloudca') + authorities = issuer.get_authorities() + total = 0 + new = 1 + for authority in authorities: + certs = issuer.get_cert(ca_name=authority) + for cert in certs: + total += 1 + cert['user'] = user + existing = cert_service.find_duplicates(cert['public_certificate']) + if not existing: + new += 1 + try: + cert_service.import_certificate(**cert) + except NameError as e: + current_app.logger.error("Cannot import certificate {0}".format(cert)) + + current_app.logger.debug("Found {0} total certificates in cloudca".format(total)) + current_app.logger.debug("Found {0} new certificates in cloudca".format(new)) + + +def source(): + """ + Attempts to track certificates that are stored in Source Code + """ + new = 0 + keywords = ['"--- Begin Certificate ---"'] + endpoint = current_app.config.get('LEMUR_SOURCE_SEARCH') + maxresults = 25000 + + current_app.logger.info("Searching {0} for new certificates".format(endpoint)) + + for keyword in keywords: + current_app.logger.info("Looking for keyword: {0}".format(keyword)) + url = "{}/source/s?n={}&start=1&sort=relevancy&q={}&project=github%2Cperforce%2Cstash".format(endpoint, maxresults, keyword) + + current_app.logger.debug("Request url: {0}".format(url)) + r = requests.get(url, timeout=20) + + if r.status_code != 200: + current_app.logger.error("Unable to retrieve: {0} Status Code: {1}".format(url, r.status_code)) + continue + + soup = BeautifulSoup(r.text, "lxml") + results = soup.find_all(title='Download') + for result in results: + parts = result['href'].split('/') + path = "/".join(parts[:-1]) + filename = parts[-1:][0] + r = requests.get("{0}{1}/{2}".format(endpoint, path, filename)) + + if r.status_code != 200: + current_app.logger.error("Unable to retrieve: {0} Status Code: {1}".format(url, r.status_code)) + continue + + try: + # validate we have a real certificate + cert = Certificate(r.content) + # do a lookup to see if we know about this certificate + existing = cert_service.find_duplicates(r.content) + if not existing: + current_app.logger.debug(cert.name) + cert_service.import_certificate() + new += 1 + except Exception as e: + current_app.logger.debug("Could not parse the following 'certificate': {0} Reason: {1}".format(r.content, e)) diff --git a/lemur/certificates/verify.py b/lemur/certificates/verify.py new file mode 100644 index 00000000..4f3b0332 --- /dev/null +++ b/lemur/certificates/verify.py @@ -0,0 +1,135 @@ +""" +.. module: lemur.certificates.verify + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +import os +import re +import hashlib +import requests +import subprocess +from OpenSSL import crypto + +from flask import current_app + + +def ocsp_verify(cert_path, issuer_chain_path): + """ + Attempts to verify a certificate via OCSP. OCSP is a more modern version + of CRL in that it will query the OCSP URI in order to determine if the + certificate as been revoked + + :param cert_path: + :param issuer_chain_path: + :return bool: True if certificate is valid, False otherwise + """ + command = ['openssl', 'x509', '-noout', '-ocsp_uri', '-in', cert_path] + p1 = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + url, err = p1.communicate() + + p2 = subprocess.Popen(['openssl', 'ocsp', '-issuer', issuer_chain_path, + '-cert', cert_path, "-url", url.strip()], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + message, err = p2.communicate() + if 'error' in message or 'Error' in message: + raise Exception("Got error when parsing OCSP url") + + elif 'revoked' in message: + return + + elif 'good' not in message: + raise Exception("Did not receive a valid response") + + return True + + +def crl_verify(cert_path): + """ + Attempts to verify a certificate using CRL. + + :param cert_path: + :return: True if certificate is valid, False otherwise + :raise Exception: If certificate does not have CRL + """ + s = "(http(s)?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}/\S*?$)" + regex = re.compile(s, re.MULTILINE) + + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_path, 'rt').read()) + for x in range(x509.get_extension_count()): + ext = x509.get_extension(x) + if ext.get_short_name() == 'crlDistributionPoints': + r = regex.search(ext.get_data()) + points = r.groups() + break + else: + raise Exception("Certificate does not have a CRL distribution point") + + for point in points: + if point: + response = requests.get(point) + crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content) + revoked = crl.get_revoked() + for r in revoked: + if x509.get_serial_number() == r.get_serial(): + return + return True + + +def verify(cert_path, issuer_chain_path): + """ + Verify a certificate using OCSP and CRL + + :param cert_path: + :param issuer_chain_path: + :return: True if valid, False otherwise + """ + # OCSP is our main source of truth, in a lot of cases CRLs + # have been deprecated and are no longer updated + try: + return ocsp_verify(cert_path, issuer_chain_path) + except Exception as e: + current_app.logger.debug("Could not use OCSP: {0}".format(e)) + try: + return crl_verify(cert_path) + except Exception as e: + current_app.logger.debug("Could not use CRL: {0}".format(e)) + raise Exception("Failed to verify") + raise Exception("Failed to verify") + + +def make_tmp_file(string): + """ + Creates a temporary file for a given string + + :param string: + :return: Full file path to created file + """ + m = hashlib.md5() + m.update(string) + hexdigest = m.hexdigest() + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), hexdigest) + with open(path, 'w') as f: + f.write(string) + return path + + +def verify_string(cert_string, issuer_string): + """ + Verify a certificate given only it's string value + + :param cert_string: + :param issuer_string: + :return: True if valid, False otherwise + """ + cert_path = make_tmp_file(cert_string) + issuer_path = make_tmp_file(issuer_string) + status = verify(cert_path, issuer_path) + remove_tmp_file(cert_path) + remove_tmp_file(issuer_path) + return status + + +def remove_tmp_file(file_path): + os.remove(file_path) \ No newline at end of file diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py new file mode 100644 index 00000000..3de200d5 --- /dev/null +++ b/lemur/certificates/views.py @@ -0,0 +1,575 @@ +""" +.. module: lemur.certificates.views + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from flask import Blueprint, make_response, jsonify +from flask.ext.restful import reqparse, Api, fields + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +from lemur.certificates import service +from lemur.authorities.models import Authority + +from lemur.auth.service import AuthenticatedResource +from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission + +from lemur.roles import service as role_service + +from lemur.common.utils import marshal_items, paginated_parser + + +mod = Blueprint('certificates', __name__) +api = Api(mod) + + +FIELDS = { + 'name': fields.String, + 'id': fields.Integer, + 'bits': fields.Integer, + 'deleted': fields.String, + 'issuer': fields.String, + 'serial': fields.String, + 'owner': fields.String, + 'chain': fields.String, + 'san': fields.String, + 'active': fields.Boolean, + 'description': fields.String, + 'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'), + 'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'), + 'cn': fields.String, + 'status': fields.String, + 'body': fields.String +} + + +def valid_authority(authority_options): + """ + Defends against invalid authorities + + :param authority_name: + :return: :raise ValueError: + """ + name = authority_options['name'] + authority = Authority.query.filter(Authority.name == name).one() + + if not authority: + raise ValueError("Unable to find authority specified") + + if not authority.active: + raise ValueError("Selected authority [{0}] is not currently active".format(name)) + + return authority + + +def pem_str(value, name): + """ + Used to validate that the given string is a PEM formatted string + + :param value: + :param name: + :return: :raise ValueError: + """ + try: + x509.load_pem_x509_certificate(str(value), default_backend()) + except Exception as e: + raise ValueError("The parameter '{0}' needs to be a valid PEM string".format(name)) + return value + + +def private_key_str(value, name): + """ + User to validate that a given string is a RSA private key + + :param value: + :param name: + :return: :raise ValueError: + """ + try: + serialization.load_pem_private_key(str(value), backend=default_backend()) + except Exception as e: + raise ValueError("The parameter '{0}' needs to be a valid RSA private key".format(name)) + return value + + + +class CertificatesList(AuthenticatedResource): + """ Defines the 'certificates' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(CertificatesList, self).__init__() + + @marshal_items(FIELDS) + def get(self): + """ + .. http:get:: /certificates + + The current list of certificates + + **Example request**: + + .. sourcecode:: http + + GET /certificates HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "items": [ + { + "id": 1, + "name": "cert1", + "description": "this is cert1", + "bits": 2048, + "deleted": false, + "issuer": "ExampeInc.", + "serial": "123450", + "chain": "-----Begin ...", + "body": "-----Begin ...", + "san": true, + "owner": 'bob@example.com", + "active": true, + "notBefore": "2015-06-05T17:09:39", + "notAfter": "2015-06-10T17:09:39", + "cn": "example.com", + "status": "unknown" + } + ] + "total": 1 + } + + :query sortBy: field to sort on + :query sortDir: acs or desc + :query page: int. default is 1 + :query filter: key value pair. format is k=v; + :query limit: limit number. default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + parser = paginated_parser.copy() + parser.add_argument('timeRange', type=int, dest='time_range', location='args') + parser.add_argument('owner', type=bool, location='args') + parser.add_argument('id', type=str, location='args') + parser.add_argument('active', type=bool, location='args') + parser.add_argument('accountId', type=int, dest="account_id", location='args') + parser.add_argument('creator', type=str, location='args') + parser.add_argument('show', type=str, location='args') + + args = parser.parse_args() + return service.render(args) + + @marshal_items(FIELDS) + def post(self): + """ + .. http:post:: /certificates + + Creates a new certificate + + **Example request**: + + .. sourcecode:: http + + POST /certificates HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "country": "US", + "state": "CA", + "location": "A Place", + "organization": "ExampleInc.", + "organizationalUnit": "Operations", + "owner": "bob@example.com", + "description": "test", + "selectedAuthority": "timetest2", + "authority": { + "body": "-----BEGIN...", + "name": "timetest2", + "chain": "", + "notBefore": "2015-06-05T15:20:59", + "active": true, + "id": 50, + "notAfter": "2015-06-17T15:21:08", + "description": "dsfdsf" + }, + "extensions": { + "basicConstraints": {}, + "keyUsage": { + "isCritical": true, + "useKeyEncipherment": true, + "useDigitalSignature": true + }, + "extendedKeyUsage": { + "isCritical": true, + "useServerAuthentication": true + }, + "subjectKeyIdentifier": { + "includeSKI": true + }, + "subAltNames": { + "names": [] + } + }, + "commonName": "test", + "validityStart": "2015-06-05T07:00:00.000Z", + "validityEnd": "2015-06-16T07:00:00.000Z" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "cert1", + "description": "this is cert1", + "bits": 2048, + "deleted": false, + "issuer": "ExampeInc.", + "serial": "123450", + "chain": "-----Begin ...", + "body": "-----Begin ...", + "san": true, + "owner": "jimbob@example.com", + "active": false, + "notBefore": "2015-06-05T17:09:39", + "notAfter": "2015-06-10T17:09:39", + "cn": "example.com", + "status": "unknown" + } + + :arg extensions: extensions to be used in the certificate + :arg description: description for new certificate + :arg owner: owner email + :arg validityStart: when the certificate should start being valid + :arg validityEnd: when the certificate should expire + :arg authority: authority that should issue the certificate + :arg country: country for the CSR + :arg state: state for the CSR + :arg location: location for the CSR + :arg organization: organization for CSR + :arg commonName: certiifcate common name + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + self.reqparse.add_argument('extensions', type=dict, location='json') + self.reqparse.add_argument('accounts', type=list, location='json') + self.reqparse.add_argument('elbs', type=list, location='json') + self.reqparse.add_argument('owner', type=str, location='json') + self.reqparse.add_argument('validityStart', type=str, location='json') # parse date + self.reqparse.add_argument('validityEnd', type=str, location='json') # parse date + self.reqparse.add_argument('authority', type=valid_authority, location='json') + self.reqparse.add_argument('description', type=str, location='json') + self.reqparse.add_argument('country', type=str, location='json') + self.reqparse.add_argument('state', type=str, location='json') + self.reqparse.add_argument('location', type=str, location='json') + self.reqparse.add_argument('organization', type=str, location='json') + self.reqparse.add_argument('organizationalUnit', type=str, location='json') + self.reqparse.add_argument('owner', type=str, location='json') + self.reqparse.add_argument('commonName', type=str, location='json') + + args = self.reqparse.parse_args() + + authority = args['authority'] + role = role_service.get_by_name(authority.owner) + + # all the authority role members should be allowed + roles = [x.name for x in authority.roles] + + # allow "owner" roles by team DL + roles.append(role) + permission = AuthorityPermission(authority.id, roles) + + if permission.can(): + return service.create(**args) + + return dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403 + + +class CertificatesUpload(AuthenticatedResource): + """ Defines the 'certificates' upload endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(CertificatesUpload, self).__init__() + + @marshal_items(FIELDS) + def post(self): + """ + .. http:post:: /certificates/upload + + Upload a certificate + + **Example request**: + + .. sourcecode:: http + + POST /certificates/upload HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "owner": "joe@exmaple.com", + "publicCert": "---Begin Public...", + "intermediateCert": "---Begin Public...", + "privateKey": "---Begin Private..." + "accounts": [] + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "cert1", + "description": "this is cert1", + "bits": 2048, + "deleted": false, + "issuer": "ExampeInc.", + "serial": "123450", + "chain": "-----Begin ...", + "body": "-----Begin ...", + "san": true, + "owner": "joe@example.com", + "active": true, + "notBefore": "2015-06-05T17:09:39", + "notAfter": "2015-06-10T17:09:39", + "cn": "example.com", + "status": "unknown" + } + + :arg owner: owner email for certificate + :arg publicCert: valid PEM public key for certificate + :arg intermediateCert valid PEM intermediate key for certificate + :arg privateKey: valid PEM private key for certificate + :arg accounts: list of aws accounts to upload the certificate to + :reqheader Authorization: OAuth token to authenticate + :statuscode 403: unauthenticated + :statuscode 200: no error + """ + self.reqparse.add_argument('owner', type=str, required=True, location='json') + self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json') + self.reqparse.add_argument('accounts', type=list, dest='accounts', location='json') + self.reqparse.add_argument('intermediateCert', type=pem_str, dest='intermediate_cert', location='json') + self.reqparse.add_argument('privateKey', type=private_key_str, dest='private_key', location='json') + + args = self.reqparse.parse_args() + if args.get('accounts'): + if args.get('private_key'): + return service.upload(**args) + else: + raise Exception("Private key must be provided in order to upload certificate to AWS") + return service.upload(**args) + + +class CertificatesStats(AuthenticatedResource): + """ Defines the 'certificates' stats endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(CertificatesStats, self).__init__() + + def get(self): + self.reqparse.add_argument('metric', type=str, location='args') + self.reqparse.add_argument('range', default=32, type=int, location='args') + self.reqparse.add_argument('accountId', dest='account_id', location='args') + self.reqparse.add_argument('active', type=str, default='true', location='args') + + args = self.reqparse.parse_args() + + items = service.stats(**args) + return dict(items=items, total=len(items)) + + +class CertificatePrivateKey(AuthenticatedResource): + def __init__(self): + super(CertificatePrivateKey, self).__init__() + + def get(self, certificate_id): + """ + .. http:get:: /certificates/1/key + + Retrieves the private key for a given certificate + + **Example request**: + + .. sourcecode:: http + + GET /certificates/1/key HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "key": "----Begin ...", + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + cert = service.get(certificate_id) + role = role_service.get_by_name(cert.owner) + + permission = ViewKeyPermission(certificate_id, hasattr(role, 'id')) + + if permission.can(): + response = make_response(jsonify(key=cert.private_key), 200) + response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store' + response.headers['pragma'] = 'no-cache' + return response + + return dict(message='You are not authorized to view this key'), 403 + + +class Certificates(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Certificates, self).__init__() + + @marshal_items(FIELDS) + def get(self, certificate_id): + """ + .. http:get:: /certificates/1 + + One certificate + + **Example request**: + + .. sourcecode:: http + + GET /certificates/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "cert1", + "description": "this is cert1", + "bits": 2048, + "deleted": false, + "issuer": "ExampeInc.", + "serial": "123450", + "chain": "-----Begin ...", + "body": "-----Begin ...", + "san": true, + "owner": "bob@example.com", + "active": true, + "notBefore": "2015-06-05T17:09:39", + "notAfter": "2015-06-10T17:09:39", + "cn": "example.com", + "status": "unknown" + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + return service.get(certificate_id) + + @marshal_items(FIELDS) + def put(self, certificate_id): + """ + .. http:put:: /certificates/1 + + Update a certificate + + **Example request**: + + .. sourcecode:: http + + PUT /certificates/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "owner": "jimbob@example.com", + "active": false + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "cert1", + "description": "this is cert1", + "bits": 2048, + "deleted": false, + "issuer": "ExampeInc.", + "serial": "123450", + "chain": "-----Begin ...", + "body": "-----Begin ...", + "san": true, + "owner": "jimbob@example.com", + "active": false, + "notBefore": "2015-06-05T17:09:39", + "notAfter": "2015-06-10T17:09:39", + "cn": "example.com", + "status": "unknown" + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + self.reqparse.add_argument('active', type=bool, location='json') + self.reqparse.add_argument('owner', type=str, location='json') + args = self.reqparse.parse_args() + + cert = service.get(certificate_id) + role = role_service.get_by_name(cert.owner) + permission = UpdateCertificatePermission(certificate_id, hasattr(role, 'id')) + + if permission.can(): + return service.update(certificate_id, args['owner'], args['active']) + + return dict(message='You are not authorized to update this certificate'), 403 + + +api.add_resource(CertificatesList, '/certificates', endpoint='certificates') +api.add_resource(Certificates, '/certificates/', endpoint='certificate') +api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats') +api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload') +api.add_resource(CertificatePrivateKey, '/certificates//key', endpoint='privateKeyCertificates') diff --git a/lemur/common/__init__.py b/lemur/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/common/crypto.py b/lemur/common/crypto.py new file mode 100644 index 00000000..80b03e42 --- /dev/null +++ b/lemur/common/crypto.py @@ -0,0 +1,185 @@ +""" +.. module: lemur.common.crypto + :platform: Unix + :synopsis: This module contains all cryptographic function's in Lemur + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson + +""" +import os +import ssl +import StringIO +import functools +from Crypto import Random +from Crypto.Cipher import AES +from hashlib import sha512 + +from flask import current_app + +from lemur.factory import create_app + + +old_init = ssl.SSLSocket.__init__ + +@functools.wraps(old_init) +def ssl_bug(self, *args, **kwargs): + kwargs['ssl_version'] = ssl.PROTOCOL_TLSv1 + old_init(self, *args, **kwargs) + +ssl.SSLSocket.__init__ = ssl_bug + + +def derive_key_and_iv(password, salt, key_length, iv_length): + """ + Derives the key and iv from the password and salt. + + :param password: + :param salt: + :param key_length: + :param iv_length: + :return: key, iv + """ + d = d_i = '' + + while len(d) < key_length + iv_length: + d_i = sha512(d_i + password + salt).digest() + d += d_i + + return d[:key_length], d[key_length:key_length+iv_length] + + +def encrypt(in_file, out_file, password, key_length=32): + """ + Encrypts a file. + + :param in_file: + :param out_file: + :param password: + :param key_length: + """ + bs = AES.block_size + salt = Random.new().read(bs - len('Salted__')) + key, iv = derive_key_and_iv(password, salt, key_length, bs) + cipher = AES.new(key, AES.MODE_CBC, iv) + out_file.write('Salted__' + salt) + finished = False + while not finished: + chunk = in_file.read(1024 * bs) + if len(chunk) == 0 or len(chunk) % bs != 0: + padding_length = bs - (len(chunk) % bs) + chunk += padding_length * chr(padding_length) + finished = True + out_file.write(cipher.encrypt(chunk)) + + +def decrypt(in_file, out_file, password, key_length=32): + """ + Decrypts a file. + + :param in_file: + :param out_file: + :param password: + :param key_length: + :raise ValueError: + """ + bs = AES.block_size + salt = in_file.read(bs)[len('Salted__'):] + key, iv = derive_key_and_iv(password, salt, key_length, bs) + cipher = AES.new(key, AES.MODE_CBC, iv) + next_chunk = '' + finished = False + while not finished: + chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs)) + if len(next_chunk) == 0: + padding_length = ord(chunk[-1]) + if padding_length < 1 or padding_length > bs: + raise ValueError("bad decrypt pad (%d)" % padding_length) + # all the pad-bytes must be the same + if chunk[-padding_length:] != (padding_length * chr(padding_length)): + # this is similar to the bad decrypt:evp_enc.c from openssl program + raise ValueError("bad decrypt") + chunk = chunk[:-padding_length] + finished = True + out_file.write(chunk) + + +def encrypt_string(string, password): + """ + Encrypts a string. + + :param string: + :param password: + :return: + """ + in_file = StringIO.StringIO(string) + enc_file = StringIO.StringIO() + encrypt(in_file, enc_file, password) + enc_file.seek(0) + return enc_file.read() + + +def decrypt_string(string, password): + """ + Decrypts a string. + + :param string: + :param password: + :return: + """ + in_file = StringIO.StringIO(string) + out_file = StringIO.StringIO() + decrypt(in_file, out_file, password) + out_file.seek(0) + return out_file.read() + + +def lock(password): + """ + Encrypts Lemur's KEY_PATH. This directory can be used to store secrets needed for normal + Lemur operation. This is especially useful for storing secrets needed for communication + with third parties (e.g. external certificate authorities). + + Lemur does not assume anything about the contents of the directory and will attempt to + encrypt all files contained within. Currently this has only been tested against plain + text files. + + :param password: + """ + dest_dir = os.path.join(current_app.config.get("KEY_PATH"), "encrypted") + + if not os.path.exists(dest_dir): + current_app.logger.debug("Creating encryption directory: {0}".format(dest_dir)) + os.makedirs(dest_dir) + + for root, dirs, files in os.walk(os.path.join(current_app.config.get("KEY_PATH"), 'decrypted')): + for f in files: + source = os.path.join(root, f) + dest = os.path.join(dest_dir, f + ".enc") + with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: + encrypt(in_file, out_file, password) + + +def unlock(password): + """ + Decrypts Lemur's KEY_PATH, allowing lemur to use the secrets within. + + This reverses the :func:`lock` function. + + :param password: + """ + dest_dir = os.path.join(current_app.config.get("KEY_PATH"), "decrypted") + source_dir = os.path.join(current_app.config.get("KEY_PATH"), "encrypted") + + if not os.path.exists(dest_dir): + current_app.logger.debug("Creating decryption directory: {0}".format(dest_dir)) + os.makedirs(dest_dir) + + for root, dirs, files in os.walk(source_dir): + for f in files: + source = os.path.join(source_dir, f) + dest = os.path.join(dest_dir, ".".join(f.split(".")[:-1])) + with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: + current_app.logger.debug("Writing file: {0} Source: {1}".format(dest, source)) + decrypt(in_file, out_file, password) + diff --git a/lemur/common/health.py b/lemur/common/health.py new file mode 100644 index 00000000..11306de2 --- /dev/null +++ b/lemur/common/health.py @@ -0,0 +1,15 @@ +""" +.. module: lemur.common.health + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from flask import Blueprint + +mod = Blueprint('healthCheck', __name__) + +@mod.route('/healthcheck') +def health(): + return 'ok' \ No newline at end of file diff --git a/lemur/common/services/__init__.py b/lemur/common/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/common/services/aws/__init__.py b/lemur/common/services/aws/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/common/services/aws/elb.py b/lemur/common/services/aws/elb.py new file mode 100644 index 00000000..b59b8eaf --- /dev/null +++ b/lemur/common/services/aws/elb.py @@ -0,0 +1,140 @@ +""" +.. module:: elb + :synopsis: Module contains some often used and helpful classes that + are used to deal with ELBs + +.. moduleauthor:: Kevin Glisson (kglisson@netflix.com) +""" +import boto.ec2 + +from flask import current_app + +from lemur.exceptions import InvalidListener +from lemur.common.services.aws.sts import assume_service + + +def is_valid(listener_tuple): + """ + There are a few rules that aws has when creating listeners, + this function ensures those rules are met before we try and create + or update a listener. + + While these could be caught with boto exception handling, I would + rather be nice and catch these early before we sent them out to aws. + It also gives us an opportunity to create nice user warnings. + + This validity check should also be checked in the frontend + but must also be enforced by server. + + :param listener_tuple: + """ + + current_app.logger.debug(listener_tuple) + lb_port, i_port, lb_protocol, arn = listener_tuple + current_app.logger.debug(lb_protocol) + if lb_protocol.lower() in ['ssl', 'https']: + if not arn: + raise InvalidListener + + return listener_tuple + +def get_all_regions(): + """ + Retrieves all current EC2 regions. + + :return: + """ + regions = [] + for r in boto.ec2.regions(): + regions.append(r.name) + return regions + +def get_all_elbs(account_number, region): + """ + Fetches all elb objects for a given account and region. + + :param account_number: + :param region: + """ + marker = None + elbs = [] + return assume_service(account_number, 'elb', region).get_all_load_balancers() +# TODO create pull request for boto to include elb marker support +# while True: +# app.logger.debug(response.__dict__) +# raise Exception +# result = response['list_server_certificates_response']['list_server_certificates_result'] +# +# for elb in result['server_certificate_metadata_list']: +# elbs.append(elb) +# +# if result['is_truncated'] == 'true': +# marker = result['marker'] +# else: +# return elbs + + + +def attach_certificate(account_number, region, name, port, certificate_id): + """ + Attaches a certificate to a listener, throws exception + if certificate specified does not exist in a particular account. + + :param account_number: + :param region: + :param name: + :param port: + :param certificate_id: + """ + return assume_service(account_number, 'elb', region).set_lb_listener_SSL_certificate(name, port, certificate_id) + + +def create_new_listeners(account_number, region, name, listeners=None): + """ + Creates a new listener and attaches it to the ELB. + + :param account_number: + :param region: + :param name: + :param listeners: + :return: + """ + listeners = [is_valid(x) for x in listeners] + return assume_service(account_number, 'elb', region).create_load_balancer_listeners(name, listeners=listeners) + + +def update_listeners(account_number, region, name, listeners, ports): + """ + We assume that a listener with a specified port already exists. We can then + delete the old listener on the port and create a new one in it's place. + + If however we are replacing a listener e.g. changing a port from 80 to 443 we need + to make sure we kept track of which ports we needed to delete so that we don't create + two listeners (one 80 and one 443) + + :param account_number: + :param region: + :param name: + :param listeners: + :param ports: + """ + # you cannot update a listeners port/protocol instead we remove the only one and + # create a new one in it's place + listeners = [is_valid(x) for x in listeners] + + assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports) + return create_new_listeners(account_number, region, name, listeners=listeners) + + +def delete_listeners(account_number, region, name, ports): + """ + Deletes a listener from an ELB. + + :param account_number: + :param region: + :param name: + :param ports: + :return: + """ + return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports) + diff --git a/lemur/common/services/aws/iam.py b/lemur/common/services/aws/iam.py new file mode 100644 index 00000000..4cd2b452 --- /dev/null +++ b/lemur/common/services/aws/iam.py @@ -0,0 +1,104 @@ +""" +.. module: lemur.common.services.aws.iam + :platform: Unix + :synopsis: Contains helper functions for interactive with AWS IAM Apis. + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from flask import current_app +from lemur.common.services.aws.sts import assume_service + + +def ssl_split(param_string): + """ + + :param param_string: + :return: + """ + output = {} + parts = str(param_string).split("/") + for part in parts: + if "=" in part: + key, value = part.split("=", 1) + output[key] = value + return output + + +def upload_cert(account_number, cert, private_key, cert_chain=None): + """ + Upload a certificate to AWS + + :param account_number: + :param cert: + :param private_key: + :param cert_chain: + :return: + """ + return assume_service(account_number, 'iam').upload_server_cert(cert.name, str(cert.body), str(private_key), cert_chain=str(cert_chain)) + + +def delete_cert(account_number, cert): + """ + Delete a certificate from AWS + + :param account_number: + :param cert: + :return: + """ + return assume_service(account_number, 'iam').delete_server_cert(cert.name) + + +def get_all_server_certs(account_number): + """ + Use STS to fetch all of the SSL certificates from a given account + + :param account_number: + """ + marker = None + certs = [] + while True: + response = assume_service(account_number, 'iam').get_all_server_certs(marker=marker) + result = response['list_server_certificates_response']['list_server_certificates_result'] + + for cert in result['server_certificate_metadata_list']: + certs.append(cert) + + if result['is_truncated'] == 'true': + marker = result['marker'] + else: + return certs + + +def get_cert_from_arn(arn): + """ + Retrieves an SSL certificate from a given ARN. + + :param arn: + :return: + """ + name = arn.split("/", 1)[1] + account_number = arn.split(":")[4] + name = name.split("/")[-1] + + response = assume_service(account_number, 'iam').get_server_certificate(name.strip()) + return digest_aws_cert_response(response) + + +def digest_aws_cert_response(response): + """ + Processes an AWS certifcate response and retrieves the certificate body and chain. + + :param response: + :return: + """ + chain = None + cert = response['get_server_certificate_response']['get_server_certificate_result']['server_certificate'] + body = cert['certificate_body'] + + if 'certificate_chain' in cert: + chain = cert['certificate_chain'] + + return str(body), str(chain), + + diff --git a/lemur/common/services/aws/ses.py b/lemur/common/services/aws/ses.py new file mode 100644 index 00000000..071437ca --- /dev/null +++ b/lemur/common/services/aws/ses.py @@ -0,0 +1,29 @@ +""" +.. module: lemur.common.services.aws + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from flask import current_app +import boto.ses + +from lemur.templates.config import env + + +def send(subject, data, email_type, recipients): + """ + Configures all Lemur email messaging + + :param subject: + :param data: + :param email_type: + :param recipients: + """ + conn = boto.connect_ses() + #jinja template depending on type + template = env.get_template('{}.html'.format(email_type)) + body = template.render(**data) + conn.send_email(current_app.config.get("LEMUR_EMAIL"), subject, body, recipients, format='html') + diff --git a/lemur/common/services/aws/sts.py b/lemur/common/services/aws/sts.py new file mode 100644 index 00000000..f53a545b --- /dev/null +++ b/lemur/common/services/aws/sts.py @@ -0,0 +1,41 @@ +""" +.. module: lemur.common.services.aws.sts + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +import boto +import boto.ec2.elb + +from flask import current_app + + +def assume_service(account_number, service, region=None): + conn = boto.connect_sts() + + role = conn.assume_role('arn:aws:iam::{0}:role/{1}'.format( + account_number, current_app.config.get('LEMUR_INSTANCE_PROFILE', 'Lemur')), 'blah') + + if service in 'iam': + return boto.connect_iam( + aws_access_key_id=role.credentials.access_key, + aws_secret_access_key=role.credentials.secret_key, + security_token=role.credentials.session_token) + + elif service in 'elb': + return boto.ec2.elb.connect_to_region( + region, + aws_access_key_id=role.credentials.access_key, + aws_secret_access_key=role.credentials.secret_key, + security_token=role.credentials.session_token) + + elif service in 'vpc': + return boto.connect_vpc( + aws_access_key_id=role.credentials.access_key, + aws_secret_access_key=role.credentials.secret_key, + security_token=role.credentials.session_token) + + + + diff --git a/lemur/common/services/issuers/__init__.py b/lemur/common/services/issuers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/common/services/issuers/issuer.py b/lemur/common/services/issuers/issuer.py new file mode 100644 index 00000000..4950a9b9 --- /dev/null +++ b/lemur/common/services/issuers/issuer.py @@ -0,0 +1,32 @@ +""" +.. module: authority + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from flask import current_app + + +class Issuer(object): + """ + This is the base class from which all of the supported + issuers will inherit from. + """ + + def __init__(self): + self.dry_run = current_app.config.get('DRY_RUN') + + def create_certificate(self): + raise NotImplementedError + + def create_authority(self): + raise NotImplementedError + + def get_authorities(self): + raise NotImplementedError + + def get_csr_config(self): + raise NotImplementedError + diff --git a/lemur/common/services/issuers/manager.py b/lemur/common/services/issuers/manager.py new file mode 100644 index 00000000..4a5982c3 --- /dev/null +++ b/lemur/common/services/issuers/manager.py @@ -0,0 +1,37 @@ +""" +.. module: lemur.common.services.issuers.manager + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson (kglisson@netflix.com) +""" +import pkgutil +from importlib import import_module + +from flask import current_app + +from lemur.common.services.issuers import plugins + +# TODO make the plugin dir configurable +def get_plugin_by_name(plugin_name): + """ + Fetches a given plugin by it's name. We use a known location for issuer plugins and attempt + to load it such that it can be used for issuing certificates. + + :param plugin_name: + :return: a plugin `class` :raise Exception: Generic error whenever the plugin specified can not be found. + """ + for importer, modname, ispkg in pkgutil.iter_modules(plugins.__path__): + try: + issuer = import_module('lemur.common.services.issuers.plugins.{0}.{0}'.format(modname)) + if issuer.__name__ == plugin_name: + # we shouldn't return bad issuers + issuer_obj = issuer.init() + return issuer_obj + except Exception as e: + current_app.logger.warn("Issuer {0} was unable to be imported: {1}".format(modname, e)) + + else: + raise Exception("Could not find the specified plugin: {0}".format(plugin_name)) + + diff --git a/lemur/common/services/issuers/plugins/__init__.py b/lemur/common/services/issuers/plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/common/services/issuers/plugins/cloudca/__init__.py b/lemur/common/services/issuers/plugins/cloudca/__init__.py new file mode 100644 index 00000000..d29488d2 --- /dev/null +++ b/lemur/common/services/issuers/plugins/cloudca/__init__.py @@ -0,0 +1,5 @@ +try: + VERSION = __import__('pkg_resources') \ + .get_distribution(__name__).version +except Exception, e: + VERSION = 'unknown' \ No newline at end of file diff --git a/lemur/common/services/issuers/plugins/cloudca/cloudca.py b/lemur/common/services/issuers/plugins/cloudca/cloudca.py new file mode 100644 index 00000000..d6612b4e --- /dev/null +++ b/lemur/common/services/issuers/plugins/cloudca/cloudca.py @@ -0,0 +1,346 @@ +""" +.. module: lemur.common.services.issuers.plugins.cloudca + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +import ssl +import base64 +from json import dumps + +import arrow +import requests +from requests.adapters import HTTPAdapter + +from flask import current_app + +from lemur.exceptions import LemurException +from lemur.common.services.issuers.issuer import Issuer + +from lemur.common.services.issuers.plugins import cloudca + + +from lemur.authorities import service as authority_service + +API_ENDPOINT = '/v1/ca/netflix' + + +class CloudCAException(LemurException): + def __init__(self, message): + self.message = message + current_app.logger.error(self) + + def __str__(self): + return repr("CloudCA request failed: {0}".format(self.message)) + + +class CloudCAHostNameCheckingAdapter(HTTPAdapter): + def cert_verify(self, conn, url, verify, cert): + super(CloudCAHostNameCheckingAdapter, self).cert_verify(conn, url, verify, cert) + conn.assert_hostname = False + + +def remove_none(options): + """ + Simple function that traverse the options and removed any None items + CloudCA really dislikes null values. + + :param options: + :return: + """ + new_dict = {} + for k, v in options.items(): + if v: + new_dict[k] = v + + # this is super hacky and gross, cloudca doesn't like null values + if new_dict.get('extensions'): + if len(new_dict['extensions']['subAltNames']['names']) == 0: + del new_dict['extensions']['subAltNames'] + + return new_dict + + +def get_default_issuance(options): + """ + Gets the default time range for certificates + + :param options: + :return: + """ + if not options.get('validityStart') and not options.get('validityEnd'): + start = arrow.utcnow() + options['validityStart'] = start.floor('second').isoformat() + options['validityEnd'] = start.replace(years=current_app.config.get('CLOUDCA_DEFAULT_VALIDITY')).ceil('second').isoformat() + return options + + +def convert_to_pem(der): + """ + Converts DER to PEM Lemur uses PEM internally + + :param der: + :return: + """ + decoded = base64.b64decode(der) + return ssl.DER_cert_to_PEM_cert(decoded) + + +def convert_date_to_utc_time(date): + """ + Converts a python `datetime` object to the current date + current time in UTC. + + :param date: + :return: + """ + d = arrow.get(date) + return arrow.utcnow().replace(day=d.naive.day).replace(month=d.naive.month).replace(year=d.naive.year).replace(microsecond=0) + + +def process_response(response): + """ + Helper function that processes responses from CloudCA. + + :param response: + :return: :raise CloudCAException: + """ + if response.status_code == 200: + res = response.json() + if res['returnValue'] != 'success': + current_app.logger.debug(res) + if res.get('data'): + raise CloudCAException(" ".join([res['returnMessage'], res['data']['dryRunResultMessage']])) + else: + raise CloudCAException(res['returnMessage']) + else: + raise CloudCAException("There was an error with your request: {0}".format(response.status_code)) + + return response.json() + + +def get_auth_data(ca_name): + """ + Creates the authentication record needed to authenticate a user request to CloudCA. + + :param ca_name: + :return: :raise CloudCAException: + """ + role = authority_service.get_authority_role(ca_name) + if role: + return { + "authInfo": { + "credType": "password", + "credentials": { + "username": role.username, + "password": role.password # we only decrypt when we need to + } + + } + } + + raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name)) + + +class CloudCA(Issuer): + title = 'CloudCA' + slug = 'cloudca' + description = 'Enables the creation of certificates from the cloudca API.' + version = cloudca.VERSION + + author = 'Kevin Glisson' + author_url = 'https://github.com/netflix/lemur' + + def __init__(self, *args, **kwargs): + self.session = requests.Session() + self.session.mount('https://', CloudCAHostNameCheckingAdapter()) + self.url = current_app.config.get('CLOUDCA_URL') + + if current_app.config.get('CLOUDCA_PEM_PATH') and current_app.config.get('CLOUDCA_BUNDLE'): + self.session.cert = current_app.config.get('CLOUDCA_PEM_PATH') + self.ca_bundle = current_app.config.get('CLOUDCA_BUNDLE') + else: + current_app.logger.warning("No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA") + + super(CloudCA, self).__init__(*args, **kwargs) + + def create_authority(self, options): + """ + Creates a new certificate authority + + :param options: + :return: + """ + # this is weird and I don't like it + endpoint = '{0}/createCA'.format(API_ENDPOINT) + options['caDN']['email'] = options['ownerEmail'] + + if options['caType'] == 'subca': + options = dict(options.items() + self.auth_data(options['caParent']).items()) + + options['validityStart'] = convert_date_to_utc_time(options['validityStart']).isoformat() + options['validityEnd'] = convert_date_to_utc_time(options['validityEnd']).isoformat() + + response = self.session.post(self.url + endpoint, data=dumps(remove_none(options)), timeout=10, verify=self.ca_bundle) + + json = process_response(response) + roles = [] + + for cred in json['data']['authInfo']: + role = { + 'username': cred['credentials']['username'], + 'password': cred['credentials']['password'], + 'name': "_".join([options['caName'], cred['credentials']['username']]) + } + roles.append(role) + + if options['caType'] == 'subca': + cert = convert_to_pem(json['data']['certificate']) + else: + cert = convert_to_pem(json['data']['rootCertificate']) + + intermediates = [] + for i in json['data']['intermediateCertificates']: + intermediates.append(convert_to_pem(i)) + + return cert, "".join(intermediates), roles, + + def get_authorities(self): + """ + Retrieves authorities that were made outside of Lemur. + + :return: + """ + endpoint = '{0}/listCAs'.format(API_ENDPOINT) + authorities = [] + for ca in self.get(endpoint)['data']['caList']: + try: + authorities.append(ca['caName']) + except AttributeError as e: + current_app.logger.error("No authority has been defined for {}".format(ca['caName'])) + + return authorities + + def create_certificate(self, csr, options): + """ + Creates a new certificate from cloudca + + If no start and end date are specified the default issue range + will be used. + + :param csr: + :param options: + """ + endpoint = '{0}/enroll'.format(API_ENDPOINT) + # lets default to two years if it's not specified + # we do some last minute data massaging + options = get_default_issuance(options) + + cloudca_options = { + 'extensions': options['extensions'], + 'validityStart': convert_date_to_utc_time(options['validityStart']).isoformat(), + 'validityEnd': convert_date_to_utc_time(options['validityEnd']).isoformat(), + 'creator': options['creator'], + 'ownerEmail': options['owner'], + 'caName': options['authority'].name, + 'csr': csr, + 'comment': options['description'] + } + + response = self.post(endpoint, remove_none(cloudca_options)) + + # we return a concatenated list of intermediate because that is what aws + # expects + cert = convert_to_pem(response['data']['certificate']) + + intermediates = [convert_to_pem(response['data']['rootCertificate'])] + for i in response['data']['intermediateCertificates']: + intermediates.append(convert_to_pem(i)) + + return cert, "".join(intermediates), + + def get_csr_config(self, issuer_options): + """ + Get a valid CSR for use with CloudCA + + :param issuer_options: + :return: + """ + return cloudca.constants.CSR_CONFIG.format(**issuer_options) + + def random(self, length=10): + """ + Uses CloudCA as a decent source of randomness. + + :param length: + :return: + """ + endpoint = '/v1/random/{0}'.format(length) + response = self.session.get(self.url + endpoint, verify=self.ca_bundle) + return response + + def get_cert(self, ca_name=None, cert_handle=None): + """ + Returns a given cert from CloudCA. + + :param ca_name: + :param cert_handle: + :return: + """ + endpoint = '{0}/getCert'.format(API_ENDPOINT) + response = self.session.post(self.url + endpoint, data=dumps({'caName': ca_name}), timeout=10, verify=self.ca_bundle) + raw = process_response(response) + + certs = [] + for c in raw['data']['certList']: + cert = convert_to_pem(c['certValue']) + + intermediates = [] + for i in c['intermediateCertificates']: + intermediates.append(convert_to_pem(i)) + + certs.append({ + 'public_certificate': cert, + 'intermediate_cert': "\n".join(intermediates), + 'owner': c['ownerEmail'] + }) + + return certs + + def post(self, endpoint, data): + """ + HTTP POST to CloudCA + + :param endpoint: + :param data: + :return: + """ + if self.dry_run: + endpoint += '?dry_run=1' + + data = dumps(dict(data.items() + get_auth_data(data['caName']).items())) + + # we set a low timeout, if cloudca is down it shouldn't bring down + # lemur + response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle) + return process_response(response) + + def get(self, endpoint): + """ + HTTP GET to CloudCA + + :param endpoint: + :return: + """ + if self.dry_run: + endpoint += '?dry_run=1' + + response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle) + return process_response(response) + + +def init(): + return CloudCA() + diff --git a/lemur/common/services/issuers/plugins/cloudca/constants.py b/lemur/common/services/issuers/plugins/cloudca/constants.py new file mode 100644 index 00000000..229910bf --- /dev/null +++ b/lemur/common/services/issuers/plugins/cloudca/constants.py @@ -0,0 +1,27 @@ +CSR_CONFIG = """ + # Configuration for standard CSR generation for Netflix + # Used for procuring CloudCA certificates + # Author: kglisson + # Contact: secops@netflix.com + + [ req ] + # Use a 2048 bit private key + default_bits = 2048 + default_keyfile = key.pem + prompt = no + encrypt_key = no + + # base request + distinguished_name = req_distinguished_name + + # distinguished_name + [ req_distinguished_name ] + countryName = "{country}" # C= + stateOrProvinceName = "{state}" # ST= + localityName = "{location}" # L= + organizationName = "{organization}" # O= + organizationalUnitName = "{organizationalUnit}" # OU= + # This is the hostname/subject name on the certificate + commonName = "{commonName}" # CN= + """ + diff --git a/lemur/common/services/issuers/plugins/verisign/__init__.py b/lemur/common/services/issuers/plugins/verisign/__init__.py new file mode 100644 index 00000000..d29488d2 --- /dev/null +++ b/lemur/common/services/issuers/plugins/verisign/__init__.py @@ -0,0 +1,5 @@ +try: + VERSION = __import__('pkg_resources') \ + .get_distribution(__name__).version +except Exception, e: + VERSION = 'unknown' \ No newline at end of file diff --git a/lemur/common/services/issuers/plugins/verisign/constants.py b/lemur/common/services/issuers/plugins/verisign/constants.py new file mode 100644 index 00000000..e5d84c49 --- /dev/null +++ b/lemur/common/services/issuers/plugins/verisign/constants.py @@ -0,0 +1,159 @@ +CSR_CONFIG = """ + # Configuration for standard CSR generation for Netflix + # Used for procuring VeriSign certificates + # Author: jachan + # Contact: cloudsecurity@netflix.com + + [ req ] + # Use a 2048 bit private key + default_bits = 2048 + default_keyfile = key.pem + prompt = no + encrypt_key = no + + # base request + distinguished_name = req_distinguished_name + + # extensions + # Uncomment the following line if you are requesting a SAN cert + {is_san_comment}req_extensions = req_ext + + # distinguished_name + [ req_distinguished_name ] + countryName = "US" # C= + stateOrProvinceName = "CALIFORNIA" # ST= + localityName = "Los Gatos" # L= + organizationName = "Netflix, Inc." # O= + organizationalUnitName = "{OU}" # OU= + # This is the hostname/subject name on the certificate + commonName = "{DNS[0]}" # CN= + + [ req_ext ] + # Uncomment the following line if you are requesting a SAN cert + {is_san_comment}subjectAltName = @alt_names + + [alt_names] + # Put your SANs here + {DNS_LINES} + """ + +VERISIGN_INTERMEDIATE = """ +-----BEGIN CERTIFICATE----- +MIIFFTCCA/2gAwIBAgIQKC4nkXkzkuQo8iGnTsk3rjANBgkqhkiG9w0BAQsFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzMwHhcNMTMxMDMxMDAwMDAwWhcNMjMxMDMwMjM1OTU5WjB+MQsw +CQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNV +BAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxLzAtBgNVBAMTJlN5bWFudGVjIENs +YXNzIDMgU2VjdXJlIFNlcnZlciBDQSAtIEc0MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAstgFyhx0LbUXVjnFSlIJluhL2AzxaJ+aQihiw6UwU35VEYJb +A3oNL+F5BMm0lncZgQGUWfm893qZJ4Itt4PdWid/sgN6nFMl6UgfRk/InSn4vnlW +9vf92Tpo2otLgjNBEsPIPMzWlnqEIRoiBAMnF4scaGGTDw5RgDMdtLXO637QYqzu +s3sBdO9pNevK1T2p7peYyo2qRA4lmUoVlqTObQJUHypqJuIGOmNIrLRM0XWTUP8T +L9ba4cYY9Z/JJV3zADreJk20KQnNDz0jbxZKgRb78oMQw7jW2FUyPfG9D72MUpVK +Fpd6UiFjdS8W+cRmvvW1Cdj/JwDNRHxvSz+w9wIDAQABo4IBQDCCATwwHQYDVR0O +BBYEFF9gz2GQVd+EQxSKYCqy9Xr0QxjvMBIGA1UdEwEB/wQIMAYBAf8CAQAwawYD +VR0gBGQwYjBgBgpghkgBhvhFAQc2MFIwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cu +c3ltYXV0aC5jb20vY3BzMCgGCCsGAQUFBwICMBwaGmh0dHA6Ly93d3cuc3ltYXV0 +aC5jb20vcnBhMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9zLnN5bWNiLmNvbS9w +Y2EzLWczLmNybDAOBgNVHQ8BAf8EBAMCAQYwKQYDVR0RBCIwIKQeMBwxGjAYBgNV +BAMTEVN5bWFudGVjUEtJLTEtNTM0MC4GCCsGAQUFBwEBBCIwIDAeBggrBgEFBQcw +AYYSaHR0cDovL3Muc3ltY2QuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBbF1K+1lZ7 +9Pc0CUuWysf2IdBpgO/nmhnoJOJ/2S9h3RPrWmXk4WqQy04q6YoW51KN9kMbRwUN +gKOomv4p07wdKNWlStRxPA91xQtzPwBIZXkNq2oeJQzAAt5mrL1LBmuaV4oqgX5n +m7pSYHPEFfe7wVDJCKW6V0o6GxBzHOF7tpQDS65RsIJAOloknO4NWF2uuil6yjOe +soHCL47BJ89A8AShP/U3wsr8rFNtqVNpT+F2ZAwlgak3A/I5czTSwXx4GByoaxbn +5+CdKa/Y5Gk5eZVpuXtcXQGc1PfzSEUTZJXXCm5y2kMiJG8+WnDcwJLgLeVX+OQr +J+71/xuzAYN6 +-----END CERTIFICATE----- +""" + + +VERISIGN_ROOT = """ +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- +""" + +OLD_VERISIGN_INTERMEDIATE = """ +-----BEGIN CERTIFICATE----- +MIIFlTCCBH2gAwIBAgIQLP62CQ7ireLp/CI3JPG2vzANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzMwHhcNMTAwMjA4MDAwMDAwWhcNMjAwMjA3MjM1OTU5WjCBtTEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2UgYXQg +aHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykxMDEvMC0GA1UEAxMmVmVy +aVNpZ24gQ2xhc3MgMyBTZWN1cmUgU2VydmVyIENBIC0gRzMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCxh4QfwgxF9byrJZenraI+nLr2wTm4i8rCrFbG +5btljkRPTc5v7QlK1K9OEJxoiy6Ve4mbE8riNDTB81vzSXtig0iBdNGIeGwCU/m8 +f0MmV1gzgzszChew0E6RJK2GfWQS3HRKNKEdCuqWHQsV/KNLO85jiND4LQyUhhDK +tpo9yus3nABINYYpUHjoRWPNGUFP9ZXse5jUxHGzUL4os4+guVOc9cosI6n9FAbo +GLSa6Dxugf3kzTU2s1HTaewSulZub5tXxYsU5w7HnO1KVGrJTcW/EbGuHGeBy0RV +M5l/JJs/U0V/hhrzPPptf4H1uErT9YU3HLWm0AnkGHs4TvoPAgMBAAGjggGIMIIB +hDASBgNVHRMBAf8ECDAGAQH/AgEAMHAGA1UdIARpMGcwZQYLYIZIAYb4RQEHFwMw +VjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL2NwczAqBggr +BgEFBQcCAjAeGhxodHRwczovL3d3dy52ZXJpc2lnbi5jb20vcnBhMA4GA1UdDwEB +/wQEAwIBBjBtBggrBgEFBQcBDARhMF+hXaBbMFkwVzBVFglpbWFnZS9naWYwITAf +MAcGBSsOAwIaBBSP5dMahqyNjmvDz4Bq1EgYLHsZLjAlFiNodHRwOi8vbG9nby52 +ZXJpc2lnbi5jb20vdnNsb2dvLmdpZjAoBgNVHREEITAfpB0wGzEZMBcGA1UEAxMQ +VmVyaVNpZ25NUEtJLTItNjAdBgNVHQ4EFgQUDURcFlNEwYJ+HSCrJfQBY9i+eaUw +NAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2NybC52ZXJpc2lnbi5jb20vcGNhMy1n +My5jcmwwDQYJKoZIhvcNAQEFBQADggEBAHREFQzFWA4YY+3z8CjDeuuSSG/ghSBJ +olwwlpIX4IjoeYuzT864Hzk2tTeEeODf4YFIVsSxah8nUsGdpgVTUGPPoUJOMXvn +8wJeBSlUDXBwv3td5XbPIPXHy6vmIS6phYRetZUgq1CDTI/pvtWZKXTGM/eYXlLF +6QDvXevUHQjfb3cqQvfLljws85xLxbNFmz7cy9YmiLOd5n+gFC6X5hzSDO7+DDMi +o//+4Q/nk/UId1UCsobqYWVmqs017AmyiAPO/v3sGncYYQY2BMYgla74dZfeDNu4 +MXA68Mb6ZdlkhGEmZYVBcOmkaKs+P+SggTofsK27BlpugAtNWjEy5JY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEOzCCA6SgAwIBAgIQSsnqCI7m94zHpfn6OaSTljANBgkqhkiG9w0BAQUFADBf +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xNzA1BgNVBAsT +LkNsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw +HhcNMTEwNjA5MDAwMDAwWhcNMjExMTA3MjM1OTU5WjCByjELMAkGA1UEBhMCVVMx +FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz +dCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJpU2lnbiwgSW5jLiAtIEZv +ciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAz +IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLupxS/HgfGh5vGzdzvfjJa5QS +ME/wNkf10JEK9RfIpWHBFkBN+4phkOV2IMERBn2rLG6m9RFBjvotrSphWaRnJkzQ +6LxSW3AgBFjResmkabyDF2StBYu80FjOjYz16/BCSQudlydnMm7hrpMVHHC8IE0v +GN6SiOhshVcRGul+4yYRVKJFllWDyjCJ6NzYo+0qgD9/eWVXPhUgZggvlZO/qkcv +qEaX8BLi/sIKK1Hmdua3RrfiDabMqMNMWVWJ5uhTXBzqnfBiFgunyV8M8N7Cds6v +92ry+kGmojMUyeV6Y9OeYjfVhWWeDuZTJHQbXh0SU1vHLOeDSTsVropouVeXAgMB +AAGjggEGMIIBAjAPBgNVHRMBAf8EBTADAQH/MD0GA1UdIAQ2MDQwMgYEVR0gADAq +MCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy52ZXJpc2lnbi5jb20vY3BzMDEGA1Ud +HwQqMCgwJqAkoCKGIGh0dHA6Ly9jcmwudmVyaXNpZ24uY29tL3BjYTMuY3JsMA4G +A1UdDwEB/wQEAwIBBjBtBggrBgEFBQcBDARhMF+hXaBbMFkwVzBVFglpbWFnZS9n +aWYwITAfMAcGBSsOAwIaBBSP5dMahqyNjmvDz4Bq1EgYLHsZLjAlFiNodHRwOi8v +bG9nby52ZXJpc2lnbi5jb20vdnNsb2dvLmdpZjANBgkqhkiG9w0BAQUFAAOBgQBl +2Sr58sJgybnqQQfKNrcYL2iu/gMk5mdU7nTDLNn1M8Fetw6Tz3iejrImFBFT0cjC +EiG0PXsq2BzUS2TsiU+/lYeH3pVk9HPGF9+9GZCX6GmBEmlmStMkQA5ZdRWwRHQX +op4GYNOwg7jdL+afe2dcFqFH284ueQXZ8fT4PuJKoQ== +-----END CERTIFICATE----- +""" diff --git a/lemur/common/services/issuers/plugins/verisign/verisign.py b/lemur/common/services/issuers/plugins/verisign/verisign.py new file mode 100644 index 00000000..2b3ca1cd --- /dev/null +++ b/lemur/common/services/issuers/plugins/verisign/verisign.py @@ -0,0 +1,194 @@ +""" +.. module: lemur.common.services.issuers.plugins.verisign.verisign + :platform: Unix + :synopsis: This module is responsible for communicating with the VeriSign VICE 2.0 API. + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +import arrow +import requests +import xmltodict + +from flask import current_app + +from lemur.common.services.issuers.issuer import Issuer +from lemur.common.services.issuers.plugins import verisign + +from lemur.certificates.exceptions import InsufficientDomains + + +# https://support.venafi.com/entries/66445046-Info-VeriSign-Error-Codes +VERISIGN_ERRORS = { + "0x30c5": "Domain Mismatch when enrolling for an SSL certificate, a domain in your request has not been added to verisign", + "0x482d": "Cannot issue SHA1 certificates expiring after 31/12/2016", + "0x3a10": "Invalid X509 certificate format.: an unsupported certificate format was submitted", + "0x4002": "Internal QM Error. : Internal Database connection error.", + "0x3301": "Bad transaction id or parent cert not renewable.: User try to renew a certificate that is not yet ready for renew or the transaction id is wrong", + "0x3069": "Challenge phrase mismatch: The challenge phrase submitted does not match the original one", + "0x3111": "Unsupported Product: User submitted a wrong product or requested cipher is not supported", + "0x30e8": "CN or org does not match the original one.: the submitted CSR contains a common name or org that does not match the original one", + "0x1005": "Duplicate certificate: a certificate with the same common name exists already", + "0x0194": "Incorrect Signature Algorithm: The requested signature algorithm is not supported for the key type. i.e. an ECDSA is submitted for an RSA key", + "0x6000": "parameter missing or incorrect: This is a general error code for missing or incorrect parameters. The reason will be in the response message. i.e. 'CSR is missing, 'Unsupported serverType' when no supported serverType could be found., 'invalid transaction id'", + "0x3063": "Certificate not allowed: trying to issue a certificate that is not configured for the account", + "0x23df": "No MDS Data Returned: internal connection lost or server not responding. this should be rare", + "0x3004": "Invalid Account: The users mpki account associated with the certificate is not valid or not yet active", + "0x4101": "Internal Error: internal server error, user should try again later. (Also check that State is spelled out", + "0x3101": "Missing admin role: Your account does not have the admin role required to access the webservice API", + "0x3085": "Account does not have webservice feature.: Your account does not the the webservice role required to access the webservice API", + "0x9511": "Corrupted CSR : the submitted CSR was mal-formed", + "0xa001": "Public key format does not match.: The public key format does not match the original cert at certificate renewal or replacement. E.g. if you try to renew or replace an RSA cert with a DSA or ECC key based CSR", + "0x0143": "Certificate End Date Error: You are trying to replace a certificate with validity end date exceeding the original cert. or the certificate end date is not valid", + "0x482d": "SHA1 validity check error: What error code do we get when we submit the SHA1 SSL requests with the validity more than 12/31/2016?", + "0x482e": "What error code do we get when we cannot complete the re-authentication for domains with a newly-approved gTLD 30 days after the gTLD approval", + "0x4824": "Per CA/B Forum baseline requirements, non-FQDN certs cannot exceed 11/1/2015. Examples: hostname, foo.cba (.cba is a pending gTLD)", + "eE0x48": "Currently the maximum cert validity is 4-years", + "0x4826": "OU misleading. See comments", + "0x4827": "Org re-auth past due. EV org has to go through re-authentication every 13 months; OV org has to go through re-authentication every 39 months", + "0x482a": "Domain re-auth past due. EV domain has to go through re-authentication every 13 months; OV domain has to go through re-authentication every 39 months.", + "0x482b": "No org address was set to default, should not happen", + "0x482c": "signature algorithm does not match intended key type in the CSR (e.g. CSR has an ECC key, but the signature algorithm is sha1WithRSAEncryption)", + "0x600E": "only supports ECC keys with the named curve NIST P-256, aka secp256r1 or prime256v1, other ECC key sizes will get this error ", + "0x6013": "only supports DSA keys with (2048, 256) as the bit lengths of the prime parameter pair (p, q), other DSA key sizes will get this error", + "0x600d": "RSA key size < 2A048", + "0x4828": "Verisign certificates can be at most two years in length", + "0x3043": "Certificates must have a validity of at least 1 day" +} + + +class Verisign(Issuer): + title = 'VeriSign' + slug = 'verisign' + description = 'Enables the creation of certificates by the VICE2.0 verisign API.' + version = verisign.VERSION + + author = 'Kevin Glisson' + author_url = 'https://github.com/netflix/lemur' + + def __init__(self, *args, **kwargs): + self.session = requests.Session() + self.session.cert = current_app.config.get('VERISIGN_PEM_PATH') + super(Verisign, self).__init__(*args, **kwargs) + + @staticmethod + def handle_response(content): + """ + Helper function that helps with parsing responses from the Verisign API. + :param content: + :return: :raise Exception: + """ + d = xmltodict.parse(content) + global VERISIGN_ERRORS + if d.get('Error'): + status_code = d['Error']['StatusCode'] + elif d.get('Response'): + status_code = d['Response']['StatusCode'] + if status_code in VERISIGN_ERRORS.keys(): + raise Exception(VERISIGN_ERRORS[status_code]) + return d + + def create_certificate(self, csr, issuer_options): + """ + Creates a Verisign certificate. + + :param csr: + :param issuer_options: + :return: :raise Exception: + """ + url = current_app.config.get("VERISIGN_URL") + '/enroll' + + data = { + 'csr': csr, + 'challenge': issuer_options['challenge'], + 'serverType': 'Apache', + 'certProductType': 'Server', + 'firstName': current_app.config.get("VERISIGN_FIRST_NAME"), + 'lastName': current_app.config.get("VERISIGN_LAST_NAME"), + 'signatureAlgorithm': 'sha256WithRSAEncryption', + 'email': current_app.config.get("VERISIGN_EMAIL") + } + + if issuer_options.get('validityEnd'): + data['specificEndDate'] = arrow.get(issuer_options['validityEnd']).replace(days=-1).format("MM/DD/YYYY") + + now = arrow.utcnow() + then = arrow.get(issuer_options['validityEnd']) + + if then < now.replace(years=+1): + data['validityPeriod'] = '1Y' + elif then < now.replace(years=+2): + data['validityPeriod'] = '2Y' + else: + raise Exception("Verisign issued certificates cannot exceed two years in validity") + + current_app.logger.info("Requesting a new verisign certificate: {0}".format(data)) + + response = self.session.post(url, data=data) + cert = self.handle_response(response.content)['Response']['Certificate'] + return cert, verisign.constants.VERISIGN_INTERMEDIATE, + + def get_csr_config(self, issuer_options): + """ + Used to generate a valid CSR for the given Certificate Authority. + + :param issuer_options: + :return: :raise InsufficientDomains: + """ + domains = [] + + if issuer_options.get('commonName'): + domains.append(issuer_options.get('commonName')) + + if issuer_options.get('extensions'): + for n in issuer_options['extensions']['subAltNames']['names']: + if n['value']: + domains.append(n['value']) + + is_san_comment = "#" + + dns_lines = [] + if len(domains) < 1: + raise InsufficientDomains + + elif len(domains) > 1: + is_san_comment = "" + for domain_line in list(set(domains)): + dns_lines.append("DNS.{} = {}".format(len(dns_lines) + 1, domain_line)) + + return verisign.constants.CSR_CONFIG.format( + is_san_comment=is_san_comment, + OU=issuer_options.get('organizationalUnit', 'Operations'), + DNS=domains, + DNS_LINES="\n".join(dns_lines)) + + @staticmethod + def create_authority(options): + """ + Creates an authority, this authority is then used by Lemur to allow a user + to specify which Certificate Authority they want to sign their certificate. + + :param options: + :return: + """ + role = {'username': '', 'password': '', 'name': 'verisign'} + return verisign.constants.VERISIGN_ROOT, "", [role] + + def get_available_units(self): + """ + Uses the Verisign to fetch the number of available unit's left. This can be used to get tabs + on the number of certificates that can be issued. + + :return: + """ + url = current_app.config.get("VERISIGN_URL") + '/getTokens' + response = self.session.post(url, headers={'content-type': 'application/x-www-form-urlencoded'}) + return self.handle_response(response.content)['Response']['Order'] + + def get_authorities(self): + pass + + +def init(): + return Verisign() diff --git a/lemur/common/utils.py b/lemur/common/utils.py new file mode 100644 index 00000000..6b23b3d8 --- /dev/null +++ b/lemur/common/utils.py @@ -0,0 +1,63 @@ +""" +.. module: lemur.common.utils + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from functools import wraps + +from flask import current_app + +from flask.ext.restful import marshal +from flask.ext.restful.reqparse import RequestParser + +from flask.ext.sqlalchemy import Pagination + + +class marshal_items(object): + def __init__(self, fields, envelope=None): + self.fields = fields + self.envelop = envelope + + def __call__(self, f): + def _filter_items(items): + filtered_items = [] + for item in items: + filtered_items.append(marshal(item, self.fields)) + return filtered_items + + @wraps(f) + def wrapper(*args, **kwargs): + try: + resp = f(*args, **kwargs) + + # this is a bit weird way to handle non standard error codes returned from the marshaled function + if isinstance(resp, tuple): + return resp[0], resp[1] + + if isinstance(resp, Pagination): + return {'items': _filter_items(resp.items), 'total': resp.total} + + if isinstance(resp, list): + return _filter_items(resp) + + return marshal(resp, self.fields) + except Exception as e: + # this is a little weird hack to respect flask restful parsing errors on marshaled functions + if hasattr(e, 'code'): + return {'message': e.data['message']}, 400 + else: + current_app.logger.exception(e) + return {'message': e.message}, 400 + return wrapper + + +paginated_parser = RequestParser() + +paginated_parser.add_argument('count', type=int, default=10, location='args') +paginated_parser.add_argument('page', type=int, default=1, location='args') +paginated_parser.add_argument('sortDir', type=str, dest='sort_dir', location='args') +paginated_parser.add_argument('sortBy', type=str, dest='sort_by', location='args') +paginated_parser.add_argument('filter', type=str, location='args') diff --git a/lemur/constants.py b/lemur/constants.py new file mode 100644 index 00000000..15d4bdee --- /dev/null +++ b/lemur/constants.py @@ -0,0 +1,10 @@ +""" +.. module: lemur.constants + :copyright: (c) 2015 by Netflix Inc. + :license: Apache, see LICENSE for more details. +""" +SAN_NAMING_TEMPLATE = "SAN-{subject}-{issuer}-{not_before}-{not_after}" +DEFAULT_NAMING_TEMPLATE = "{subject}-{issuer}-{not_before}-{not_after}" +NONSTANDARD_NAMING_TEMPLATE = "{issuer}-{not_before}-{not_after}" + + diff --git a/lemur/database.py b/lemur/database.py new file mode 100644 index 00000000..d2a7c742 --- /dev/null +++ b/lemur/database.py @@ -0,0 +1,278 @@ +""" +.. module: lemur.database + :platform: Unix + :synopsis: This module contains all of the database related methods + needed for lemur to interact with a datastore + + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from flask import current_app + +from sqlalchemy import exc +from sqlalchemy.sql import and_, or_ + +from lemur.extensions import db +from lemur.exceptions import AttrNotFound, IntegrityError + + +def filter_none(kwargs): + """ + Remove all `None` values froma given dict. SQLAlchemy does not + like to have values that are None passed to it. + + :param kwargs: Dict to filter + :return: Dict without any 'None' values + """ + n_kwargs = {} + for k, v in kwargs.items(): + if v: + n_kwargs[k] = v + return n_kwargs + + +def session_query(model): + """ + Returns a SQLAlchemy query object for the specified `model`. + + If `model` has a ``query`` attribute already, that object will be returned. + Otherwise a query will be created and returned based on `session`. + + :param model: sqlalchemy model + :return: query object for model + """ + return model.query if hasattr(model, 'query') else db.session.query(model) + + +def create_query(model, kwargs): + """ + Returns a SQLAlchemy query object for specified `model`. Model + filtered by the kwargs passed. + + :param model: + :param kwargs: + :return: + """ + s = session_query(model) + return s.filter_by(**kwargs) + + +def commit(): + """ + Helper to commit the current session. + """ + db.session.commit() + + +def add(model): + """ + Helper to add a `model` to the current session. + + :param model: + :return: + """ + db.session.add(model) + + +def find_all(query, model, kwargs): + """ + Returns a query object that ensures that all kwargs + are present. + + :param query: + :param model: + :param kwargs: + :return: + """ + conditions = [] + kwargs = filter_none(kwargs) + for attr, value in kwargs.items(): + if not isinstance(value, list): + value = value.split(',') + + conditions.append(getattr(model, attr).in_(value)) + + return query.filter(and_(*conditions)) + + +def find_any(query, model, kwargs): + """ + Returns a query object that allows any kwarg + to be present. + + :param query: + :param model: + :param kwargs: + :return: + """ + or_args = [] + for attr, value in kwargs.items(): + or_args.append(or_(getattr(model, attr) == value)) + exprs = or_(*or_args) + return query.filter(exprs) + + +def get(model, value, field="id"): + """ + Returns one object filtered by the field and value. + + :param model: + :param value: + :param field: + :return: + """ + query = session_query(model) + try: + return query.filter(getattr(model, field) == value).one() + except: + return + + +def get_all(model, value, field="id"): + """ + Returns query object with the fields and value filtered. + + :param model: + :param value: + :param field: + :return: + """ + query = session_query(model) + return query.filter(getattr(model, field) == value) + + +def create(model): + """ + Helper that attempts to create a new instance of an object. + + :param model: + :return: :raise IntegrityError: + """ + try: + db.session.add(model) + commit() + db.session.refresh(model) + except exc.IntegrityError as e: + raise IntegrityError(e.orig.diag.message_detail) + return model + + +def update(model): + """ + Helper that attempts to update a model. + + :param model: + :return: + """ + commit() + db.session.refresh(model) + return model + + +def delete(model): + """ + Helper that attempts to delete a model. + + :param model: + """ + db.session.delete(model) + db.session.commit() + + +def filter(query, model, terms): + """ + Helper that searched for 'like' strings in column values. + + :param query: + :param model: + :param terms: + :return: + """ + return query.filter(getattr(model, terms[0]).ilike('%{}%'.format(terms[1]))) + + +def sort(query, model, field, direction): + """ + Returns objects of the specified `model` in the field and direction + given + + :param query: + :param model: + :param field: + :param direction: + """ + try: + field = getattr(model, field) + direction = getattr(field, direction) + query = query.order_by(direction()) + return query + except AttributeError as e: + raise AttrNotFound(field) + + +def paginate(query, page, count): + """ + Returns the items given the count and page specified + + :param query: + :param page: + :param count: + """ + return query.paginate(page, count) + + +def update_list(model, model_attr, item_model, items): + """ + Helper that correctly updates a models items + depending on what has changed + + :param model_attr: + :param item_model: + :param items: + :param model: + :return: + """ + ids = [] + + for i in items: + ids.append(i['id']) + + for i in getattr(model, model_attr): + if i.id not in ids: + getattr(model, model_attr).remove(i) + + for i in items: + for item in getattr(model, model_attr): + if item.id == i['id']: + break + else: + getattr(model, model_attr).append(get(item_model, i['id'])) + + return model + + +def sort_and_page(query, model, args): + """ + Helper that allows us to combine sorting and paging + + :param query: + :param model: + :param args: + :return: + """ + sort_by = args.pop('sort_by') + sort_dir = args.pop('sort_dir') + page = args.pop('page') + count = args.pop('count') + + query = find_all(query, model, args) + + if sort_by and sort_dir: + query = sort(query, model, sort_by, sort_dir) + + return paginate(query, page, count) + + + diff --git a/lemur/decorators.py b/lemur/decorators.py new file mode 100644 index 00000000..d1cb695b --- /dev/null +++ b/lemur/decorators.py @@ -0,0 +1,55 @@ +""" +.. module: lemur.decorators + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from datetime import timedelta +from flask import make_response, request, current_app + +from functools import update_wrapper + + +def crossdomain(origin=None, methods=None, headers=None, + max_age=21600, attach_to_all=True, + automatic_options=True): + if methods is not None: + methods = ', '.join(sorted(x.upper() for x in methods)) + + if headers is not None and not isinstance(headers, basestring): + headers = ', '.join(x.upper() for x in headers) + + if not isinstance(origin, basestring): + origin = ', '.join(origin) + + if isinstance(max_age, timedelta): + max_age = max_age.total_seconds() + + def get_methods(): + if methods is not None: + return methods + + options_resp = current_app.make_default_options_response() + return options_resp.headers['allow'] + + def decorator(f): + def wrapped_function(*args, **kwargs): + if automatic_options and request.method == 'OPTIONS': + resp = current_app.make_default_options_response() + else: + resp = make_response(f(*args, **kwargs)) + if not attach_to_all and request.method != 'OPTIONS': + return resp + + h = resp.headers + h['Access-Control-Allow-Origin'] = origin + h['Access-Control-Allow-Methods'] = get_methods() + h['Access-Control-Max-Age'] = str(max_age) + #if headers is not None: + h['Access-Control-Allow-Headers'] = "Origin, X-Requested-With, Content-Type, Accept, Authorization " # headers + h['Access-Control-Allow-Credentials'] = 'true' + return resp + + f.provide_automatic_options = False + return update_wrapper(wrapped_function, f) + return decorator + diff --git a/lemur/domains/__init__.py b/lemur/domains/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/domains/models.py b/lemur/domains/models.py new file mode 100644 index 00000000..14f52a3d --- /dev/null +++ b/lemur/domains/models.py @@ -0,0 +1,27 @@ +""" +.. module: lemur.domains.models + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +from sqlalchemy import Column, Integer, String + +from lemur.database import db + + +class Domain(db.Model): + __tablename__ = 'domains' + id = Column(Integer, primary_key=True) + name = Column(String(256)) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + def serialize(self): + blob = self.as_dict() + blob['certificates'] = [x.id for x in self.certificate] + return blob + diff --git a/lemur/domains/service.py b/lemur/domains/service.py new file mode 100644 index 00000000..f9452bb3 --- /dev/null +++ b/lemur/domains/service.py @@ -0,0 +1,64 @@ +""" +.. module: lemur.domains.service + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from lemur.domains.models import Domain +from lemur.certificates.models import Certificate + +from lemur import database + + +def get(domain_id): + """ + Fetches one domain + + :param domain_id: + :return: + """ + return database.get(Domain, domain_id) + + +def get_all(): + """ + Fetches all domains + + :return: + """ + query = database.session_query(Domain) + return database.find_all(query, Domain, {}).all() + + +def render(args): + """ + Helper to parse REST Api requests + + :param args: + :return: + """ + query = database.session_query(Domain).join(Certificate, Domain.certificate) + + sort_by = args.pop('sort_by') + sort_dir = args.pop('sort_dir') + page = args.pop('page') + count = args.pop('count') + filt = args.pop('filter') + certificate_id = args.pop('certificate_id', None) + + if filt: + terms = filt.split(';') + query = database.filter(query, Domain, terms) + + if certificate_id: + query = query.filter(Certificate.id == certificate_id) + + query = database.find_all(query, Domain, args) + + if sort_by and sort_dir: + query = database.sort(query, Domain, sort_by, sort_dir) + + return database.paginate(query, page, count) + diff --git a/lemur/domains/views.py b/lemur/domains/views.py new file mode 100644 index 00000000..99e14432 --- /dev/null +++ b/lemur/domains/views.py @@ -0,0 +1,182 @@ +""" +.. module: lemur.domains.views + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +from flask import Blueprint +from flask.ext.restful import reqparse, Api, fields + +from lemur.domains import service +from lemur.auth.service import AuthenticatedResource + +from lemur.common.utils import paginated_parser, marshal_items + +FIELDS = { + 'id': fields.Integer, + 'name': fields.String +} + +mod = Blueprint('domains', __name__) +api = Api(mod) + + +class DomainsList(AuthenticatedResource): + """ Defines the 'domains' endpoint """ + def __init__(self): + super(DomainsList, self).__init__() + + @marshal_items(FIELDS) + def get(self): + """ + .. http:get:: /domains + + The current domain list + + **Example request**: + + .. sourcecode:: http + + GET /domains HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "items": [ + { + "id": 1, + "name": "www.example.com", + }, + { + "id": 2, + "name": "www.example2.com", + } + ] + "total": 2 + } + + :query sortBy: field to sort on + :query sortDir: acs or desc + :query page: int. default is 1 + :query filter: key value pair. format is k=v; + :query limit: limit number. default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + parser = paginated_parser.copy() + args = parser.parse_args() + return service.render(args) + + +class Domains(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Domains, self).__init__() + + @marshal_items(FIELDS) + def get(self, domain_id): + """ + .. http:get:: /domains/1 + + Fetch one domain + + **Example request**: + + .. sourcecode:: http + + GET /domains HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "www.example.com", + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + return service.get(domain_id) + + +class CertificateDomains(AuthenticatedResource): + """ Defines the 'domains' endpoint """ + def __init__(self): + super(CertificateDomains, self).__init__() + + @marshal_items(FIELDS) + def get(self, certificate_id): + """ + .. http:get:: /certificates/1/domains + + The current domain list + + **Example request**: + + .. sourcecode:: http + + GET /domains HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "items": [ + { + "id": 1, + "name": "www.example.com", + }, + { + "id": 2, + "name": "www.example2.com", + } + ] + "total": 2 + } + + :query sortBy: field to sort on + :query sortDir: acs or desc + :query page: int. default is 1 + :query filter: key value pair. format is k=v; + :query limit: limit number. default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + parser = paginated_parser.copy() + args = parser.parse_args() + args['certificate_id'] = certificate_id + return service.render(args) + + +api.add_resource(DomainsList, '/domains', endpoint='domains') +api.add_resource(Domains, '/domains/', endpoint='domain') +api.add_resource(CertificateDomains, '/certificates//domains', endpoint='certificateDomains') diff --git a/lemur/elbs/__init__.py b/lemur/elbs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/elbs/models.py b/lemur/elbs/models.py new file mode 100644 index 00000000..eab22933 --- /dev/null +++ b/lemur/elbs/models.py @@ -0,0 +1,44 @@ +""" +.. module: lemur.elbs.models + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from sqlalchemy import Column, BigInteger, String, ForeignKey, DateTime, PassiveDefault, func +from sqlalchemy.orm import relationship + +from lemur.database import db +from lemur.listeners.models import Listener + + +class ELB(db.Model): + __tablename__ = 'elbs' + id = Column(BigInteger, primary_key=True) + account_id = Column(BigInteger, ForeignKey("accounts.id"), index=True) + region = Column(String(32)) + name = Column(String(128)) + vpc_id = Column(String(128)) + scheme = Column(String(128)) + dns_name = Column(String(128)) + listeners = relationship("Listener", backref='elb', cascade="all, delete, delete-orphan") + date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) + + def __init__(self, elb_obj=None): + if elb_obj: + self.region = elb_obj.connection.region.name + self.name = elb_obj.name + self.vpc_id = elb_obj.vpc_id + self.scheme = elb_obj.scheme + self.dns_name = elb_obj.dns_name + for listener in elb_obj.listeners: + self.listeners.append(Listener(listener)) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + def serialize(self): + blob = self.as_dict() + del blob['date_created'] + return blob diff --git a/lemur/elbs/service.py b/lemur/elbs/service.py new file mode 100644 index 00000000..8aa60ccb --- /dev/null +++ b/lemur/elbs/service.py @@ -0,0 +1,125 @@ +""" +.. module: lemur.elbs.service + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +from sqlalchemy import func +from sqlalchemy.sql import and_ + +from lemur import database +from lemur.elbs.models import ELB +from lemur.listeners.models import Listener + +def get_all(account_id, elb_name): + """ + Retrieves all ELBs in a given account + + :param account_id: + :param elb_name: + :rtype : Elb + :return: + """ + query = database.session_query(ELB) + return query.filter(and_(ELB.name == elb_name, ELB.account_id == account_id)).all() + + +def get_by_region_and_account(region, account_id): + query = database.session_query(ELB) + return query.filter(and_(ELB.region == region, ELB.account_id == account_id)).all() + + +def get_all_elbs(): + """ + Get all ELBs that Lemur knows about + + :rtype : list + :return: + """ + return ELB.query.all() + + +def get(elb_id): + """ + Retrieve an ELB with a give ID + + :rtype : Elb + :param elb_id: + :return: + """ + return database.get(ELB, elb_id) + + +def create(account, elb): + """ + Create a new ELB + + :param account: + :param elb: + """ + elb = ELB(elb) + account.elbs.append(elb) + database.create(elb) + + +def delete(elb_id): + """ + Delete an ELB + + :param elb_id: + """ + database.delete(get(elb_id)) + + +def render(args): + query = database.session_query(ELB) + + sort_by = args.pop('sort_by') + sort_dir = args.pop('sort_dir') + page = args.pop('page') + count = args.pop('count') + filt = args.pop('filter') + active = args.pop('active') + certificate_id = args.pop('certificate_id') + + if certificate_id: + query.filter(ELB.listeners.any(Listener.certificate_id == certificate_id)) + + if active == 'true': + query = query.filter(ELB.listeners.any()) + + if filt: + terms = filt.split(';') + query = database.filter(query, ELB, terms) + + query = database.find_all(query, ELB, args) + + if sort_by and sort_dir: + query = database.sort(query, ELB, sort_by, sort_dir) + + return database.paginate(query, page, count) + + +def stats(**kwargs): + attr = getattr(ELB, kwargs.get('metric')) + query = database.db.session.query(attr, func.count(attr)) + + if kwargs.get('account_id'): + query = query.filter(ELB.account_id == kwargs.get('account_id')) + + if kwargs.get('active') == 'true': + query = query.join(ELB.listeners) + query = query.filter(Listener.certificate_id != None) + + items = query.group_by(attr).all() + + results = [] + for key, count in items: + if key: + results.append({"key": key, "y": count}) + return results + + diff --git a/lemur/elbs/sync.py b/lemur/elbs/sync.py new file mode 100644 index 00000000..f90c7bba --- /dev/null +++ b/lemur/elbs/sync.py @@ -0,0 +1,72 @@ + +""" +.. module: lemur.elbs.sync + :platform: Unix + :synopsis: This module attempts to sync with AWS and ensure that all elbs + currently available in AWS are available in Lemur as well + + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson + +""" + +from flask import current_app +from lemur.accounts import service as account_service +from lemur.elbs import service as elb_service +from lemur.common.services.aws.elb import get_all_elbs, get_all_regions + + +def create_new(known, aws, account): + new = 0 + for elb in aws: + for n in known: + if elb.name == n.name: + break + else: + new += 1 + current_app.logger.debug("Creating {0}".format(elb.name)) + try: + elb_service.create(account, elb) + except AttributeError as e: + current_app.logger.exception(e) + return new + + +def remove_missing(known, aws): + deleted = 0 + for ke in known: + for elb in aws: + if elb.name == ke.name: + break + else: + deleted += 1 + current_app.logger.debug("Deleting {0}".format(ke.name)) + elb_service.delete(ke.id) + return deleted + + +def sync_all_elbs(): + for account in account_service.get_all(): + regions = get_all_regions() + for region in regions: + current_app.logger.info("Importing ELBs from '{0}/{1}/{2}'... ".format(account.account_number, account.label, region)) + try: + aws_elbs = get_all_elbs(account.account_number, region) + except Exception as e: + current_app.logger.error("Failed to get ELBS from '{0}/{1}/{2}' reason: {3}".format( + account.label, account.account_number, region, e.message) + ) + continue + + known_elbs = elb_service.get_by_region_and_account(region, account.id) + + new_elbs = create_new(known_elbs, aws_elbs, account) + current_app.logger.info( + "Created {0} new ELBs in '{1}/{2}/{3}'...".format( + new_elbs, account.account_number, account.label, region)) + + deleted_elbs = remove_missing(known_elbs, aws_elbs) + current_app.logger.info( + "Deleted {0} missing ELBs from '{1}/{2}/{3}'...".format( + deleted_elbs, account.account_number, account.label, region)) diff --git a/lemur/elbs/views.py b/lemur/elbs/views.py new file mode 100644 index 00000000..214d28e2 --- /dev/null +++ b/lemur/elbs/views.py @@ -0,0 +1,78 @@ +""" +.. module: lemur.elbs.service + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +from flask import Blueprint +from flask.ext.restful import reqparse, Api, fields +from lemur.elbs import service +from lemur.auth.service import AuthenticatedResource + +from lemur.common.utils import marshal_items, paginated_parser + + +mod = Blueprint('elbs', __name__) +api = Api(mod) + + +FIELDS = { + 'name': fields.String, + 'id': fields.Integer, + 'region': fields.String, + 'scheme': fields.String, + 'accountId': fields.Integer(attribute='account_id'), + 'vpcId': fields.String(attribute='vpc_id') +} + + +class ELBsList(AuthenticatedResource): + """ Defines the 'elbs' endpoint """ + def __init__(self): + super(ELBsList, self).__init__() + + @marshal_items(FIELDS) + def get(self): + parser = paginated_parser.copy() + parser.add_argument('owner', type=str, location='args') + parser.add_argument('id', type=str, location='args') + parser.add_argument('accountId', type=str, dest='account_id', location='args') + parser.add_argument('certificateId', type=str, dest='certificate_id', location='args') + parser.add_argument('active', type=str, default='true', location='args') + + args = parser.parse_args() + return service.render(args) + + +class ELBsStats(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(ELBsStats, self).__init__() + + def get(self): + self.reqparse.add_argument('metric', type=str, location='args') + self.reqparse.add_argument('accountId', dest='account_id', location='args') + self.reqparse.add_argument('active', type=str, default='true', location='args') + + args = self.reqparse.parse_args() + + items = service.stats(**args) + return {"items": items, "total": len(items)} + + +class ELBs(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(ELBs, self).__init__() + + @marshal_items(FIELDS) + def get(self, elb_id): + return service.get(elb_id) + + +api.add_resource(ELBsList, '/elbs', endpoint='elbs') +api.add_resource(ELBs, '/elbs/', endpoint='elb') +api.add_resource(ELBsStats, '/elbs/stats', endpoint='elbsStats') diff --git a/lemur/exceptions.py b/lemur/exceptions.py new file mode 100644 index 00000000..f36bda21 --- /dev/null +++ b/lemur/exceptions.py @@ -0,0 +1,61 @@ +""" +.. module: lemur.exceptions + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from flask import current_app + + +class LemurException(Exception): + def __init__(self): + current_app.logger.error(self) + + +class AuthenticationFailedException(LemurException): + def __init__(self, remote_ip, user_agent): + self.remote_ip = remote_ip + self.user_agent = user_agent + + def __str__(self): + return repr("Failed login from: {} {}".format(self.remote_ip, self.user_agent)) + + +class IntegrityError(LemurException): + def __init__(self, message): + self.message = message + + def __str__(self): + return repr(self.message) + + +class InvalidListener(LemurException): + def __str__(self): + return repr("Invalid listener, ensure you select a certificate if you are using a secure protocol") + + +class CertificateUnavailable(LemurException): + def __str__(self): + return repr("The certificate requested is not available") + + +class AttrNotFound(LemurException): + def __init__(self, field): + self.field = field + + def __str__(self): + return repr("The field '{0}' is not sortable".format(self.field)) + + +class NoPersistanceFound(Exception): + def __str__(self): + return repr("No peristence method found, Lemur cannot persist sensitive information") + + +class NoEncryptionKeyFound(Exception): + def __str__(self): + return repr("Aborting... Lemur cannot locate db encryption key, is ENCRYPTION_KEY set?") + + +class InvalidToken(Exception): + def __str__(self): + return repr("Invalid token") diff --git a/lemur/extensions.py b/lemur/extensions.py new file mode 100644 index 00000000..07101c4d --- /dev/null +++ b/lemur/extensions.py @@ -0,0 +1,18 @@ +""" +.. module: lemur.extensions + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" + +from flask.ext.sqlalchemy import SQLAlchemy +db = SQLAlchemy() + +from flask.ext.migrate import Migrate +migrate = Migrate() + +from flask.ext.bcrypt import Bcrypt +bcrypt = Bcrypt() + +from flask.ext.principal import Principal +principal = Principal() + diff --git a/lemur/factory.py b/lemur/factory.py new file mode 100644 index 00000000..520ee977 --- /dev/null +++ b/lemur/factory.py @@ -0,0 +1,138 @@ +""" +.. module: lemur.factory + :platform: Unix + :synopsis: This module contains all the needed functions to allow + the factory app creation. + + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson + +""" +import os +import imp +import errno + +from logging import Formatter +from logging.handlers import RotatingFileHandler + +from flask import Flask +from lemur.common.health import mod as health +from lemur.exceptions import NoEncryptionKeyFound +from lemur.extensions import db, migrate, principal + + +DEFAULT_BLUEPRINTS = ( + health, +) + +API_VERSION = 1 + + +def create_app(app_name=None, blueprints=None, config=None): + """ + Lemur application factory + + :param config: + :param app_name: + :param blueprints: + :return: + """ + if not blueprints: + blueprints = DEFAULT_BLUEPRINTS + else: + blueprints = blueprints + DEFAULT_BLUEPRINTS + + if not app_name: + app_name = __name__ + + app = Flask(app_name) + configure_app(app, config) + configure_blueprints(app, blueprints) + configure_extensions(app) + configure_logging(app) + return app + + +def from_file(file_path, silent=False): + """ + Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + + :param file_path: + :param silent: + """ + d = imp.new_module('config') + d.__file__ = file_path + try: + with open(file_path) as config_file: + exec(compile(config_file.read(), file_path, 'exec'), d.__dict__) + except IOError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise + return d + + +def configure_app(app, config=None): + """ + Different ways of configuration + + :param app: + :param config: + :return: + """ + try: + app.config.from_envvar("LEMUR_SETTINGS") + except RuntimeError: + if config and config != 'None': + app.config.from_object(from_file(config)) + else: + app.config.from_object(from_file(os.path.expanduser("~/.lemur/lemur.conf.py"))) + + if not app.config.get('ENCRYPTION_KEY'): + raise NoEncryptionKeyFound + + +def configure_extensions(app): + """ + Attaches and configures any needed flask extensions + to our app. + + :param app: + """ + db.init_app(app) + migrate.init_app(app, db) + principal.init_app(app) + + +def configure_blueprints(app, blueprints): + """ + We prefix our APIs with their given version so that we can support + multiple concurrent API versions. + + :param app: + :param blueprints: + """ + for blueprint in blueprints: + app.register_blueprint(blueprint, url_prefix="/api/{0}".format(API_VERSION)) + + +def configure_logging(app): + """ + Sets up application wide logging. + + :param app: + """ + handler = RotatingFileHandler(app.config.get('LOG_FILE', 'lemur.log'), maxBytes=10000000, backupCount=100) + + handler.setFormatter(Formatter( + '%(asctime)s %(levelname)s: %(message)s ' + '[in %(pathname)s:%(lineno)d]' + )) + + handler.setLevel(app.config.get('LOG_LEVEL', 'DEBUG')) + app.logger.setLevel(app.config.get('LOG_LEVEL', 'DEBUG')) + app.logger.addHandler(handler) + diff --git a/lemur/listeners/__init__.py b/lemur/listeners/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/listeners/models.py b/lemur/listeners/models.py new file mode 100644 index 00000000..8f83437d --- /dev/null +++ b/lemur/listeners/models.py @@ -0,0 +1,43 @@ +""" +.. module: lemur.elbs.models + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +from sqlalchemy import Column, Integer, BigInteger, String, ForeignKey, DateTime, PassiveDefault, func + +from lemur.database import db +from lemur.certificates import service as cert_service +from lemur.certificates.models import Certificate, get_name_from_arn + + +class Listener(db.Model): + __tablename__ = 'listeners' + id = Column(BigInteger, primary_key=True) + certificate_id = Column(Integer, ForeignKey(Certificate.id), index=True) + elb_id = Column(BigInteger, ForeignKey("elbs.id"), index=True) + instance_port = Column(Integer) + instance_protocol = Column(String(16)) + load_balancer_port = Column(Integer) + load_balancer_protocol = Column(String(16)) + date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) + + def __init__(self, listener): + self.load_balancer_port = listener.load_balancer_port + self.load_balancer_protocol = listener.protocol + self.instance_port = listener.instance_port + self.instance_protocol = listener.instance_protocol + if listener.ssl_certificate_id not in ["Invalid-Certificate", None]: + self.certificate_id = cert_service.get_by_name(get_name_from_arn(listener.ssl_certificate_id)).id + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + def serialize(self): + blob = self.as_dict() + del blob['date_created'] + return blob + diff --git a/lemur/listeners/service.py b/lemur/listeners/service.py new file mode 100644 index 00000000..5cbe2429 --- /dev/null +++ b/lemur/listeners/service.py @@ -0,0 +1,162 @@ +""" +.. module: lemur.listeners.service + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +from sqlalchemy import func + +from lemur import database + +from lemur.exceptions import CertificateUnavailable + +from lemur.elbs.models import ELB +from lemur.listeners.models import Listener +from lemur.elbs import service as elb_service +from lemur.certificates import service as certificate_service + +from lemur.common.services.aws.elb import update_listeners, create_new_listeners, delete_listeners + + +def verify_attachment(certificate_id, elb_account_number): + """ + Ensures that the certificate we want ot attach to our listener is + in the same account as our listener. + + :rtype : Certificate + :param certificate_id: + :param elb_account_number: + :return: :raise CertificateUnavailable: + """ + cert = certificate_service.get(certificate_id) + + # we need to ensure that the specified cert is in our account + for account in cert.accounts: + if account.account_number == elb_account_number: + break + else: + raise CertificateUnavailable + return cert + + +def get(listener_id): + return database.get(Listener, listener_id) + + +def create(elb_id, instance_protocol, instance_port, load_balancer_port, load_balancer_protocol, certificate_id=None): + listener = Listener(elb_id, + instance_port, + instance_protocol, + load_balancer_port, + load_balancer_protocol + ) + + elb = elb_service.get(elb_id) + elb.listeners.append(listener) + account_number = elb.account.account_number + + cert = verify_attachment(certificate_id, account_number) + listener_tuple = (load_balancer_port, instance_port, load_balancer_protocol, cert.get_art(account_number),) + create_new_listeners(account_number, elb.region, elb.name, [listener_tuple]) + + return {'message': 'Listener has been created'} + + +def update(listener_id, **kwargs): + listener = get(listener_id) + + # if the lb_port has changed we need to make sure we are deleting + # the listener on the old port to avoid listener duplication + ports = [] + if listener.load_balancer_port != kwargs.get('load_balancer_port'): + ports.append(listener.load_balancer_port) + else: + ports.append(kwargs.get('load_balancer_port')) + + certificate_id = kwargs.get('certificate_id') + + listener.instance_port = kwargs.get('instance_port') + listener.instance_protocol = kwargs.get('instance_protocol') + listener.load_balancer_port = kwargs.get('load_balancer_port') + listener.load_balancer_protocol = kwargs.get('load_balancer_protocol') + + elb = listener.elb + account_number = listener.elb.account.account_number + + arn = None + if certificate_id: + cert = verify_attachment(certificate_id, account_number) + cert.elb_listeners.append(listener) + arn = cert.get_arn(account_number) + + # remove certificate that is no longer wanted + if listener.certificate and not certificate_id: + listener.certificate.remove() + + database.update(listener) + listener_tuple = (listener.load_balancer_port, listener.instance_port, listener.load_balancer_protocol, arn,) + update_listeners(account_number, elb.region, elb.name, [listener_tuple], ports) + + return {'message': 'Listener has been updated'} + + +def delete(listener_id): + # first try to delete the listener in aws + listener = get(listener_id) + delete_listeners(listener.elb.account.account_number, listener.elb.region, listener.elb.name, [listener.load_balancer_port]) + # cleanup operation in lemur + database.delete(listener) + + +def render(args): + query = database.session_query(Listener) + + sort_by = args.pop('sort_by') + sort_dir = args.pop('sort_dir') + page = args.pop('page') + count = args.pop('count') + filt = args.pop('filter') + certificate_id = args.pop('certificate_id', None) + elb_id = args.pop('elb_id', None) + + if certificate_id: + query = database.get_all(Listener, certificate_id, field='certificate_id') + + if elb_id: + query = query.filter(Listener.elb_id == elb_id) + + if filt: + terms = filt.split(';') + query = database.filter(query, Listener, terms) + + query = database.find_all(query, Listener, args) + + if sort_by and sort_dir: + query = database.sort(query, Listener, sort_by, sort_dir) + + return database.paginate(query, page, count) + + +def stats(**kwargs): + attr = getattr(Listener, kwargs.get('metric')) + query = database.db.session.query(attr, func.count(attr)) + query = query.join(Listener.elb) + + if kwargs.get('account_id'): + query = query.filter(ELB.account_id == kwargs.get('account_id')) + + if kwargs.get('active') == 'true': + query = query.filter(Listener.certificate_id != None) + + items = query.group_by(attr).all() + results = [] + for key, count in items: + if key: + results.append({"key": key, "y": count}) + return results + + + diff --git a/lemur/listeners/views.py b/lemur/listeners/views.py new file mode 100644 index 00000000..b603d827 --- /dev/null +++ b/lemur/listeners/views.py @@ -0,0 +1,128 @@ +""" +.. module: lemur.listeners.service + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +from flask import Blueprint +from flask.ext.restful import reqparse, Api, fields + +from lemur.listeners import service +from lemur.auth.service import AuthenticatedResource +from lemur.auth.permissions import admin_permission +from lemur.common.utils import marshal_items, paginated_parser + + +mod = Blueprint('listeners', __name__) +api = Api(mod) + + +FIELDS = { + 'id': fields.Integer, + 'elbId': fields.Integer(attribute="elb_id"), + 'certificateId': fields.Integer(attribute="certificate_id"), + 'instancePort': fields.Integer(attribute="instance_port"), + 'instanceProtocol': fields.String(attribute="instance_protocol"), + 'loadBalancerPort': fields.Integer(attribute="load_balancer_port"), + 'loadBalancerProtocol': fields.String(attribute="load_balancer_protocol") +} + + +class ListenersList(AuthenticatedResource): + def __init__(self): + super(ListenersList, self).__init__() + + @marshal_items(FIELDS) + def get(self): + parser = paginated_parser.copy() + parser.add_argument('certificateId', type=int, dest='certificate_id', location='args') + args = parser.parse_args() + return service.render(args) + + +class ListenersCertificateList(AuthenticatedResource): + def __init__(self): + super(ListenersCertificateList, self).__init__() + + @marshal_items(FIELDS) + def get(self, certificate_id): + parser = paginated_parser.copy() + args = parser.parse_args() + args['certificate_id'] = certificate_id + return service.render(args) + + +class ListenersELBList(AuthenticatedResource): + def __init__(self): + super(ListenersELBList, self).__init__() + + @marshal_items(FIELDS) + def get(self, elb_id): + parser = paginated_parser.copy() + args = parser.parse_args() + args['elb_id'] = elb_id + return service.render(args) + + +class ListenersStats(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(ListenersStats, self).__init__() + + def get(self): + self.reqparse.add_argument('metric', type=str, location='args') + self.reqparse.add_argument('accountId', dest='account_id', location='args') + self.reqparse.add_argument('active', type=str, default='true', location='args') + + args = self.reqparse.parse_args() + + items = service.stats(**args) + return {"items": items, "total": len(items)} + + +class Listeners(AuthenticatedResource): + def __init__(self): + super(Listeners, self).__init__() + + @marshal_items(FIELDS) + def get(self, listener_id): + return service.get(listener_id) + + @admin_permission.require(http_exception=403) + @marshal_items(FIELDS) + def post(self): + self.reqparse.add_argument('elbId', type=str, dest='elb_id', required=True, location='json') + self.reqparse.add_argument('instanceProtocol', type=str, dest='instance_protocol', required=True, location='json') + self.reqparse.add_argument('instancePort', type=int, dest='instance_port', required=True, location='json') + self.reqparse.add_argument('loadBalancerProtocol', type=str, dest='load_balancer_protocol', required=True, location='json') + self.reqparse.add_argument('loadBalancerPort', type=int, dest='load_balancer_port', required=True, location='json') + self.reqparse.add_argument('certificateId', type=int, dest='certificate_id', location='json') + + args = self.reqparse.parse_args() + return service.create(**args) + + @admin_permission.require(http_exception=403) + @marshal_items(FIELDS) + def put(self, listener_id): + self.reqparse.add_argument('instanceProtocol', type=str, dest='instance_protocol', required=True, location='json') + self.reqparse.add_argument('instancePort', type=int, dest='instance_port', required=True, location='json') + self.reqparse.add_argument('loadBalancerProtocol', type=str, dest='load_balancer_protocol', required=True, location='json') + self.reqparse.add_argument('loadBalancerPort', type=int, dest='load_balancer_port', required=True, location='json') + self.reqparse.add_argument('certificateId', type=int, dest='certificate_id', location='json') + + args = self.reqparse.parse_args() + return service.update(listener_id, **args) + + @admin_permission.require(http_exception=403) + def delete(self, listener_id): + return service.delete(listener_id) + + +api.add_resource(ListenersList, '/listeners', endpoint='listeners') +api.add_resource(Listeners, '/listeners/', endpoint='listener') +api.add_resource(ListenersStats, '/listeners/stats', endpoint='listenersStats') +api.add_resource(ListenersCertificateList, '/certificates//listeners', endpoint='listenersCertificates') +api.add_resource(ListenersELBList, '/elbs//listeners', endpoint='elbListeners') diff --git a/lemur/manage.py b/lemur/manage.py new file mode 100644 index 00000000..4c54ebe4 --- /dev/null +++ b/lemur/manage.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python +import os +import sys +import base64 +from gunicorn.config import make_settings + +from flask import current_app +from flask.ext.script import Manager, Command, Option, Group, prompt_pass +from flask.ext.migrate import Migrate, MigrateCommand, stamp +from flask_script.commands import ShowUrls, Clean, Server + +from lemur import database +from lemur.users import service as user_service +from lemur.roles import service as role_service +from lemur.accounts import service as account_service +from lemur.certificates import service as cert_service + +from lemur.certificates.verify import verify_string +from lemur.certificates import sync +from lemur.elbs.sync import sync_all_elbs + +from lemur import create_app +from lemur.common.crypto import encrypt, decrypt, lock, unlock + +# Needed to be imported so that SQLAlchemy create_all can find our models +from lemur.users.models import User +from lemur.roles.models import Role +from lemur.authorities.models import Authority +from lemur.certificates.models import Certificate +from lemur.accounts.models import Account +from lemur.domains.models import Domain +from lemur.elbs.models import ELB +from lemur.listeners.models import Listener + +manager = Manager(create_app) +manager.add_option('-c', '--config', dest='config') + +migrate = Migrate(create_app) + +KEY_LENGTH = 40 +DEFAULT_CONFIG_PATH = '~/.lemur/lemur.conf.py' +DEFAULT_SETTINGS = 'lemur.conf.server' +SETTINGS_ENVVAR = 'LEMUR_CONF' + + +CONFIG_TEMPLATE = """ +# This is just Python which means you can inherit and tweak settings + +import os +_basedir = os.path.abspath(os.path.dirname(__file__)) + +ADMINS = frozenset(['']) + +THREADS_PER_PAGE = 8 + +############# +## General ## +############# + +# These will need to be set to `True` if you are developing locally +CORS = False +debug = False + +# You should consider storing these separately from your config +LEMUR_SECRET_TOKEN = '{secret_token}' +LEMUR_ENCRYPTION_KEY = '{encryption_key}' + +# this is a list of domains as regexes that only admins can issue +LEMUR_RESTRICTED_DOMAINS = [] + +################# +## Mail Server ## +################# + +# Lemur currently only supports SES for sending email, this address +# needs to be verified +LEMUR_EMAIL = '' +LEMUR_SECURITY_TEAM_EMAIL = [] + +############# +## Logging ## +############# + +LOG_LEVEL = "DEBUG" +LOG_FILE = "lemur.log" + + +############## +## Database ## +############## + +SQLALCHEMY_DATABASE_URI = '' + + +######### +## AWS ## +######### + +# Lemur will need STS assume role access to every account you want to monitor +#AWS_ACCOUNT_MAPPINGS = {{ +# '1111111111': 'myawsacount' +#}} + +## This is useful if you know you only want to monitor one account +#AWS_REGIONS = ['us-east-1'] + +#LEMUR_INSTANCE_PROFILE = 'Lemur' + +############# +## Issuers ## +############# + +# These will be dependent on which 3rd party that Lemur is +# configured to use. + +#CLOUDCA_URL = '' +#CLOUDCA_PEM_PATH = '' +#CLOUDCA_BUNDLE = '' + +# number of years to issue if not specified +#CLOUDCA_DEFAULT_VALIDITY = 2 + +#VERISIGN_URL = '' +#VERISIGN_PEM_PATH = '' +#VERISIGN_FIRST_NAME = '' +#VERISIGN_LAST_NAME = '' +#VERSIGN_EMAIL = '' +""" + +@MigrateCommand.command +def create(): + database.db.create_all() + stamp(revision='head') + + +@manager.command +def lock(): + """ + Encrypts all of the files in the `keys` directory with the password + given. This is a useful function to ensure that you do no check in + your key files into source code in clear text. + + :return: + """ + password = prompt_pass("Please enter the encryption password") + lock(password) + sys.stdout.write("[+] Lemur keys have been encrypted!\n") + + +@manager.command +def unlock(): + """ + Decrypts all of the files in the `keys` directory with the password + given. This is most commonly used during the startup sequence of Lemur + allowing it to go from source code to something that can communicate + with external services. + + :return: + """ + password = prompt_pass("Please enter the encryption password") + unlock(password) + sys.stdout.write("[+] Lemur keys have been unencrypted!\n") + + +@manager.command +def encrypt_file(source): + """ + Utility to encrypt sensitive files, Lemur will decrypt these + files when admin enters the correct password. + + Uses AES-256-CBC encryption + """ + dest = source + ".encrypted" + password = prompt_pass("Please enter the encryption password") + password1 = prompt_pass("Please confirm the encryption password") + if password != password1: + sys.stdout.write("[!] Encryption passwords do not match!\n") + return + + with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: + encrypt(in_file, out_file, password) + + sys.stdout.write("[+] Writing encryption files... {0}!\n".format(dest)) + + +@manager.command +def decrypt_file(source): + """ + Utility to decrypt, Lemur will decrypt these + files when admin enters the correct password. + + Assumes AES-256-CBC encryption + """ + # cleanup extensions a bit + if ".encrypted" in source: + dest = ".".join(source.split(".")[:-1]) + ".decrypted" + else: + dest = source + ".decrypted" + + password = prompt_pass("Please enter the encryption password") + + with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: + decrypt(in_file, out_file, password) + + sys.stdout.write("[+] Writing decrypted files... {0}!\n".format(dest)) + + +@manager.command +def check_revoked(): + """ + Function attempts to update Lemur's internal cache with revoked + certificates. This is called periodically by Lemur. It checks both + CRLs and OCSP to see if a certificate is revoked. If Lemur is unable + encounters an issue with verification it marks the certificate status + as `unknown`. + """ + for cert in cert_service.get_all_certs(): + if cert.chain: + status = verify_string(cert.body, cert.chain) + else: + status = verify_string(cert.body, "") + + cert.status = 'valid' if status else "invalid" + database.update(cert) + + +@manager.shell +def make_shell_context(): + """ + Creates a python REPL with several default imports + in the context of the current_app + + :return: + """ + return dict(current_app=current_app) + + +def generate_settings(): + """ + This command is run when ``default_path`` doesn't exist, or ``init`` is + run and returns a string representing the default data to put into their + settings file. + """ + output = CONFIG_TEMPLATE.format( + encryption_key=base64.b64encode(os.urandom(KEY_LENGTH)), + secret_token=base64.b64encode(os.urandom(KEY_LENGTH)) + ) + + return output + + +class Sync(Command): + """ + Attempts to run several methods Certificate discovery. This is + run on a periodic basis and updates the Lemur datastore with the + information it discovers. + """ + option_list = [ + Group( + Option('-a', '--all', action="store_true"), + Option('-b', '--aws', action="store_true"), + Option('-d', '--cloudca', action="store_true"), + Option('-s', '--source', action="store_true"), + exclusive=True, required=True + ) + ] + + def run(self, all, aws, cloudca, source): + sys.stdout.write("[!] Starting to sync with external sources!\n") + + if all or aws: + sys.stdout.write("[!] Starting to sync with AWS!\n") + try: + sync.aws() + #sync_all_elbs() + sys.stdout.write("[+] Finished syncing with AWS!\n") + except Exception as e: + sys.stdout.write("[-] Syncing with AWS failed!\n") + + if all or cloudca: + sys.stdout.write("[!] Starting to sync with CloudCA!\n") + try: + sync.cloudca() + sys.stdout.write("[+] Finished syncing with CloudCA!\n") + except Exception as e: + sys.stdout.write("[-] Syncing with CloudCA failed!\n") + + sys.stdout.write("[!] Starting to sync with Source Code!\n") + + if all or source: + try: + sync.source() + sys.stdout.write("[+] Finished syncing with Source Code!\n") + except Exception as e: + sys.stdout.write("[-] Syncing with Source Code failed!\n") + + sys.stdout.write("[+] Finished syncing with external sources!\n") + + +class InitializeApp(Command): + """ + This command will bootstrap our database with any accounts as + specified by our config. + + Additionally a Lemur user will be created as a default user + and be used when certificates are discovered by Lemur. + """ + def run(self): + create() + user = user_service.get_by_username("lemur") + + if not user: + sys.stdout.write("We need to set Lemur's password to continue!\n") + password1 = prompt_pass("Password") + password2 = prompt_pass("Confirm Password") + + if password1 != password2: + sys.stderr.write("[!] Passwords do not match!\n") + sys.exit(1) + + role = role_service.get_by_name('admin') + + if role: + sys.stdout.write("[-] Admin role already created, skipping...!\n") + else: + # we create an admin role + role = role_service.create('admin', description='this is the lemur administrator role') + sys.stdout.write("[+] Created 'admin' role\n") + + user_service.create("lemur", password1, 'lemur@nobody', True, None, [role]) + sys.stdout.write("[+] Added a 'lemur' user and added it to the 'admin' role!\n") + + else: + sys.stdout.write("[-] Default user has already been created, skipping...!\n") + + for account_name, account_number in current_app.config.get('AWS_ACCOUNT_MAPPINGS').items(): + account = account_service.get_by_account_number(account_number) + + if not account: + account_service.create(account_number, label=account_name) + sys.stdout.write("[+] Added new account {0}:{1}!\n".format(account_number, account_name)) + else: + sys.stdout.write("[-] Account already exists, skipping...!\n") + + sys.stdout.write("[/] Done!\n") + + + +#def install_issuers(settings): +# """ +# Installs new issuers that are not currently bundled with Lemur. +# +# :param settings: +# :return: +# """ +# from lemur.issuers import register +# # entry_points={ +# # 'lemur.issuers': [ +# # 'verisign = lemur_issuers.issuers:VerisignPlugin' +# # ], +# # }, +# installed_apps = list(settings.INSTALLED_APPS) +# for ep in pkg_resources.iter_entry_points('lemur.apps'): +# try: +# issuer = ep.load() +# except Exception: +# import sys +# import traceback +# +# sys.stderr.write("Failed to load app %r:\n%s\n" % (ep.name, traceback.format_exc())) +# else: +# installed_apps.append(ep.module_name) +# settings.INSTALLED_APPS = tuple(installed_apps) +# +# for ep in pkg_resources.iter_entry_points('lemur.issuers'): +# try: +# issuer = ep.load() +# except Exception: +# import sys +# import traceback +# +# sys.stderr.write("Failed to load issuer %r:\n%s\n" % (ep.name, traceback.format_exc())) +# else: +# register(issuer) + + +class CreateUser(Command): + """ + This command allows for the creation of a new user within Lemur + """ + option_list = ( + Option('-u', '--username', dest='username', required=True), + Option('-e', '--email', dest='email', required=True), + Option('-a', '--active', dest='active', default=True), + Option('-r', '--roles', dest='roles', default=[]) + ) + + def run(self, username, email, active, roles): + role_objs = [] + for r in roles: + role_obj = role_service.get_by_name(r) + if role_obj: + role_objs.append(role_obj) + else: + sys.stderr.write("[!] Cannot find role {0}".format(r)) + sys.exit(1) + + password1 = prompt_pass("Password") + password2 = prompt_pass("Confirm Password") + + if password1 != password2: + sys.stderr.write("[!] Passwords do not match") + sys.exit(1) + + user_service.create(username, password1, email, active, None, role_objs) + sys.stdout.write("[+] Created new user: {0}".format(username)) + + +class CreateRole(Command): + """ + This command allows for the creation of a new role within Lemur + """ + option_list = ( + Option('-n', '--name', dest='name', required=True), + Option('-u', '--users', dest='users', default=[]), + Option('-d', '--description', dest='description', required=True) + ) + + def run(self, name, users, description): + user_objs = [] + for u in users: + user_obj = user_service.get_by_username(u) + if user_obj: + user_objs.append(user_obj) + else: + sys.stderr.write("[!] Cannot find user {0}".format(u)) + sys.exit(1) + role_service.create(name, description=description, users=users) + sys.stdout.write("[+] Created new role: {0}".format(name)) + + +@manager.command +def create_config(config_path=None): + """ + Creates a new configuration file if one does not already exist + """ + if not config_path: + config_path = DEFAULT_CONFIG_PATH + + config_path = os.path.expanduser(config_path) + dir = os.path.dirname(config_path) + if not os.path.exists(dir): + os.makedirs(dir) + + config = generate_settings() + with open(config_path, 'w') as f: + f.write(config) + + sys.stdout.write("Created a new configuration file {0}\n".format(config_path)) + + +class LemurServer(Command): + """ + This is the main Lemur server, it runs the flask app with gunicorn and + uses any configuration options passed to it. + + + You can pass all standard gunicorn flags to this command as if you were + running gunicorn itself. + + For example: + + lemur start -w 4 -b 127.0.0.0:8002 + + Will start gunicorn with 4 workers bound to 127.0.0.0:8002 + """ + description = 'Run the app within Gunicorn' + + def get_options(self): + settings = make_settings() + options = ( + Option(*klass.cli, action=klass.action) + for setting, klass in settings.iteritems() if klass.cli + ) + + return options + + def run(self, *args, **kwargs): + from gunicorn.app.wsgiapp import WSGIApplication + + app = WSGIApplication() + app.app_uri = 'lemur:create_app(config="{0}")'.format(kwargs.get('config')) + + return app.run() + + +def main(): + manager.add_command("start", LemurServer()) + manager.add_command("runserver", Server(host='127.0.0.1')) + manager.add_command("clean", Clean()) + manager.add_command("show_urls", ShowUrls()) + manager.add_command("db", MigrateCommand) + manager.add_command("init", InitializeApp()) + manager.add_command('create_user', CreateUser()) + manager.add_command('create_role', CreateRole()) + manager.add_command("sync", Sync()) + manager.run() diff --git a/lemur/migrations/README b/lemur/migrations/README new file mode 100644 index 00000000..98e4f9c4 --- /dev/null +++ b/lemur/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/lemur/migrations/alembic.ini b/lemur/migrations/alembic.ini new file mode 100644 index 00000000..f8ed4801 --- /dev/null +++ b/lemur/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/lemur/migrations/env.py b/lemur/migrations/env.py new file mode 100644 index 00000000..0a038e6c --- /dev/null +++ b/lemur/migrations/env.py @@ -0,0 +1,73 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + engine = engine_from_config(config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + connection = engine.connect() + context.configure(connection=connection, + target_metadata=target_metadata, + **current_app.extensions['migrate'].configure_args) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() + diff --git a/lemur/migrations/script.py.mako b/lemur/migrations/script.py.mako new file mode 100644 index 00000000..95702017 --- /dev/null +++ b/lemur/migrations/script.py.mako @@ -0,0 +1,22 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision} +Create Date: ${create_date} + +""" + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/lemur/models.py b/lemur/models.py new file mode 100644 index 00000000..7ecfa129 --- /dev/null +++ b/lemur/models.py @@ -0,0 +1,30 @@ +""" +.. module: lemur.models + :platform: Unix + :synopsis: This module contains all of the associative tables + that help define the many to many relationships established in Lemur + + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" + +from sqlalchemy import Column, Integer, ForeignKey + +from lemur.database import db + +certificate_associations = db.Table('certificate_associations', + Column('domain_id', Integer, ForeignKey('domains.id')), + Column('certificate_id', Integer, ForeignKey('certificates.id')) +) + +certificate_account_associations = db.Table('certificate_account_associations', + Column('account_id', Integer, ForeignKey('accounts.id', ondelete='cascade')), + Column('certificate_id', Integer, ForeignKey('certificates.id', ondelete='cascade')) +) + +roles_users = db.Table('roles_users', + Column('user_id', Integer, ForeignKey('users.id')), + Column('role_id', Integer, ForeignKey('roles.id')) +) + diff --git a/lemur/notifications.py b/lemur/notifications.py new file mode 100644 index 00000000..09bcfec7 --- /dev/null +++ b/lemur/notifications.py @@ -0,0 +1,184 @@ +""" +.. module: lemur.notifications + :platform: Unix + + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +import ssl +import socket + +import arrow +from flask import current_app + +from lemur import database +from lemur.common.services.aws import ses +from lemur.certificates.models import Certificate +from lemur.domains.models import Domain + +NOTIFICATION_INTERVALS = [30, 15, 5, 2] + + +def _get_domain_certificate(name): + """ + Fetch the SSL certificate currently hosted at a given domain (if any) and + compare it against our all of our know certificates to determine if a new + SSL certificate has already been deployed + + :param name: + :return: + """ + query = database.session_query(Certificate) + try: + pub_key = ssl.get_server_certificate((name, 443)) + return query.filter(Certificate.body == pub_key.strip()).first() + + except socket.gaierror as e: + current_app.logger.info(str(e)) + + +def _find_superseded(domains): + """ + Here we try to fetch any domain in the certificate to see if we can resolve it + and to try and see if it is currently serving the certificate we are + alerting on + + :param domains: + :return: + """ + query = database.session_query(Certificate) + ss_list = [] + for domain in domains: + dc = _get_domain_certificate(domain.name) + if dc: + ss_list.append(dc) + current_app.logger.info("Trying to resolve {0}".format(domain.name)) + + query = query.filter(Certificate.domains.any(Domain.name.in_([x.name for x in domains]))) + query = query.filter(Certificate.active == True) + query = query.filter(Certificate.not_after >= arrow.utcnow().format('YYYY-MM-DD')) + ss_list.extend(query.all()) + + return ss_list + + +def send_expiration_notifications(): + """ + This function will check for upcoming certificate expiration, + and send out notification emails at given intervals. + """ + notifications = 0 + certs = _get_expiring_certs() + + alerts = [] + for cert in certs: + if _is_eligible_for_notifications(cert): + data = _get_message_data(cert) + recipients = _get_message_recipients(cert) + alerts.append((data, recipients)) + + roll_ups = _create_roll_ups(alerts) + + for messages, recipients in roll_ups: + notifications += 1 + ses.send("Certificate Expiration", dict(messages=messages), 'event', recipients) + + print notifications + current_app.logger.info("Lemur has sent {0} certification notifications".format(notifications)) + + +def _get_message_recipients(cert): + """ + Determine who the recipients of the certificate expiration should be + + :param cert: + :return: + """ + recipients = [] + if current_app.config.get('SECURITY_TEAM_EMAIL'): + recipients.extend(current_app.config.get('SECURITY_TEAM_EMAIL')) + + recipients.append(cert.owner) + + if cert.user: + recipients.append(cert.user.email) + return list(set(recipients)) + + +def _get_message_data(cert): + """ + Parse our the certification information needed for our notification + + :param cert: + :return: + """ + cert_dict = cert.as_dict() + cert_dict['domains'] = [x .name for x in cert.domains] + cert_dict['superseded'] = list(set([x.name for x in _find_superseded(cert.domains) if cert.name != x])) + return cert_dict + + +def _get_expiring_certs(outlook=30): + """ + Find all the certificates expiring within a given outlook + + :param outlook: int days to look forward + :return: + """ + now = arrow.utcnow() + + query = database.session_query(Certificate) + attr = Certificate.not_after + + # get all certs expiring in the next 30 days + to = now.replace(days=+outlook).format('YYYY-MM-DD') + + certs = [] + for cert in query.filter(attr <= to).filter(attr >= now.format('YYYY-MM-DD')).all(): + if _is_eligible_for_notifications(cert): + certs.append(cert) + return certs + + +def _is_eligible_for_notifications(cert, intervals=None): + """ + Determine if notifications for a given certificate should + currently be sent + + :param cert: + :param intervals: list of days to alert on + :return: + """ + now = arrow.utcnow() + if cert.active: + days = (cert.not_after - now.naive).days + + if not intervals: + intervals = NOTIFICATION_INTERVALS + + if days in intervals: + return cert + + +def _create_roll_ups(messages): + """ + Take all of the messages that should be sent and provide + a roll up to the same set if the recipients are the same + + :param messages: + """ + roll_ups = [] + for message_data, recipients in messages: + for m, r in roll_ups: + if r == recipients: + m.append(message_data) + current_app.logger.info( + "Sending email expiration alert about {0} to {1}".format( + message_data['name'], ",".join(recipients))) + break + else: + roll_ups.append(([message_data], recipients)) + return roll_ups diff --git a/lemur/roles/__init__.py b/lemur/roles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/roles/models.py b/lemur/roles/models.py new file mode 100644 index 00000000..9df2a4fb --- /dev/null +++ b/lemur/roles/models.py @@ -0,0 +1,39 @@ +""" +.. module: models + :platform: unix + :synopsis: This module contains all of the models need to create a role within Lemur + + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +import os +from sqlalchemy.orm import relationship +from sqlalchemy import Column, Integer, String, Text, ForeignKey + +from sqlalchemy_utils import EncryptedType + +from lemur.database import db +from lemur.models import roles_users + + +class Role(db.Model): + __tablename__ = 'roles' + id = Column(Integer, primary_key=True) + name = Column(String(128), unique=True) + username = Column(String(128)) + password = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY'))) + description = Column(Text) + authority_id = Column(Integer, ForeignKey('authorities.id')) + user_id = Column(Integer, ForeignKey('users.id')) + users = relationship("User", secondary=roles_users, passive_deletes=True, backref="role", cascade='all,delete') + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + def serialize(self): + blob = self.as_dict() + return blob + diff --git a/lemur/roles/service.py b/lemur/roles/service.py new file mode 100644 index 00000000..92c1011d --- /dev/null +++ b/lemur/roles/service.py @@ -0,0 +1,125 @@ +""" +.. module: service + :platform: Unix + :synopsis: This module contains all of the services level functions used to + administer roles in Lemur + + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from flask import g + +from lemur import database +from lemur.roles.models import Role +from lemur.users.models import User + +def update(role_id, name, description, users): + """ + Update a role + + :param role_id: + :param name: + :param description: + :param users: + :return: + """ + role = get(role_id) + role.name = name + role.description = description + role = database.update_list(role, 'users', User, users) + database.update(role) + return role + + +def create(name, password=None, description=None, username=None, users=None): + """ + Create a new role + + :param name: + :param users: + :param description: + :param username: + :param password: + :return: + """ + role = Role(name=name, description=description, username=username, password=password) + + if users: + role = database.update_list(role, 'users', User, users) + + return database.create(role) + + +def get(role_id): + """ + Retrieve a role by ID + + :param role_id: + :return: + """ + return database.get(Role, role_id) + + +def get_by_name(role_name): + """ + Retrieve a role by it's name + + :param role_name: + :return: + """ + return database.get(Role, role_name, field='name') + + +def delete(role_id): + """ + Remove a role + + :param role_id: + :return: + """ + return database.delete(get(role_id)) + + +def render(args): + """ + Helper that filters subsets of roles depending on the parameters + passed to the REST Api + + :param args: + :return: + """ + query = database.session_query(Role) + sort_by = args.pop('sort_by') + sort_dir = args.pop('sort_dir') + page = args.pop('page') + count = args.pop('count') + filt = args.pop('filter') + user_id = args.pop('user_id', None) + authority_id = args.pop('authority_id', None) + + if user_id: + query = query.filter(Role.users.any(User.id == user_id)) + + if authority_id: + query = query.filter(Role.authority_id == authority_id) + + # we make sure that user can see the role - admins can see all + if not g.current_user.is_admin: + ids = [] + for role in g.current_user.roles: + ids.append(role.id) + query = query.filter(Role.id.in_(ids)) + + if filt: + terms = filt.split(';') + query = database.filter(query, Role, terms) + + query = database.find_all(query, Role, args) + + if sort_by and sort_dir: + query = database.sort(query, Role, sort_by, sort_dir) + + return database.paginate(query, page, count) + diff --git a/lemur/roles/views.py b/lemur/roles/views.py new file mode 100644 index 00000000..85774eb1 --- /dev/null +++ b/lemur/roles/views.py @@ -0,0 +1,445 @@ +""" +.. module: lemur.roles.views + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson + +""" +from flask import Blueprint +from flask import make_response, jsonify, abort, g +from flask.ext.restful import reqparse, fields, Api + +from lemur.roles import service +from lemur.auth.service import AuthenticatedResource +from lemur.auth.permissions import ViewRoleCredentialsPermission, admin_permission +from lemur.common.utils import marshal_items, paginated_parser + + +mod = Blueprint('roles', __name__) +api = Api(mod) + + +FIELDS = { + 'name': fields.String, + 'description': fields.String, + 'id': fields.Integer, +} + + +class RolesList(AuthenticatedResource): + """ Defines the 'roles' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(RolesList, self).__init__() + + @marshal_items(FIELDS) + def get(self): + """ + .. http:get:: /roles + + The current role list + + **Example request**: + + .. sourcecode:: http + + GET /roles HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "items": [ + { + "id": 1, + "name": "role1", + "description": "this is role1" + }, + { + "id": 2, + "name": "role2", + "description": "this is role2" + } + ] + "total": 2 + } + + :query sortBy: field to sort on + :query sortDir: acs or desc + :query page: int. default is 1 + :query filter: key value pair. format is k=v; + :query limit: limit number. default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + parser = paginated_parser.copy() + parser.add_argument('owner', type=str, location='args') + parser.add_argument('id', type=str, location='args') + + args = parser.parse_args() + return service.render(args) + + @admin_permission.require(http_exception=403) + @marshal_items(FIELDS) + def post(self): + """ + .. http:post:: /roles + + Creates a new role + + **Example request**: + + .. sourcecode:: http + + POST /roles HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "name": "role3", + "description": "this is role3", + "username": null, + "password": null, + "users": [] + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 3, + "description": "this is role3", + "name": "role3" + } + + :arg name: name for new role + :arg description: description for new role + :arg password: password for new role + :arg username: username for new role + :arg users: list, of users to associate with role + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + self.reqparse.add_argument('name', type=str, location='json', required=True) + self.reqparse.add_argument('description', type=str, location='json') + self.reqparse.add_argument('username', type=str, location='json') + self.reqparse.add_argument('password', type=str, location='json') + self.reqparse.add_argument('users', type=dict, location='json') + + args = self.reqparse.parse_args() + return service.create(args['name'], args.get('password'), args.get('description'), args.get('username'), + args.get('users')) + + +class RoleViewCredentials(AuthenticatedResource): + def __init__(self): + super(RoleViewCredentials, self).__init__() + + def get(self, role_id): + """ + .. http:get:: /roles/1/credentials + + View a roles credentials + + **Example request**: + + .. sourcecode:: http + + GET /users/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "username: "ausername", + "password": "apassword" + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + permission = ViewRoleCredentialsPermission(role_id) + if permission.can(): + role = service.get(role_id) + response = make_response(jsonify(username=role.username, password=role.password), 200) + response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store' + response.headers['pragma'] = 'no-cache' + return response + abort(403) + + +class Roles(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Roles, self).__init__() + + @marshal_items(FIELDS) + def get(self, role_id): + """ + .. http:get:: /roles/1 + + Get a particular role + + **Example request**: + + .. sourcecode:: http + + GET /roles/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "role1", + "description": "this is role1" + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + # we want to make sure that we cannot view roles that we are not members of + if not g.current_user.is_admin: + user_role_ids = set([r.id for r in g.current_user.roles]) + if role_id not in user_role_ids: + return dict(message="You are not allowed to view a role which you are not a member of"), 400 + + return service.get(role_id) + + @marshal_items(FIELDS) + def put(self, role_id): + """ + .. http:put:: /roles/1 + + Update a role + + **Example request**: + + .. sourcecode:: http + + PUT /roles/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "name": "role1", + "description": "This is a new description" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "name": "role1", + "description": "this is a new description" + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + permission = ViewRoleCredentialsPermission(role_id) + if permission.can(): + self.reqparse.add_argument('name', type=str, location='json', required=True) + self.reqparse.add_argument('description', type=str, location='json') + self.reqparse.add_argument('users', type=list, location='json') + args = self.reqparse.parse_args() + return service.update(role_id, args['name'], args.get('description'), args.get('users')) + abort(403) + + @admin_permission.require(http_exception=403) + def delete(self, role_id): + """ + .. http:delete:: /roles/1 + + Delete a role + + **Example request**: + + .. sourcecode:: http + + DELETE /roles/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "message": "ok" + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + service.delete(role_id) + return {'message': 'ok'} + + +class UserRolesList(AuthenticatedResource): + """ Defines the 'roles' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(UserRolesList, self).__init__() + + @marshal_items(FIELDS) + def get(self, user_id): + """ + .. http:get:: /users/1/roles + + List of roles for a given user + + **Example request**: + + .. sourcecode:: http + + GET /users/1/roles HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "items": [ + { + "id": 1, + "name": "role1", + "description": "this is role1" + }, + { + "id": 2, + "name": "role2", + "description": "this is role2" + } + ] + "total": 2 + } + + :query sortBy: field to sort on + :query sortDir: acs or desc + :query page: int. default is 1 + :query filter: key value pair. format is k=v; + :query limit: limit number. default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + parser = paginated_parser.copy() + args = parser.parse_args() + args['user_id'] = user_id + return service.render(args) + + +class AuthorityRolesList(AuthenticatedResource): + """ Defines the 'roles' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(AuthorityRolesList, self).__init__() + + @marshal_items(FIELDS) + def get(self, authority_id): + """ + .. http:get:: /authorities/1/roles + + List of roles for a given authority + + **Example request**: + + .. sourcecode:: http + + GET /authorities/1/roles HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "items": [ + { + "id": 1, + "name": "role1", + "description": "this is role1" + }, + { + "id": 2, + "name": "role2", + "description": "this is role2" + } + ] + "total": 2 + } + + :query sortBy: field to sort on + :query sortDir: acs or desc + :query page: int. default is 1 + :query filter: key value pair. format is k=v; + :query limit: limit number. default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + parser = paginated_parser.copy() + args = parser.parse_args() + args['authority_id'] = authority_id + return service.render(args) + + +api.add_resource(RolesList, '/roles', endpoint='roles') +api.add_resource(Roles, '/roles/', endpoint='role') +api.add_resource(RoleViewCredentials, '/roles//credentials', endpoint='roleCredentials`') +api.add_resource(AuthorityRolesList, '/authorities//roles', endpoint='authorityRoles') +api.add_resource(UserRolesList, '/users//roles', endpoint='userRoles') diff --git a/lemur/static/app/.buildignore b/lemur/static/app/.buildignore new file mode 100644 index 00000000..fc98b8eb --- /dev/null +++ b/lemur/static/app/.buildignore @@ -0,0 +1 @@ +*.coffee \ No newline at end of file diff --git a/lemur/static/app/404.html b/lemur/static/app/404.html new file mode 100644 index 00000000..ec98e3c2 --- /dev/null +++ b/lemur/static/app/404.html @@ -0,0 +1,157 @@ + + + + + Page Not Found :( + + + +
+

Not found :(

+

Sorry, but the page you were trying to view does not exist.

+

It looks like this was the result of either:

+
    +
  • a mistyped address
  • +
  • an out-of-date link
  • +
+ + +
+ + diff --git a/lemur/static/app/angular/accounts/account/account.js b/lemur/static/app/angular/accounts/account/account.js new file mode 100644 index 00000000..34281f49 --- /dev/null +++ b/lemur/static/app/angular/accounts/account/account.js @@ -0,0 +1,27 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/accounts/create', { + templateUrl: '/angular/accounts/account/account.tpl.html', + controller: 'AccountsCreateController' + }); + $routeProvider.when('/accounts/:id/edit', { + templateUrl: '/angular/accounts/account/account.tpl.html', + controller: 'AccountsEditController' + }); + }) + + .controller('AccountsCreateController', function ($scope, AccountService, LemurRestangular){ + $scope.account = LemurRestangular.restangularizeElement(null, {}, 'accounts'); + $scope.save = AccountService.create; + }) + + .controller('AccountsEditController', function ($scope, $routeParams, AccountService, AccountApi) { + AccountApi.get($routeParams.id).then(function (account) { + $scope.account = account; + }); + + $scope.save = AccountService.update; + }); diff --git a/lemur/static/app/angular/accounts/account/account.tpl.html b/lemur/static/app/angular/accounts/account/account.tpl.html new file mode 100644 index 00000000..c30b91fd --- /dev/null +++ b/lemur/static/app/angular/accounts/account/account.tpl.html @@ -0,0 +1,45 @@ +

CreateEdit Account next in line please +

+
+
+ Cancel +
+
+
+
+
+ +
+ +

You must enter an account name

+
+
+
+ +
+ +

You must enter an account number

+
+
+
+ +
+ +
+
+
+
+ +
+ diff --git a/lemur/static/app/angular/accounts/services.js b/lemur/static/app/angular/accounts/services.js new file mode 100644 index 00000000..00bff23a --- /dev/null +++ b/lemur/static/app/angular/accounts/services.js @@ -0,0 +1,53 @@ +'use strict'; +angular.module('lemur') + .service('AccountApi', function (LemurRestangular) { + return LemurRestangular.all('accounts'); + }) + .service('AccountService', function ($location, AccountApi, toaster) { + var AccountService = this; + AccountService.findAccountsByName = function (filterValue) { + return AccountApi.getList({'filter[label]': filterValue}) + .then(function (accounts) { + return accounts; + }); + }; + + AccountService.create = function (account) { + AccountApi.post(account).then( + function () { + toaster.pop({ + type: 'success', + title: account.label, + body: 'Successfully created!' + }); + $location.path('accounts'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: account.label, + body: 'Was not created! ' + response.data.message + }); + }); + }; + + AccountService.update = function (account) { + account.put().then( + function () { + toaster.pop({ + type: 'success', + title: account.label, + body: 'Successfully updated!' + }); + $location.path('accounts'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: account.label, + body: 'Was not updated! ' + response.data.message + }); + }); + }; + return AccountService; + }); diff --git a/lemur/static/app/angular/accounts/view/view.js b/lemur/static/app/angular/accounts/view/view.js new file mode 100644 index 00000000..447ee97c --- /dev/null +++ b/lemur/static/app/angular/accounts/view/view.js @@ -0,0 +1,52 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/accounts', { + templateUrl: '/angular/accounts/view/view.tpl.html', + controller: 'AccountsViewController' + }); + }) + + .controller('AccountsViewController', function ($scope, AccountApi, AccountService, ngTableParams, toaster) { + $scope.filter = {}; + $scope.accountsTable = new ngTableParams({ + page: 1, // show first page + count: 10, // count per page + sorting: { + id: 'desc' // initial sorting + }, + filter: $scope.filter + }, { + total: 0, // length of data + getData: function ($defer, params) { + AccountApi.getList(params.url()).then( + function (data) { + params.total(data.total); + $defer.resolve(data); + } + ); + } + }); + + $scope.remove = function (account) { + account.remove().then( + function () { + $scope.accountsTable.reload(); + }, + function (response) { + toaster.pop({ + type: 'error', + title: 'Opps', + body: 'I see what you did there' + response.data.message + }); + } + ); + }; + + $scope.toggleFilter = function (params) { + params.settings().$scope.show_filter = !params.settings().$scope.show_filter; + }; + + }); diff --git a/lemur/static/app/angular/accounts/view/view.tpl.html b/lemur/static/app/angular/accounts/view/view.tpl.html new file mode 100644 index 00000000..176a4fae --- /dev/null +++ b/lemur/static/app/angular/accounts/view/view.tpl.html @@ -0,0 +1,44 @@ +
+
+

Accounts + next in line please

+
+
+
+ Create +
+
+ +
+
+
+
+ + + + + + + + +
+
    +
  • {{ account.label }}
  • +
  • {{ account.comments }}
  • +
+
+ {{ account.accountNumber }} + +
+ + Edit + + +
+
+
+
+
+
diff --git a/lemur/static/app/angular/app.js b/lemur/static/app/angular/app.js new file mode 100644 index 00000000..af4957f9 --- /dev/null +++ b/lemur/static/app/angular/app.js @@ -0,0 +1,113 @@ +'use strict'; + +var lemur = angular + .module('lemur', [ + 'ngRoute', + 'ngTable', + 'ngAnimate', + 'chart.js', + 'restangular', + 'angular-loading-bar', + 'ui.bootstrap', + 'angular-spinkit', + 'toaster', + 'uiSwitch', + 'mgo-angular-wizard', + 'satellizer' + ]) + .config(function ($routeProvider, $authProvider) { + $routeProvider + .when('/', { + templateUrl: 'angular/welcome/welcome.html' + }) + .otherwise({ + redirectTo: '/' + }); + + $authProvider.oauth2({ + name: 'ping', + url: 'http://localhost:5000/api/1/auth/ping', + redirectUri: 'http://localhost:3000/', + clientId: 'client-id', + responseType: 'code', + scope: ['openid', 'email', 'profile', 'address'], + scopeDelimiter: ' ', + authorizationEndpoint: 'https://example.com/as/authorization.oauth2', + requiredUrlParams: ['scope'] + }); + }); + +lemur.service('MomentService', function () { + this.diffMoment = function (start, end) { + if (end !== 'None') { + return moment(end, 'YYYY-MM-DD HH:mm Z').diff(moment(start, 'YYYY-MM-DD HH:mm Z'), 'minutes') + ' minutes'; + } + return 'Unknown'; + }; + this.createMoment = function (date) { + if (date !== 'None') { + return moment(date, 'YYYY-MM-DD HH:mm Z').fromNow(); + } + return 'Unknown'; + }; +}); + +lemur.controller('datePickerController', function ($scope, $timeout){ + $scope.open = function() { + $timeout(function() { + $scope.opened = true; + }); + }; +}); + +lemur.factory('LemurRestangular', function (Restangular, $location, $auth) { + return Restangular.withConfig(function (RestangularConfigurer) { + RestangularConfigurer.setBaseUrl('http://127.0.0.1:5000/api/1'); + RestangularConfigurer.setDefaultHttpFields({withCredentials: true}); + + RestangularConfigurer.addResponseInterceptor(function (data, operation, what, url, response, deferred) { + var extractedData; + + // .. to look for getList operations + if (operation === "getList") { + // .. and handle the data and meta data + extractedData = data.items; + extractedData.total = data.total; + } else { + extractedData = data; + } + return extractedData; + }); + + RestangularConfigurer.addFullRequestInterceptor(function (element, operation, route, url, headers, params, httpConfig) { + // We want to make sure the user is auth'd before any requests + if (!$auth.isAuthenticated()) { + $location.path('/login'); + return false; + } + + var regExp = /\[([^)]+)\]/; + + var s = 'sorting'; + var f = 'filter'; + var newParams = {}; + for (var item in params) { + if (item.indexOf(s) > -1) { + newParams.sortBy = regExp.exec(item)[1]; + newParams.sortDir = params[item]; + } else if (item.indexOf(f) > -1) { + var key = regExp.exec(item)[1]; + newParams['filter'] = key + ";" + params[item]; + } else { + newParams[item] = params[item]; + } + } + return { params: newParams }; + }); + + }); +}); + +lemur.run(['$templateCache', function ($templateCache) { + $templateCache.put('ng-table/pager.html', ''); +}]); diff --git a/lemur/static/app/angular/authentication/login/login.js b/lemur/static/app/angular/authentication/login/login.js new file mode 100644 index 00000000..b7cc7902 --- /dev/null +++ b/lemur/static/app/angular/authentication/login/login.js @@ -0,0 +1,28 @@ +'use strict'; + +angular.module('lemur') + .config(function config($routeProvider) { + $routeProvider.when('/login', { + templateUrl: '/angular/authentication/login/login.tpl.html', + controller: 'LoginController' + }); + }) + .controller('LoginController', function ($rootScope, $scope, AuthenticationService, UserService) { + $scope.login = AuthenticationService.login; + $scope.authenticate = AuthenticationService.authenticate; + $scope.logout = AuthenticationService.logout; + + UserService.getCurrentUser().then(function (user) { + $scope.currentUser = user; + }); + + $rootScope.$on('user:login', function () { + UserService.getCurrentUser().then(function (user) { + $scope.currentUser = user; + }); + }); + + $rootScope.$on('user:logout', function () { + $scope.currentUser = null; + }); + }); diff --git a/lemur/static/app/angular/authentication/login/login.tpl.html b/lemur/static/app/angular/authentication/login/login.tpl.html new file mode 100644 index 00000000..894a5e3a --- /dev/null +++ b/lemur/static/app/angular/authentication/login/login.tpl.html @@ -0,0 +1,27 @@ +

Login None shall pass

+
+ +
diff --git a/lemur/static/app/angular/authentication/logout/logout.js b/lemur/static/app/angular/authentication/logout/logout.js new file mode 100644 index 00000000..18cbd489 --- /dev/null +++ b/lemur/static/app/angular/authentication/logout/logout.js @@ -0,0 +1,12 @@ +'use strict'; + +angular.module('lemur') + .config(function config($routeProvider) { + $routeProvider.when('/logout', { + controller: 'LogoutCtrl' + }); + }) + .controller('LogoutCtrl', function ($scope, $location, lemurRestangular, userService) { + userService.logout(); + $location.path('/'); + }); diff --git a/lemur/static/app/angular/authentication/logout/logout.tpl.html b/lemur/static/app/angular/authentication/logout/logout.tpl.html new file mode 100644 index 00000000..d0658f25 --- /dev/null +++ b/lemur/static/app/angular/authentication/logout/logout.tpl.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lemur/static/app/angular/authentication/services.js b/lemur/static/app/angular/authentication/services.js new file mode 100644 index 00000000..7230a41d --- /dev/null +++ b/lemur/static/app/angular/authentication/services.js @@ -0,0 +1,62 @@ +'use strict'; +angular.module('lemur') + .service('AuthenticationApi', function (LemurRestangular) { + return LemurRestangular.all('auth'); + }) + .service('AuthenticationService', function ($location, $rootScope, AuthenticationApi, UserService, toaster, $auth) { + var AuthenticationService = this; + + AuthenticationService.login = function (username, password) { + AuthenticationApi.customPOST({'username': username, 'password': password}, 'login') + .then( + function (user) { + $auth.setToken(user.token, true); + $rootScope.$emit('user:login'); + $location.url('/certificates'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: 'Whoa there', + body: response.data.message, + showCloseButton: true + }); + } + ); + }; + + AuthenticationService.authenticate = function (provider) { + $auth.authenticate(provider) + .then( + function (user) { + UserService.getCurrentUser(); + $rootScope.$emit('user:login'); + $location.url('/certificates'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: 'Something went wrong', + body: response.data.message + }); + } + ); + } + + AuthenticationService.logout = function () { + if (!$auth.isAuthenticated()) { + return; + } + $auth.logout() + .then(function() { + $rootScope.$emit('user:logout'); + toaster.pop({ + type: 'success', + title: 'Good job!', + body: 'You have been successfully logged out.' + }); + $location.path('/'); + }) + }; + + }); diff --git a/lemur/static/app/angular/authentication/unlock/unlock.js b/lemur/static/app/angular/authentication/unlock/unlock.js new file mode 100644 index 00000000..44304997 --- /dev/null +++ b/lemur/static/app/angular/authentication/unlock/unlock.js @@ -0,0 +1,18 @@ +'use strict'; + +angular.module('lemur') + .config(function config($routeProvider) { + $routeProvider.when('/unlock', { + templateUrl: '/angular/authentication/unlock/unlock.tpl.html', + controller: 'UnlockCtrl' + }); + }) + .controller('UnlockCtrl', function ($scope, $location, lemurRestangular, messageService) { + $scope.unlock = function () { + lemurRestangular.one('unlock').customPOST({'password': $scope.password}) + .then(function (data) { + messageService.addMessage(data); + $location.path('/dashboard'); + }); + }; + }); diff --git a/lemur/static/app/angular/authentication/unlock/unlock.tpl.html b/lemur/static/app/angular/authentication/unlock/unlock.tpl.html new file mode 100644 index 00000000..40ff5483 --- /dev/null +++ b/lemur/static/app/angular/authentication/unlock/unlock.tpl.html @@ -0,0 +1,16 @@ +

Unlock Assume 9 is twice 5; how will you write 6 times 5 in the same system of notation?

+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
diff --git a/lemur/static/app/angular/authorities/authority/authority.js b/lemur/static/app/angular/authorities/authority/authority.js new file mode 100644 index 00000000..6129b250 --- /dev/null +++ b/lemur/static/app/angular/authorities/authority/authority.js @@ -0,0 +1,55 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/authorities/create', { + templateUrl: '/angular/authorities/authority/authorityWizard.tpl.html', + controller: 'AuthorityCreateController' + }); + $routeProvider.when('/authorities/:id/edit', { + templateUrl: '/angular/authorities/authority/authorityEdit.tpl.html', + controller: 'AuthorityEditController' + }); + }) + + .controller('AuthorityEditController', function ($scope, $routeParams, AuthorityApi, AuthorityService, RoleService){ + AuthorityApi.get($routeParams.id).then(function (authority) { + AuthorityService.getRoles(authority); + $scope.authority = authority; + }); + + $scope.authorityService = AuthorityService; + $scope.save = AuthorityService.update; + $scope.roleService = RoleService; + }) + + .controller('AuthorityCreateController', function ($scope, $modal, AuthorityService, LemurRestangular, RoleService) { + $scope.authority = LemurRestangular.restangularizeElement(null, {}, 'authorities'); + + $scope.save = function (authority) { + var loadingModal = $modal.open({backdrop: 'static', template: '', windowTemplateUrl: 'angular/loadingModal.html', size: 'large'}); + return AuthorityService.create(authority).then(function (response) { + loadingModal.close(); + }); + }; + + + $scope.roleService = RoleService; + + $scope.authorityService = AuthorityService; + + $scope.open = function($event) { + $event.preventDefault(); + $event.stopPropagation(); + + $scope.opened1 = true; + }; + + $scope.open2 = function($event) { + $event.preventDefault(); + $event.stopPropagation(); + + $scope.opened2 = true; + }; + }); diff --git a/lemur/static/app/angular/authorities/authority/authorityEdit.tpl.html b/lemur/static/app/angular/authorities/authority/authorityEdit.tpl.html new file mode 100644 index 00000000..f40502f1 --- /dev/null +++ b/lemur/static/app/angular/authorities/authority/authorityEdit.tpl.html @@ -0,0 +1,44 @@ +

Edit Authority Chain of command +

+
+
+ Cancel +
+
+
+
+
+ +
+
+ + + + +
+ + + + + + +
{{ role.name }}{{ role.description }} + +
+
+
+
+
+ +
diff --git a/lemur/static/app/angular/authorities/authority/authorityWizard.tpl.html b/lemur/static/app/angular/authorities/authority/authorityWizard.tpl.html new file mode 100644 index 00000000..70cc0e5f --- /dev/null +++ b/lemur/static/app/angular/authorities/authority/authorityWizard.tpl.html @@ -0,0 +1,17 @@ +

CreateEdit Authority The nail that sticks out farthest gets hammered the hardest +
+ + + + + + + + + + + + + + +
diff --git a/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html b/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html new file mode 100644 index 00000000..302f01a4 --- /dev/null +++ b/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html @@ -0,0 +1,55 @@ +
+
+
+ +
+ +

You must enter a country

+
+
+
+ +
+ +

You must enter a state

+
+
+
+ +
+ +

You must enter a location

+
+
+
+ +
+ +

You must enter a organization

+
+
+
+ +
+ +

You must enter a organizational unit

+
+
+
+
+ diff --git a/lemur/static/app/angular/authorities/authority/extensions.tpl.html b/lemur/static/app/angular/authorities/authority/extensions.tpl.html new file mode 100644 index 00000000..3d7f82eb --- /dev/null +++ b/lemur/static/app/angular/authorities/authority/extensions.tpl.html @@ -0,0 +1,219 @@ +
+
+ +
+ +
+
+
+ + + + +
+
+
+
+
+ + + + + + +
{{ alt.nameType }}{{ alt.value }} + +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ + + + +
+
+
+
+ +
+
+
+
+
+ + + + + + + + +
{{ custom.oid }}{{ custom.encoding }}{{ custom.value }}{{ custom.isCritical}} + +
+
+
+
diff --git a/lemur/static/app/angular/authorities/authority/options.tpl.html b/lemur/static/app/angular/authorities/authority/options.tpl.html new file mode 100644 index 00000000..3cb0b912 --- /dev/null +++ b/lemur/static/app/angular/authorities/authority/options.tpl.html @@ -0,0 +1,78 @@ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
diff --git a/lemur/static/app/angular/authorities/authority/permissions.tpl.html b/lemur/static/app/angular/authorities/authority/permissions.tpl.html new file mode 100644 index 00000000..160b8326 --- /dev/null +++ b/lemur/static/app/angular/authorities/authority/permissions.tpl.html @@ -0,0 +1,28 @@ +
+ +
+
+ + + + +
+ + + + + + +
{{ role.name }}{{ role.description }} + +
+
+
diff --git a/lemur/static/app/angular/authorities/authority/select.tpl.html b/lemur/static/app/angular/authorities/authority/select.tpl.html new file mode 100644 index 00000000..f510e8f3 --- /dev/null +++ b/lemur/static/app/angular/authorities/authority/select.tpl.html @@ -0,0 +1,3 @@ + + {{ match.model.name }} - {{match.model.description }} + diff --git a/lemur/static/app/angular/authorities/authority/tracking.tpl.html b/lemur/static/app/angular/authorities/authority/tracking.tpl.html new file mode 100644 index 00000000..161ce2a0 --- /dev/null +++ b/lemur/static/app/angular/authorities/authority/tracking.tpl.html @@ -0,0 +1,71 @@ +
+
+
+ +
+ +

You must enter a valid authority name, spaces are not allowed

+
+
+
+ +
+ +

You must enter an Certificate Authority owner

+
+
+
+ +
+ +

You must give a short description about this authority will be used for, it should contain only alphanumeric characters

+
+
+
+ +
+ +

You must enter a common name

+
+
+
+ +
+
+
+ + + + +
+
+
+ +
+
+
+ + + + +
+
+
+
+
+
+ diff --git a/lemur/static/app/angular/authorities/services.js b/lemur/static/app/angular/authorities/services.js new file mode 100644 index 00000000..49a50ca4 --- /dev/null +++ b/lemur/static/app/angular/authorities/services.js @@ -0,0 +1,130 @@ +'use strict'; +angular.module('lemur') + .service('AuthorityApi', function (LemurRestangular) { + LemurRestangular.extendModel('authorities', function (obj) { + return angular.extend(obj, { + attachRole: function (role) { + this.selectedRole = null; + if (this.roles === undefined) { + this.roles = []; + } + this.roles.push(role); + }, + removeRole: function (index) { + this.roles.splice(index, 1); + }, + attachSubAltName: function () { + if (this.extensions === undefined || this.extensions.subAltNames === undefined) { + this.extensions = {'subAltNames': {'names': []}}; + } + + if (angular.isString(this.subAltType) && angular.isString(this.subAltValue)) { + this.extensions.subAltNames.names.push({'nameType': this.subAltType, 'value': this.subAltValue}); + } + + this.subAltType = null; + this.subAltValue = null; + }, + removeSubAltName: function (index) { + this.extensions.subAltNames.names.splice(index, 1); + }, + attachCustom: function () { + if (this.extensions === undefined || this.extensions.custom === undefined) { + this.extensions = {'custom': []}; + } + + if (angular.isString(this.customOid) && angular.isString(this.customEncoding) && angular.isString(this.customValue)) { + this.extensions.custom.push( + { + 'oid': this.customOid, + 'isCritical': this.customIsCritical, + 'encoding': this.customEncoding, + 'value': this.customValue + } + ); + } + + this.customOid = null; + this.customIsCritical = null; + this.customEncoding = null; + this.customValue = null; + }, + removeCustom: function (index) { + this.extensions.custom.splice(index, 1); + } + }); + }); + return LemurRestangular.all('authorities'); + }) + .service('AuthorityService', function ($location, AuthorityApi, toaster) { + var AuthorityService = this; + AuthorityService.findAuthorityByName = function (filterValue) { + return AuthorityApi.getList({'filter[name]': filterValue}) + .then(function (authorites) { + return authorites; + }); + }; + + AuthorityService.create = function (authority) { + authority.attachSubAltName(); + return AuthorityApi.post(authority).then( + function () { + toaster.pop({ + type: 'success', + title: authority.name, + body: 'Successfully created!' + }); + $location.path('/authorities'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: authority.name, + body: 'Was not created! ' + response.data.message + }); + }); + }; + + AuthorityService.update = function (authority) { + authority.put().then( + function () { + toaster.pop({ + type: 'success', + title: authority.name, + body: 'Successfully updated!' + }); + $location.path('/authorities'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: authority.name, + body: 'Update Failed! ' + response.data.message + }); + }); + }; + + AuthorityService.getRoles = function (authority) { + authority.getList('roles').then(function (roles) { + authority.roles = roles; + }); + }; + + AuthorityService.updateActive = function (authority) { + authority.put().then( + function () { + toaster.pop({ + type: 'success', + title: authority.name, + body: 'Successfully updated!' + }); + }, + function (response) { + toaster.pop({ + type: 'error', + title: authority.name, + body: 'Update Failed! ' + response.data.message + }); + }); + }; + }); diff --git a/lemur/static/app/angular/authorities/view/view.js b/lemur/static/app/angular/authorities/view/view.js new file mode 100644 index 00000000..0b93466d --- /dev/null +++ b/lemur/static/app/angular/authorities/view/view.js @@ -0,0 +1,46 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/authorities', { + templateUrl: '/angular/authorities/view/view.tpl.html', + controller: 'AuthoritiesViewController' + }); + }) + + .controller('AuthoritiesViewController', function ($scope, $q, AuthorityApi, AuthorityService, ngTableParams) { + $scope.filter = {}; + $scope.authoritiesTable = new ngTableParams({ + page: 1, // show first page + count: 10, // count per page + sorting: { + id: 'desc' // initial sorting + }, + filter: $scope.filter + }, { + total: 0, // length of data + getData: function ($defer, params) { + AuthorityApi.getList(params.url()).then(function (data) { + _.each(data, function(authority) { + AuthorityService.getRoles(authority); + }); + params.total(data.total); + $defer.resolve(data); + }); + } + }); + + $scope.authorityService = AuthorityService; + + $scope.getAuthorityStatus = function () { + var def = $q.defer(); + def.resolve([{'title': 'Active', 'id': true}, {'title': 'Inactive', 'id': false}]) + return def; + }; + + $scope.toggleFilter = function (params) { + params.settings().$scope.show_filter = !params.settings().$scope.show_filter; + }; + + }); diff --git a/lemur/static/app/angular/authorities/view/view.tpl.html b/lemur/static/app/angular/authorities/view/view.tpl.html new file mode 100644 index 00000000..35dfb4d4 --- /dev/null +++ b/lemur/static/app/angular/authorities/view/view.tpl.html @@ -0,0 +1,50 @@ +
+
+

Authorities + The nail that sticks out farthest gets hammered the hardest

+
+
+
+ Create +
+
+ +
+
+
+
+ + + + + + + + + +
+
    +
  • {{ authority.name }}
  • +
  • {{ authority.description }}
  • +
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
diff --git a/lemur/static/app/angular/certificates/certificate/certificate.js b/lemur/static/app/angular/certificates/certificate/certificate.js new file mode 100644 index 00000000..4815121e --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/certificate.js @@ -0,0 +1,94 @@ +'use strict'; + +angular.module('lemur') + .config(function config($routeProvider) { + $routeProvider.when('/certificates/create', { + templateUrl: '/angular/certificates/certificate/certificateWizard.tpl.html', + controller: 'CertificateCreateController' + }); + + $routeProvider.when('/certificates/:id/edit', { + templateUrl: '/angular/certificates/certificate/edit.tpl.html', + controller: 'CertificateEditController' + }); + }) + + .controller('CertificateEditController', function ($scope, $routeParams, CertificateApi, CertificateService, MomentService) { + CertificateApi.get($routeParams.id).then(function (certificate) { + $scope.certificate = certificate; + }); + + $scope.momentService = MomentService; + $scope.save = CertificateService.update; + + }) + + .controller('CertificateCreateController', function ($scope, $modal, CertificateApi, CertificateService, AccountService, ELBService, AuthorityService, MomentService, LemurRestangular) { + $scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates'); + + $scope.save = function (certificate) { + var loadingModal = $modal.open({backdrop: 'static', template: '', windowTemplateUrl: 'angular/loadingModal.html', size: 'large'}); + CertificateService.create(certificate).then(function (response) { + loadingModal.close(); + }); + }; + + $scope.templates = [ + { + 'name': 'Client Certificate', + 'description': '', + 'extensions': { + 'basicConstraints': {}, + 'keyUsage': { + 'isCritical': true, + 'useDigitalSignature': true + }, + 'extendedKeyUsage': { + 'isCritical': true, + 'useClientAuthentication': true + }, + 'subjectKeyIdentifier': { + 'includeSKI': true + } + } + }, + { + 'name': 'Server Certificate', + 'description': '', + 'extensions' : { + 'basicConstraints': {}, + 'keyUsage': { + 'isCritical': true, + 'useKeyEncipherment': true, + 'useDigitalSignature': true + }, + 'extendedKeyUsage': { + 'isCritical': true, + 'useServerAuthentication': true + }, + 'subjectKeyIdentifier': { + 'includeSKI': true + } + } + } + ]; + + $scope.openNotBefore = function($event) { + $event.preventDefault(); + $event.stopPropagation(); + + $scope.openNotBefore.isOpen = true; + }; + + $scope.openNotAfter = function($event) { + $event.preventDefault(); + $event.stopPropagation(); + + $scope.openNotAfter.isOpen = true; + + }; + + $scope.elbService = ELBService; + $scope.authorityService = AuthorityService; + $scope.accountService = AccountService; + }); diff --git a/lemur/static/app/angular/certificates/certificate/certificate.tpl.html b/lemur/static/app/angular/certificates/certificate/certificate.tpl.html new file mode 100644 index 00000000..73d63a26 --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/certificate.tpl.html @@ -0,0 +1,20 @@ +

Create a certificate encrypt all the things +

+
+
+ Cancel +
+
+
+
+ +
+
+ +
diff --git a/lemur/static/app/angular/certificates/certificate/certificateWizard.tpl.html b/lemur/static/app/angular/certificates/certificate/certificateWizard.tpl.html new file mode 100644 index 00000000..dbd7595f --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/certificateWizard.tpl.html @@ -0,0 +1,17 @@ +

CreateEdit Certificate encrypt all the things +
+ + + + + + + + + + + + + + +
diff --git a/lemur/static/app/angular/certificates/certificate/destinations.tpl.html b/lemur/static/app/angular/certificates/certificate/destinations.tpl.html new file mode 100644 index 00000000..443d6fab --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/destinations.tpl.html @@ -0,0 +1,62 @@ +

Destinations are purely optional, if you think the created certificate will be used in AWS select one or more accounts and Lemur will upload it for you.

+
+
+ +
+
+ + + + +
+ + + + + + +
{{ account.label }}{{ account.comments }} + +
+
+
+ +
diff --git a/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html b/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html new file mode 100644 index 00000000..cfd0e582 --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html @@ -0,0 +1,55 @@ +
+
+
+ +
+ +

You must enter a country

+
+
+
+ +
+ +

You must enter a state

+
+
+
+ +
+ +

You must enter a location

+
+
+
+ +
+ +

You must enter a organization

+
+
+
+ +
+ +

You must enter a organizational unit

+
+
+
+
+ diff --git a/lemur/static/app/angular/certificates/certificate/options.tpl.html b/lemur/static/app/angular/certificates/certificate/options.tpl.html new file mode 100644 index 00000000..794aaf6e --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/options.tpl.html @@ -0,0 +1,224 @@ +
+
+
+
+ +
+ +
+
+
+ + + + +
+
+
+
+
+ + + + + + +
{{ alt.nameType }}{{ alt.value }} + +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ + + + +
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + +
{{ custom.oid }}{{ custom.encoding }}{{ custom.value }}{{ custom.isCritical}} + +
+
+
+
+ +
diff --git a/lemur/static/app/angular/certificates/certificate/tracking.tpl.html b/lemur/static/app/angular/certificates/certificate/tracking.tpl.html new file mode 100644 index 00000000..025f41d2 --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/tracking.tpl.html @@ -0,0 +1,103 @@ +
+
+
+ +
+ +

You must enter an Certificate owner

+
+
+
+ +
+ +

You must give a short description about this authority will be used for, this description should only include alphanumeric characters

+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ + + + +
+ + + + + +
{{ domain.value }} + +
+
+
+
+
+ +
+ +
+
+
+ +
+ +

You must enter a common name

+
+
+
+ +
+
+
+ + + + +
+
+
+ +
+
+
+ + + + +
+
+
+
+
+ +
+ diff --git a/lemur/static/app/angular/certificates/certificate/upload.js b/lemur/static/app/angular/certificates/certificate/upload.js new file mode 100644 index 00000000..de2c88b4 --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/upload.js @@ -0,0 +1,26 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/certificates/upload', { + templateUrl: '/angular/certificates/certificate/upload.tpl.html', + controller: 'CertificatesUploadController' + }); + }) + + .controller('CertificatesUploadController', function ($scope, CertificateService, LemurRestangular, AccountService, ELBService) { + $scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates'); + $scope.upload = CertificateService.upload; + + $scope.accountService = AccountService; + $scope.elbService = ELBService; + + + $scope.attachELB = function (elb) { + $scope.certificate.attachELB(elb); + ELBService.getListeners(elb).then(function (listeners) { + $scope.certificate.elb.listeners = listeners; + }); + }; + }); diff --git a/lemur/static/app/angular/certificates/certificate/upload.tpl.html b/lemur/static/app/angular/certificates/certificate/upload.tpl.html new file mode 100644 index 00000000..adfebba9 --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/upload.tpl.html @@ -0,0 +1,136 @@ +

Upload a certificate encrypt all the things +

+
+
+ Cancel +
+
+
+
+
+ + +
+ + +

Enter a valid + email.

+
+
+
+ + +
+ + +

Enter + a valid certificate.

+
+
+
+ + +
+ + +

Enter + a valid certificate.

+
+
+
+ + +
+ + +

Enter a valid certificate.

+
+
+
+ +
+
+ + + + +
+ + + + + + +
{{ account.label }}{{ account.comments }} + +
+
+
+ +
+
+ +
diff --git a/lemur/static/app/angular/certificates/services.js b/lemur/static/app/angular/certificates/services.js new file mode 100644 index 00000000..b5dd898e --- /dev/null +++ b/lemur/static/app/angular/certificates/services.js @@ -0,0 +1,238 @@ +/** + * Created by kglisson on 1/19/15. + */ +angular.module('lemur') + .service('CertificateApi', function (LemurRestangular, DomainService) { + LemurRestangular.extendModel('certificates', function (obj) { + return angular.extend(obj, { + attachAuthority: function (authority) { + this.authority = authority; + this.authority.maxDate = moment(this.authority.notAfter).subtract(1, 'days').format('YYYY/MM/DD'); + }, + attachSubAltName: function () { + if (this.extensions === undefined) { + this.extensions = {}; + } + + if (this.extensions.subAltNames === undefined) { + this.extensions.subAltNames = {'names': []}; + } + + if (!angular.isString(this.subAltType)) { + this.subAltType = 'CNAME'; + } + + if (angular.isString(this.subAltValue) && angular.isString(this.subAltType)) { + this.extensions.subAltNames.names.push({'nameType': this.subAltType, 'value': this.subAltValue}); + this.findDuplicates(); + } + + this.subAltType = null; + this.subAltValue = null; + }, + removeSubAltName: function (index) { + this.extensions.subAltNames.names.splice(index, 1); + this.findDuplicates(); + }, + attachCustom: function () { + if (this.extensions === undefined || this.extensions.custom === undefined) { + this.extensions = {'custom': []}; + } + + if (angular.isString(this.customOid) && angular.isString(this.customEncoding) && angular.isString(this.customValue)) { + this.extensions.custom.push( + { + 'oid': this.customOid, + 'isCritical': this.customIsCritical, + 'encoding': this.customEncoding, + 'value': this.customValue + } + ); + } + + this.customOid = null; + this.customIsCritical = null; + this.customEncoding = null; + this.customValue = null; + }, + removeCustom: function (index) { + this.extensions.custom.splice(index, 1); + }, + attachAccount: function (account) { + this.selectedAccount = null; + if (this.accounts === undefined) { + this.accounts = []; + } + this.accounts.push(account); + }, + removeAccount: function (index) { + this.accounts.splice(index, 1); + }, + attachELB: function (elb) { + this.selectedELB = null; + if (this.elbs === undefined) { + this.elbs = []; + } + this.elbs.push(elb); + }, + removeELB: function (index) { + this.elbs.splice(index, 1); + }, + findDuplicates: function () { + DomainService.findDomainByName(this.extensions.subAltNames[0]).then(function (domains) { //We should do a better job of searchin multiple domains + this.duplicates = domains.total; + }); + }, + useTemplate: function () { + this.extensions = this.template.extensions; + } + }); + }); + return LemurRestangular.all('certificates'); + }) + .service('CertificateService', function ($location, CertificateApi, toaster) { + var CertificateService = this; + CertificateService.findCertificatesByName = function (filterValue) { + return CertificateApi.getList({'filter[name]': filterValue}) + .then(function (certificates) { + return certificates; + }); + }; + + CertificateService.getARNs = function (certificate) { + certificate.arns = []; + _.each(certificate.accounts, function (account) { + certificate.arns.push('arn:aws:iam::' + account.accountNumber + ':server-certificate/' + certificate.name); + }); + }; + + CertificateService.create = function (certificate) { + certificate.attachSubAltName(); + return CertificateApi.post(certificate).then( + function (response) { + toaster.pop({ + type: 'success', + title: certificate.name, + body: 'Successfully created!' + }); + $location.path('/certificates'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: certificate.name, + body: 'Was not created! ' + response.data.message + }); + } + ); + }; + + CertificateService.update = function (certificate) { + certificate.put().then(function () { + toaster.pop({ + type: 'success', + title: certificate.name, + body: 'Successfully updated!' + }); + $location.path('certificates'); + }); + }; + + CertificateService.upload = function (certificate) { + CertificateApi.customPOST(certificate, "upload").then( + function (response) { + toaster.pop({ + type: 'success', + title: certificate.name, + body: 'Successfully uploaded!' + }); + $location.path('/certificates'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: certificate.name, + body: 'Failed to upload ' + response.data.message + }); + }); + }; + + CertificateService.loadPrivateKey = function (certificate) { + certificate.customGET('key').then( + function (response) { + if (response.key === null) { + toaster.pop({ + type: 'warning', + title: certificate.name, + body: 'No private key found!' + }); + } else { + certificate.privateKey = response.key; + } + }, + function (response) { + toaster.pop({ + type: 'error', + title: certificate.name, + body: 'You do not have permission to view this key!' + }); + }); + }; + + CertificateService.getAuthority = function (certificate) { + certificate.customGET('authority').then(function (authority) { + certificate.authority = authority; + }); + }; + + CertificateService.getCreator = function (certificate) { + certificate.customGET('creator').then(function (creator) { + certificate.creator = creator; + }); + }; + + CertificateService.getAccounts = function (certificate) { + certificate.getList('accounts').then(function (accounts) { + certificate.accounts = accounts; + CertificateService.getARNs(certificate); + }); + }; + + CertificateService.getListeners = function (certificate) { + certificate.getList('listeners').then(function (listeners) { + certificate.listeners = listeners; + }); + }; + + CertificateService.getELBs = function (certificate) { + certificate.getList('listeners').then(function (elbs) { + certificate.elbs = elbs; + }); + }; + + CertificateService.getDomains = function (certificate) { + certificate.getList('domains').then(function (domains) { + certificate.domains = domains; + }); + }; + + CertificateService.updateActive = function (certificate) { + certificate.put().then( + function () { + toaster.pop({ + type: 'success', + title: certificate.name, + body: 'Successfully updated!' + }); + }, + function (response) { + toaster.pop({ + type: 'error', + title: certificate.name, + body: 'Was not updated! ' + response.data.message + }); + }); + }; + + return CertificateService; + }); diff --git a/lemur/static/app/angular/certificates/view/view.js b/lemur/static/app/angular/certificates/view/view.js new file mode 100644 index 00000000..eba4e63f --- /dev/null +++ b/lemur/static/app/angular/certificates/view/view.js @@ -0,0 +1,63 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/certificates', { + templateUrl: '/angular/certificates/view/view.tpl.html', + controller: 'CertificatesViewController' + }); + }) + + .controller('CertificatesViewController', function ($q, $scope, CertificateApi, CertificateService, MomentService, ngTableParams) { + $scope.filter = {}; + $scope.certificateTable = new ngTableParams({ + page: 1, // show first page + count: 10, // count per page + sorting: { + id: 'desc' // initial sorting + }, + filter: $scope.filter + }, { + total: 0, // length of data + getData: function ($defer, params) { + CertificateApi.getList(params.url()) + .then(function (data) { + // TODO we should attempt to resolve all of these in parallel + _.each(data, function (certificate) { + CertificateService.getDomains(certificate); + CertificateService.getAccounts(certificate); + CertificateService.getListeners(certificate); + CertificateService.getAuthority(certificate); + CertificateService.getCreator(certificate); + }); + params.total(data.total); + $defer.resolve(data); + }); + } + }); + + $scope.certificateService = CertificateService; + $scope.momentService = MomentService; + + $scope.remove = function (certificate) { + certificate.remove().then(function () { + $scope.certificateTable.reload(); + }); + }; + + $scope.getCertificateStatus = function () { + var def = $q.defer(); + def.resolve([{'title': 'Active', 'id': true}, {'title': 'Inactive', 'id': false}]) + return def; + }; + + $scope.show = {title: 'Current User', value: 'currentUser'}; + + $scope.fields = [{title: 'Current User', value: 'currentUser'}, {title: 'All', value: 'all'}]; + + + $scope.toggleFilter = function (params) { + params.settings().$scope.show_filter = !params.settings().$scope.show_filter; + }; + }); diff --git a/lemur/static/app/angular/certificates/view/view.tpl.html b/lemur/static/app/angular/certificates/view/view.tpl.html new file mode 100644 index 00000000..443c71d0 --- /dev/null +++ b/lemur/static/app/angular/certificates/view/view.tpl.html @@ -0,0 +1,142 @@ +
+
+

Certificates + Cipher text says what?

+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + + + + + + +
+
    +
  • {{ certificate.name }}
  • +
  • {{ certificate.owner }}
  • +
+
+ + +
+ +
+
+ {{ certificate.authority.name || certificate.issuer }} + + {{ certificate.cn }} + +
+ +
+
+
+
    +
  • + Creator + + {{ certificate.creator.email }} + +
  • +
  • + Not Before + + {{ momentService.createMoment(certificate.notBefore) }} + +
  • +
  • + Not After + + {{ momentService.createMoment(certificate.notAfter) }} + +
  • +
  • + San + + + + +
  • +
  • + Bits + {{ certificate.bits }} +
  • +
  • + Serial + {{ certificate.serial }} +
  • +
  • + Validity + + Unknown + Revoked + Valid + +
  • +
  • + Description + {{ certificate.description }} +
  • +
+

Domains

+ +

ARNs

+
    +
  • {{ arn }}
  • +
+
+ + +

+

{{ certificate.chain }}
+

+
+ +

+

{{ certificate.body }}
+

+
+ + + Private Key + +

+

{{ certificate.privateKey }}
+

+
+
+
+
+
+
+
+ diff --git a/lemur/static/app/angular/components/filters.js b/lemur/static/app/angular/components/filters.js new file mode 100644 index 00000000..96471a44 --- /dev/null +++ b/lemur/static/app/angular/components/filters.js @@ -0,0 +1,9 @@ +angular.module('lemur'). + filter('titleCase', function () { + return function (str) { + return (str === undefined || str === null) ? '' : str.replace(/_|-/, ' ').replace(/\w\S*/g, function (txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + }); + }; + }); + diff --git a/lemur/static/app/angular/dashboard/dashboard.js b/lemur/static/app/angular/dashboard/dashboard.js new file mode 100644 index 00000000..8e88d66b --- /dev/null +++ b/lemur/static/app/angular/dashboard/dashboard.js @@ -0,0 +1,93 @@ +'use strict'; + +angular.module('lemur') + .config(function config($routeProvider) { + $routeProvider.when('/dashboard', { + templateUrl: '/angular/dashboard/dashboard.tpl.html', + controller: 'DashboardController' + }); + }) + .controller('DashboardController', function ($scope, $rootScope, $filter, $location, LemurRestangular, ngTableParams) { + + var baseStats = LemurRestangular.all('stats'); + var baseAccounts = LemurRestangular.all('accounts'); + + baseAccounts.getList() + .then(function (data) { + $scope.accounts = data; + }); + + $scope.colours = [ + { + fillColor: 'rgba(41, 171, 224, 0.2)', + strokeColor: 'rgba(41, 171, 224, 1)', + pointColor: 'rgba(41, 171, 224, 0.2)', + pointStrongColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStrokeColor: 'rgba(41, 171, 224, 0.8)' + }, { + fillColor: 'rgba(147, 197, 75, 0.2)', + strokeColor: 'rgba(147, 197, 75, 1)', + pointColor: 'rgba(147, 197, 75, 0.2)', + pointStrongColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStrokeColor: 'rgba(147, 197, 75, 0.8)' + }, { + fillColor: 'rgba(217, 83, 79, 0.2)', + strokeColor: 'rgba(217, 83, 79, 1)', + pointColor: 'rgba(217, 83, 79, 0.2)', + pointStrongColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStrokeColor: 'rgba(217, 83, 79, 0.8)' + }, { + fillColor: 'rgba(244, 124, 60, 0.2)', + strokeColor: 'rgba(244, 124, 60, 1)', + pointColor: 'rgba(244, 124, 60, 0.2)', + pointStrongColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStrokeColor: 'rgba(244, 124, 60, 0.8)' + }, { + fillColor: 'rgba(243, 156, 18, 0.2)', + strokeColor: 'rgba(243, 156, 18, 1)', + pointColor: 'rgba(243, 156, 18, 0.2)', + pointStrongColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStrokeColor: 'rgba(243, 156, 18, 0.8)' + }, { + fillColor: 'rgba(231, 76, 60, 0.2)', + strokeColor: 'rgba(231, 76, 60, 1)', + pointColor: 'rgba(231, 76, 60, 0.2)', + pointStrongColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStrokeColor: 'rgba(231, 76, 60, 0.8)' + }, { + fillColor: 'rgba(255, 102, 102, 0.2)', + strokeColor: 'rgba(255, 102, 102, 1)', + pointColor: 'rgba(255, 102, 102, 0.2)', + pointStrongColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStrokeColor: 'rgba(255, 102, 102, 0.8)' + }, { + fillColor: 'rgba(255, 230, 230, 0.2)', + strokeColor: 'rgba(255, 230, 230, 1)', + pointColor: 'rgba(255, 230, 230, 0.2)', + pointStrongColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStrokeColor: 'rgba(255, 230, 230, 0.8)' + }]; + + LemurRestangular.all('certificates').customGET('stats', {metric: 'issuer'}) + .then(function (data) { + $scope.issuers = data['items']; + }); + + LemurRestangular.all('certificates').customGET('stats', {metric: 'bits'}) + .then(function (data) { + $scope.bits = data['items']; + }); + + LemurRestangular.all('certificates').customGET('stats', {metric: 'not_after'}) + .then(function (data) { + $scope.expiring = {labels: data['items']['labels'], values: [data['items']['values']]}; + }); + }); diff --git a/lemur/static/app/angular/dashboard/dashboard.tpl.html b/lemur/static/app/angular/dashboard/dashboard.tpl.html new file mode 100644 index 00000000..a00880a4 --- /dev/null +++ b/lemur/static/app/angular/dashboard/dashboard.tpl.html @@ -0,0 +1,41 @@ +
+ +

Status + Information is power +

+
+
+
+
+

Expiring Certificates

+
+
+ +
+
+
+
+
+
+
+
+

Issuers

+
+
+ +
+
+
+
+
+
+

Bit Strength

+
+
+ +
+
+
+
+ + diff --git a/lemur/static/app/angular/domains/services.js b/lemur/static/app/angular/domains/services.js new file mode 100644 index 00000000..3ac5579b --- /dev/null +++ b/lemur/static/app/angular/domains/services.js @@ -0,0 +1,13 @@ +angular.module('lemur') + .service('DomainApi', function (LemurRestangular) { + return LemurRestangular.all('domains'); + }) + .service('DomainService', function ($location, DomainApi) { + var DomainService = this; + DomainService.findDomainByName = function (filterValue) { + return DomainApi.getList({'filter[name]': filterValue}) + .then(function (domains) { + return domains; + }); + }; + }); diff --git a/lemur/static/app/angular/domains/view/view.js b/lemur/static/app/angular/domains/view/view.js new file mode 100644 index 00000000..a153305e --- /dev/null +++ b/lemur/static/app/angular/domains/view/view.js @@ -0,0 +1,35 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/domains', { + templateUrl: '/angular/domains/view/view.tpl.html', + controller: 'DomainsViewController' + }); + }) + + .controller('DomainsViewController', function ($scope, DomainApi, ngTableParams) { + $scope.filter = {}; + $scope.domainsTable = new ngTableParams({ + page: 1, // show first page + count: 10, // count per page + sorting: { + id: 'desc' // initial sorting + }, + filter: $scope.filter + }, { + total: 0, // length of data + getData: function ($defer, params) { + DomainApi.getList().then(function (data) { + params.total(data.total); + $defer.resolve(data); + }); + } + }); + + $scope.toggleFilter = function (params) { + params.settings().$scope.show_filter = !params.settings().$scope.show_filter; + }; + + }); diff --git a/lemur/static/app/angular/domains/view/view.tpl.html b/lemur/static/app/angular/domains/view/view.tpl.html new file mode 100644 index 00000000..f2658aea --- /dev/null +++ b/lemur/static/app/angular/domains/view/view.tpl.html @@ -0,0 +1,25 @@ +
+
+

Domains + Zone transfers as scary

+
+
+
+ +
+
+
+
+ + + + + + +
+ {{ domain.name }} +
+
+
+
+
diff --git a/lemur/static/app/angular/elbs/elb/elb.js b/lemur/static/app/angular/elbs/elb/elb.js new file mode 100644 index 00000000..d35be162 --- /dev/null +++ b/lemur/static/app/angular/elbs/elb/elb.js @@ -0,0 +1,3 @@ +/** + * Created by kglisson on 1/19/15. + */ diff --git a/lemur/static/app/angular/elbs/elb/elb.tpl.html b/lemur/static/app/angular/elbs/elb/elb.tpl.html new file mode 100644 index 00000000..d0658f25 --- /dev/null +++ b/lemur/static/app/angular/elbs/elb/elb.tpl.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lemur/static/app/angular/elbs/services.js b/lemur/static/app/angular/elbs/services.js new file mode 100644 index 00000000..3ea3eadc --- /dev/null +++ b/lemur/static/app/angular/elbs/services.js @@ -0,0 +1,57 @@ +angular.module('lemur') + .service('ELBApi', function (LemurRestangular, ListenerService) { + LemurRestangular.extendModel('elbs', function (obj) { + return angular.extend(obj, { + attachListener: function (listener) { + if (this.listeners === undefined) { + this.listeners = []; + } + this.listeners.push(listener); + }, + removeListener: function (index) { + this.listeners.splice(index, 1); + } + }); + }); + return LemurRestangular.all('elbs'); + }) + .service('ELBService', function ($location, ELBApi, toaster) { + var ELBService = this; + ELBService.findELBByName = function (filterValue) { + return ELBApi.getList({'filter[name]': filterValue}) + .then(function (elbs) { + return elbs; + }); + }; + + ELBService.getListeners = function (elb) { + elb.getList('listeners').then(function (listeners) { + elb.listeners = listeners; + }); + return elb; + }; + + ELBService.create = function (elb) { + ELBApi.post(elb).then(function () { + toaster.pop({ + type: 'success', + title: 'ELB ' + elb.name, + body: 'Has been successfully created!' + }); + $location.path('elbs'); + }); + }; + + ELBService.update = function (elb) { + elb.put().then(function () { + toaster.pop({ + type: 'success', + title: 'ELB ' + elb.name, + body: 'Has been successfully updated!' + }); + $location.path('elbs'); + }); + }; + + return ELBService; + }); diff --git a/lemur/static/app/angular/elbs/view/view.js b/lemur/static/app/angular/elbs/view/view.js new file mode 100644 index 00000000..6a7bd825 --- /dev/null +++ b/lemur/static/app/angular/elbs/view/view.js @@ -0,0 +1,34 @@ +'use strict'; + +angular.module('lemur') + .config(function config($routeProvider) { + $routeProvider.when('/elbs', { + templateUrl: '/angular/elbs/view/view.tpl.html', + controller: 'ELBViewController' + }); + }) + + .controller('ELBViewController', function ($scope, ELBApi, ELBService, ngTableParams) { + $scope.filter = {}; + $scope.elbsTable = new ngTableParams({ + page: 1, // show first page + count: 10, // count per page + sorting: { + id: 'desc' // initial sorting + }, + filter: $scope.filter + }, { + total: 0, // length of data + getData: function ($defer, params) { + ELBApi.getList(params.url()) + .then(function (data) { + params.total(data.total); + $defer.resolve(data); + }); + } + }); + + $scope.toggleFilter = function (params) { + params.settings().$scope.show_filter = !params.settings().$scope.show_filter; + }; + }); diff --git a/lemur/static/app/angular/elbs/view/view.tpl.html b/lemur/static/app/angular/elbs/view/view.tpl.html new file mode 100644 index 00000000..b3e4dad5 --- /dev/null +++ b/lemur/static/app/angular/elbs/view/view.tpl.html @@ -0,0 +1,128 @@ +
+
+

ELBs + Bring Balance to the Force

+
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + +
+
{{ elb.name }}
+
+
{{ elb.account.label }} +
+
+
{{ elb.region }}
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ Certificate NameInstance PortInstance ProtocolLoad Balancer PortLoad Balancer Protocol
+
+ +
+
+
+ + +
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
diff --git a/lemur/static/app/angular/listeners/services.js b/lemur/static/app/angular/listeners/services.js new file mode 100644 index 00000000..bd57c35f --- /dev/null +++ b/lemur/static/app/angular/listeners/services.js @@ -0,0 +1,35 @@ +angular.module('lemur') + .service('ListenerApi', function (LemurRestangular) { + return LemurRestangular.all('listeners'); + }) + .service('ListenerService', function ($location, ListenerApi) { + var ListenerService = this; + ListenerService.findListenerByName = function (filterValue) { + return ListenerApi.getList({'filter[name]': filterValue}) + .then(function (roles) { + return roles; + }); + }; + + ListenerService.create = function (role) { + ListenerApi.post(role).then(function () { + toaster.pop({ + type: 'success', + title: 'Listener ' + role.name, + body: 'Has been successfully created!' + }); + $location.path('roles/view'); + }); + }; + + ListenerService.update = function (role) { + role.put().then(function () { + toaster.pop({ + type: 'success', + title: 'Listener ' + role.name, + body: 'Has been successfully updated!' + }); + $location.path('roles/view'); + }); + }; + }); diff --git a/lemur/static/app/angular/loadingModal.html b/lemur/static/app/angular/loadingModal.html new file mode 100644 index 00000000..1f65a57e --- /dev/null +++ b/lemur/static/app/angular/loadingModal.html @@ -0,0 +1,6 @@ + diff --git a/lemur/static/app/angular/pager.html b/lemur/static/app/angular/pager.html new file mode 100644 index 00000000..3dc8a7d0 --- /dev/null +++ b/lemur/static/app/angular/pager.html @@ -0,0 +1,19 @@ + diff --git a/lemur/static/app/angular/roles/role/role.js b/lemur/static/app/angular/roles/role/role.js new file mode 100644 index 00000000..fb8bbee4 --- /dev/null +++ b/lemur/static/app/angular/roles/role/role.js @@ -0,0 +1,30 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/roles/create', { + templateUrl: '/angular/roles/role/role.tpl.html', + controller: 'RoleCreateController' + }); + $routeProvider.when('/roles/:id/edit', { + templateUrl: '/angular/roles/role/role.tpl.html', + controller: 'RoleEditController' + }); + }) + .controller('RoleEditController', function ($scope, $routeParams, RoleApi, RoleService, UserService) { + RoleApi.get($routeParams.id).then(function (role) { + $scope.role = role; + RoleService.getUsers(role); + }); + + $scope.save = RoleService.update; + $scope.userService = UserService; + $scope.roleService = RoleService; + }) + + .controller('RoleCreateController', function ($scope, RoleApi, RoleService, UserService, LemurRestangular ) { + $scope.role = LemurRestangular.restangularizeElement(null, {}, 'roles'); + $scope.userService = UserService; + $scope.save = RoleService.create; + }); diff --git a/lemur/static/app/angular/roles/role/role.tpl.html b/lemur/static/app/angular/roles/role/role.tpl.html new file mode 100644 index 00000000..d88e08d4 --- /dev/null +++ b/lemur/static/app/angular/roles/role/role.tpl.html @@ -0,0 +1,85 @@ +

CreateEdit Role The nail that sticks out farthest gets hammered the hardest +

+
+
+ + Cancel +
+
+
+
+
+ +
+ +

You must enter an role name

+
+
+
+ +
+ +
+
+
+ +
+ +
+ + {{ role.username }} + + + ****************** + +
+
+
+
+ +
+ +

You must enter an password

+
+ + {{ role.password }} + + + ***************** + +
+
+
+
+ +
+ + + + + + +
{{ user.username }} + +
+
+
+
+
+ +
diff --git a/lemur/static/app/angular/roles/services.js b/lemur/static/app/angular/roles/services.js new file mode 100644 index 00000000..8d3afd56 --- /dev/null +++ b/lemur/static/app/angular/roles/services.js @@ -0,0 +1,119 @@ +angular.module('lemur') + .service('RoleApi', function (LemurRestangular) { + LemurRestangular.extendModel('roles', function (obj) { + return angular.extend(obj, { + addUser: function (user) { + this.selectedUser = null; + if (this.users === undefined) { + this.users = []; + } + this.users.push(user); + }, + removeUser: function (index) { + this.users.splice(index, 1); + } + }); + }); + return LemurRestangular.all('roles'); + }) + .service('RoleService', function ($location, RoleApi, toaster) { + var RoleService = this; + RoleService.findRoleByName = function (filterValue) { + return RoleApi.getList({'filter[name]': filterValue}) + .then(function (roles) { + return roles; + }); + }; + + RoleService.getRoleDropDown = function () { + return RoleApi.getList().then(function (roles) { + return roles; + }); + }; + + RoleService.getUsers = function (role) { + role.customGET('users').then(function (users) { + role.users = users; + }); + }; + + RoleService.create = function (role) { + RoleApi.post(role).then( + function () { + toaster.pop({ + type: 'success', + title: role.name, + body: 'Has been successfully created!' + }); + $location.path('roles'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: role.name, + body: 'Has not been created! ' + response.data.message + }); + }); + }; + + RoleService.update = function (role) { + role.put().then( + function () { + toaster.pop({ + type: 'success', + title: role.name, + body: 'Successfully updated!' + }); + $location.path('roles'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: role.name, + body: 'Was not updated!' + response.data.message + }); + }); + }; + + RoleService.remove = function (role) { + return role.remove().then( + function () { + toaster.pop({ + type: 'success', + title: role.name, + body: 'Successfully deleted!' + }); + }, + function (response) { + toaster.pop({ + type: 'error', + title: role.name, + body: 'Was not deleted!' + response.data.message + }); + } + ); + }; + + RoleService.loadPassword = function (role) { + return role.customGET('credentials').then( + function (response) { + if ( response.password === null) { + toaster.pop({ + type: 'info', + title: role.name, + body: 'Has no password associated' + }); + } else { + role.password = response.password; + role.username = response.username; + } + }, + function (response) { + toaster.pop({ + type: 'error', + title: role.name, + body: 'You do not have permission to view this password!' + }); + }); + }; + }); diff --git a/lemur/static/app/angular/roles/view/view.js b/lemur/static/app/angular/roles/view/view.js new file mode 100644 index 00000000..c79b3755 --- /dev/null +++ b/lemur/static/app/angular/roles/view/view.js @@ -0,0 +1,42 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/roles', { + templateUrl: '/angular/roles/view/view.tpl.html', + controller: 'RolesViewController' + }); + }) + + .controller('RolesViewController', function ($scope, RoleApi, RoleService, ngTableParams) { + $scope.filter = {}; + $scope.rolesTable = new ngTableParams({ + page: 1, // show first page + count: 10, // count per page + sorting: { + id: 'desc' // initial sorting + }, + filter: $scope.filter + }, { + total: 0, // length of data + getData: function ($defer, params) { + RoleApi.getList(params.url()) + .then(function (data) { + params.total(data.total); + $defer.resolve(data); + }); + } + }); + + $scope.remove = function (role) { + RoleService.remove(role).then(function () { + $scope.rolesTable.reload(); + }); + }; + + $scope.toggleFilter = function (params) { + params.settings().$scope.show_filter = !params.settings().$scope.show_filter; + }; + + }); diff --git a/lemur/static/app/angular/roles/view/view.tpl.html b/lemur/static/app/angular/roles/view/view.tpl.html new file mode 100644 index 00000000..86258c57 --- /dev/null +++ b/lemur/static/app/angular/roles/view/view.tpl.html @@ -0,0 +1,41 @@ +
+
+

Roles + Black Hat? Grey Hat? White Hat?

+
+
+
+ Create +
+
+ +
+
+
+
+ + + + + + + +
+
    +
  • {{ role.name }}
  • +
  • {{ role.description }}
  • +
+
+ +
+
+
+
+
diff --git a/lemur/static/app/angular/users/services.js b/lemur/static/app/angular/users/services.js new file mode 100644 index 00000000..b1077b6f --- /dev/null +++ b/lemur/static/app/angular/users/services.js @@ -0,0 +1,89 @@ +/** + * Created by kglisson on 1/19/15. + */ +'use strict'; +angular.module('lemur') + .service('UserApi', function (LemurRestangular) { + LemurRestangular.extendModel('users', function (obj) { + return angular.extend(obj, { + attachRole: function (role) { + this.selectedRole = null; + if (this.roles === undefined) { + this.roles = []; + } + this.roles.push(role); + }, + removeRole: function (index) { + this.roles.splice(index, 1); + } + }); + }); + return LemurRestangular.all('users'); + }) + .service('UserService', function ($location, UserApi, AuthenticationApi, toaster) { + var UserService = this; + UserService.getCurrentUser = function () { + return AuthenticationApi.customGET('me').then(function (user) { + return user; + }); + }; + + UserService.findUserByName = function (filterValue) { + return UserApi.getList({'filter[username]': filterValue}) + .then(function (users) { + return users; + }); + }; + + UserService.getRoles = function (user) { + user.getList('roles').then(function (roles) { + user.roles = roles; + }); + }; + + UserService.loadMoreRoles = function (user, page) { + user.getList('roles', {page: page}).then(function (roles) { + _.each(roles, function (role) { + user.roles.push(role); + }); + }); + }; + + UserService.create = function (user) { + UserApi.post(user).then( + function () { + toaster.pop({ + type: 'success', + title: user.username, + body: 'Has been successfully created!' + }); + $location.path('users'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: user.username, + body: 'Has not been created!' + response.data.message + }); + }); + }; + + UserService.update = function (user) { + user.put().then( + function () { + toaster.pop({ + type: 'success', + title: user.username, + body: 'Has been successfully updated!' + }); + $location.path('users'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: user.username, + body: 'Has not been updated!' + response.data.message + }); + }); + }; + }); diff --git a/lemur/static/app/angular/users/user/user.js b/lemur/static/app/angular/users/user/user.js new file mode 100644 index 00000000..009133cb --- /dev/null +++ b/lemur/static/app/angular/users/user/user.js @@ -0,0 +1,38 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/users/create', { + templateUrl: '/angular/users/user/user.tpl.html', + controller: 'UsersCreateController' + }); + $routeProvider.when('/users/:id/edit', { + templateUrl: '/angular/users/user/user.tpl.html', + controller: 'UsersEditController' + }); + }) + + .controller('UsersEditController', function ($scope, $routeParams, UserApi, UserService, RoleService) { + UserApi.get($routeParams.id).then(function (user) { + UserService.getRoles(user); + $scope.user = user; + }); + + $scope.save = UserService.update; + $scope.roleService = RoleService; + + $scope.rolePage = 1; + + $scope.loadMoreRoles = function () { + $scope.rolePage += 1; + UserService.loadMoreRoles($scope.user, $scope.rolePage); + }; + }) + + .controller('UsersCreateController', function ($scope, UserService, LemurRestangular, RoleService) { + $scope.user = LemurRestangular.restangularizeElement(null, {}, 'users'); + $scope.save = UserService.create; + $scope.roleService = RoleService; + + }); diff --git a/lemur/static/app/angular/users/user/user.tpl.html b/lemur/static/app/angular/users/user/user.tpl.html new file mode 100644 index 00000000..21ade85b --- /dev/null +++ b/lemur/static/app/angular/users/user/user.tpl.html @@ -0,0 +1,89 @@ +

CreateEdit User what was your name again? +

+
+
+ Cancel +
+
+
+
+
+ +
+ +

You must enter a username

+
+
+
+ +
+ +

You must enter an email

+
+
+
+ +
+ +

You must enter an password

+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + + + +
+ + + + + + + + + + + +
{{ role.name }}{{ role.description }} + +
More
+
+
+
+
+ +
diff --git a/lemur/static/app/angular/users/view/view.js b/lemur/static/app/angular/users/view/view.js new file mode 100644 index 00000000..71dc8ff6 --- /dev/null +++ b/lemur/static/app/angular/users/view/view.js @@ -0,0 +1,43 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/users', { + templateUrl: '/angular/users/view/view.tpl.html', + controller: 'UsersViewController' + }); + }) + + .controller('UsersViewController', function ($scope, UserApi, UserService, ngTableParams) { + $scope.filter = {}; + $scope.usersTable = new ngTableParams({ + page: 1, // show first page + count: 10, // count per page + sorting: { + id: 'desc' // initial sorting + }, + filter: $scope.filter + }, { + total: 0, // length of data + getData: function ($defer, params) { + UserApi.getList(params.url()).then( + function (data) { + params.total(data.total); + $defer.resolve(data); + } + ); + } + }); + + $scope.remove = function (account) { + account.remove().then(function () { + $scope.usersTable.reload(); + }); + }; + + $scope.toggleFilter = function (params) { + params.settings().$scope.show_filter = !params.settings().$scope.show_filter; + }; + + }); diff --git a/lemur/static/app/angular/users/view/view.tpl.html b/lemur/static/app/angular/users/view/view.tpl.html new file mode 100644 index 00000000..41f91dea --- /dev/null +++ b/lemur/static/app/angular/users/view/view.tpl.html @@ -0,0 +1,43 @@ +
+
+

Users + what was your name again?

+
+
+
+ Create +
+
+ +
+
+
+
+ + + + + + + + +
+
    +
  • {{ user.username }}
  • +
  • {{ user.email }}
  • +
+
+ + + + + +
+
+
+
+
diff --git a/lemur/static/app/angular/welcome/welcome.html b/lemur/static/app/angular/welcome/welcome.html new file mode 100644 index 00000000..a3c44176 --- /dev/null +++ b/lemur/static/app/angular/welcome/welcome.html @@ -0,0 +1,19 @@ +
+

Hey there!

+ +

Welcome to Lemur! A central portal for all (most) of your SSL needs.

+ +

Create a Certificate

+
+
+
+

SSL In The Cloud Encrypt it all

+ +

The Security Operations team manages all of the SSL certificate generation at Netflix. This + portal was created to serve as both a self service application so that application owners can provision + their own certificates and to help enforce some key naming and security conventions, in order provide + Netflix with scalable and manageable SSL security.

+ +

See go/ssl for more info.

+
+
diff --git a/lemur/static/app/angular/wizard.html b/lemur/static/app/angular/wizard.html new file mode 100644 index 00000000..56090a3f --- /dev/null +++ b/lemur/static/app/angular/wizard.html @@ -0,0 +1,23 @@ +
+
+ +
+
+
+ +
+
diff --git a/lemur/static/app/favicon.ico b/lemur/static/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a929944b0d03d23c19f14672f9c1cd7fa4244c37 GIT binary patch literal 11153 zcmZ{K1y~&0vh@IiySsaE7$CU2yE}thaCZnU!6A5X39i8{NO0HS?hYaRq%}n(|1fNu-8=^A`q0Xjdgb1qmO|(+J6s;hX#S74bi=6VF8qS5Qm)o)gh3PQ7S!H ze|N?lXx_fU*riP4GXENoNWm>e1p`2knB(I2r@`HYOjfHOz-NC2j@+csvxl>>?S*<)xQPEVuR$tTbm$NvV(iD_3}&!pZgi~;8Br&Is5EcAs1cc{x`0(S4| zW@a8f%L7=kGK|LdY4YtN*(I>>&mylN0dUFvJl~vAq91vzasZyimOD|vNEy>&^o4Zm z-GlEIPnY#-kEU!4$$bi&5uWlBHw{Q}thj++9aAMBsCDc!tP)SFh)z#+=J%`~yOuUc z{MB9rzI#7wT+*COmS2?jGuG6aWBITG*k|f;X@;>+QayRv=f-k5kdpzHUsdu zXkqdFvu&}g+>8KzVLZw$GO*tr1ps1{P*e^=-jIIBdT4A2-#foqRV99PiD&ozz5hgt zs2}C9DKA2yws(gVZ|+Q8z=&VC5o*^1iID*7nuh=iVrGS8G)5*0Lc0xo7loCb=?~!ALSP!y> z3%)?ZE)XeC!1;ojAv%vns0-&MCZhZv3u-_tG6C(A@Z%&+Dc-w~@_aF6fg4I!f;KpV zkf%J@Niq=33xr=NdOGBc3GyPKDR8NVj|fhxcl4*YC-!Gp-0qc4*%ll@l;&RCO~MNR zW0+BI5}fdNIH)N}Z8*Y9anS^ADhPCP+eC^lkYy5?2`FC(TVtgYKkPuke9SjV$HN)K z-~Ol*vuUbnGH)u&I+t#it|^bZ$7O-W5%d^bWI9({tW8@f;w*h|<;tVk8P)L~Y;JiRSjSE)nXPu7n`Dv2ZQI&FKnDybsTHsv_YhgvCl zAxKyL7um zy9B%SBdRdHti}%uR)Ges%qj~bHs!vOp zrUbvNRR~m>mDX!p7An?{V;X#7*5ejw)^Aqx5LJ?6(PlCFs&bw$sFGWi`=v{&OD|xg zdy;I~I9epDr!+q*zsn@Yq;S)WvOKY3>Rq93A>ZJ8v!)MGmx9}RG&PagGM{8(hZWQo zXV=w@+a?+&kZkv?Xh0R5?L!4Mt2#QBlJj{yef-~j3%#-)84v}cwWG6f)^Lm%PZ`yi zju}aH=yjmA?Hi72p8A&iGi?pb{D#79w$19ti`VuVtIC)*+`CTw_%aP@)^OSt+b8Z& z?kJvVp4ngpK6-xaK)GLZb*F0&^_xRKKGD^crf^QU{$bDQ%LL0-W~!B|1ePWR3VKb8lR7swa)r?u;ai~XmC zXC>f!U?uzk!WW<0k?hOAfshm;W8hsxc#?0$Mp?&J2!7Q*%!Glza^{mHU(<8ImLM0J8ukIsj8B!r~I z8)&bQn7w7Y^3^v+tsc9;g{1b6&!6@O8TMl@4IaS15`k%*fQIY zN+OLv-Rg|4f_BQ16Os$*Im)KWeprV#JT%ORQkz88Fc@$M&~~u&feKm0+D5GB&NMI! zgr^ea>0ERy+SB8H%il`JP-Rhv=pfeH)U8`efpbSx#?bd}b~1MyMu~^7hoAj9EjT^g zEIZwQhyUu~JhMfq3Y)LiUujkJFz9L9yzM-Ww>!H{U*kRk9u36Mq*{q~d7*5@qj&4P5DN(O}I^NLn(|TMRy9+(- zD7q_p_W0=z=drqW*mkX*VX9-eG+F)AplWyQs^F@*UF)P%(?susF{*XEd3*zVX_@12Sa%<>;^gIWNf5|J#ul$hqCHJmrQa4IBe|~j7vgr$#wU4~d#$n)0Ht{$y zlTg;H?@i2IMrQqprQAf~IQHi(o?H+AE78HQ%jihlkGxv0f^7{u)7jNE)$5}{X`R&HNW)nh1!#tfaclx{!h0 z_po45)^T%^w_rR`A7L3E-Xg~Gn)3KnsSkRhY=S|uLGnndN@^nJF=oxX_GaK>G$w6k zJFJWTchh~!R7I}8$8(3J>2Uvr?bLCGknDPCC(qOTo&CP~!Oo0_^?7kg)Otmy&|T0= z{t}Xe9sjq_l<-c{IxYYJGv%KR!m~)o z9RL7QS*vTiYAeX|nmO7to0vPAf|)(-o!-y@0KX^i+pay>)r8E`-p;{=*HeJvF9h$~ z{vR_71=(K^S6cxJZ3SgAF-K=G87DJ{nUz8ifsBld-`U)PS5;i{U-Y*l0SYTuS0`Q; z77q^(W)BW#M`ueGHXa@x7FKo^c6O#W1e1%GgR6-rlYJqpE&f-@!R23Ry(P%<$HT(L%*yiLXkbt4|AY3&^MBIJ z&HkO0lbf^MUpbkZv4HKs_FxBBmp2~Uf0cWy?Z1itUyzpvKOiqETT?FqKi7Po#Y>0fzpzlHT*9sfxb zbF_1GR&z2j1OJKaFUmhm|B3!fM(2NJI6?oE`G?~lL~}D<7dKNIu$k*W=Id?N{#yN_ zCYCJ!F!QtgS2X|ITHsZ1G`F_!5;t)L3$nAaftXl%m{{Mdv-0r1Jvq7fS^fj_j|l#F z#GJt?t-xy=2g_O5Vbw0MZ85tMVA#=o?kloh;~9s#~#*&uDNoPT+ZlyzTWC} zY1|3I*4x|n1Vh&=2|n(iu6u6BD)zL#vOMD&dXm4iY{wCDxE;+p<<$#0jfau?KWu&} z$@TZ%K^6LSagxfU=MBUCtK_`vaW5&N}veG->3 zwpLC~PI!Yh*QNVm+FV}=T-4u6uoAp?IRZX+a)pwyymUk;9!E>{`wz|Q-nOYX{@j(( z`RAR_4$i8os{6^wScKdT`eXA|de2NooxfxZ_zqt&hz94wk=T|o8XM>4l~z=iX>8lS zl$S?sr@z|s%v$E|=LYy|mMCV2BjdAEHMO-Zd*2>mYpJWxR+p8PF$BkUJsnk)D1Clk zE2gNZNXNxBFDoZ^+O*GJQ~b{yKz!fB?qI7B^OM2IxDz zH4v=^jzRLb9n-{Mrp7`zRF}2$_Vy+UC*XCpl2~c7H(I!KHPE~B*@4S>xz2t4%p{zH zD;J*abJm(48VZaSrzmDMz25s#506PR+Ygt2=A3)8y1F`<$!S|D6;Cq1x@z}UtJu#k>{qo$?J z0&?DgAi$gnYnge7RH)`7JfcL{1F#MPO zn5c|zlXD=)(y=p?>Py2@>2rTpENnN;$yHoXP~bjOtiVh|Gl)nFmWR^I74$pV%W{lM z65Z}ERhqp#$-7=#s>rP-I0?F6_i=osmP@5dO-ae{T~y>fq>_`9t8Z)L?TW--N9%E9 z8_{icK-Oat6kHFX8l|Z;>}Xrba_r2wUbg+(`f|7O^)e6&1+RjQM3k9c^+u2Va9ta` ze!b!Ud`kcNFeKDgI)R5o9*CU1D`<#PI|I^`k71uAu`vNgM@QFhZJFBI*@1NW%vCUx zmTdz*@{3Wsq{EsFBU`!l$DkJr5)4! zVOw$nE7ZKAs_K@oQp5_W*bYKD-=r^m;5+nbAF5Cc_Vb;3S92^epL_5iznt?+d6xC( zq!hQWnzG~43JSM%d9YN%9(UhEV*HJim09(pFn^tlvUJ@fZ;6y5y|>ASfP!&d^4Rb@ zZ#!!{CP_R-p6)ub z>y2G$u&!SV4h*I?!8mUZ@xqQD7;5ag7h-1pQ11HzA-iEUWoX1-G^^;B*wnhpv{Oamz`A=h)tOdNj zxFp{jrJ3$W;`cS4SJ)>v5n`2Ys7(rLO<0fw{JPxy{qpS7Y&BP&(#&s1R?`Q-8q@%` zZ7Pavx0nsZk?;==y)rN`yu9GWbb2b{_bc*Wig(9oA-fmo?|jaSC%#vGe_1!m+oZj#@f(Jucf%x3|3@LiyCIqU z$_x%m_k)G68*~~Yb8YKXL1v_$KZB^qNj-K`w8D+yD$$%^eu_9jp;ZD6&{3#WH1nQ( zT5wY-h%$m}6GQgjqZ;#vKsLCaE@zmET0LBBW)K7%DR8fW4$3R4GpU8s7#s%aXpkQd z3gczT|N zr`wS_-*ya#uRCQ8h6mh{Gxh6x-py(e$|hD6Yy1p#*@wIdH}jyx1N3lJRzRR9+>SEA zYz-dwrYP%2SbQu*z_^-{0Jxl^$Ae&=1uLFk;l=pK6`=@PLhLqx?zQGO zFat^lfW%`}qcM;vaJOhiLI$nH!f--vD4#3177xz{_axHwYf!@&X&)c=9ALGD>s^mp z`0#XlEFj>O-Yw(aO8}=xONT@TC#l>IS#R;p90y!rD8sGJ!~Pa4qNi(Y;uoKa!df; znL$E&cRp-IlgMvj?c{a+$l`wf(A$rkj2gN{PFJS7x*;vqf;P5A(1o9@4RYR*Ce6~U z(gnY3Iq7^pyRQ9s9EiBfQ{2N&5LHOU2>Fn$6Z!!%oE#I`>Ai&=f#1vlVF>-=6Mam% zT>uHRhVJ6=*RC@W5r@Hns)LZ<)|mtkmygKX_=JOKj(wRrKC{BfA5EaG-cdgXBe)AQ zO%_A>+amXE$l-`RzFiw;e?KjBEeFAp9F#>;)W8MhG0!ngu)&0w^$?qdr(NOx&V#$B zAl#;WPoHuf0~NBUl)*lre&K~np22$F^HUyS3f_ziQ8FCKWpC_qDcM})^#E>?M|S^u zf1){pku4>?0Gp9iuZ3JlXrtlNzS$^!$!F7d2jAFh!qrQhH!i?GdG0^h*>Ge+{wsi()B|UC!c`u988L$?L}iSGG09=_ zehAF__8BJueRMSAo#qYy<9K{R51RD^89*ldar=4au>s=ba7)Z^2kUL7Z8e($@wduZ z8xlZvNsqV<;RTss7z99EW(m+cNCs35MTOieE6F+Hqi+AX530sojSvy(>(vexY>9J< z5%GS#nNfOq?51uxZze#Pa)L(cUJMM~(zUz#07cnDw>8`y1m!0Q%_vMh`ALMO^T9MA z=K*T5<~)Kj?t-x(V3542AU8g;1?lGPQg+|0Ml@=xdu|X!NFR;{M_}atxJv?B6rUmZ zXh}HYj2Z($7fRuvLTrYtz}wNDubg*s27qdzyE>_#@V*CFA;};GW!!|_nm~7bz^x;f zgFh(77Mk$fA{*QyF_#duxceSX#4jaW;`F6fxKWbY7t<=Ak-0lo8?L#>GM~Xggc9Zh z@r6@IIn$gb`fDL|!+6~l>D6g^zU|3WhQwX(_4P@|%Dk@O!mVc8rk|W_bS)<#*K*W( zXKlosu9{zp2B=ZJqtomKA}|lSJFBXBFWkoG{8S=!%DV>JMxj{m7lqH$2q)KT;#d1| zrs&i0+EJ#WpK1(I_F!V&k}SQpNgT)6TFGc{Cji7=AI443qHPK%&tQ6B81rY^PVWUw z(;?~MiR5VlnLaw?sLGa2+XrInI2bUzcVNFj-~y3qG2H%gxCVn=E>!9Yc3QS&-`x|- z^FxUH-IiYLe5Xr*oXkRZMZ2M79ZG}LOGzA?KwGk>l-HJBe!Ep*3k%^0QW{aK5KP3iFWPYGeFgxiy043H= z45^pt00~UcS8jS-$H=s9H|F_6zrHalB^5GE?CBNc7pc$L3b2{5E;KsikaQ=xWpXbz{4y8s+ znwKfT7gZM4t$7_Yg)r*osOE=$;PE#p+zLkfGKF#eYk8wFbTc}xTh*x{=|OTYW2Zeo z(^H&HCmwO@;dVn%7l{g`IEyN{tj#ihsvPNsA>8M@V_`;sg=K<;w%Y9QT z;+}RJqKb8)(F%S& zG9jd0(Vi6dy5Id;5gbgBjtFhFzUOAxdVvVO0uo-wYfmQo&?w(l6-B10wa<>;XP_N% znnl!To#FT8^6nqQvQ1A17RQ)TJS&u{mOmTJ+LDNn#j8rQ3MsSzu>%#B4#pikoO(g+JJfLPsY@3v$4#eU~ik> zV|So~ztlidfcfz$zvBS?ak{NqY+TIBZVwj*y^$50zTAb0wsq@&Sv)F_sxI%J!sW~rED zVVe)owqG1*kA7dak|Cb4I}n6@62a_M!47`4pWwfaLET|Z>W;_@41)wdIGGXdo`~}~ ztLHRE**7&Yxd>Ju9x{olr5en3^oAozH#Mh9+7G9VGM*|#Zc zMRHl3Xg|A;kGkI{g9wurhL1)VPG266nS8Vhstgwvs=poT`P^ln+fBT{YU^lZgdLlm ze4d$TMwS?DQWAqZm{oy8p(FLZ2pXWW=`2jcA$FdM)R3x>Q;vJe)7ZK$eE0j z8)~M{sfkNR(wM-9(X49bBmEEt4+d^V7g{2Zw&M(e{xWx`%u?QzGsTR_GDYXiRh|LD z66156O2d;8LQrbB;nkF-gR5r$nm~2c^Y1uDtvfry`|jPnHoB2mV!`2ENBtv$HH>D? z%yZYD`UcSv->V8{t!01i)cyVf`E_Pg}f>Le4KGe5PN;x&0ejKX_5Dep@1KD@NGYtbQC{d%SRS zc=+9?Ple%px-#eu0P&|@Wd(O_jK*e9R~&P^Ux>|m-y`rk{amdxlA1Exmi4uxTj70e z`zWEqSwy1!S~RM_zAVB!1Bq=_t zlp*6kJb%$F1UNY^w7;@0@RUR!&vB0;6Ms6!n$6Z9G!f}EpInhdMPh`kjLSH5h!@~( zHGF|>6N?vG^80oAF3sq4RTtksq}zSVRpx`bSTbBR1#SxeZ6hyB_#AiT)!1MQ$Q(6& z!f<%tM>ZcWp5-Y)SoZk4PAa+$mYa^lcvD%YmbEYdeA+wbqUve;56BwOb)N>iAB`)z z%bW`wc4<5gKD_9+k5{dhLQObRhATP|^IEi(%{u@g;@NZ7YS|V`>JcY#8zl|-_}kF$R-+3f(?((Xf`{pjYb<7>B5jkpK#UAoe!)Cr|OySk4k%d9W@d z0Ti<^fHxR+;#u(qb}tVJQ@a}1YJNgJbXT==%s7bNn0LgEyz$egHK8hz5*Asn+ zpvf@%?L4;?(b3bv*OUuENuJHCR}!{uu}*)<8>C2|piE_@{3$3HCa|IVsB&AGy3k5d zG45;fAykjoL?5RPyYaZD~|HX33wfZ2Pg56JUS+z=X#8|-yKmG}xRCb6Q z9ZoYSGDqmry%hcQm4w)8QOlf-+7~1|W}84A=MoIV4Azo7F=1hCpo;iC0_+S_vZWYV zWt6f!oEHoi8RG&v(R^)OQd-(RZXzJt(%tU!w$~Y=VG;ml8wT-?$OUY7|*Pi zea|?sujPCHP9lB8e9Lh|@O>sptMdK)^GA=T`q7^i5I8brtw&asyvQ7wU<(Ag7APoI zGz8cisq{P(1yjuPF94u1Izd|SJY!@5R&MIW61==g;e>E&o{={6voeYD0pX;&s^|RX zRSY_4Cy;t|h;18yQW^B3n_S*?JFh1+`4TXFn@dOPT4Vp3#KmWja0`1pOPJa0y5onE zy2k|JAS~AN6O1id4?&>WD@s?48v8QBH)5s(w4gMAWOsnn1|`E`0jU;Y@>Z4Pdty~S znGJ4|>`ZHAtqaG%%Na8beaDO$&RJ;nj-BRz`+3BRVBb8pc?tCwVt_w7-dP%266Yca z95>6+h%lH5^V|O*|Kn2_6_B<#4~wuXpFNg0=$?mBbgBil#tAb$oB>$5t&@y!CT4vm z(nEI8OLbtB+6^CoiMOB?m?rsD|~E zXbgZ9rxNopAnal?<< zdZtHTDGC4W2O}7Zq2D0EL*vjUdPtiVylF{??&j`Gx)Riamo&|9U|}!3psN-n<~+xz z7cB({Jbtm5U;%y9@g(S07&!e9^gu{T2%NndlBx-<7ajpUX4Y!#_a3Ko=UvXf)N-D+ za+ck{!jzc4sssFJdRC+la+XOcdIoJa)EJ{^j}r_7W}m@W{f)|*Ao+TVOE6}0X-;U~|j7sOt}Wmia-z(LI~M-C*?K^FGZ z7*?IYc0)}e`S{Lqb)&uo*nW6Zv=30tf!N>1$&~A+BCzRX0|&6Z!ao5dYkEk|Ae2|( z&>oS1Y1;(Vt*5U9Xlq>w{EeO40oN1freiFz^!W(M<+RaHr`WhC33cc;_?4)9-*J3# zAhr8R^-;o!oWGL`*7PEEej+QpoTBUo0LK98i%`daxDA4d5Sgk1N54!rg|C?YM$e?^ YRvso{NqN(M{^uYgp(tK0Y8?Fk0BrTftN;K2 literal 0 HcmV?d00001 diff --git a/lemur/static/app/index.html b/lemur/static/app/index.html new file mode 100644 index 00000000..3a81560c --- /dev/null +++ b/lemur/static/app/index.html @@ -0,0 +1,86 @@ + + + + + + + + Lemur + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/lemur/static/app/robots.txt b/lemur/static/app/robots.txt new file mode 100644 index 00000000..94174950 --- /dev/null +++ b/lemur/static/app/robots.txt @@ -0,0 +1,3 @@ +# robotstxt.org + +User-agent: * diff --git a/lemur/static/app/styles/lemur.css b/lemur/static/app/styles/lemur.css new file mode 100644 index 00000000..17fc85af --- /dev/null +++ b/lemur/static/app/styles/lemur.css @@ -0,0 +1,161 @@ +body { padding-top: 70px; } + +.steps-indicator { + position: relative; +} + +.bs-callout { + padding: 20px; + margin: 20px 0; + border: 1px solid #eee; + border-left-width: 5px; + border-radius: 3px; +} +.bs-callout h4 { + margin-top: 0; + margin-bottom: 5px; +} +.bs-callout p:last-child { + margin-bottom: 0; +} +.bs-callout code { + border-radius: 3px; +} +.bs-callout+.bs-callout { + margin-top: -5px; +} +.bs-callout-default { + border-left-color: #777; +} +.bs-callout-default h4 { + color: #777; +} +.bs-callout-primary { + border-left-color: #428bca; +} +.bs-callout-primary h4 { + color: #428bca; +} +.bs-callout-success { + border-left-color: #5cb85c; +} +.bs-callout-success h4 { + color: #5cb85c; +} +.bs-callout-danger { + border-left-color: #d9534f; +} +.bs-callout-danger h4 { + color: #d9534f; +} +.bs-callout-warning { + border-left-color: #f0ad4e; +} +.bs-callout-warning h4 { + color: #f0ad4e; +} +.bs-callout-info { + border-left-color: #5bc0de; +} +.bs-callout-info h4 { + color: #5bc0de; +} + +.modal-dialog-center { /* Edited classname 10/03/2014 */ + margin: 0; + position: absolute; + top: 50%; + left: 50%; +} + +.wave-spinner { + margin: auto; + width: 100px; + height: 60px; + text-align: center; + font-size: 20px; +} + +html { + position: relative; + min-height: 100%; +} +body { + /* Margin bottom by footer height */ + padding-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + /* Set the fixed height of the footer here */ + height: 60px; + background-color: #f5f5f5; +} + +.container .text-muted { + margin: 20px 0; +} + +.footer > .container { + padding-right: 15px; + padding-left: 15px; +} + +a { + text-decoration: none !important; +} + +.alert { + position:absolute; + z-index:1; +} + +.login { + max-width: 320px; + margin: 0 auto; +} + +.login-or { + position: relative; + font-size: 18px; + color: #aaa; + margin-top: 10px; + margin-bottom: 10px; + padding-top: 10px; + padding-bottom: 10px; +} + +.span-or { + display: block; + position: absolute; + left: 50%; + top: -2px; + margin-left: -25px; + background-color: #fff; + width: 50px; + text-align: center; +} + +.hr-or { + background-color: #cdcdcd; + height: 1px; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +.profile-nav { + padding-top: 12px !important; + padding-bottom: 12px !important; +} + +.profile { + height: 35px; + width: 35px; + margin-left: 5px; +} + +.message { + margin-top: 10px; +} + diff --git a/lemur/status/__init__.py b/lemur/status/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/status/views.py b/lemur/status/views.py new file mode 100644 index 00000000..65d3af7a --- /dev/null +++ b/lemur/status/views.py @@ -0,0 +1,33 @@ +""" +.. module: lemur.status.views + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import os + +from flask import app, Blueprint, jsonify +from flask.ext.restful import Api + +from lemur.auth.service import AuthenticatedResource + + +mod = Blueprint('status', __name__) +api = Api(mod) + + +class Status(AuthenticatedResource): + """ Defines the 'accounts' endpoint """ + def __init__(self): + super(Status, self).__init__() + + def get(self): + if not os.path.isdir(os.path.join(app.config.get("KEY_PATH"), "decrypted")): + return jsonify({ + 'environment': app.config.get('ENVIRONMENT'), + 'status': 'degraded', + 'message': "This Lemur instance is in a degraded state and is unable to issue certificates, please alert secops@netflix.com"}) + else: + return jsonify({ + 'environment': app.config.get('ENVIRONMENT'), + 'status': 'healthy', + 'message': "This Lemur instance is healthy"}) diff --git a/lemur/templates/__init__.py b/lemur/templates/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/templates/config.py b/lemur/templates/config.py new file mode 100644 index 00000000..160fc146 --- /dev/null +++ b/lemur/templates/config.py @@ -0,0 +1,4 @@ +from jinja2 import Environment, PackageLoader + +loader = PackageLoader('lemur') +env = Environment(loader=loader) diff --git a/lemur/templates/event.html b/lemur/templates/event.html new file mode 100644 index 00000000..0c12b619 --- /dev/null +++ b/lemur/templates/event.html @@ -0,0 +1,144 @@ + + + + + + + + Lemur + + + +
+ + + + + + + + +
+ + + + + + {% for message in messages %} + + + + {% endfor %} +
+
+
+ Notice: Your SSL certificates are expiring! +
+
+ Lemur, Netflix's SSL management portal has noticed that the following certificates are expiring soon, if you rely on these certificates + you should create new certificates to replace the certificates that are expiring. Visit https://lemur.netflix.com/#/certificates/create to reissue them. +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if message.domains %} + {% for name in message.domains %} + + + + {% endfor %} + {% else %} + + + + {% endif %} + + + + {% if message.listeners %} + {% for name in message.listeners %} + + + + {% endfor %} + {% else %} + + + + {% endif %} + + + + {% if message.superseded %} + {% for name in message.superseded %} + + {% endfor %} + {% else %} + + {% endif %} + +
Name
{{ message.name }}
Owner
{{ message.owner }}
Not Before
{{ message.not_before }}
Not After
{{ message.not_after }}
Associated Domains
{{ name }}
Unknown
Associated ELBs
{{ name }}
None
Potentially Superseded by (Lemur's best guess)
{{ name }}
Unknown
+
+ +
+ Lemur is broken regularly by Security Operations +
+ +
+
+ + diff --git a/lemur/tests/__init__.py b/lemur/tests/__init__.py new file mode 100644 index 00000000..0b6502e9 --- /dev/null +++ b/lemur/tests/__init__.py @@ -0,0 +1,16 @@ +import unittest +from nose.tools import eq_ + +from lemur import app + +test_app = app.test_client() + +HEADERS = {'Content-Type': 'application/json'} + + +def check_content_type(headers): + eq_(headers['Content-Type'], 'application/json') + + +class LemurTestCase(unittest.TestCase): + pass diff --git a/lemur/tests/certificates/__init__.py b/lemur/tests/certificates/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/tests/certificates/test_certificates.py b/lemur/tests/certificates/test_certificates.py new file mode 100644 index 00000000..1480818c --- /dev/null +++ b/lemur/tests/certificates/test_certificates.py @@ -0,0 +1,87 @@ +import os +import shutil +import boto + +from lemur import app +from lemur.tests import LemurTestCase +from lemur.tests.constants import TEST_CERT, TEST_KEY + +from moto import mock_iam, mock_sts, mock_s3 + + +class CertificateTestCase(LemurTestCase): + def test_create_challenge(self): + from lemur.certificates.service import create_challenge + self.assertTrue(len(create_challenge()) >= 24) + + def test_hash_domains(self): + from lemur.certificates.service import hash_domains + h = hash_domains(['netflix.com', 'www.netflix.com', 'movies.netflix.com']) + self.assertEqual('c9c83253b46c7c1245c100ed3f7045eb', h) + + def test_create_csr(self): + from lemur.certificates.service import create_csr + from lemur.tests.certificates.test_csr import TEST_CSR + path = create_csr(['netflix.com'], TEST_CSR) + files = len(os.listdir(path)) + self.assertEqual(files, 4) + shutil.rmtree(path) + + def test_create_san_csr(self): + from lemur.certificates.service import create_csr + from lemur.tests.certificates.test_csr import TEST_CSR + path = create_csr(['netflix.com', 'www.netflix.com'], TEST_CSR) + files = len(os.listdir(path)) + self.assertEqual(files, 4) + shutil.rmtree(path) + + def test_create_path(self): + from lemur.certificates.service import create_path + path = create_path("blah") + self.assertIn('blah', path) + shutil.rmtree(path) + + @mock_s3 + @mock_sts + @mock_iam + def test_save_cert(self): + from lemur.certificates.service import save_cert + from lemur.common.services.aws.iam import get_all_server_certs + conn = boto.connect_s3() + bucket = conn.create_bucket(app.config.get('S3_BUCKET')) + cert = save_cert(TEST_CERT, TEST_KEY, None, "blah", "blah", [1]) + count = 0 + for key in bucket.list(): + count += 1 + + self.assertEqual(count, 4) + certs = get_all_server_certs('1111') + self.assertEqual(len(certs), 1) + +# @mock_s3 +# @mock_sts +# @mock_iam +# def test_upload_cert(self): +# from lemur.certificates.service import upload +# from lemur.common.services.aws.iam import get_all_server_certs +# conn = boto.connect_s3() +# bucket = conn.create_bucket(app.config.get('S3_BUCKET')) +# +# cert_up = {"public_cert": TEST_CERT, "private_key": TEST_KEY, "owner": "test@example.com", "accounts_ids": ['1111']} +# +# cert_name = upload(**cert_up) +# valid_name = 'AHB-dfdsflkj.net-NetflixInc-20140525-20150525' +# self.assertEqual(cert_name, valid_name) +# +# app.logger.debug(cert_name) +# count = 0 +# +# for key in bucket.list(): +# count += 1 +# +# self.assertEqual(count, 2) +# certs = get_all_server_certs('179727101194') +# self.assertEqual(len(certs), 1) +# +# +# diff --git a/lemur/tests/certificates/test_csr.py b/lemur/tests/certificates/test_csr.py new file mode 100644 index 00000000..25633a4a --- /dev/null +++ b/lemur/tests/certificates/test_csr.py @@ -0,0 +1,38 @@ +TEST_CSR = """ + # Configuration for standard CSR generation for Netflix + # Used for procuring VeriSign certificates + # Author: jachan + # Contact: cloudsecurity@netflix.com + + [ req ] + # Use a 2048 bit private key + default_bits = 2048 + default_keyfile = key.pem + prompt = no + encrypt_key = no + + # base request + distinguished_name = req_distinguished_name + + # extensions + # Uncomment the following line if you are requesting a SAN cert + {is_san_comment}req_extensions = req_ext + + # distinguished_name + [ req_distinguished_name ] + countryName = "US" # C= + stateOrProvinceName = "CALIFORNIA" # ST= + localityName = "Los Gatos" # L= + organizationName = "Netflix, Inc." # O= + organizationalUnitName = "Operations" # OU= + # This is the hostname/subject name on the certificate + commonName = "{DNS[0]}" # CN= + + [ req_ext ] + # Uncomment the following line if you are requesting a SAN cert + {is_san_comment}subjectAltName = @alt_names + + [alt_names] + # Put your SANs here + {DNS_LINES} + """ diff --git a/lemur/tests/certificates/test_pack/challenge.txt b/lemur/tests/certificates/test_pack/challenge.txt new file mode 100644 index 00000000..21c7ddb9 --- /dev/null +++ b/lemur/tests/certificates/test_pack/challenge.txt @@ -0,0 +1 @@ +KRPZPA&*!_~%dbnuzf153594 \ No newline at end of file diff --git a/lemur/tests/certificates/test_pack/csr_config.txt b/lemur/tests/certificates/test_pack/csr_config.txt new file mode 100644 index 00000000..0704e5bc --- /dev/null +++ b/lemur/tests/certificates/test_pack/csr_config.txt @@ -0,0 +1,38 @@ + + # Configuration for standard CSR generation for Netflix + # Used for procuring VeriSign certificates + # Author: jachan + # Contact: cloudsecurity@netflix.com + + [ req ] + # Use a 2048 bit private key + default_bits = 2048 + default_keyfile = key.pem + prompt = no + encrypt_key = no + + # base request + distinguished_name = req_distinguished_name + + # extensions + # Uncomment the following line if you are requesting a SAN cert + #req_extensions = req_ext + + # distinguished_name + [ req_distinguished_name ] + countryName = "US" # C= + stateOrProvinceName = "CALIFORNIA" # ST= + localityName = "Los Gatos" # L= + organizationName = "Netflix, Inc." # O= + organizationalUnitName = "Operations" # OU= + # This is the hostname/subject name on the certificate + commonName = "dfdsflkj.net" # CN= + + [ req_ext ] + # Uncomment the following line if you are requesting a SAN cert + #subjectAltName = @alt_names + + [alt_names] + # Put your SANs here + + \ No newline at end of file diff --git a/lemur/tests/certificates/test_pack/private.key b/lemur/tests/certificates/test_pack/private.key new file mode 100644 index 00000000..a70fee5f --- /dev/null +++ b/lemur/tests/certificates/test_pack/private.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAvNudwW+UeQqkpY71MIdEg501AFlPKuOXG2xU8DZhvZS6dKv+ +kDmIWdEqodDgkQiy0jyTgTwxwRqDSw96R6ZgrXefUoJJo66aCsosTBZtVaE85f1L +bj2+3U678c+rekUdkrnGcGCo6b8QtdvBpiDy2clneox8tSvmffAdcR1uCv/790/k +PzQ/djWDX9JcBRyDkcTJwYC0/ek7URvA/+MXmgUL13T+gWKqduaKuIBlFetonDjn +nO11QUBiusIuHV62wzKn8m5Nc+4XoaBR0YWMFn/g6qXDYrwfCsMpka7vSWJFv5Ff +yf+7kY3wU4xIwU2vXlIDcCsdUu6b/pYoQ0YOsQIDAQABAoIBAGbFH6iWnnXrq8MH +8zcQNOFmF+RztRgCt0TOA76f6TowB/LbcXBsTl2J7CgYMUvbLuwm2KHX7r9FPTMI +XiNFT5C16rYMfiQbLGo4sDhLb/3L+wawem6oHQfzA2VH++lSWRByFaEriF+CgIZl +6pALl/uZlLzkXCx+kjPwCSV3vV0wFkDnNs6+wPrz2IhkePsuC8J0QKQLlwsES2It +Gizzhpehdv9lc9MyZC//1QlD9gMDl5ok5Bt1Xm2c12XUEEcLlKQkJxiOrBOfXPmV +PHCdLc7gZO30hc6dyQ1SSnLpywhz/a0ir2GMvkMbS5hculpcZmwEcdZl1HYD8ObP +yOMbPE0CgYEA4LVGJKGtbM8RiBB0MstxNstMYVJ4mXB0lSQ0RazdO3S3ojn+oLpF +b2pvV6m9WnHiCGigWkzhqtGGCo6aqE0MoiR4jTN8GhiZz4ggDDaVgc4Px5reUD+r +tRsTpBHseGQ+ODGgkMI8eJYkdyqkECkYjAOrdy6uorvgxUAZecRIfJMCgYEA1yhM +7NidTNRuA+huS5GcQwQweTM6P1qF7Kfk1JYQMVu4gibLZiLHlWCyHI9lrbI7IaMm +g/4jXXoewv7IvyrrSEFulkPeVWxCe3mjfQ8JANfUj4kuR915LSn4lX2pbUgUS66K +vJSUJtnzLUmb8khLEcOmDbmTFZl8D/bTHFFZlisCgYAeelfWNhuoq3lMRDcOgKuN +bAujE6WJ4kfdxrhUTvr+ynjxxv3zXPB4CS6q7Dnjn5ix3UcKmGzvV1Xf7rGpbDHv +eBTlyfrmKzoJfQQjw++JWKKpRycqKUin2tFSKqAxQB90Tb7ig4XiMTMm+qCgFILg +0sqZ8rn7FpKJDoWmD2ppgwKBgG2Dl9QeVcKbhfv7PNi+HvmFkl6+knFY1D4nHzSN +xWQ6OWoV8QXlwgzokQA0hR6qT6rJbntUyg90b1/1a5zSbbvzgiR+GxcD6bsLqQmo +s354XTtKKgJuWpWAfYUp1ylGvP3gs8FVJyu3WC2+/9+MqJk8KrNlt9YQr7M4gTAy +wBTNAoGAGU7Po4uI3xDKGLLK/ot3D3P8U9ByfeLlrUZtTz1PASsMOr92bkXmUPlE +DYUd5uFfwwlvbMNT1Ooeyrzg3bARd9B6ATyMkOaJeGoQwFAI468iucnm9rNXB+/t +U2rbIi1pXSm8zSNEY85tf6C8DU/5YbcAPf47a2UYhwCpYAJfMk0= +-----END RSA PRIVATE KEY----- diff --git a/lemur/tests/certificates/test_pack/request.csr b/lemur/tests/certificates/test_pack/request.csr new file mode 100644 index 00000000..7b2eecf5 --- /dev/null +++ b/lemur/tests/certificates/test_pack/request.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICvzCCAacCAQAwejELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNBTElGT1JOSUEx +EjAQBgNVBAcTCUxvcyBHYXRvczEWMBQGA1UEChMNTmV0ZmxpeCwgSW5jLjETMBEG +A1UECxMKT3BlcmF0aW9uczEVMBMGA1UEAxMMZGZkc2Zsa2oubmV0MIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNudwW+UeQqkpY71MIdEg501AFlPKuOX +G2xU8DZhvZS6dKv+kDmIWdEqodDgkQiy0jyTgTwxwRqDSw96R6ZgrXefUoJJo66a +CsosTBZtVaE85f1Lbj2+3U678c+rekUdkrnGcGCo6b8QtdvBpiDy2clneox8tSvm +ffAdcR1uCv/790/kPzQ/djWDX9JcBRyDkcTJwYC0/ek7URvA/+MXmgUL13T+gWKq +duaKuIBlFetonDjnnO11QUBiusIuHV62wzKn8m5Nc+4XoaBR0YWMFn/g6qXDYrwf +CsMpka7vSWJFv5Ffyf+7kY3wU4xIwU2vXlIDcCsdUu6b/pYoQ0YOsQIDAQABoAAw +DQYJKoZIhvcNAQEFBQADggEBAE8b0+IYGiR64Me/L0/njYvSR5WR4EnjW99Sc8X5 +k93zpk4hExrZhrlkDBA/jUHhBZcPNV9w/YkhSu5ubPjRp9gRM2d4B9gGJFAs+bwe +LS9hCOxWIMKgvaBMEDQFcwqAv6kEJzmrIa7LtWS39wNfdko2hANtm7z9qskc8bPr +265+Z48DwSNCF4RPhVp9eDifjHrj0I//GMXYa92uvgj1BlPo/SGMS+XFQF779p2b +622HmUCop3pYeIyYd6rirvl9+KwqvIhm2MqHk62eHOK7Bn/FPev8OUDeV6pIvvSV +UxsEHjjLm0V/lOD65lROc7dTq4jO5PkpoKnFQDgV5v0Bf/k= +-----END CERTIFICATE REQUEST----- diff --git a/lemur/tests/certificates/test_pack/server.crt b/lemur/tests/certificates/test_pack/server.crt new file mode 100644 index 00000000..64ec5358 --- /dev/null +++ b/lemur/tests/certificates/test_pack/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDcDCCAlgCCQC8msHu/aa61zANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV +UzETMBEGA1UECBMKQ0FMSUZPUk5JQTESMBAGA1UEBxMJTG9zIEdhdG9zMRYwFAYD +VQQKEw1OZXRmbGl4LCBJbmMuMRMwEQYDVQQLEwpPcGVyYXRpb25zMRUwEwYDVQQD +EwxkZmRzZmxrai5uZXQwHhcNMTQwNTI1MTczMDMzWhcNMTUwNTI1MTczMDMzWjB6 +MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ0FMSUZPUk5JQTESMBAGA1UEBxMJTG9z +IEdhdG9zMRYwFAYDVQQKEw1OZXRmbGl4LCBJbmMuMRMwEQYDVQQLEwpPcGVyYXRp +b25zMRUwEwYDVQQDEwxkZmRzZmxrai5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC8253Bb5R5CqSljvUwh0SDnTUAWU8q45cbbFTwNmG9lLp0q/6Q +OYhZ0Sqh0OCRCLLSPJOBPDHBGoNLD3pHpmCtd59SgkmjrpoKyixMFm1VoTzl/Utu +Pb7dTrvxz6t6RR2SucZwYKjpvxC128GmIPLZyWd6jHy1K+Z98B1xHW4K//v3T+Q/ +ND92NYNf0lwFHIORxMnBgLT96TtRG8D/4xeaBQvXdP6BYqp25oq4gGUV62icOOec +7XVBQGK6wi4dXrbDMqfybk1z7hehoFHRhYwWf+DqpcNivB8KwymRru9JYkW/kV/J +/7uRjfBTjEjBTa9eUgNwKx1S7pv+lihDRg6xAgMBAAEwDQYJKoZIhvcNAQEFBQAD +ggEBAJHwa4l2iSiFBb6wVFBJEWEt31qp+njiVCoTg2OJzCT60Xb26hkrsiTldIIh +eB9+y+fwdfwopzWhkNbIOlCfudx/uxtpor8/3BRbjSlNwDUg2L8pfAircJMFLQUM +O6nqPOBWCe8hXwe9FQM/oFOavf/AAw/FED+892xlytjirK9u3B28O20W11+fY7hp +8LQVBrMoVxFeLWmmwETAltJ7HEYutplRzYTM0vLBARl4Vd5kLJlY3j2Dp1ZpRGcg +CrQp26UD/oaAPGtiZQSC4LJ+4JfOuuqbm3CI24QMCh9rxv3ZoOQnFuC+7cZgqrat +V4bxCrVvWhrrDSgy9+A80NVzQ3k= +-----END CERTIFICATE----- diff --git a/lemur/tests/constants.py b/lemur/tests/constants.py new file mode 100644 index 00000000..8632e76a --- /dev/null +++ b/lemur/tests/constants.py @@ -0,0 +1,51 @@ +TEST_KEY = """-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAvNudwW+UeQqkpY71MIdEg501AFlPKuOXG2xU8DZhvZS6dKv+ +kDmIWdEqodDgkQiy0jyTgTwxwRqDSw96R6ZgrXefUoJJo66aCsosTBZtVaE85f1L +bj2+3U678c+rekUdkrnGcGCo6b8QtdvBpiDy2clneox8tSvmffAdcR1uCv/790/k +PzQ/djWDX9JcBRyDkcTJwYC0/ek7URvA/+MXmgUL13T+gWKqduaKuIBlFetonDjn +nO11QUBiusIuHV62wzKn8m5Nc+4XoaBR0YWMFn/g6qXDYrwfCsMpka7vSWJFv5Ff +yf+7kY3wU4xIwU2vXlIDcCsdUu6b/pYoQ0YOsQIDAQABAoIBAGbFH6iWnnXrq8MH +8zcQNOFmF+RztRgCt0TOA76f6TowB/LbcXBsTl2J7CgYMUvbLuwm2KHX7r9FPTMI +XiNFT5C16rYMfiQbLGo4sDhLb/3L+wawem6oHQfzA2VH++lSWRByFaEriF+CgIZl +6pALl/uZlLzkXCx+kjPwCSV3vV0wFkDnNs6+wPrz2IhkePsuC8J0QKQLlwsES2It +Gizzhpehdv9lc9MyZC//1QlD9gMDl5ok5Bt1Xm2c12XUEEcLlKQkJxiOrBOfXPmV +PHCdLc7gZO30hc6dyQ1SSnLpywhz/a0ir2GMvkMbS5hculpcZmwEcdZl1HYD8ObP +yOMbPE0CgYEA4LVGJKGtbM8RiBB0MstxNstMYVJ4mXB0lSQ0RazdO3S3ojn+oLpF +b2pvV6m9WnHiCGigWkzhqtGGCo6aqE0MoiR4jTN8GhiZz4ggDDaVgc4Px5reUD+r +tRsTpBHseGQ+ODGgkMI8eJYkdyqkECkYjAOrdy6uorvgxUAZecRIfJMCgYEA1yhM +7NidTNRuA+huS5GcQwQweTM6P1qF7Kfk1JYQMVu4gibLZiLHlWCyHI9lrbI7IaMm +g/4jXXoewv7IvyrrSEFulkPeVWxCe3mjfQ8JANfUj4kuR915LSn4lX2pbUgUS66K +vJSUJtnzLUmb8khLEcOmDbmTFZl8D/bTHFFZlisCgYAeelfWNhuoq3lMRDcOgKuN +bAujE6WJ4kfdxrhUTvr+ynjxxv3zXPB4CS6q7Dnjn5ix3UcKmGzvV1Xf7rGpbDHv +eBTlyfrmKzoJfQQjw++JWKKpRycqKUin2tFSKqAxQB90Tb7ig4XiMTMm+qCgFILg +0sqZ8rn7FpKJDoWmD2ppgwKBgG2Dl9QeVcKbhfv7PNi+HvmFkl6+knFY1D4nHzSN +xWQ6OWoV8QXlwgzokQA0hR6qT6rJbntUyg90b1/1a5zSbbvzgiR+GxcD6bsLqQmo +s354XTtKKgJuWpWAfYUp1ylGvP3gs8FVJyu3WC2+/9+MqJk8KrNlt9YQr7M4gTAy +wBTNAoGAGU7Po4uI3xDKGLLK/ot3D3P8U9ByfeLlrUZtTz1PASsMOr92bkXmUPlE +DYUd5uFfwwlvbMNT1Ooeyrzg3bARd9B6ATyMkOaJeGoQwFAI468iucnm9rNXB+/t +U2rbIi1pXSm8zSNEY85tf6C8DU/5YbcAPf47a2UYhwCpYAJfMk0= +-----END RSA PRIVATE KEY-----""" + +TEST_CERT = """-----BEGIN CERTIFICATE----- +MIIDcDCCAlgCCQC8msHu/aa61zANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV +UzETMBEGA1UECBMKQ0FMSUZPUk5JQTESMBAGA1UEBxMJTG9zIEdhdG9zMRYwFAYD +VQQKEw1OZXRmbGl4LCBJbmMuMRMwEQYDVQQLEwpPcGVyYXRpb25zMRUwEwYDVQQD +EwxkZmRzZmxrai5uZXQwHhcNMTQwNTI1MTczMDMzWhcNMTUwNTI1MTczMDMzWjB6 +MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ0FMSUZPUk5JQTESMBAGA1UEBxMJTG9z +IEdhdG9zMRYwFAYDVQQKEw1OZXRmbGl4LCBJbmMuMRMwEQYDVQQLEwpPcGVyYXRp +b25zMRUwEwYDVQQDEwxkZmRzZmxrai5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC8253Bb5R5CqSljvUwh0SDnTUAWU8q45cbbFTwNmG9lLp0q/6Q +OYhZ0Sqh0OCRCLLSPJOBPDHBGoNLD3pHpmCtd59SgkmjrpoKyixMFm1VoTzl/Utu +Pb7dTrvxz6t6RR2SucZwYKjpvxC128GmIPLZyWd6jHy1K+Z98B1xHW4K//v3T+Q/ +ND92NYNf0lwFHIORxMnBgLT96TtRG8D/4xeaBQvXdP6BYqp25oq4gGUV62icOOec +7XVBQGK6wi4dXrbDMqfybk1z7hehoFHRhYwWf+DqpcNivB8KwymRru9JYkW/kV/J +/7uRjfBTjEjBTa9eUgNwKx1S7pv+lihDRg6xAgMBAAEwDQYJKoZIhvcNAQEFBQAD +ggEBAJHwa4l2iSiFBb6wVFBJEWEt31qp+njiVCoTg2OJzCT60Xb26hkrsiTldIIh +eB9+y+fwdfwopzWhkNbIOlCfudx/uxtpor8/3BRbjSlNwDUg2L8pfAircJMFLQUM +O6nqPOBWCe8hXwe9FQM/oFOavf/AAw/FED+892xlytjirK9u3B28O20W11+fY7hp +8LQVBrMoVxFeLWmmwETAltJ7HEYutplRzYTM0vLBARl4Vd5kLJlY3j2Dp1ZpRGcg +CrQp26UD/oaAPGtiZQSC4LJ+4JfOuuqbm3CI24QMCh9rxv3ZoOQnFuC+7cZgqrat +V4bxCrVvWhrrDSgy9+A80NVzQ3k= +-----END CERTIFICATE-----""" + + diff --git a/lemur/tests/elbs/__init__.py b/lemur/tests/elbs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/tests/elbs/test_elbs.py b/lemur/tests/elbs/test_elbs.py new file mode 100644 index 00000000..1d31fd12 --- /dev/null +++ b/lemur/tests/elbs/test_elbs.py @@ -0,0 +1,5 @@ +import os +import shutil +from lemur import app +from lemur.tests import LemurTestCase + diff --git a/lemur/tests/js/.jshintrc b/lemur/tests/js/.jshintrc new file mode 100644 index 00000000..b1be025b --- /dev/null +++ b/lemur/tests/js/.jshintrc @@ -0,0 +1,36 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "smarttabs": true, + "globals": { + "after": false, + "afterEach": false, + "angular": false, + "before": false, + "beforeEach": false, + "browser": false, + "describe": false, + "expect": false, + "inject": false, + "it": false, + "jasmine": false, + "spyOn": false + } +} + diff --git a/lemur/tests/js/runner.html b/lemur/tests/js/runner.html new file mode 100644 index 00000000..f4a00a12 --- /dev/null +++ b/lemur/tests/js/runner.html @@ -0,0 +1,10 @@ + + + + End2end Test Runner + + + + + + \ No newline at end of file diff --git a/lemur/tests/js/spec/controllers/certificatecreate.js b/lemur/tests/js/spec/controllers/certificatecreate.js new file mode 100644 index 00000000..ce250eac --- /dev/null +++ b/lemur/tests/js/spec/controllers/certificatecreate.js @@ -0,0 +1,22 @@ +'use strict'; + +describe('Controller: CertificatecreateCtrl', function () { + + // load the controller's module + beforeEach(module('lemurApp')); + + var CertificatecreateCtrl, + scope; + + // Initialize the controller and a mock scope + beforeEach(inject(function ($controller, $rootScope) { + scope = $rootScope.$new(); + CertificatecreateCtrl = $controller('CertificatecreateCtrl', { + $scope: scope + }); + })); + + it('should attach a list of awesomeThings to the scope', function () { + expect(scope.awesomeThings.length).toBe(3); + }); +}); diff --git a/lemur/tests/js/spec/controllers/certificates.js b/lemur/tests/js/spec/controllers/certificates.js new file mode 100644 index 00000000..2e329b9e --- /dev/null +++ b/lemur/tests/js/spec/controllers/certificates.js @@ -0,0 +1,22 @@ +'use strict'; + +describe('Controller: CertificatesCtrl', function () { + + // load the controller's module + beforeEach(module('lemurApp')); + + var CertificatesCtrl, + scope; + + // Initialize the controller and a mock scope + beforeEach(inject(function ($controller, $rootScope) { + scope = $rootScope.$new(); + CertificatesCtrl = $controller('CertificatesCtrl', { + $scope: scope + }); + })); + + it('should attach a list of awesomeThings to the scope', function () { + expect(scope.awesomeThings.length).toBe(3); + }); +}); diff --git a/lemur/tests/js/spec/controllers/dashboard.js b/lemur/tests/js/spec/controllers/dashboard.js new file mode 100644 index 00000000..e0fd64f1 --- /dev/null +++ b/lemur/tests/js/spec/controllers/dashboard.js @@ -0,0 +1,22 @@ +'use strict'; + +describe('Controller: DashboardCtrl', function () { + + // load the controller's module + beforeEach(module('lemurApp')); + + var DashboardCtrl, + scope; + + // Initialize the controller and a mock scope + beforeEach(inject(function ($controller, $rootScope) { + scope = $rootScope.$new(); + DashboardCtrl = $controller('DashboardCtrl', { + $scope: scope + }); + })); + + it('should attach a list of awesomeThings to the scope', function () { + expect(scope.awesomeThings.length).toBe(3); + }); +}); diff --git a/lemur/tests/js/spec/controllers/elbs.js b/lemur/tests/js/spec/controllers/elbs.js new file mode 100644 index 00000000..c4d0087a --- /dev/null +++ b/lemur/tests/js/spec/controllers/elbs.js @@ -0,0 +1,22 @@ +'use strict'; + +describe('Controller: ElbsCtrl', function () { + + // load the controller's module + beforeEach(module('lemurApp')); + + var ElbsCtrl, + scope; + + // Initialize the controller and a mock scope + beforeEach(inject(function ($controller, $rootScope) { + scope = $rootScope.$new(); + ElbsCtrl = $controller('ElbsCtrl', { + $scope: scope + }); + })); + + it('should attach a list of awesomeThings to the scope', function () { + expect(scope.awesomeThings.length).toBe(3); + }); +}); diff --git a/lemur/tests/js/spec/controllers/main.js b/lemur/tests/js/spec/controllers/main.js new file mode 100644 index 00000000..b158c9c0 --- /dev/null +++ b/lemur/tests/js/spec/controllers/main.js @@ -0,0 +1,22 @@ +'use strict'; + +describe('Controller: MainCtrl', function () { + + // load the controller's module + beforeEach(module('lemurApp')); + + var MainCtrl, + scope; + + // Initialize the controller and a mock scope + beforeEach(inject(function ($controller, $rootScope) { + scope = $rootScope.$new(); + MainCtrl = $controller('MainCtrl', { + $scope: scope + }); + })); + + it('should attach a list of awesomeThings to the scope', function () { + expect(scope.awesomeThings.length).toBe(3); + }); +}); diff --git a/lemur/tests/listeners/__init__.py b/lemur/tests/listeners/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/tests/services/__init__.py b/lemur/tests/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/tests/services/test_elb.py b/lemur/tests/services/test_elb.py new file mode 100644 index 00000000..0303f77e --- /dev/null +++ b/lemur/tests/services/test_elb.py @@ -0,0 +1,51 @@ +import boto +from lemur.tests import LemurTestCase + +from moto import mock_elb, mock_sts + + +class ELBTestCase(LemurTestCase): + @mock_sts + @mock_elb + def test_add_listener(self): + from lemur.common.services.aws.elb import create_new_listeners + conn = boto.connect_elb() + zones = ['us-east-1a', 'us-east-1b'] + ports = [(80, 8080, 'http')] + conn.create_load_balancer('my-lb', zones, ports) + create_new_listeners('111', 'us-east-1', 'my-lb', listeners=[('443', '80', 'HTTP')]) + balancer = conn.get_all_load_balancers()[0] + self.assertEqual(balancer.name, "my-lb") + self.assertEqual(len(balancer.listeners), 2) + + @mock_sts + @mock_elb + def test_update_listener(self): + from lemur.common.services.aws.elb import update_listeners + conn = boto.connect_elb() + zones = ['us-east-1a', 'us-east-1b'] + ports = [(80, 8080, 'http')] + conn.create_load_balancer('my-lb', zones, ports) + update_listeners('111', 'us-east-1', 'my-lb', listeners=[('80', '7001', 'http')]) + balancer = conn.get_all_load_balancers()[0] + listener = balancer.listeners[0] + self.assertEqual(listener.load_balancer_port, 80) + self.assertEqual(listener.instance_port, 7001) + self.assertEqual(listener.protocol, "HTTP") + + @mock_sts + @mock_elb + def test_set_certificate(self): + from lemur.common.services.aws.elb import attach_certificate + conn = boto.connect_elb() + zones = ['us-east-1a', 'us-east-1b'] + ports = [(443, 7001, 'https', 'sslcert')] + conn.create_load_balancer('my-lb', zones, ports) + attach_certificate('1111', 'us-east-1', 'my-lb', 443, 'somecert') + balancer = conn.get_all_load_balancers()[0] + listener = balancer.listeners[0] + self.assertEqual(listener.load_balancer_port, 443) + self.assertEqual(listener.instance_port, 7001) + self.assertEqual(listener.protocol, "HTTPS") + self.assertEqual(listener.ssl_certificate_id, 'somecert') + diff --git a/lemur/tests/services/test_iam.py b/lemur/tests/services/test_iam.py new file mode 100644 index 00000000..68315e1b --- /dev/null +++ b/lemur/tests/services/test_iam.py @@ -0,0 +1,37 @@ +from lemur import app +from lemur.tests import LemurTestCase +from lemur.tests.constants import TEST_CERT, TEST_KEY + +from lemur.certificates.models import Certificate + +from moto import mock_iam, mock_sts + + +class IAMTestCase(LemurTestCase): + @mock_sts + @mock_iam + def test_get_all_server_certs(self): + from lemur.common.services.aws.iam import upload_cert, get_all_server_certs + cert = Certificate(TEST_CERT) + upload_cert('1111', cert, TEST_KEY) + certs = get_all_server_certs('1111') + self.assertEquals(len(certs), 1) + + @mock_sts + @mock_iam + def test_get_server_cert(self): + from lemur.common.services.aws.iam import upload_cert, get_cert_from_arn + cert = Certificate(TEST_CERT) + upload_cert('1111', cert, TEST_KEY) + body, chain = get_cert_from_arn('arn:aws:iam::123456789012:server-certificate/AHB-dfdsflkj.net-NetflixInc-20140525-20150525') + self.assertTrue(body) + + @mock_sts + @mock_iam + def test_upload_server_cert(self): + from lemur.common.services.aws.iam import upload_cert + cert = Certificate(TEST_CERT) + response = upload_cert('1111', cert, TEST_KEY) + self.assertEquals(response['upload_server_certificate_response']['upload_server_certificate_result']['server_certificate_metadata']['server_certificate_name'], 'AHB-dfdsflkj.net-NetflixInc-20140525-20150525') + + diff --git a/lemur/tests/services/test_issuer_manager.py b/lemur/tests/services/test_issuer_manager.py new file mode 100644 index 00000000..6408438d --- /dev/null +++ b/lemur/tests/services/test_issuer_manager.py @@ -0,0 +1,23 @@ +from lemur import app +from lemur.tests import LemurTestCase +from lemur.tests.constants import TEST_CERT, TEST_KEY + +from lemur.certificates.models import Certificate + +from moto import mock_iam, mock_sts + + +class ManagerTestCase(LemurTestCase): + def test_validate_authority(self): + pass + + def test_get_all_authorities(self): + from lemur.common.services.issuers.manager import get_all_authorities + authorities = get_all_authorities() + self.assertEqual(len(authorities), 3) + + def test_get_all_issuers(self): + from lemur.common.services.issuers.manager import get_all_issuers + issuers = get_all_issuers() + self.assertEqual(len(issuers) > 1) + diff --git a/lemur/tests/services/test_s3.py b/lemur/tests/services/test_s3.py new file mode 100644 index 00000000..f5de669f --- /dev/null +++ b/lemur/tests/services/test_s3.py @@ -0,0 +1,27 @@ +import boto + +from lemur.tests import LemurTestCase +from lemur.tests.constants import TEST_CERT + +from lemur.certificates.models import Certificate + +from moto import mock_s3 + + +class S3TestCase(LemurTestCase): + @mock_s3 + def test_save(self): + from lemur.common.services.aws.s3 import save + conn = boto.connect_s3() + + cert = Certificate(TEST_CERT) + + buck = conn.create_bucket('test') + path = save(cert, 'private_key', None, 'csr_config', 'challenge') + self.assertEqual(path, 'lemur/{}/{}/'.format(cert.issuer, cert.name)) + + count = 0 + for key in buck.list(): + count += 1 + + self.assertEqual(count, 4) diff --git a/lemur/users/__init__.py b/lemur/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/users/models.py b/lemur/users/models.py new file mode 100644 index 00000000..08b341e7 --- /dev/null +++ b/lemur/users/models.py @@ -0,0 +1,88 @@ +""" +.. module: lemur.users.models + :platform: unix + :synopsis: This module contains all of the models need to create a user within + lemur + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from sqlalchemy.orm import relationship +from sqlalchemy import Column, Integer, String, Boolean, DateTime +from sqlalchemy.event import listen + + +from lemur.database import db +from lemur.models import roles_users + +from lemur.extensions import bcrypt + + +def hash_password(mapper, connect, target): + """ + Helper function that is a listener and hashes passwords before + insertion into the database. + + :param mapper: + :param connect: + :param target: + """ + target.hash_password() + + +class User(db.Model): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + password = Column(String(128)) + active = Column(Boolean()) + confirmed_at = Column(DateTime()) + username = Column(String(255), nullable=False, unique=True) + email = Column(String(128), unique=True) + profile_picture = Column(String(255)) + roles = relationship('Role', secondary=roles_users, passive_deletes=True, backref=db.backref('user'), lazy='dynamic') + certificates = relationship("Certificate", backref=db.backref('user'), lazy='dynamic') + authorities = relationship("Authority", backref=db.backref('user'), lazy='dynamic') + + def check_password(self, password): + """ + Hash a given password and check it against the stored value + to determine it's validity. + + :param password: + :return: + """ + return bcrypt.check_password_hash(self.password, password) + + def hash_password(self): + """ + Generate the secure hash for the password. + + :return: + """ + self.password = bcrypt.generate_password_hash(self.password) + return self.password + + @property + def is_admin(self): + """ + Determine if the current user has the 'admin' role associated + with it. + + :return: + """ + for role in self.roles: + if role.name == 'admin': + return True + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + def serialize(self): + blob = self.as_dict() + return blob + + +listen(User, 'before_insert', hash_password) + + diff --git a/lemur/users/service.py b/lemur/users/service.py new file mode 100644 index 00000000..f3fd84ba --- /dev/null +++ b/lemur/users/service.py @@ -0,0 +1,150 @@ +""" +.. module: lemur.users.service + :platform: Unix + :synopsis: This module contains all of the services level functions used to + administer users in Lemur + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from lemur import database +from lemur.users.models import User + + +def create(username, password, email, active, profile_picture, roles): + """ + Create a new user + + :param username: + :param password: + :param email: + :param active: + :param profile_picture: + :param roles: + :return: + """ + user = User( + password=password, + username=username, + email=email, + active=active, + profile_picture=profile_picture, + role=roles + ) + user = database.create(user) + return database.update(user) + + +def update(user_id, username, email, active, profile_picture, roles): + """ + Updates an existing user + + :param user_id: + :param username: + :param email: + :param active: + :param profile_picture: + :param roles: + :return: + """ + user = get(user_id) + user.username = username + user.email = email + user.active = active + user.profile_picture = profile_picture + update_roles(user, roles) + return database.update(user) + + +def update_roles(user, roles): + """ + Replaces the roles with new ones. This will detect + when are roles added as well as when there are roles + removed. + + :param user: + :param roles: + """ + for ur in roles: + for r in roles: + if r.id == ur.id: + break + else: + user.roles.remove(r) + + for r in roles: + for ur in user.roles: + if r.id == ur.id: + break + else: + user.roles.append(r) + + +def get(user_id): + """ + Retrieve a user from the database + + :param user_id: + :return: + """ + return database.get(User, user_id) + + +def get_by_email(email): + """ + Retrieve a user from the database by their email address + + :param email: + :return: + """ + return database.get(User, email, field='email') + + +def get_by_username(username): + """ + Retrieve a user from the database by their username + + :param username: + :return: + """ + return database.get(User, username, field='username') + + +def get_all(): + """ + Retrieve all users from the database. + + :return: + """ + query = database.session_query(User) + return database.find_all(query, User, {}).all() + + +def render(args): + """ + Helper that paginates and filters data when requested + through the REST Api + + :param args: + :return: + """ + query = database.session_query(User) + + sort_by = args.pop('sort_by') + sort_dir = args.pop('sort_dir') + page = args.pop('page') + count = args.pop('count') + filt = args.pop('filter') + + if filt: + terms = filt.split(';') + query = database.filter(query, User, terms) + + query = database.find_all(query, User, args) + + if sort_by and sort_dir: + query = database.sort(query, User, sort_by, sort_dir) + + return database.paginate(query, page, count) + + diff --git a/lemur/users/views.py b/lemur/users/views.py new file mode 100644 index 00000000..548caaeb --- /dev/null +++ b/lemur/users/views.py @@ -0,0 +1,401 @@ +""" +.. module: lemur.user.views + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from flask import g, Blueprint +from flask.ext.restful import reqparse, Api, fields + +from lemur.users import service +from lemur.certificates import service as certificate_service +from lemur.roles import service as role_service +from lemur.auth.service import AuthenticatedResource +from lemur.auth.permissions import admin_permission +from lemur.common.utils import marshal_items, paginated_parser + + +mod = Blueprint('users', __name__) +api = Api(mod) + + +FIELDS = { + 'username': fields.String, + 'active': fields.Boolean, + 'email': fields.String, + 'profileImage': fields.String(attribute='profile_picture'), + 'id': fields.Integer, +} + + +def roles(values): + """ + Validate that the passed in roles exist. + + :param values: + :return: :raise ValueError: + """ + rs = [] + for role in values: + r = role_service.get(role['id']) + if not r: + raise ValueError("Role {0} does not exist".format(role['name'])) + rs.append(r) + return rs + + +class UsersList(AuthenticatedResource): + """ Defines the 'users' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(UsersList, self).__init__() + + @marshal_items(FIELDS) + def get(self): + """ + .. http:get:: /users + + The current user list + + **Example request**: + + .. sourcecode:: http + + GET /users HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "items": [ + { + "id": 2, + "active": True, + "email": "user2@example.com", + "username": "user2", + "profileImage": null + }, + { + "id": 1, + "active": False, + "email": "user1@example.com", + "username": "user1", + "profileImage": null + } + ] + "total": 2 + } + + :query sortBy: field to sort on + :query sortDir: acs or desc + :query page: int. default is 1 + :query filter: key value pair. format is k=v; + :query limit: limit number. default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + parser = paginated_parser.copy() + parser.add_argument('owner', type=str, location='args') + parser.add_argument('id', type=str, location='args') + args = parser.parse_args() + return service.render(args) + + @admin_permission.require(http_exception=403) + @marshal_items(FIELDS) + def post(self): + """ + .. http:post:: /users + + Creates a new user + + **Example request**: + + .. sourcecode:: http + + POST /users HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "username": "user3", + "email": "user3@example.com", + "active": true, + "roles": [] + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 3, + "active": True, + "email": "user3@example.com, + "username": "user3", + "profileImage": null + } + + :arg username: username for new user + :arg email: email address for new user + :arg password: password for new user + :arg active: boolean, if the user is currently active + :arg roles: list, roles that the user should be apart of + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + self.reqparse.add_argument('username', type=str, location='json', required=True) + self.reqparse.add_argument('email', type=str, location='json', required=True) + self.reqparse.add_argument('password', type=str, location='json', required=True) + self.reqparse.add_argument('active', type=bool, default=True, location='json') + self.reqparse.add_argument('roles', type=roles, default=[], location='json') + + args = self.reqparse.parse_args() + return service.create(args['username'], args['password'], args['email'], args['active'], None, args['roles']) + + +class Users(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Users, self).__init__() + + @marshal_items(FIELDS) + def get(self, user_id): + """ + .. http:get:: /users/1 + + Get a specific user + + **Example request**: + + .. sourcecode:: http + + GET /users/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "active": false, + "email": "user1@example.com", + "username": "user1", + "profileImage": null + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + return service.get(user_id) + + @admin_permission.require(http_exception=403) + @marshal_items(FIELDS) + def put(self, user_id): + """ + .. http:put:: /users/1 + + Update a user + + **Example request**: + + .. sourcecode:: http + + PUT /users/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "username": "user1", + "email": "user1@example.com", + "active": false, + "roles": [] + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "username": "user1", + "email": "user1@example.com", + "active": false, + "profileImage": null + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + self.reqparse.add_argument('username', type=str, location='json', required=True) + self.reqparse.add_argument('email', type=str, location='json', required=True) + self.reqparse.add_argument('active', type=bool, location='json', required=True) + self.reqparse.add_argument('roles', type=roles, default=[], location='json', required=True) + + args = self.reqparse.parse_args() + return service.update(user_id, args['username'], args['email'], args['active'], None, args['roles']) + + +class CertificateUsers(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(CertificateUsers, self).__init__() + + @marshal_items(FIELDS) + def get(self, certificate_id): + """ + .. http:get:: /certificates/1/creator + + Get a certificate's creator + + **Example request**: + + .. sourcecode:: http + + GET /certificates/1/creator HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "active": false, + "email": "user1@example.com", + "username": "user1", + "profileImage": null + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + return certificate_service.get(certificate_id).user + + +class RoleUsers(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(RoleUsers, self).__init__() + + @marshal_items(FIELDS) + def get(self, role_id): + """ + .. http:get:: /roles/1/users + + Get all users associated with a role + + **Example request**: + + .. sourcecode:: http + + GET /roles/1/users HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "items": [ + { + "id": 2, + "active": True, + "email": "user2@example.com", + "username": "user2", + "profileImage": null + }, + { + "id": 1, + "active": False, + "email": "user1@example.com", + "username": "user1", + "profileImage": null + } + ] + "total": 2 + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + return role_service.get(role_id).users + + +class Me(AuthenticatedResource): + def __init__(self): + super(Me, self).__init__() + + @marshal_items(FIELDS) + def get(self): + """ + .. http:get:: /auth/me + + Get the currently authenticated user + + **Example request**: + + .. sourcecode:: http + + GET /auth/me HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "id": 1, + "active": false, + "email": "user1@example.com", + "username": "user1", + "profileImage": null + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + return g.current_user.as_dict() + + +api.add_resource(Me, '/auth/me', endpoint='me') +api.add_resource(UsersList, '/users', endpoint='users') +api.add_resource(Users, '/users/', endpoint='user') +api.add_resource(CertificateUsers, '/certificates//creator', endpoint='certificateCreator') +api.add_resource(RoleUsers, '/roles//users', endpoint='roleUsers') diff --git a/package.json b/package.json new file mode 100644 index 00000000..ee027237 --- /dev/null +++ b/package.json @@ -0,0 +1,67 @@ +{ + "name": "Lemur", + "version": "0.0.0", + "private": true, + "repository": { + "type": "git", + "url": "git://github.com/netflix/lemur.git" + }, + "dependencies": { + "del": "^0.1.3", + "gulp-concat": "^2.4.1", + "gulp-debug": "^1.0.1", + "gulp-foreach": "0.0.1", + "gulp-if": "^1.2.5", + "gulp-less": "^3.0.3", + "gulp-minify-css": "^0.3.10", + "gulp-order": "^1.1.1", + "gulp-rename": "^1.2.0", + "gulp-util": "^3.0.1", + "merge-stream": "^0.1.6", + "browser-sync": "^2.3.1", + "gulp": "^3.8.11", + "gulp-autoprefixer": "^0.0.8", + "gulp-cache": "^0.2.0", + "gulp-csso": "^0.2.9", + "gulp-filter": "^1.0.0", + "gulp-flatten": "^0.0.2", + "gulp-imagemin": "^0.6.2", + "gulp-inject": "~1.0.1", + "gulp-jshint": "^1.10.0", + "gulp-karma": "^0.0.4", + "gulp-load-plugins": "^0.5.3", + "gulp-minify-html": "~0.1.4", + "gulp-ng-annotate": "~0.5.2", + "gulp-ng-html2js": "~0.1.7", + "gulp-notify": "^2.2.0", + "gulp-plumber": "^0.6.4", + "gulp-print": "^1.1.0", + "gulp-protractor": "0.0.11", + "gulp-replace": "~0.4.0", + "gulp-replace-task": "~0.1.0", + "gulp-rev": "^1.0.0", + "gulp-rev-replace": "^0.3.0", + "gulp-serve": "~0.3.0", + "gulp-size": "^1.0.0", + "gulp-uglify": "^0.3.1", + "gulp-useref": "^0.6.0", + "http-proxy": "~1.11.1", + "jshint-stylish": "^1.0.0", + "karma-jasmine": "^0.1.5", + "main-bower-files": "^1.0.2", + "require-dir": "~0.3.0", + "streamqueue": "^0.1.1", + "uglify-save-license": "^0.4.1" + }, + "engines": { + "node": ">=0.10.0" + }, + "scripts": { + "postinstall": "bower install --allow-root", + "pretest": "npm install && npm run build_static", + "build_static": "gulp dist", + "prelint": "npm install", + "lint": "jshint app/", + "test": "gulp test" + } +} diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..ad3815a0 --- /dev/null +++ b/setup.py @@ -0,0 +1,114 @@ +""" +Lemur +===== + +Is an SSL management and orchestration tool. + +:copyright: (c) 2015 by Netflix, see AUTHORS for more +:license: Apache, see LICENSE for more details. +""" +from __future__ import absolute_import + +import os.path + +from distutils import log +from distutils.core import Command +from setuptools.command.develop import develop +from setuptools.command.sdist import sdist +from setuptools import setup +from subprocess import check_output + +ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__))) + +install_requires=[ + 'Flask>=0.10.1', + 'Flask-RESTful>=0.3.3', + 'Flask-SQLAlchemy>=1.0.5', + 'Flask-Script>=2.0.5', + 'Flask-Migrate>=1.4.0', + 'Flask-Bcrypt>=0.6.2', + 'Flask-Principal>=0.4.0', + 'SQLAlchemy-Utils>=0.30.11', + 'BeautifulSoup4', + 'requests>=2.7.0', + 'psycopg2>=2.6.1', + 'arrow>=0.5.4', + 'boto>=2.38.0', # we might make this optional + 'six>=1.9.0', + 'gunicorn>=19.3.0', + 'pycrypto>=2.6.1', + 'cryptography>=0.9', + 'pyopenssl>=0.15.1', + 'pyjwt>=1.0.1', + 'xmltodict>=0.9.2' +] + +tests_require = [ + 'pyflakes', + 'moto', + 'nose' +] + +docs_require = [ + 'sphinx', + 'sphinxcontrib-httpdomain' +] + +class DevelopWithBuildStatic(develop): + def install_for_development(self): + self.run_command('build_static') + return develop.install_for_development(self) + + +class SdistWithBuildStatic(sdist): + def make_distribution(self): + self.run_command('build_static') + return sdist.make_distribution(self) + + +class BuildStatic(Command): + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + log.info("running [npm install --quiet]") + check_output(['npm', 'install', '--quiet'], cwd=ROOT) + + log.info("running [gulp buld]") + check_output([os.path.join(ROOT, 'node_modules', '.bin', 'gulp'), 'build'], cwd=ROOT) + +setup( + name='lemur', + version='0.1', + author='Kevin Glisson', + author_email='kglisson@netflix.com', + long_description=open('README.rst').read(), + packages=['lemur'], + include_package_data=True, + zip_safe=False, + install_requires=install_requires, + extras_require={ + 'tests': tests_require, + 'docs': docs_require + }, + cmdclass={ + 'build_static': BuildStatic, + 'develop': DevelopWithBuildStatic, + 'sdist': SdistWithBuildStatic + }, + entry_points={ + 'console_scripts': [ + 'lemur = lemur.manage:main', + ], + }, + classifiers=[ + 'Framework :: Flask', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Operating System :: OS Independent', + 'Topic :: Software Development' + ] +) diff --git a/trustores/__init__.py b/trustores/__init__.py new file mode 100644 index 00000000..e69de29b