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