Drew Short
7 years ago
commit
bcbb1750b8
39 changed files with 1840 additions and 0 deletions
-
4.dockerignore
-
7.gitignore
-
19.gitlab-ci.yml
-
23Dockerfile
-
201LICENSE
-
1server/.env
-
23server/Pipfile
-
368server/Pipfile.lock
-
83server/atheneum/__init__.py
-
1server/atheneum/api/__init__.py
-
45server/atheneum/api/authentication_api.py
-
25server/atheneum/api/decorators.py
-
6server/atheneum/api/model.py
-
4server/atheneum/default_settings.py
-
0server/atheneum/middleware/__init__.py
-
87server/atheneum/middleware/authentication_middleware.py
-
1server/atheneum/model/__init__.py
-
42server/atheneum/model/user_model.py
-
0server/atheneum/service/__init__.py
-
74server/atheneum/service/authentication_service.py
-
48server/atheneum/service/user_service.py
-
52server/atheneum/service/user_token_service.py
-
18server/atheneum/utility.py
-
10server/entrypoint.sh
-
124server/manage.py
-
1server/migrations/README
-
45server/migrations/alembic.ini
-
89server/migrations/env.py
-
24server/migrations/script.py.mako
-
56server/migrations/versions/96442b147e22_.py
-
11server/mypy.ini
-
9server/run_tests.sh
-
12server/server.iml
-
10server/setup.py
-
3server/test_settings.py
-
22server/tests/api/test_authentication_api.py
-
33server/tests/api/test_decorators.py
-
127server/tests/conftest.py
-
132server/tests/middleware/test_authentication_middleware.py
@ -0,0 +1,4 @@ |
|||||
|
server/instance/ |
||||
|
server/setup.py |
||||
|
server/test/ |
||||
|
.admin_credentials |
@ -0,0 +1,7 @@ |
|||||
|
instance/ |
||||
|
.idea |
||||
|
.admin_credentials |
||||
|
*__pycache__/ |
||||
|
.pytest_cache/ |
||||
|
.coverage |
||||
|
.mypy_cache/ |
@ -0,0 +1,19 @@ |
|||||
|
stages: |
||||
|
- test |
||||
|
|
||||
|
Atheneum:Tests: |
||||
|
image: python:3.6-slim-stretch |
||||
|
stage: test |
||||
|
script: |
||||
|
- python3 --version |
||||
|
- python3 -m pip --version |
||||
|
- python3 -m pip install pipenv |
||||
|
- python3 -m pipenv --version |
||||
|
- cd server |
||||
|
- pipenv install --dev --system |
||||
|
- pycodestyle atheneum tests |
||||
|
- mypy atheneum tests |
||||
|
- PYTHONPATH=$(pwd) coverage run --source atheneum -m pytest |
||||
|
- coverage report --fail-under=85 -m --skip-covered |
||||
|
tags: |
||||
|
- docker |
@ -0,0 +1,23 @@ |
|||||
|
FROM python:3.6-slim-stretch |
||||
|
MAINTAINER Drew Short <warrick@sothr.com> |
||||
|
|
||||
|
ENV ATHENEUM_APP_DIRECTORY /opt/atheneum |
||||
|
ENV ATHENEUM_CONFIG_DIRECTORY /srv/atheneum/config |
||||
|
ENV ATHENEUM_DATA_DIRECTORY /srv/atheneum/data |
||||
|
|
||||
|
RUN mkdir -p ${ATHENEUM_APP_DIRECTORY} \ |
||||
|
&& mkdir -p ${ATHENEUM_CONFIG_DIRECTORY} \ |
||||
|
&& mkdir -p ${ATHENEUM_DATA_DIRECTORY} \ |
||||
|
&& pip install pipenv gunicorn |
||||
|
|
||||
|
VOLUME ${ATHENEUM_CONFIG_DIRECTORY} |
||||
|
VOLUME ${ATHENEUM_DATA_DIRECTORY} |
||||
|
|
||||
|
COPY ./server/ ${ATHENEUM_APP_DIRECTORY}/ |
||||
|
|
||||
|
RUN cd ${ATHENEUM_APP_DIRECTORY} \ |
||||
|
&& pipenv install --system --deploy --ignore-pipfile |
||||
|
|
||||
|
WORKDIR ${ATHENEUM_APP_DIRECTORY} |
||||
|
|
||||
|
CMD ./entrypoint.sh |
@ -0,0 +1,201 @@ |
|||||
|
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 2018 Drew Short |
||||
|
|
||||
|
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. |
@ -0,0 +1 @@ |
|||||
|
FLASK_APP=atheneum |
@ -0,0 +1,23 @@ |
|||||
|
[[source]] |
||||
|
url = "https://pypi.org/simple" |
||||
|
verify_ssl = true |
||||
|
name = "pypi" |
||||
|
|
||||
|
[packages] |
||||
|
flask = ">=1.0,<1.1" |
||||
|
flask-sqlalchemy = ">=2.3,<2.4" |
||||
|
flask-migrate = ">=2.1,<2.2" |
||||
|
pynacl = ">=1.2,<1.3" |
||||
|
click = "*" |
||||
|
"rfc3339" = "*" |
||||
|
|
||||
|
[dev-packages] |
||||
|
python-dotenv = "*" |
||||
|
pytest = "*" |
||||
|
coverage = "*" |
||||
|
pycodestyle = "*" |
||||
|
mypy = "*" |
||||
|
mock = "*" |
||||
|
|
||||
|
[requires] |
||||
|
python_version = "3.6" |
@ -0,0 +1,368 @@ |
|||||
|
{ |
||||
|
"_meta": { |
||||
|
"hash": { |
||||
|
"sha256": "a8ad1b3822122643e380c48cefa0ab6356d268b7b9756f55d8040466825fea25" |
||||
|
}, |
||||
|
"pipfile-spec": 6, |
||||
|
"requires": { |
||||
|
"python_version": "3.6" |
||||
|
}, |
||||
|
"sources": [ |
||||
|
{ |
||||
|
"name": "pypi", |
||||
|
"url": "https://pypi.org/simple", |
||||
|
"verify_ssl": true |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
"default": { |
||||
|
"alembic": { |
||||
|
"hashes": [ |
||||
|
"sha256:1cd32df9a3b8c1749082ef60ffbe05ff16617b6afadfdabc680dcb9344af33d7" |
||||
|
], |
||||
|
"version": "==0.9.10" |
||||
|
}, |
||||
|
"cffi": { |
||||
|
"hashes": [ |
||||
|
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", |
||||
|
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", |
||||
|
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", |
||||
|
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", |
||||
|
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30", |
||||
|
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", |
||||
|
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", |
||||
|
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b", |
||||
|
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", |
||||
|
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e", |
||||
|
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", |
||||
|
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", |
||||
|
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", |
||||
|
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", |
||||
|
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", |
||||
|
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", |
||||
|
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", |
||||
|
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", |
||||
|
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5", |
||||
|
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", |
||||
|
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", |
||||
|
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", |
||||
|
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", |
||||
|
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", |
||||
|
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2", |
||||
|
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", |
||||
|
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", |
||||
|
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", |
||||
|
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", |
||||
|
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", |
||||
|
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f", |
||||
|
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb" |
||||
|
], |
||||
|
"version": "==1.11.5" |
||||
|
}, |
||||
|
"click": { |
||||
|
"hashes": [ |
||||
|
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", |
||||
|
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==6.7" |
||||
|
}, |
||||
|
"flask": { |
||||
|
"hashes": [ |
||||
|
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", |
||||
|
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==1.0.2" |
||||
|
}, |
||||
|
"flask-migrate": { |
||||
|
"hashes": [ |
||||
|
"sha256:493f9b3795985b9b4915bf3b7d16946697f027b73545384e7d9e3a79f989d2fe", |
||||
|
"sha256:b709ca8642559c3c5a81a33ab10839fa052177accd5ba821047a99db635255ed" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==2.1.1" |
||||
|
}, |
||||
|
"flask-sqlalchemy": { |
||||
|
"hashes": [ |
||||
|
"sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", |
||||
|
"sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==2.3.2" |
||||
|
}, |
||||
|
"itsdangerous": { |
||||
|
"hashes": [ |
||||
|
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" |
||||
|
], |
||||
|
"version": "==0.24" |
||||
|
}, |
||||
|
"jinja2": { |
||||
|
"hashes": [ |
||||
|
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", |
||||
|
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" |
||||
|
], |
||||
|
"version": "==2.10" |
||||
|
}, |
||||
|
"mako": { |
||||
|
"hashes": [ |
||||
|
"sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae" |
||||
|
], |
||||
|
"version": "==1.0.7" |
||||
|
}, |
||||
|
"markupsafe": { |
||||
|
"hashes": [ |
||||
|
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" |
||||
|
], |
||||
|
"version": "==1.0" |
||||
|
}, |
||||
|
"pycparser": { |
||||
|
"hashes": [ |
||||
|
"sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226" |
||||
|
], |
||||
|
"version": "==2.18" |
||||
|
}, |
||||
|
"pynacl": { |
||||
|
"hashes": [ |
||||
|
"sha256:04e30e5bdeeb2d5b34107f28cd2f5bbfdc6c616f3be88fc6f53582ff1669eeca", |
||||
|
"sha256:0bfa0d94d2be6874e40f896e0a67e290749151e7de767c5aefbad1121cad7512", |
||||
|
"sha256:11aa4e141b2456ce5cecc19c130e970793fa3a2c2e6fbb8ad65b28f35aa9e6b6", |
||||
|
"sha256:13bdc1fe084ff9ac7653ae5a924cae03bf4bb07c6667c9eb5b6eb3c570220776", |
||||
|
"sha256:14339dc233e7a9dda80a3800e64e7ff89d0878ba23360eea24f1af1b13772cac", |
||||
|
"sha256:1d33e775fab3f383167afb20b9927aaf4961b953d76eeb271a5703a6d756b65b", |
||||
|
"sha256:2a42b2399d0428619e58dac7734838102d35f6dcdee149e0088823629bf99fbb", |
||||
|
"sha256:2dce05ac8b3c37b9e2f65eab56c544885607394753e9613fd159d5e2045c2d98", |
||||
|
"sha256:63cfccdc6217edcaa48369191ae4dca0c390af3c74f23c619e954973035948cd", |
||||
|
"sha256:6453b0dae593163ffc6db6f9c9c1597d35c650598e2c39c0590d1757207a1ac2", |
||||
|
"sha256:73a5a96fb5fbf2215beee2353a128d382dbca83f5341f0d3c750877a236569ef", |
||||
|
"sha256:8abb4ef79161a5f58848b30ab6fb98d8c466da21fdd65558ce1d7afc02c70b5f", |
||||
|
"sha256:8ac1167195b32a8755de06efd5b2d2fe76fc864517dab66aaf65662cc59e1988", |
||||
|
"sha256:8f505f42f659012794414fa57c498404e64db78f1d98dfd40e318c569f3c783b", |
||||
|
"sha256:9c8a06556918ee8e3ab48c65574f318f5a0a4d31437fc135da7ee9d4f9080415", |
||||
|
"sha256:a1e25fc5650cf64f01c9e435033e53a4aca9de30eb9929d099f3bb078e18f8f2", |
||||
|
"sha256:be71cd5fce04061e1f3d39597f93619c80cdd3558a6c9ba99a546f144a8d8101", |
||||
|
"sha256:c5b1a7a680218dee9da0f1b5e24072c46b3c275d35712bc1d505b85bb03441c0", |
||||
|
"sha256:cb785db1a9468841a1265c9215c60fe5d7af2fb1b209e3316a152704607fc582", |
||||
|
"sha256:cf6877124ae6a0698404e169b3ba534542cfbc43f939d46b927d956daf0a373a", |
||||
|
"sha256:d0eb5b2795b7ee2cbcfcadacbe95a13afbda048a262bd369da9904fecb568975", |
||||
|
"sha256:d3a934e2b9f20abac009d5b6951067cfb5486889cb913192b4d8288b216842f1", |
||||
|
"sha256:d795f506bcc9463efb5ebb0f65ed77921dcc9e0a50499dedd89f208445de9ecb", |
||||
|
"sha256:d8aaf7e5d6b0e0ef7d6dbf7abeb75085713d0100b4eb1a4e4e857de76d77ac45", |
||||
|
"sha256:de2aaca8386cf4d70f1796352f2346f48ddb0bed61dc43a3ce773ba12e064031", |
||||
|
"sha256:e0d38fa0a75f65f556fb912f2c6790d1fa29b7dd27a1d9cc5591b281321eaaa9", |
||||
|
"sha256:eb2acabbd487a46b38540a819ef67e477a674481f84a82a7ba2234b9ba46f752", |
||||
|
"sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0", |
||||
|
"sha256:f5836463a3c0cca300295b229b6c7003c415a9d11f8f9288ddbd728e2746524c", |
||||
|
"sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053", |
||||
|
"sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==1.2.1" |
||||
|
}, |
||||
|
"python-dateutil": { |
||||
|
"hashes": [ |
||||
|
"sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", |
||||
|
"sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" |
||||
|
], |
||||
|
"version": "==2.7.3" |
||||
|
}, |
||||
|
"python-editor": { |
||||
|
"hashes": [ |
||||
|
"sha256:a3c066acee22a1c94f63938341d4fb374e3fdd69366ed6603d7b24bed1efc565" |
||||
|
], |
||||
|
"version": "==1.0.3" |
||||
|
}, |
||||
|
"rfc3339": { |
||||
|
"hashes": [ |
||||
|
"sha256:589c8b8cab8a35f85313cb80f1b0b0b3ca16a527f354beadb59882fd4473f187", |
||||
|
"sha256:a8167214d37449a6af9b463285baffc87a6d65e013507fd1ba63a48a60b62043" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==6.0" |
||||
|
}, |
||||
|
"six": { |
||||
|
"hashes": [ |
||||
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", |
||||
|
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" |
||||
|
], |
||||
|
"version": "==1.11.0" |
||||
|
}, |
||||
|
"sqlalchemy": { |
||||
|
"hashes": [ |
||||
|
"sha256:e21e5561a85dcdf16b8520ae4daec7401c5c24558e0ce004f9b60be75c4b6957" |
||||
|
], |
||||
|
"version": "==1.2.9" |
||||
|
}, |
||||
|
"werkzeug": { |
||||
|
"hashes": [ |
||||
|
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", |
||||
|
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" |
||||
|
], |
||||
|
"version": "==0.14.1" |
||||
|
} |
||||
|
}, |
||||
|
"develop": { |
||||
|
"atomicwrites": { |
||||
|
"hashes": [ |
||||
|
"sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585", |
||||
|
"sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6" |
||||
|
], |
||||
|
"version": "==1.1.5" |
||||
|
}, |
||||
|
"attrs": { |
||||
|
"hashes": [ |
||||
|
"sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", |
||||
|
"sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" |
||||
|
], |
||||
|
"version": "==18.1.0" |
||||
|
}, |
||||
|
"coverage": { |
||||
|
"hashes": [ |
||||
|
"sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", |
||||
|
"sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", |
||||
|
"sha256:104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a", |
||||
|
"sha256:15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd", |
||||
|
"sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", |
||||
|
"sha256:1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2", |
||||
|
"sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", |
||||
|
"sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", |
||||
|
"sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", |
||||
|
"sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", |
||||
|
"sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", |
||||
|
"sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", |
||||
|
"sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", |
||||
|
"sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", |
||||
|
"sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", |
||||
|
"sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", |
||||
|
"sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", |
||||
|
"sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", |
||||
|
"sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", |
||||
|
"sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", |
||||
|
"sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", |
||||
|
"sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", |
||||
|
"sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", |
||||
|
"sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", |
||||
|
"sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", |
||||
|
"sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", |
||||
|
"sha256:9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4", |
||||
|
"sha256:ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91", |
||||
|
"sha256:b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d", |
||||
|
"sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", |
||||
|
"sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", |
||||
|
"sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", |
||||
|
"sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", |
||||
|
"sha256:e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77", |
||||
|
"sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80", |
||||
|
"sha256:f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==4.5.1" |
||||
|
}, |
||||
|
"mock": { |
||||
|
"hashes": [ |
||||
|
"sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1", |
||||
|
"sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==2.0.0" |
||||
|
}, |
||||
|
"more-itertools": { |
||||
|
"hashes": [ |
||||
|
"sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8", |
||||
|
"sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3", |
||||
|
"sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0" |
||||
|
], |
||||
|
"version": "==4.2.0" |
||||
|
}, |
||||
|
"mypy": { |
||||
|
"hashes": [ |
||||
|
"sha256:1b899802a89b67bb68f30d788bba49b61b1f28779436f06b75c03495f9d6ea5c", |
||||
|
"sha256:f472645347430282d62d1f97d12ccb8741f19f1572b7cf30b58280e4e0818739" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==0.610" |
||||
|
}, |
||||
|
"pbr": { |
||||
|
"hashes": [ |
||||
|
"sha256:3747c6f017f2dc099986c325239661948f9f5176f6880d9fdef164cb664cd665", |
||||
|
"sha256:a9c27eb8f0e24e786e544b2dbaedb729c9d8546342b5a6818d8eda098ad4340d" |
||||
|
], |
||||
|
"version": "==4.0.4" |
||||
|
}, |
||||
|
"pluggy": { |
||||
|
"hashes": [ |
||||
|
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", |
||||
|
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", |
||||
|
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" |
||||
|
], |
||||
|
"version": "==0.6.0" |
||||
|
}, |
||||
|
"py": { |
||||
|
"hashes": [ |
||||
|
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", |
||||
|
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" |
||||
|
], |
||||
|
"version": "==1.5.4" |
||||
|
}, |
||||
|
"pycodestyle": { |
||||
|
"hashes": [ |
||||
|
"sha256:74abc4e221d393ea5ce1f129ea6903209940c1ecd29e002e8c6933c2b21026e0", |
||||
|
"sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", |
||||
|
"sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==2.4.0" |
||||
|
}, |
||||
|
"pytest": { |
||||
|
"hashes": [ |
||||
|
"sha256:0453c8676c2bee6feb0434748b068d5510273a916295fd61d306c4f22fbfd752", |
||||
|
"sha256:4b208614ae6d98195430ad6bde03641c78553acee7c83cec2e85d613c0cd383d" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==3.6.3" |
||||
|
}, |
||||
|
"python-dotenv": { |
||||
|
"hashes": [ |
||||
|
"sha256:4965ed170bf51c347a89820e8050655e9c25db3837db6602e906b6d850fad85c", |
||||
|
"sha256:509736185257111613009974e666568a1b031b028b61b500ef1ab4ee780089d5" |
||||
|
], |
||||
|
"index": "pypi", |
||||
|
"version": "==0.8.2" |
||||
|
}, |
||||
|
"six": { |
||||
|
"hashes": [ |
||||
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", |
||||
|
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" |
||||
|
], |
||||
|
"version": "==1.11.0" |
||||
|
}, |
||||
|
"typed-ast": { |
||||
|
"hashes": [ |
||||
|
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", |
||||
|
"sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", |
||||
|
"sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", |
||||
|
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", |
||||
|
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", |
||||
|
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", |
||||
|
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", |
||||
|
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", |
||||
|
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", |
||||
|
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", |
||||
|
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", |
||||
|
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", |
||||
|
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", |
||||
|
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", |
||||
|
"sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", |
||||
|
"sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", |
||||
|
"sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", |
||||
|
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", |
||||
|
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", |
||||
|
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", |
||||
|
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", |
||||
|
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", |
||||
|
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" |
||||
|
], |
||||
|
"version": "==1.1.0" |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,83 @@ |
|||||
|
import os |
||||
|
from logging.config import dictConfig |
||||
|
|
||||
|
from flask import Flask |
||||
|
from flask_migrate import Migrate, upgrade |
||||
|
from flask_sqlalchemy import SQLAlchemy |
||||
|
|
||||
|
from atheneum import utility |
||||
|
|
||||
|
db: SQLAlchemy = SQLAlchemy() |
||||
|
|
||||
|
dictConfig({ |
||||
|
'version': 1, |
||||
|
'formatters': {'default': { |
||||
|
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', |
||||
|
}}, |
||||
|
'handlers': {'wsgi': { |
||||
|
'class': 'logging.StreamHandler', |
||||
|
'stream': 'ext://flask.logging.wsgi_errors_stream', |
||||
|
'formatter': 'default' |
||||
|
}}, |
||||
|
'root': { |
||||
|
'level': 'INFO', |
||||
|
'handlers': ['wsgi'] |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
def create_app(test_config: dict = None) -> Flask: |
||||
|
app = Flask(__name__, instance_relative_config=True) |
||||
|
app.logger.debug('Creating Atheneum Server') |
||||
|
|
||||
|
data_directory = os.getenv('ATHENEUM_DATA_DIRECTORY', '/tmp') |
||||
|
app.logger.debug('Atheneum Data Directory: %s', data_directory) |
||||
|
|
||||
|
default_database_uri = 'sqlite:///{}/atheneum.db'.format(data_directory) |
||||
|
app.config.from_mapping( |
||||
|
SECRET_KEY='dev', |
||||
|
SQLALCHEMY_DATABASE_URI=default_database_uri, |
||||
|
SQLALCHEMY_TRACK_MODIFICATIONS=False |
||||
|
) |
||||
|
|
||||
|
if test_config is None: |
||||
|
app.logger.debug('Loading configurations') |
||||
|
app.config.from_object('atheneum.default_settings') |
||||
|
app.config.from_pyfile('config.py', silent=True) |
||||
|
if os.getenv('ATHENEUM_SETTINGS', None): |
||||
|
app.config.from_envvar('ATHENEUM_SETTINGS') |
||||
|
else: |
||||
|
app.logger.debug('Loading test configuration') |
||||
|
app.config.from_object(test_config) |
||||
|
|
||||
|
try: |
||||
|
os.makedirs(app.instance_path) |
||||
|
except OSError: |
||||
|
pass |
||||
|
|
||||
|
app.json_encoder = utility.CustomJSONEncoder |
||||
|
|
||||
|
app.logger.debug('Initializing Application') |
||||
|
db.init_app(app) |
||||
|
app.logger.debug('Registering Database Models') |
||||
|
Migrate(app, db) |
||||
|
|
||||
|
return app |
||||
|
|
||||
|
|
||||
|
def register_blueprints(app: Flask) -> None: |
||||
|
from atheneum.api import auth_blueprint |
||||
|
app.register_blueprint(auth_blueprint) |
||||
|
|
||||
|
|
||||
|
app = create_app() |
||||
|
register_blueprints(app) |
||||
|
|
||||
|
|
||||
|
def init_db() -> None: |
||||
|
"""Clear existing data and create new tables.""" |
||||
|
upgrade('migrations') |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
app.run() |
@ -0,0 +1 @@ |
|||||
|
from atheneum.api.authentication_api import auth_blueprint |
@ -0,0 +1,45 @@ |
|||||
|
from flask import Blueprint, g |
||||
|
|
||||
|
from atheneum.api.decorators import return_json |
||||
|
from atheneum.api.model import APIResponse |
||||
|
from atheneum.middleware import authentication_middleware |
||||
|
from atheneum.service import user_token_service, authentication_service |
||||
|
|
||||
|
auth_blueprint = Blueprint( |
||||
|
name='auth', import_name=__name__, url_prefix='/auth') |
||||
|
|
||||
|
|
||||
|
@auth_blueprint.route('/login', methods=['POST']) |
||||
|
@return_json |
||||
|
@authentication_middleware.require_basic_auth |
||||
|
def login() -> APIResponse: |
||||
|
""" |
||||
|
Get a token for continued authentication |
||||
|
:return: A login token for continued authentication |
||||
|
""" |
||||
|
user_token = user_token_service.create(g.user) |
||||
|
return APIResponse({'token': user_token.token}, 200) |
||||
|
|
||||
|
|
||||
|
@auth_blueprint.route('/bump', methods=['POST']) |
||||
|
@return_json |
||||
|
@authentication_middleware.require_token_auth |
||||
|
def login_bump() -> APIResponse: |
||||
|
""" |
||||
|
Update the user last seen timestamp |
||||
|
:return: A time stamp for the bumped login |
||||
|
""" |
||||
|
authentication_service.bump_login(g.user) |
||||
|
return APIResponse({'last_login_time': g.user.last_login_time}, 200) |
||||
|
|
||||
|
|
||||
|
@auth_blueprint.route('/logout', methods=['POST']) |
||||
|
@return_json |
||||
|
@authentication_middleware.require_token_auth |
||||
|
def logout() -> APIResponse: |
||||
|
""" |
||||
|
logout and delete a token |
||||
|
:return: |
||||
|
""" |
||||
|
authentication_service.logout(g.user_token) |
||||
|
return APIResponse(None, 200) |
@ -0,0 +1,25 @@ |
|||||
|
from functools import wraps |
||||
|
from typing import Callable, Any |
||||
|
|
||||
|
from flask import jsonify, Response |
||||
|
|
||||
|
from atheneum.api.model import APIResponse |
||||
|
|
||||
|
|
||||
|
def return_json(func: Callable) -> Callable: |
||||
|
""" |
||||
|
If an Response object is not returned, jsonify the result and return it |
||||
|
:param func: |
||||
|
:return: |
||||
|
""" |
||||
|
|
||||
|
@wraps(func) |
||||
|
def decorate(*args: list, **kwargs: dict) -> Any: |
||||
|
result = func(*args, **kwargs) |
||||
|
if isinstance(result, Response): |
||||
|
return result |
||||
|
if isinstance(result, APIResponse): |
||||
|
return jsonify(result.payload), result.status |
||||
|
return jsonify(result) |
||||
|
|
||||
|
return decorate |
@ -0,0 +1,6 @@ |
|||||
|
from typing import Any, NamedTuple |
||||
|
|
||||
|
|
||||
|
class APIResponse(NamedTuple): |
||||
|
payload: Any |
||||
|
status: int |
@ -0,0 +1,4 @@ |
|||||
|
DEBUG = False |
||||
|
SECRET_KEY = b'\xb4\x89\x0f\x0f\xe5\x88\x97\xfe\x8d<\x0b@d\xe9\xa5\x87%' \ |
||||
|
b'\xc6\xf0@l1\xe3\x90g\xfaA.?u=s' # CHANGE ME IN REAL CONFIG |
||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False |
@ -0,0 +1,87 @@ |
|||||
|
import base64 |
||||
|
from functools import wraps |
||||
|
from typing import Optional, Callable, Any |
||||
|
|
||||
|
from flask import request, Response, g |
||||
|
from werkzeug.datastructures import Authorization |
||||
|
from werkzeug.http import bytes_to_wsgi, wsgi_to_bytes |
||||
|
|
||||
|
from atheneum.service import ( |
||||
|
authentication_service, |
||||
|
user_service, |
||||
|
user_token_service |
||||
|
) |
||||
|
|
||||
|
|
||||
|
def authenticate_with_password(name: str, password: str) -> bool: |
||||
|
user = user_service.find_by_name(name) |
||||
|
if user is not None \ |
||||
|
and authentication_service.is_valid_password(user, password): |
||||
|
g.user = user |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def authenticate_with_token(name: str, token: str) -> bool: |
||||
|
user = user_service.find_by_name(name) |
||||
|
if user is not None: |
||||
|
user_token = user_token_service.find_by_user_and_token(user, token) |
||||
|
if user is not None \ |
||||
|
and authentication_service.is_valid_token(user_token): |
||||
|
g.user = user |
||||
|
g.user_token = user_token |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def authentication_failed(auth_type: str) -> Response: |
||||
|
return Response( |
||||
|
status=401, |
||||
|
headers={ |
||||
|
'WWW-Authenticate': '%s realm="Login Required"' % auth_type |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
def parse_token_authorization_header( |
||||
|
header_value: str) -> Optional[Authorization]: |
||||
|
if not header_value: |
||||
|
return None |
||||
|
value = wsgi_to_bytes(header_value) |
||||
|
try: |
||||
|
auth_type, auth_info = value.split(None, 1) |
||||
|
auth_type = auth_type.lower() |
||||
|
except ValueError: |
||||
|
return None |
||||
|
if auth_type == b'token': |
||||
|
try: |
||||
|
username, token = base64.b64decode(auth_info).split(b':', 1) |
||||
|
except Exception: |
||||
|
return None |
||||
|
return Authorization('token', {'username': bytes_to_wsgi(username), |
||||
|
'password': bytes_to_wsgi(token)}) |
||||
|
return None |
||||
|
|
||||
|
|
||||
|
def require_basic_auth(func: Callable) -> Callable: |
||||
|
@wraps(func) |
||||
|
def decorate(*args: list, **kwargs: dict) -> Any: |
||||
|
auth = request.authorization |
||||
|
if auth and authenticate_with_password(auth.username, auth.password): |
||||
|
return func(*args, **kwargs) |
||||
|
else: |
||||
|
return authentication_failed('Basic') |
||||
|
|
||||
|
return decorate |
||||
|
|
||||
|
|
||||
|
def require_token_auth(func: Callable) -> Callable: |
||||
|
@wraps(func) |
||||
|
def decorate(*args: list, **kwargs: dict) -> Any: |
||||
|
token = parse_token_authorization_header( |
||||
|
request.headers.get('Authorization', None)) |
||||
|
if token and authenticate_with_token(token.username, token.password): |
||||
|
return func(*args, **kwargs) |
||||
|
else: |
||||
|
return authentication_failed('Bearer') |
||||
|
|
||||
|
return decorate |
@ -0,0 +1 @@ |
|||||
|
from atheneum.model.user_model import User, UserToken |
@ -0,0 +1,42 @@ |
|||||
|
from atheneum import db |
||||
|
|
||||
|
|
||||
|
class User(db.Model): |
||||
|
__tablename__ = 'user' |
||||
|
|
||||
|
ROLE_USER = 'USER' |
||||
|
ROLE_ADMIN = 'ADMIN' |
||||
|
|
||||
|
id = db.Column(db.Integer, primary_key=True) |
||||
|
name = db.Column(db.Unicode(60), unique=True, nullable=False) |
||||
|
role = db.Column( |
||||
|
'role', |
||||
|
db.Unicode(32), |
||||
|
nullable=False, |
||||
|
default=ROLE_USER, ) |
||||
|
password_hash = db.Column('password_hash', db.Unicode(128), nullable=False) |
||||
|
password_revision = db.Column( |
||||
|
'password_revision', db.SmallInteger, default=0, nullable=False) |
||||
|
creation_time = db.Column('creation_time', db.DateTime, nullable=False) |
||||
|
last_login_time = db.Column('last_login_time', db.DateTime) |
||||
|
version = db.Column('version', db.Integer, default=1, nullable=False) |
||||
|
|
||||
|
|
||||
|
class UserToken(db.Model): |
||||
|
__tablename__ = 'user_token' |
||||
|
|
||||
|
user_token_id = db.Column('id', db.Integer, primary_key=True) |
||||
|
user_id = db.Column( |
||||
|
'user_id', |
||||
|
db.Integer, |
||||
|
db.ForeignKey('user.id', ondelete='CASCADE'), |
||||
|
nullable=False, |
||||
|
index=True) |
||||
|
token = db.Column('token', db.Unicode(36), nullable=False) |
||||
|
note = db.Column('note', db.Unicode(128), nullable=True) |
||||
|
enabled = db.Column('enabled', db.Boolean, nullable=False, default=True) |
||||
|
expiration_time = db.Column('expiration_time', db.DateTime, nullable=True) |
||||
|
creation_time = db.Column('creation_time', db.DateTime, nullable=False) |
||||
|
last_edit_time = db.Column('last_edit_time', db.DateTime) |
||||
|
last_usage_time = db.Column('last_usage_time', db.DateTime) |
||||
|
version = db.Column('version', db.Integer, default=1, nullable=False) |
@ -0,0 +1,74 @@ |
|||||
|
import uuid |
||||
|
from datetime import datetime |
||||
|
from typing import Optional, Tuple |
||||
|
|
||||
|
from nacl import pwhash |
||||
|
from nacl.exceptions import InvalidkeyError |
||||
|
|
||||
|
from atheneum.model import User, UserToken |
||||
|
from atheneum.service import user_service, user_token_service |
||||
|
|
||||
|
|
||||
|
def generate_token() -> uuid.UUID: |
||||
|
return uuid.uuid4() |
||||
|
|
||||
|
|
||||
|
def get_password_hash(password: str) -> Tuple[str, int]: |
||||
|
""" |
||||
|
Retrieve argon2id password hash. |
||||
|
|
||||
|
:param password: plaintext password to convert |
||||
|
:return: Tuple[password_hash, password_revision] |
||||
|
""" |
||||
|
return pwhash.argon2id.str(password.encode('utf8')).decode('utf8'), 1 |
||||
|
|
||||
|
|
||||
|
def is_valid_password(user: User, password: str) -> bool: |
||||
|
assert user |
||||
|
|
||||
|
try: |
||||
|
return pwhash.verify( |
||||
|
user.password_hash.encode('utf8'), password.encode('utf8')) |
||||
|
except InvalidkeyError: |
||||
|
pass |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def is_valid_token(user_token: Optional[UserToken]) -> bool: |
||||
|
""" |
||||
|
Token must be enabled and if it has an expiration, it must be |
||||
|
greater than now. |
||||
|
|
||||
|
:param user_token: |
||||
|
:return: |
||||
|
""" |
||||
|
if user_token is None: |
||||
|
return False |
||||
|
if not user_token.enabled: |
||||
|
return False |
||||
|
if (user_token.expiration_time is not None |
||||
|
and user_token.expiration_time < datetime.utcnow()): |
||||
|
return False |
||||
|
return True |
||||
|
|
||||
|
|
||||
|
def bump_login(user: Optional[User]) -> None: |
||||
|
""" |
||||
|
Update the last login time for the user |
||||
|
|
||||
|
:param user: |
||||
|
:return: |
||||
|
""" |
||||
|
if user is not None: |
||||
|
user_service.update_last_login_time(user) |
||||
|
|
||||
|
|
||||
|
def logout(user_token: Optional[UserToken] = None) -> None: |
||||
|
""" |
||||
|
Remove a user_token associated with a client session |
||||
|
|
||||
|
:param user_token: |
||||
|
:return: |
||||
|
""" |
||||
|
if user_token is not None: |
||||
|
user_token_service.delete(user_token) |
@ -0,0 +1,48 @@ |
|||||
|
from datetime import datetime |
||||
|
from typing import Optional |
||||
|
|
||||
|
from atheneum import app, db |
||||
|
from atheneum.model import User |
||||
|
from atheneum.service import authentication_service |
||||
|
|
||||
|
|
||||
|
def register(name: str, password: str, role: str) -> User: |
||||
|
pw_hash, pw_revision = authentication_service.get_password_hash(password) |
||||
|
|
||||
|
new_user = User( |
||||
|
name=name, |
||||
|
role=role, |
||||
|
password_hash=pw_hash, |
||||
|
password_revision=pw_revision, |
||||
|
creation_time=datetime.now(), |
||||
|
version=0) |
||||
|
db.session.add(new_user) |
||||
|
db.session.commit() |
||||
|
|
||||
|
app.logger.info('Registered new user: %s with role: %s', name, role) |
||||
|
return new_user |
||||
|
|
||||
|
|
||||
|
def delete(user: User) -> bool: |
||||
|
existing_user = db.session.delete(user) |
||||
|
if existing_user is None: |
||||
|
db.session.commit() |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def update_last_login_time(user: User) -> None: |
||||
|
user.last_login_time = datetime.now() |
||||
|
db.session.commit() |
||||
|
|
||||
|
|
||||
|
def update_password(user: User, password: str) -> None: |
||||
|
pw_hash, pw_revision = authentication_service.get_password_hash( |
||||
|
password) |
||||
|
user.password_hash = pw_hash |
||||
|
user.password_revision = pw_revision |
||||
|
db.session.commit() |
||||
|
|
||||
|
|
||||
|
def find_by_name(name: str) -> Optional[User]: |
||||
|
return User.query.filter_by(name=name).first() |
@ -0,0 +1,52 @@ |
|||||
|
from datetime import datetime |
||||
|
from typing import Optional |
||||
|
|
||||
|
from atheneum import db |
||||
|
from atheneum.model import User, UserToken |
||||
|
from atheneum.service import authentication_service |
||||
|
|
||||
|
|
||||
|
def create( |
||||
|
user: User, |
||||
|
note: Optional[str] = None, |
||||
|
enabled: bool = True, |
||||
|
expiration_time: Optional[datetime] = None) -> UserToken: |
||||
|
""" |
||||
|
Create and save a UserToken |
||||
|
|
||||
|
:param user: The User object to bind the token to |
||||
|
:param note: An optional field to store additional information about a |
||||
|
token |
||||
|
:param enabled: A boolean to indicate whether a token can be considered |
||||
|
eligible for authentication |
||||
|
:param expiration_time: An optional argument to determine when the token |
||||
|
becomes invalid as a means of authentication. Defaults to None, which means |
||||
|
no expiration |
||||
|
:return: |
||||
|
""" |
||||
|
token = authentication_service.generate_token() |
||||
|
user_token = UserToken( |
||||
|
user_id=user.id, |
||||
|
token=token.__str__(), |
||||
|
note=note, |
||||
|
enabled=enabled, |
||||
|
creation_time=datetime.now(), |
||||
|
expiration_time=expiration_time, |
||||
|
version=0) |
||||
|
|
||||
|
db.session.add(user_token) |
||||
|
db.session.commit() |
||||
|
|
||||
|
return user_token |
||||
|
|
||||
|
|
||||
|
def delete(user_token: UserToken) -> bool: |
||||
|
existing_user_token = db.session.delete(user_token) |
||||
|
if existing_user_token is None: |
||||
|
db.session.commit() |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def find_by_user_and_token(user: User, token: str) -> Optional[UserToken]: |
||||
|
return UserToken.query.filter_by(user_id=user.id, token=token).first() |
@ -0,0 +1,18 @@ |
|||||
|
from datetime import date |
||||
|
from typing import Any |
||||
|
|
||||
|
import rfc3339 |
||||
|
from flask.json import JSONEncoder |
||||
|
|
||||
|
|
||||
|
class CustomJSONEncoder(JSONEncoder): |
||||
|
def default(self, obj: Any) -> Any: |
||||
|
try: |
||||
|
if isinstance(obj, date): |
||||
|
return rfc3339.format(obj) |
||||
|
iterable = iter(obj) |
||||
|
except TypeError: |
||||
|
pass |
||||
|
else: |
||||
|
return list(iterable) |
||||
|
return JSONEncoder.default(self, obj) |
@ -0,0 +1,10 @@ |
|||||
|
#!/usr/bin/env bash |
||||
|
|
||||
|
# Migrate the Database |
||||
|
FLASK_APP=atheneum:app flask db upgrade |
||||
|
|
||||
|
# Make sure an administrator is registered |
||||
|
python manage.py user register-admin |
||||
|
|
||||
|
# Start the application |
||||
|
gunicorn -b 0.0.0.0:8080 atheneum:app |
@ -0,0 +1,124 @@ |
|||||
|
import logging |
||||
|
import random |
||||
|
import string |
||||
|
from typing import Optional |
||||
|
from os import path |
||||
|
|
||||
|
import click |
||||
|
from click import Context |
||||
|
|
||||
|
from atheneum import app |
||||
|
from atheneum.model import User |
||||
|
from atheneum.service import user_service |
||||
|
|
||||
|
logging.basicConfig() |
||||
|
|
||||
|
|
||||
|
@click.group() |
||||
|
def main(): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
@click.group(name='user') |
||||
|
def user_command_group(): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
@click.command(name='delete') |
||||
|
@click.argument('name') |
||||
|
def delete_user(name: str): |
||||
|
logging.info('Deleting user with name \'%s\'', name) |
||||
|
existing_user = User.query.filter_by(name=name).first() |
||||
|
if existing_user is not None: |
||||
|
successful = user_service.delete(existing_user) |
||||
|
if successful: |
||||
|
logging.warning('Deleted user with name \'%s\'', name) |
||||
|
else: |
||||
|
logging.error('Failed to delete user with name \'%s\'', name) |
||||
|
else: |
||||
|
logging.warning('User with name \'%s\' doesn\'t exist', name) |
||||
|
|
||||
|
|
||||
|
@click.command('register') |
||||
|
@click.argument('name') |
||||
|
@click.argument('password', required=False) |
||||
|
@click.option('--role', |
||||
|
default=User.ROLE_USER, |
||||
|
envvar='ROLE', |
||||
|
help='Role to assign to the user. default=[USER]') |
||||
|
def register_user( |
||||
|
name: str, |
||||
|
role: str, |
||||
|
password: Optional[str] = None): |
||||
|
logging.info('Registering user with name \'%s\'', name) |
||||
|
existing_user = User.query.filter_by(name=name).first() |
||||
|
if existing_user is None: |
||||
|
user_password = password if password else ''.join( |
||||
|
random.choices(string.ascii_letters + string.digits, k=24)) |
||||
|
new_user = user_service.register(name, user_password, role) |
||||
|
logging.warning( |
||||
|
'Created new user: \'%s\' with password \'%s\' and role %s', |
||||
|
new_user.name, |
||||
|
user_password, |
||||
|
new_user.role) |
||||
|
else: |
||||
|
logging.warning('User \'%s\' already exists. Did you mean to update?', |
||||
|
name) |
||||
|
|
||||
|
|
||||
|
@click.command(name='register-admin') |
||||
|
@click.pass_context |
||||
|
def register_admin_user(ctx: Context): |
||||
|
admin_users = User.query.filter_by(role=User.ROLE_ADMIN).all() |
||||
|
if len(admin_users) == 0: |
||||
|
name = 'atheneum_administrator' |
||||
|
password = ''.join( |
||||
|
random.choices(string.ascii_letters + string.digits, k=32)) |
||||
|
ctx.invoke( |
||||
|
register_user, |
||||
|
name=name, |
||||
|
role=User.ROLE_ADMIN, |
||||
|
password=password) |
||||
|
admin_credential_file = '.admin_credentials' |
||||
|
with open(admin_credential_file, 'w') as f: |
||||
|
f.write('{}:{}'.format(name, password)) |
||||
|
logging.info( |
||||
|
'These credentials can also be retrieved from {}'.format( |
||||
|
path.abspath(admin_credential_file))) |
||||
|
|
||||
|
|
||||
|
@click.command(name='reset-password') |
||||
|
@click.argument('name') |
||||
|
@click.argument('password', required=False) |
||||
|
def reset_user_password(name: str, password: Optional[str] = None): |
||||
|
logging.info('Resetting user password for \'%s\'', name) |
||||
|
existing_user = User.query.filter_by(name=name).first() |
||||
|
if existing_user is not None: |
||||
|
user_password = password if password else ''.join( |
||||
|
random.choices(string.ascii_letters + string.digits, k=24)) |
||||
|
user_service.update_password(existing_user, user_password) |
||||
|
logging.warning( |
||||
|
'Updated user: \'%s\' with password \'%s\'', |
||||
|
name, |
||||
|
user_password) |
||||
|
else: |
||||
|
logging.warning('User with name \'%s\' doesn\'t exist', name) |
||||
|
|
||||
|
|
||||
|
@click.command(name='list') |
||||
|
def list_users(): |
||||
|
all_users = User.query.all() |
||||
|
[click.echo(user.name) for user in all_users] |
||||
|
|
||||
|
|
||||
|
main.add_command(user_command_group) |
||||
|
user_command_group.add_command(register_user) |
||||
|
user_command_group.add_command(register_admin_user) |
||||
|
user_command_group.add_command(delete_user) |
||||
|
user_command_group.add_command(reset_user_password) |
||||
|
user_command_group.add_command(list_users) |
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
logging.debug('Managing: %s', app.name) |
||||
|
with app.app_context(): |
||||
|
main() |
@ -0,0 +1 @@ |
|||||
|
Generic single-database configuration. |
@ -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 |
@ -0,0 +1,89 @@ |
|||||
|
from __future__ import with_statement |
||||
|
|
||||
|
import logging |
||||
|
from logging.config import fileConfig |
||||
|
|
||||
|
from alembic import context |
||||
|
from sqlalchemy import engine_from_config, pool |
||||
|
|
||||
|
# 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) |
||||
|
logger = logging.getLogger('alembic.env') |
||||
|
|
||||
|
# 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. |
||||
|
|
||||
|
""" |
||||
|
|
||||
|
# this callback is used to prevent an auto-migration from being generated |
||||
|
# when there are no changes to the schema |
||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html |
||||
|
def process_revision_directives(context, revision, directives): |
||||
|
if getattr(config.cmd_opts, 'autogenerate', False): |
||||
|
script = directives[0] |
||||
|
if script.upgrade_ops.is_empty(): |
||||
|
directives[:] = [] |
||||
|
logger.info('No changes in schema detected.') |
||||
|
|
||||
|
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, |
||||
|
process_revision_directives=process_revision_directives, |
||||
|
**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() |
@ -0,0 +1,24 @@ |
|||||
|
"""${message} |
||||
|
|
||||
|
Revision ID: ${up_revision} |
||||
|
Revises: ${down_revision | comma,n} |
||||
|
Create Date: ${create_date} |
||||
|
|
||||
|
""" |
||||
|
from alembic import op |
||||
|
import sqlalchemy as sa |
||||
|
${imports if imports else ""} |
||||
|
|
||||
|
# revision identifiers, used by Alembic. |
||||
|
revision = ${repr(up_revision)} |
||||
|
down_revision = ${repr(down_revision)} |
||||
|
branch_labels = ${repr(branch_labels)} |
||||
|
depends_on = ${repr(depends_on)} |
||||
|
|
||||
|
|
||||
|
def upgrade(): |
||||
|
${upgrades if upgrades else "pass"} |
||||
|
|
||||
|
|
||||
|
def downgrade(): |
||||
|
${downgrades if downgrades else "pass"} |
@ -0,0 +1,56 @@ |
|||||
|
"""Initial User DB Migration |
||||
|
Revision ID: 96442b147e22 |
||||
|
Revises: |
||||
|
Create Date: 2018-07-03 14:22:42.833390 |
||||
|
|
||||
|
""" |
||||
|
import sqlalchemy as sa |
||||
|
from alembic import op |
||||
|
|
||||
|
# revision identifiers, used by Alembic. |
||||
|
revision = '96442b147e22' |
||||
|
down_revision = None |
||||
|
branch_labels = None |
||||
|
depends_on = None |
||||
|
|
||||
|
|
||||
|
def upgrade(): |
||||
|
op.create_table('user', |
||||
|
sa.Column('id', sa.Integer(), nullable=False), |
||||
|
sa.Column('name', sa.Unicode(length=60), nullable=False), |
||||
|
sa.Column('role', sa.Unicode(length=32), nullable=False), |
||||
|
sa.Column( |
||||
|
'password_hash', |
||||
|
sa.Unicode(length=128), |
||||
|
nullable=False), |
||||
|
sa.Column( |
||||
|
'password_revision', sa.SmallInteger(), nullable=False), |
||||
|
sa.Column('creation_time', sa.DateTime(), nullable=False), |
||||
|
sa.Column('last_login_time', sa.DateTime(), nullable=True), |
||||
|
sa.Column('version', sa.Integer(), nullable=False), |
||||
|
sa.PrimaryKeyConstraint('id'), |
||||
|
sa.UniqueConstraint('name')) |
||||
|
|
||||
|
op.create_table('user_token', |
||||
|
sa.Column('id', sa.Integer(), nullable=False), |
||||
|
sa.Column('user_id', sa.Integer(), nullable=False), |
||||
|
sa.Column('token', sa.Unicode(length=36), nullable=False), |
||||
|
sa.Column('note', sa.Unicode(length=128), nullable=True), |
||||
|
sa.Column('enabled', sa.Boolean(), nullable=False), |
||||
|
sa.Column('expiration_time', sa.DateTime(), nullable=True), |
||||
|
sa.Column('creation_time', sa.DateTime(), nullable=False), |
||||
|
sa.Column('last_edit_time', sa.DateTime(), nullable=True), |
||||
|
sa.Column('last_usage_time', sa.DateTime(), nullable=True), |
||||
|
sa.Column('version', sa.Integer(), nullable=False), |
||||
|
sa.ForeignKeyConstraint( |
||||
|
['user_id'], ['user.id'], ondelete='CASCADE'), |
||||
|
sa.PrimaryKeyConstraint('id')) |
||||
|
|
||||
|
op.create_index(op.f('ix_user_token_user_id'), 'user_token', ['user_id'], |
||||
|
unique=False) |
||||
|
|
||||
|
|
||||
|
def downgrade(): |
||||
|
op.drop_index(op.f('ix_user_token_user_id'), table_name='user_token') |
||||
|
op.drop_table('user_token') |
||||
|
op.drop_table('user') |
@ -0,0 +1,11 @@ |
|||||
|
[mypy] |
||||
|
ignore_missing_imports = True |
||||
|
follow_imports = skip |
||||
|
disallow_untyped_calls = False |
||||
|
disallow_untyped_defs = True |
||||
|
check_untyped_defs = True |
||||
|
disallow_subclassing_any = False |
||||
|
warn_redundant_casts = True |
||||
|
warn_unused_ignores = True |
||||
|
strict_optional = True |
||||
|
strict_boolean = False |
@ -0,0 +1,9 @@ |
|||||
|
#!/usr/bin/env bash |
||||
|
|
||||
|
set -e |
||||
|
set -x |
||||
|
|
||||
|
pycodestyle atheneum tests |
||||
|
mypy atheneum tests |
||||
|
PYTHONPATH=$(pwd) coverage run --source atheneum -m pytest |
||||
|
coverage report --fail-under=85 -m --skip-covered |
@ -0,0 +1,12 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<module type="PYTHON_MODULE" version="4"> |
||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true"> |
||||
|
<exclude-output /> |
||||
|
<content url="file://$MODULE_DIR$"> |
||||
|
<sourceFolder url="file://$MODULE_DIR$/atheneum" isTestSource="false" /> |
||||
|
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" /> |
||||
|
</content> |
||||
|
<orderEntry type="inheritedJdk" /> |
||||
|
<orderEntry type="sourceFolder" forTests="false" /> |
||||
|
</component> |
||||
|
</module> |
@ -0,0 +1,10 @@ |
|||||
|
from setuptools import setup |
||||
|
|
||||
|
setup( |
||||
|
name='atheneum', |
||||
|
packages=['atheneum'], |
||||
|
include_package_data=True, |
||||
|
install_requires=[ |
||||
|
'flask', |
||||
|
], |
||||
|
) |
@ -0,0 +1,3 @@ |
|||||
|
DEBUG = False |
||||
|
SECRET_KEY = b'\xb4\x89\x0f\x0f\xe5\x88\x97\xfe\x8d<\x0b@d\xe9\xa5\x87%' \ |
||||
|
b'\xc6\xf0@l1\xe3\x90g\xfaA.?u=s' # CHANGE ME IN REAL CONFIG |
@ -0,0 +1,22 @@ |
|||||
|
from tests.conftest import AuthActions |
||||
|
|
||||
|
|
||||
|
def test_login_happy_path(auth: AuthActions): |
||||
|
result = auth.login() |
||||
|
assert result.status_code == 200 |
||||
|
assert result.json['token'] is not None and len(result.json['token']) > 0 |
||||
|
|
||||
|
|
||||
|
def test_bump_happy_path(auth: AuthActions): |
||||
|
auth.login() |
||||
|
result = auth.bump() |
||||
|
assert result.status_code == 200 |
||||
|
assert (result.json['last_login_time'] is not None |
||||
|
and len(result.json['last_login_time']) > 0) |
||||
|
|
||||
|
|
||||
|
def test_logout_happy_path(auth: AuthActions): |
||||
|
auth.login() |
||||
|
result = auth.logout() |
||||
|
assert result.status_code == 200 |
||||
|
assert result.json is None |
@ -0,0 +1,33 @@ |
|||||
|
from typing import Any |
||||
|
|
||||
|
from flask import Response, Flask |
||||
|
|
||||
|
from atheneum.api.decorators import return_json |
||||
|
from atheneum.api.model import APIResponse |
||||
|
|
||||
|
|
||||
|
@return_json |
||||
|
def return_jsonified_result(obj: Any) -> Any: |
||||
|
return obj |
||||
|
|
||||
|
|
||||
|
def test_return_json_response(): |
||||
|
result = return_jsonified_result(Response(status=200)) |
||||
|
assert isinstance(result, Response) |
||||
|
assert result.status_code == 200 |
||||
|
|
||||
|
|
||||
|
def test_return_json_apiresponse(app: Flask): |
||||
|
with app.app_context(): |
||||
|
result = return_jsonified_result(APIResponse(payload={}, status=200)) |
||||
|
assert len(result) == 2 |
||||
|
assert isinstance(result[0], Response) |
||||
|
assert isinstance(result[1], int) |
||||
|
assert result[0].status_code == 200 |
||||
|
|
||||
|
|
||||
|
def test_return_json_dict(app: Flask): |
||||
|
with app.app_context(): |
||||
|
result = return_jsonified_result({'status': 200}) |
||||
|
assert isinstance(result, Response) |
||||
|
assert result.status_code == 200 |
@ -0,0 +1,127 @@ |
|||||
|
import base64 |
||||
|
import os |
||||
|
import random |
||||
|
import string |
||||
|
import tempfile |
||||
|
from typing import Tuple, Any |
||||
|
|
||||
|
import pytest |
||||
|
from flask import Flask |
||||
|
from flask.testing import FlaskClient, FlaskCliRunner |
||||
|
from werkzeug.test import Client |
||||
|
|
||||
|
from atheneum import create_app, init_db, register_blueprints |
||||
|
from atheneum.model import User |
||||
|
from atheneum.service import user_service |
||||
|
|
||||
|
|
||||
|
def add_test_user() -> Tuple[str, str]: |
||||
|
test_username = 'test_' + ''.join( |
||||
|
random.choices(string.ascii_letters + string.digits, k=17)).strip() |
||||
|
test_password = ''.join( |
||||
|
random.choices(string.ascii_letters + string.digits, k=32)).strip() |
||||
|
user_service.register(test_username, test_password, User.ROLE_ADMIN) |
||||
|
return test_username, test_password |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def app() -> Flask: |
||||
|
"""Create and configure a new app instance for each test.""" |
||||
|
# create a temporary file to isolate the database for each test |
||||
|
db_fd, db_path = tempfile.mkstemp() |
||||
|
# create the app with common test config |
||||
|
app = create_app({ |
||||
|
'TESTING': True, |
||||
|
'DATABASE': db_path, |
||||
|
}) |
||||
|
register_blueprints(app) |
||||
|
|
||||
|
# create the database and load test data |
||||
|
with app.app_context(): |
||||
|
init_db() |
||||
|
test_username, test_password = add_test_user() |
||||
|
app.config['test_username'] = test_username |
||||
|
app.config['test_password'] = test_password |
||||
|
# get_db().executescript(_data_sql) |
||||
|
|
||||
|
yield app |
||||
|
|
||||
|
# close and remove the temporary database |
||||
|
os.close(db_fd) |
||||
|
os.unlink(db_path) |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def client(app: Flask) -> FlaskClient: |
||||
|
"""A test client for the app.""" |
||||
|
return app.test_client() |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def runner(app: Flask) -> FlaskCliRunner: |
||||
|
"""A test runner for the app's Click commands.""" |
||||
|
return app.test_cli_runner() |
||||
|
|
||||
|
|
||||
|
class AuthActions(object): |
||||
|
def __init__(self, |
||||
|
client: Client, |
||||
|
username: str = "", |
||||
|
password: str = "") -> None: |
||||
|
self._client = client |
||||
|
self.username: str = username |
||||
|
self.password: str = password |
||||
|
self.token: str = "" |
||||
|
|
||||
|
def configure(self, username: str, password: str) -> Any: |
||||
|
self.username = username |
||||
|
self.password = password |
||||
|
return self |
||||
|
|
||||
|
def login(self) -> Any: |
||||
|
auth_header = self.get_authorization_header_basic() |
||||
|
result = self._client.post( |
||||
|
'/auth/login', |
||||
|
headers={ |
||||
|
auth_header[0]: auth_header[1] |
||||
|
} |
||||
|
) |
||||
|
self.token = result.json['token'] |
||||
|
return result |
||||
|
|
||||
|
def bump(self) -> Any: |
||||
|
auth_header = self.get_authorization_header_token() |
||||
|
return self._client.post( |
||||
|
'/auth/bump', |
||||
|
headers={ |
||||
|
auth_header[0]: auth_header[1] |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
def logout(self) -> Any: |
||||
|
auth_header = self.get_authorization_header_token() |
||||
|
return self._client.post( |
||||
|
'/auth/logout', |
||||
|
headers={ |
||||
|
auth_header[0]: auth_header[1] |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
def get_authorization_header_basic(self) -> Tuple[str, str]: |
||||
|
credentials = base64.b64encode( |
||||
|
'{}:{}'.format(self.username, self.password).encode('utf8')) \ |
||||
|
.decode('utf8').strip() |
||||
|
return 'Authorization', 'Basic {}'.format(credentials) |
||||
|
|
||||
|
def get_authorization_header_token(self) -> Tuple[str, str]: |
||||
|
credentials = base64.b64encode( |
||||
|
'{}:{}'.format(self.username, self.token).encode('utf8')) \ |
||||
|
.decode('utf8').strip() |
||||
|
return 'Authorization', 'Token {}'.format(credentials) |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def auth(client: Client) -> AuthActions: |
||||
|
return AuthActions(client, |
||||
|
client.application.config.get('test_username'), |
||||
|
client.application.config.get('test_password')) |
@ -0,0 +1,132 @@ |
|||||
|
from mock import patch, MagicMock, Mock |
||||
|
|
||||
|
from atheneum.middleware.authentication_middleware import \ |
||||
|
authenticate_with_password, authenticate_with_token |
||||
|
|
||||
|
middleware_module = 'atheneum.middleware.authentication_middleware' |
||||
|
|
||||
|
|
||||
|
@patch(middleware_module + '.g') |
||||
|
@patch(middleware_module + '.authentication_service.is_valid_password') |
||||
|
@patch(middleware_module + '.user_service.find_by_name') |
||||
|
def test_authenticate_with_password_happy_path( |
||||
|
mock_user_service: MagicMock, |
||||
|
mock_authentication_service: MagicMock, |
||||
|
mock_g: MagicMock): |
||||
|
mock_g.user = Mock() |
||||
|
mock_user_service.return_value = Mock() |
||||
|
mock_authentication_service.return_value = True |
||||
|
assert authenticate_with_password('test', 'test') |
||||
|
mock_user_service.assert_called_once() |
||||
|
mock_authentication_service.assert_called_once() |
||||
|
mock_g.user.assert_not_called() |
||||
|
|
||||
|
|
||||
|
@patch(middleware_module + '.g') |
||||
|
@patch(middleware_module + '.authentication_service.is_valid_password') |
||||
|
@patch(middleware_module + '.user_service.find_by_name') |
||||
|
def test_authenticate_with_password_no_user( |
||||
|
mock_user_service: MagicMock, |
||||
|
mock_authentication_service: MagicMock, |
||||
|
mock_g: MagicMock): |
||||
|
mock_g.user = Mock() |
||||
|
mock_user_service.return_value = None |
||||
|
mock_authentication_service.return_value = True |
||||
|
assert not authenticate_with_password('test', 'test') |
||||
|
mock_user_service.assert_called_once() |
||||
|
mock_authentication_service.assert_not_called() |
||||
|
mock_g.user.assert_not_called() |
||||
|
|
||||
|
|
||||
|
@patch(middleware_module + '.g') |
||||
|
@patch(middleware_module + '.authentication_service.is_valid_password') |
||||
|
@patch(middleware_module + '.user_service.find_by_name') |
||||
|
def test_authenticate_with_password_invalid_password( |
||||
|
mock_user_service: MagicMock, |
||||
|
mock_authentication_service: MagicMock, |
||||
|
mock_g: MagicMock): |
||||
|
mock_g.user = Mock() |
||||
|
mock_user_service.return_value = Mock() |
||||
|
mock_authentication_service.return_value = False |
||||
|
assert not authenticate_with_password('test', 'test') |
||||
|
mock_user_service.assert_called_once() |
||||
|
mock_authentication_service.assert_called_once() |
||||
|
mock_g.user.assert_not_called() |
||||
|
|
||||
|
|
||||
|
@patch(middleware_module + '.g') |
||||
|
@patch(middleware_module + '.authentication_service.is_valid_token') |
||||
|
@patch(middleware_module + '.user_token_service.find_by_user_and_token') |
||||
|
@patch(middleware_module + '.user_service.find_by_name') |
||||
|
def test_authenticate_with_token_happy_path( |
||||
|
mock_user_service: MagicMock, |
||||
|
mock_user_token_service: MagicMock, |
||||
|
mock_authentication_service: MagicMock, |
||||
|
mock_g: MagicMock): |
||||
|
mock_g.user = Mock() |
||||
|
mock_user_service.return_value = Mock() |
||||
|
mock_user_token_service.return_value = Mock() |
||||
|
mock_authentication_service.return_value = True |
||||
|
assert authenticate_with_token('test', 'test') |
||||
|
mock_user_service.assert_called_once() |
||||
|
mock_user_token_service.assert_called_once() |
||||
|
mock_authentication_service.assert_called_once() |
||||
|
mock_g.user.assert_not_called() |
||||
|
|
||||
|
|
||||
|
@patch(middleware_module + '.g') |
||||
|
@patch(middleware_module + '.authentication_service.is_valid_token') |
||||
|
@patch(middleware_module + '.user_token_service.find_by_user_and_token') |
||||
|
@patch(middleware_module + '.user_service.find_by_name') |
||||
|
def test_authenticate_with_token_no_user( |
||||
|
mock_user_service: MagicMock, |
||||
|
mock_user_token_service: MagicMock, |
||||
|
mock_authentication_service: MagicMock, |
||||
|
mock_g: MagicMock): |
||||
|
mock_g.user = Mock() |
||||
|
mock_user_service.return_value = None |
||||
|
assert not authenticate_with_token('test', 'test') |
||||
|
mock_user_service.assert_called_once() |
||||
|
mock_user_token_service.assert_not_called() |
||||
|
mock_authentication_service.assert_not_called() |
||||
|
mock_g.user.assert_not_called() |
||||
|
|
||||
|
|
||||
|
@patch(middleware_module + '.g') |
||||
|
@patch(middleware_module + '.authentication_service.is_valid_token') |
||||
|
@patch(middleware_module + '.user_token_service.find_by_user_and_token') |
||||
|
@patch(middleware_module + '.user_service.find_by_name') |
||||
|
def test_authenticate_with_token_no_user_token( |
||||
|
mock_user_service: MagicMock, |
||||
|
mock_user_token_service: MagicMock, |
||||
|
mock_authentication_service: MagicMock, |
||||
|
mock_g: MagicMock): |
||||
|
mock_g.user = Mock() |
||||
|
mock_user_service.return_value = Mock() |
||||
|
mock_user_token_service.return_value = None |
||||
|
mock_authentication_service.return_value = False |
||||
|
assert not authenticate_with_token('test', 'test') |
||||
|
mock_user_service.assert_called_once() |
||||
|
mock_user_token_service.assert_called_once() |
||||
|
mock_authentication_service.assert_called_once() |
||||
|
mock_g.user.assert_not_called() |
||||
|
|
||||
|
|
||||
|
@patch(middleware_module + '.g') |
||||
|
@patch(middleware_module + '.authentication_service.is_valid_token') |
||||
|
@patch(middleware_module + '.user_token_service.find_by_user_and_token') |
||||
|
@patch(middleware_module + '.user_service.find_by_name') |
||||
|
def test_authenticate_with_token_invalid_token( |
||||
|
mock_user_service: MagicMock, |
||||
|
mock_user_token_service: MagicMock, |
||||
|
mock_authentication_service: MagicMock, |
||||
|
mock_g: MagicMock): |
||||
|
mock_g.user = Mock() |
||||
|
mock_user_service.return_value = Mock() |
||||
|
mock_user_token_service.return_value = Mock() |
||||
|
mock_authentication_service.return_value = False |
||||
|
assert not authenticate_with_token('test', 'test') |
||||
|
mock_user_service.assert_called_once() |
||||
|
mock_user_token_service.assert_called_once() |
||||
|
mock_authentication_service.assert_called_once() |
||||
|
mock_g.user.assert_not_called() |
Write
Preview
Loading…
Cancel
Save
Reference in new issue