├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── app.py └── sikr ├── db ├── connector.py ├── mixins.py └── syncdb.py ├── middleware ├── handle_404.py ├── headers.py ├── https.py └── json.py ├── models ├── __init__.py ├── entries.py └── users.py ├── resources ├── __init__.py ├── auth │ ├── basicauth.py │ ├── decorators.py │ ├── facebook.py │ ├── github.py │ ├── google.py │ ├── linkedin.py │ ├── twitter.py │ └── utils.py ├── categories.py ├── items.py ├── main.py ├── services.py └── sharing.py ├── settings ├── __init__.py ├── development.py ├── production.py └── staging.py └── utils ├── checks.py ├── cryptofunctions.py ├── email.py ├── logs.py └── tokens.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # Avoid the development settings 4 | **/defaults.py 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | bin 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | lib 22 | lib64 23 | __pycache__ 24 | 25 | # Logs 26 | pip-log.txt 27 | sikr.log 28 | 29 | # Unit test / coverage reports 30 | .coverage 31 | .tox 32 | nosetests.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | #**/development.py 42 | 43 | *.scssc 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.7-dev" 5 | install: 6 | - "pip install cython" 7 | - "pip install -r requirements/common.txt" 8 | script: nosetests 9 | notifications: 10 | email: 11 | - oscar.carballal@esmorga.eu 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2018 Esmorga Software and other contributors 2 | as noted in the individual souirce code files. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | cython = "*" 8 | ujson = "*" 9 | falcon = "*" 10 | sqlalchemy = "*" 11 | pyjwt = "*" 12 | ipdb = "*" 13 | ipython = "*" 14 | uwsgi = "*" 15 | "psycopg2-binary" = "*" 16 | 17 | [dev-packages] 18 | 19 | [requires] 20 | python_version = "3.6" 21 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "18a5b4696c2387c8c0467f75ca316faa872db7223955ff601a6ecde57d6fed98" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "backcall": { 20 | "hashes": [ 21 | "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", 22 | "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" 23 | ], 24 | "version": "==0.2.0" 25 | }, 26 | "cython": { 27 | "hashes": [ 28 | "sha256:03db8c1b8120039f72493b95494a595be13b01b6860cfc93e2a651a001847b3b", 29 | "sha256:0d2ccb812d73e67557fd16e7aa7bc5bac18933c1dfe306133cd0680ccab89f33", 30 | "sha256:24f8ea864de733f5a447896cbeec2cac212247e33272539670b9f466f43f23db", 31 | "sha256:30a8fd029eb932a7b5a74e158316d1d069ccb67a8607aa7b6c4ed19fab7fbd4a", 32 | "sha256:37e680901e6a4b97ab67717f9b43fc58542cd10a77431efd2d8801d21d5a37d4", 33 | "sha256:4984e097bc9da37862d97c1f66dacf2c80fadaea488d96ba0b5ea9d84dbc7521", 34 | "sha256:4cfda677227af41e4502e088ee9875e71922238a207d0c40785a0fb09c703c21", 35 | "sha256:4ec60a4086a175a81b9258f810440a6dd2671aa4b419d8248546d85a7de6a93f", 36 | "sha256:51c7d48ea4cba532d11a6d128ebbc15373013f816e5d1c3a3946650b582a30b8", 37 | "sha256:634e2f10fc8d026c633cffacb45cd8f4582149fa68e1428124e762dbc566e68a", 38 | "sha256:67e0359709c8addc3ecb19e1dec6d84d67647e3906da618b953001f6d4480275", 39 | "sha256:6a93d4ba0461edc7a359241f4ebbaa8f9bc9490b3540a8dd0460bef8c2c706db", 40 | "sha256:6ba89d56c3ee45716378cda4f0490c3abe1edf79dce8b997f31608b14748a52b", 41 | "sha256:6ca5436d470584ba6fd399a802c9d0bcf76cf1edb0123725a4de2f0048f9fa07", 42 | "sha256:7656895cdd59d56dd4ed326d1ee9ede727020d4a5d8778a05af2d8e25af4b13d", 43 | "sha256:85f7432776870d65639fed00f951a3c05ef1e534bc72a73cd1200d79b9a7d7d0", 44 | "sha256:96dd674e72281d3feed74fd5adcf0514ba02884f123cdf4fb78567e7be6b1694", 45 | "sha256:97bf06a89bcf9e8d7633cde89274d42b3b661dc974b58fca066fad762e46b4d8", 46 | "sha256:9a465e7296a4629139be5d2015577f2ae5e08196eb7dc4c407beea130f362dc3", 47 | "sha256:9a60355edca1cc9006be086e2633e190542aad2bf9e46948792a48b3ae28ed97", 48 | "sha256:9eab3696f2cb88167db109d737c787fb9dd34ca414bd1e0c424e307956e02c94", 49 | "sha256:c3ae7d40ebceb0d944dfeeceaf1fbf17e528f5327d97b008a8623ddddd1ecee3", 50 | "sha256:c623d19fcc60ea27882f20cf484218926ddf6f978b958dae1070600a1974f809", 51 | "sha256:c719a6e86d7c737afcc9729994f76b284d1c512099ee803eff11c2a9e6e33a42", 52 | "sha256:cf17af0433218a1e33dc6f3069dd9e7cd0c80fe505972c3acd548e25f67973fd", 53 | "sha256:daf96e0d232605e979995795f62ffd24c5c6ecea4526e4cbb86d80f01da954b2", 54 | "sha256:db40de7d03842d3c4625028a74189ade52b27f8efaeb0d2ca06474f57e0813b2", 55 | "sha256:deea1ef59445568dd7738fa3913aea7747e4927ff4ae3c10737844b8a5dd3e22", 56 | "sha256:e05d28b5ce1ee5939d83e50344980659688ecaed65c5e10214d817ecf5d1fe6a", 57 | "sha256:f5f6694ce668eb7a9b59550bfe4265258809c9b0665c206b26d697df2eef2a8b" 58 | ], 59 | "index": "pypi", 60 | "version": "==0.28.2" 61 | }, 62 | "decorator": { 63 | "hashes": [ 64 | "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", 65 | "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" 66 | ], 67 | "version": "==4.4.2" 68 | }, 69 | "falcon": { 70 | "hashes": [ 71 | "sha256:0a66b33458fab9c1e400a9be1a68056abda178eb02a8cb4b8f795e9df20b053b", 72 | "sha256:3981f609c0358a9fcdb25b0e7fab3d9e23019356fb429c635ce4133135ae1bc4" 73 | ], 74 | "index": "pypi", 75 | "version": "==1.4.1" 76 | }, 77 | "ipdb": { 78 | "hashes": [ 79 | "sha256:7081c65ed7bfe7737f83fa4213ca8afd9617b42ff6b3f1daf9a3419839a2a00a" 80 | ], 81 | "index": "pypi", 82 | "version": "==0.11" 83 | }, 84 | "ipython": { 85 | "hashes": [ 86 | "sha256:a0c96853549b246991046f32d19db7140f5b1a644cc31f0dc1edc86713b7676f", 87 | "sha256:eca537aa61592aca2fef4adea12af8e42f5c335004dfa80c78caf80e8b525e5c" 88 | ], 89 | "index": "pypi", 90 | "version": "==6.4.0" 91 | }, 92 | "ipython-genutils": { 93 | "hashes": [ 94 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 95 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 96 | ], 97 | "version": "==0.2.0" 98 | }, 99 | "jedi": { 100 | "hashes": [ 101 | "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93", 102 | "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707" 103 | ], 104 | "version": "==0.18.0" 105 | }, 106 | "parso": { 107 | "hashes": [ 108 | "sha256:15b00182f472319383252c18d5913b69269590616c947747bc50bf4ac768f410", 109 | "sha256:8519430ad07087d4c997fda3a7918f7cfa27cb58972a8c89c2a0295a1c940e9e" 110 | ], 111 | "version": "==0.8.1" 112 | }, 113 | "pexpect": { 114 | "hashes": [ 115 | "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", 116 | "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" 117 | ], 118 | "markers": "sys_platform != 'win32'", 119 | "version": "==4.8.0" 120 | }, 121 | "pickleshare": { 122 | "hashes": [ 123 | "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", 124 | "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" 125 | ], 126 | "version": "==0.7.5" 127 | }, 128 | "prompt-toolkit": { 129 | "hashes": [ 130 | "sha256:37925b37a4af1f6448c76b7606e0285f79f434ad246dda007a27411cca730c6d", 131 | "sha256:dd4fca02c8069497ad931a2d09914c6b0d1b50151ce876bc15bde4c747090126", 132 | "sha256:f7eec66105baf40eda9ab026cd8b2e251337eea8d111196695d82e0c5f0af852" 133 | ], 134 | "version": "==1.0.18" 135 | }, 136 | "psycopg2-binary": { 137 | "hashes": [ 138 | "sha256:02eb674e3d5810e19b4d5d00720b17130e182da1ba259dda608aaf33d787347d", 139 | "sha256:3a14baeabcebd4662f12f4bff03e0574a2369a2e41baf829e6fb4a24c95cf88b", 140 | "sha256:436a503eda41f6adb08f292f40a3784fce0a5f351b6ae7b19a911904db53af93", 141 | "sha256:465ff1d427ed42c31e456dbbd9edab3552be18a0edaef7450c5b3e6fee745052", 142 | "sha256:4a1a5ea2fa4b53191637b162873a82822d92a85a08beefe28296b8eb5cf2fea5", 143 | "sha256:4a4f23a08fbccbe40ecdb5384d807bcb469ea71dd87e6be2e80b036b8e6d47df", 144 | "sha256:77a2fc622a1f2d08a707673c9be5769d521f03d867d305f172bb417fa7882754", 145 | "sha256:8014c06a9ed7b78ba81beff3ae71acd78c212390f8ed839e9ce22735880bd5b4", 146 | "sha256:83af04029bcb4b56c852e5876fef71340dcb465fa44fc99f80bac72e10fb0b74", 147 | "sha256:86c0d2587f56776f25d52cca8e275adf495c8e01933fbfc2ca23b124610ab761", 148 | "sha256:9305d7cbc802aaefac5c75a3df725f2654797369f32b18d4d0adb382dfab6c09", 149 | "sha256:9b5ddbed85ec73293695d7116589d956ef0dd3fcf7bf3b2a3bc1e8e54c1d543a", 150 | "sha256:a3d2cc0cb0b988dbfd0d11f7fac34058b25a6ce533ed5b8e88d6cb315e77d54a", 151 | "sha256:ab1db8f3e96570d9f7ebc45133ce2574804b2280499baade178e163d022107b5", 152 | "sha256:b039f51bca1ddd70234cc3f84f94f42ad43861b931bdfb497f887c60c39a6565", 153 | "sha256:b287ddf4cafcfb632974907d1e7862119e36bb758228bdb07dd247553e4cdfc0", 154 | "sha256:b6b2b26590304d97ef2af28d153ee99ace6fe0806934f4618edfc87216c77f91", 155 | "sha256:c4c6004d410c77bfa5389ae9485498ce32805447a67afbfe8db0d247a5c88fa1", 156 | "sha256:c606bff0978ee4858d86d40f6b6ab0c4cac4474f627bd054683dc03a4fc1a366", 157 | "sha256:c8220c521a408b41c4f14036004a621ed0d965941286b978cd2ea2623fabd755", 158 | "sha256:cb07184a4bfad304831f0a88b1c13fbd8cf9fcdf1f11e71c477dd6d7b1b078a0", 159 | "sha256:cf3911fba0c47fc1313b5783183cda301032b14637a0b7a336766ae46998c7ee", 160 | "sha256:d0972f062c73956332e9681dfdb133168618f0abfecc96e89f0205ac89cd454b", 161 | "sha256:d1dd3eb8edd354083f5d27b968c5a17854c41347ba5a480b520be85ec1a8495c", 162 | "sha256:d51c7ed810fce1e50464088c37cc8da05534de8afb12a732500827ebcc480081", 163 | "sha256:d8940b5104588d6313315e037f0f5ed68d2e5f62ccc1c429d3cff11d2ba6de3f", 164 | "sha256:de4f88f823037a71ea5ef3c1041d96b8a68d73343133edda684fd42f575bd9d7" 165 | ], 166 | "index": "pypi", 167 | "version": "==2.7.4" 168 | }, 169 | "ptyprocess": { 170 | "hashes": [ 171 | "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", 172 | "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" 173 | ], 174 | "version": "==0.7.0" 175 | }, 176 | "pygments": { 177 | "hashes": [ 178 | "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", 179 | "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" 180 | ], 181 | "version": "==2.8.0" 182 | }, 183 | "pyjwt": { 184 | "hashes": [ 185 | "sha256:bca523ef95586d3a8a5be2da766fe6f82754acba27689c984e28e77a12174593", 186 | "sha256:dacba5786fe3bf1a0ae8673874e29f9ac497860955c501289c63b15d3daae63a" 187 | ], 188 | "index": "pypi", 189 | "version": "==1.6.1" 190 | }, 191 | "python-mimeparse": { 192 | "hashes": [ 193 | "sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78", 194 | "sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282" 195 | ], 196 | "version": "==1.6.0" 197 | }, 198 | "simplegeneric": { 199 | "hashes": [ 200 | "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" 201 | ], 202 | "version": "==0.8.1" 203 | }, 204 | "six": { 205 | "hashes": [ 206 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 207 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 208 | ], 209 | "version": "==1.15.0" 210 | }, 211 | "sqlalchemy": { 212 | "hashes": [ 213 | "sha256:11ead7047ff3f394ed0d4b62aded6c5d970a9b718e1dc6add9f5e79442cc5b14" 214 | ], 215 | "index": "pypi", 216 | "version": "==1.3.0" 217 | }, 218 | "traitlets": { 219 | "hashes": [ 220 | "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", 221 | "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7" 222 | ], 223 | "version": "==4.3.3" 224 | }, 225 | "ujson": { 226 | "hashes": [ 227 | "sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86" 228 | ], 229 | "index": "pypi", 230 | "version": "==1.35" 231 | }, 232 | "uwsgi": { 233 | "hashes": [ 234 | "sha256:3dc2e9b48db92b67bfec1badec0d3fdcc0771316486c5efa3217569da3528bf2" 235 | ], 236 | "index": "pypi", 237 | "version": "==2.0.17" 238 | }, 239 | "wcwidth": { 240 | "hashes": [ 241 | "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", 242 | "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" 243 | ], 244 | "version": "==0.2.5" 245 | } 246 | }, 247 | "develop": {} 248 | } 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sikr 2 | 3 | [![Build Status](https://travis-ci.org/sikrvault/sikr.svg?branch=master)](https://travis-ci.org/sikrvault/sikr) 4 | [![Coverage Status](https://coveralls.io/repos/github/sikrvault/sikr/badge.svg?branch=master)](https://coveralls.io/github/sikrvault/sikr?branch=master) 5 | [![Docs Status](https://readthedocs.org/projects/sikre/badge/?version=latest)](http://sikre.rtfd.io/) 6 | 7 | **Please note:** *Sikr is in early development stages, it's not recommended to use it in production yet.* 8 | 9 | Sikre is a high-security backend API to store your passwords and sensitive data 10 | securely (like SSH keys and SSL certificates). 11 | 12 | It's made with paranoid security in mind, that means the server will never know 13 | anything about the data you uploaded, it's going to get encrypted so there is 14 | no way for the person managing the instance or the server itself to decrypt or 15 | read your information. 16 | 17 | This is just the backend part, so unless you are \ you 18 | will probably need a frontend to work with. You can use the default 19 | [sikr-frontend](https://github.com/sikrvault/sikr-frontend) project. 20 | 21 | ## Who uses it? 22 | 23 | The official password storage service called `Sikr` (as Sikre/Sikr in Danish, to ensure, protect) is going to use it. It's still on development (tied to the project) but you can reach it on http://sikr.io and http://api.sikr.io 24 | 25 | ## What does it use? 26 | 27 | * [Falcon microframework](http://falconframework.org/) 28 | * [Python Requests](http://docs.python-requests.org/en/latest/) 29 | * [PyJWT](https://github.com/jpadilla/pyjwt) 30 | * [SQLAlchemy ORM](http://www.sqlalchemy.org/) 31 | 32 | ## Requirements 33 | 34 | * A GNU/Linux server 35 | * Python >= 3.6 (tested up to 3.7) 36 | * A valid SSL certificate (not necessary if DEBUG=True) 37 | 38 | Please note that this project **won't run** on Python 2.7.x series or 39 | Python 3.x below 3.6 40 | 41 | ## How to install 42 | 43 | To install please follow these steps (these instructions use Pipenv, if you 44 | don't know what that is, please check it [here](https://docs.pipenv.org/)) 45 | 46 | * Install the dependencies 47 | 48 | `$ cd ` 49 | 50 | `$ pipenv install` 51 | 52 | * Activate the virttual environment 53 | 54 | `$ pipenv shell` 55 | 56 | That should install and lock all the necessary requirements. If you're confused 57 | about how pipenv works, let's just say it takes care of the creation, isolation 58 | and activation of the correct virtual environment on its own, you don't need to 59 | do anything. 60 | 61 | ## How to run 62 | 63 | There is two ways of running the application. One is the main wsgi application 64 | that will serve all the requests. The other is running it as a management 65 | script. 66 | 67 | ### Run as service 68 | 69 | To run it for testing you can use gunicorn or uwsgi or any other wsgi 70 | interface. To run it with uwsgi for example: 71 | 72 | `$ uwsgi --http :8080 --wsgi-file app.py --callable api` 73 | 74 | Now you can visit your application going to `localhost:8080` in your browser. 75 | Please remember that this is the backend, so it will only reply to the API 76 | endpoints, you will not be able to see anything else. 77 | 78 | There is a test endpoint while in debug mode which you can visit in: 79 | `localhost:8080/test_api` 80 | 81 | ### Run as management script 82 | 83 | To run the application as a management script you just need to invoke it 84 | from python: 85 | 86 | `$ python app.py` 87 | 88 | This script contains multiple actions that take care of the service. At 89 | the moment of writing this document these are the functions: 90 | 91 | * `syncdb` Creates the database schema necessary to run the application 92 | * `generate` Fills the database with random data. This command only runs if DEBUG=True 93 | 94 | ## License and copyright 95 | 96 | This project is licensed under the MIT license. Copyright Esmorga Software and contributors 97 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Main application functions. 4 | 5 | This file is the core of the application and it defines the intantiation of the 6 | API and their URLs, as well as some extra functionality if it's run from the 7 | command line to generate or syncronize the database. 8 | """ 9 | 10 | import sys 11 | import argparse 12 | 13 | import falcon 14 | 15 | from sikr.middleware import json, https, headers, handle_404 16 | # from sikr.resources import categories, items, services, main, tests, sharing 17 | # from sikr.resources.auth import github, facebook, google, twitter, linkedin 18 | from sikr.resources import main 19 | from sikr.utils.logs import logger 20 | from sikr.utils.checks import check_python 21 | from sikr import settings 22 | 23 | check_python() 24 | 25 | # If the aplication is run directly through terminal, run this. 26 | if __name__ == "__main__": 27 | parser = argparse.ArgumentParser( 28 | formatter_class=argparse.RawDescriptionHelpFormatter, 29 | epilog="If you are trying to run the " 30 | "application itself, you must call " 31 | "the constructor \nin the following " 32 | "fashion (uWSGI example):\n\n" 33 | "uwsgi --http :8080 --wsgi-file app.py --callable api \n\n") 34 | parser.add_argument("-s", "--syncdb", 35 | help="Create the database schema", 36 | action="store_true") 37 | parser.add_argument("-g", "--generate", 38 | help="Fill the database with random data", 39 | action="store_true") 40 | args = parser.parse_args() 41 | 42 | # If there's no input print help 43 | if not len(sys.argv) > 1: 44 | parser.print_help() 45 | 46 | if args.syncdb: 47 | from sikr.db.syncdb import generate_schema 48 | generate_schema() 49 | 50 | if args.generate: 51 | if not settings.DEBUG: 52 | print("\n`generate` command is not available when debug " 53 | " mode is disabled. Please set DEBUG=True in the " 54 | "settings to use it.\n") 55 | else: 56 | print('In development') 57 | # Else create the API instance, referenced as api 58 | else: 59 | api = falcon.API( 60 | media_type='application/json; charset=UTF-8', 61 | middleware=[ 62 | json.RequireJSON(), 63 | json.JSONTranslator(), 64 | https.RequireHTTPS(), 65 | headers.BaseHeaders(), 66 | handle_404.WrongURL() 67 | ] 68 | ) 69 | 70 | # URLs 71 | api_version = '/' + settings.DEFAULT_API 72 | api.add_route(api_version, main.APIInfo()) 73 | logger.debug("API service started") 74 | -------------------------------------------------------------------------------- /sikr/db/connector.py: -------------------------------------------------------------------------------- 1 | """Main database connector module. 2 | 3 | This module organizes the connections to teh database accrding to the settings 4 | file and values provided. It also creates a base model from where the rest of 5 | models have to inherit from so they connect to the same database. 6 | """ 7 | import sys 8 | import logging 9 | 10 | import sqlalchemy 11 | from sqlalchemy.orm import sessionmaker 12 | from sqlalchemy.ext.declarative import declarative_base 13 | 14 | from sikr import settings 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | def get_dict_values(dict, key, default_value=''): 20 | """Get dictionary values accounting for empty values when key exists.""" 21 | # Returns False if key doesnt exist, False if value is empty 22 | if bool(dict.get(key)): 23 | return dict.get(key) 24 | else: 25 | return default_value 26 | 27 | 28 | db_conf = settings.DATABASE 29 | db_user = get_dict_values(db_conf, 'USER', 'root') 30 | db_host = get_dict_values(db_conf, 'HOST', 'localhost') 31 | db_name = get_dict_values(db_conf, 'NAME', 'mydatabase') 32 | db_engine = get_dict_values(db_conf, 'ENGINE') 33 | db_password = get_dict_values(db_conf, 'PASSWORD') 34 | db_postgres_port = get_dict_values(db_conf, 'PORT', '5432') 35 | db_mysql_port = get_dict_values(db_conf, 'PORT', '3306') 36 | 37 | if db_engine == 'postgresql': 38 | engine = sqlalchemy.create_engine("{}://{}:{}@{}:{}/{}".format( 39 | db_engine, db_user, db_password, db_host, db_postgres_port, db_name 40 | )) 41 | elif db_engine == 'mysql': 42 | engine = sqlalchemy.create_engine("{}://{}:{}@{}:{}/{}".format( 43 | db_engine, db_user, db_password, db_host, db_mysql_port, db_name 44 | )) 45 | elif db_engine == 'sqlite': 46 | engine = sqlalchemy.create_engine("{}:///{}".format(db_engine, db_name)) 47 | else: 48 | error_msg = "Database engine not supported. Valid options are: " \ 49 | "postgresql, mysql, sqlite" 50 | logger.error(error_msg) 51 | sys.exit(error_msg) 52 | 53 | # Establish declarative mapping for models 54 | Base = declarative_base() 55 | 56 | # Establish ORM DB session 57 | Session = sessionmaker(bind=engine) 58 | -------------------------------------------------------------------------------- /sikr/db/mixins.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declared_attr 2 | from sqlalchemy import Boolean, Column, Integer 3 | 4 | 5 | class SikrModelMixin(object): 6 | """Base model mixin for all Sikr models.""" 7 | 8 | @declared_attr 9 | def __tablename__(cls): 10 | return f"sikr_{cls.__name__.lower()}" 11 | 12 | #__table_args__ = {'mysql_engine': 'InnoDB'} 13 | #__mapper_args__= {'always_refresh': True} 14 | 15 | id = Column(Integer, primary_key=True) 16 | active = Column(Boolean) 17 | -------------------------------------------------------------------------------- /sikr/db/syncdb.py: -------------------------------------------------------------------------------- 1 | from sikr.db.connector import Base, engine 2 | from sikr.models.users import UserGroup, User 3 | from sikr.models.entries import Group, Entry 4 | from sikr.utils.logs import logger 5 | 6 | 7 | def generate_schema(): 8 | """Generate the initial schema for the database.""" 9 | start_msg = "Creating database schema..." 10 | end_msg = "Database schema created" 11 | print(f"[ -- ] {start_msg}") 12 | logger.info(start_msg) 13 | try: 14 | Base.metadata.create_all(engine) 15 | print(f"[ OK ] {end_msg}") 16 | logger.info(end_msg) 17 | except Exception as e: 18 | error_msg = f"Error creating schema: {e}" 19 | print(f"[ERROR] {error_msg}") 20 | logger.error(error_msg) 21 | sys.exit(1) 22 | -------------------------------------------------------------------------------- /sikr/middleware/handle_404.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import falcon 4 | 5 | from sikr import settings 6 | 7 | 8 | class WrongURL(object): 9 | 10 | def process_response(self, req, resp, resource=''): 11 | 12 | """Intercept main 404 response by Falcon 13 | 14 | If the API hits a non existing endpoint, it will trigger a customized 15 | 404 response that will redirect people to the documentation. 16 | 17 | Raises: 18 | HTTP 404: A falcon.HTTP_404 error 19 | 20 | Returns: 21 | JSON: A customized JSON response 22 | """ 23 | if resp.status == falcon.HTTP_404: 24 | resp.body = json.dumps({"message": "Resource not found", 25 | "documentation": settings.__docs__}) 26 | -------------------------------------------------------------------------------- /sikr/middleware/headers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import falcon 4 | 5 | from sikr import settings 6 | from sikr.utils.logs import logger 7 | 8 | 9 | class BaseHeaders(object): 10 | # Unused regular expression to check that origin is always a website. 11 | # expression = re.compile("^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$") 12 | 13 | def process_request(self, req, res): 14 | 15 | """Process the request before entering in the API 16 | 17 | Before we process anything in the API, we reset the Origin header to 18 | match the address from the request. 19 | 20 | Args: 21 | Access-Control-Allow-Origin: Change the origin to the URL that made 22 | the request. 23 | 24 | Raises: 25 | HTTP Error: An HTTP error in case the Origin header doesn't match 26 | the predefined regular expression. 27 | 28 | Return: 29 | HTTP headers: A modified set of headers. 30 | 31 | """ 32 | 33 | origin_domain = req.get_header('Origin') 34 | logger.debug("Origin domain is: {}, type: {}".format(origin_domain, type(origin_domain))) 35 | origin_header = origin_domain if settings.CORS_ACTIVE and origin_domain else "*" 36 | logger.debug("Origin header is: {}, type: {}".format(origin_header, type(origin_header))) 37 | 38 | res.set_headers([ 39 | ('Cache-Control', 'no-store, must-revalidate, no-cache, max-age=0'), 40 | ('Content-Type', 'application/json; charset=utf-8'), 41 | ('Access-Control-Allow-Credentials', 'true'), 42 | ('Access-Control-Allow-Origin', origin_header), 43 | ('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, x-auth-user, x-auth-password, Authorization'), 44 | ('Access-Control-Allow-Methods', 'GET, PUT, POST, OPTIONS, DELETE') 45 | ]) 46 | 47 | def process_response(self, req, res, resource): 48 | 49 | """Process the response before returning it to the client. 50 | 51 | In the reutrning reponse we change some values to be able to overcome 52 | the CORS protection and mask the origin server. The CORS interaction 53 | is protected by a check agains a regular expression to make sure the 54 | origin is a website-like URL. 55 | 56 | Warning: 57 | If you are really concerned about security, you can deactivate 58 | the CORS allowance by turning CORS_ACTIVE to `False` in your settings 59 | file. That will force the application to answer to the SITE_DOMAIN 60 | domain. 61 | 62 | Args: 63 | Server (string): Changes the server name sent to the browser in the 64 | response to avoid exposure of name and version of the same. 65 | Access-Control-Allow-Origin (string): Change the origin name to 66 | match the one that made the request. That way we can allow CORS 67 | anywhere. 68 | 69 | Raises: 70 | HTTP Error: An HTTP error in case the Origin header doesn't match 71 | the predefined regular expression. 72 | 73 | Returns: 74 | HTTP headers: A modified set of headers 75 | """ 76 | 77 | origin_domain = req.get_header('Origin') 78 | logger.debug("Origin domain is: {}, type: {}".format(origin_domain, type(origin_domain))) 79 | origin_header = origin_domain if settings.CORS_ACTIVE and origin_domain else "*" 80 | logger.debug("Origin header is: {}, type: {}".format(origin_header, type(origin_header))) 81 | 82 | res.set_headers([ 83 | ('Cache-Control', 'no-store, must-revalidate, no-cache, max-age=0'), 84 | ('Content-Type', 'application/json; charset=utf-8'), 85 | ('Server', settings.SERVER_NAME), 86 | ('Access-Control-Allow-Credentials', 'true'), 87 | ('Access-Control-Allow-Origin', origin_header), 88 | ('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, x-auth-user, x-auth-password, Authorization'), 89 | ('Access-Control-Allow-Methods', 'GET, PUT, POST, OPTIONS, DELETE') 90 | ]) 91 | -------------------------------------------------------------------------------- /sikr/middleware/https.py: -------------------------------------------------------------------------------- 1 | import falcon 2 | 3 | from sikr import settings 4 | 5 | 6 | class RequireHTTPS(object): 7 | 8 | """Force the connection to be HTTPS. 9 | 10 | Middleware that intercepts all the requests and checks that is over an HTTPS 11 | protocol before continuing. The only exception to this is the DEBUG mode, 12 | in which we allow connections from non-HTTPS sources. 13 | 14 | Raises: 15 | HTTP Bad Request: If the connection is not HTTPS the API will complain 16 | 17 | Returns: 18 | JSON: Error mentioning the HTTPS connection is required 19 | """ 20 | def process_request(self, req, resp): 21 | if req.protocol == "http" and not settings.DEBUG: 22 | raise falcon.HTTPBadRequest(title="Client error. HTTP Not Allowed", 23 | description="API connections over HTTPS only.", 24 | href=settings.__docs__) 25 | -------------------------------------------------------------------------------- /sikr/middleware/json.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import falcon 4 | 5 | from sikr import settings 6 | 7 | 8 | class RequireJSON(object): 9 | 10 | def process_request(self, req, resp): 11 | if not req.client_accepts_json: 12 | raise falcon.HTTPNotAcceptable( 13 | 'This API only supports responses encoded as JSON.', 14 | href=settings.__docs__) 15 | 16 | if req.method in ('POST', 'PUT'): 17 | if 'application/json' not in req.content_type: 18 | raise falcon.HTTPUnsupportedMediaType( 19 | 'This API only supports requests encoded as JSON.', 20 | href=settings.__docs__) 21 | 22 | 23 | class JSONTranslator(object): 24 | 25 | def process_request(self, req, resp): 26 | # req.stream corresponds to the WSGI wsgi.input environ variable, 27 | # and allows you to read bytes from the request body. 28 | # 29 | # See also: PEP 3333 30 | if req.content_length in (None, 0): 31 | # Nothing to do 32 | return 33 | 34 | body = req.stream.read() 35 | if not body: 36 | raise falcon.HTTPBadRequest('Empty request body', 37 | 'A valid JSON document is required.') 38 | 39 | try: 40 | req.context['doc'] = json.loads(body.decode('utf-8')) 41 | 42 | except (ValueError, UnicodeDecodeError): 43 | raise falcon.HTTPError(falcon.HTTP_753, 44 | 'Malformed JSON', 45 | 'Could not decode the request body. The ' 46 | 'JSON was incorrect or not encoded as ' 47 | 'UTF-8.') 48 | 49 | def process_response(self, req, resp, resource): 50 | if 'result' not in req.context: 51 | return 52 | 53 | resp.body = json.dumps(req.context['result']) 54 | -------------------------------------------------------------------------------- /sikr/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sikrvault/sikr/715df0227f8f888a9b2486326f50151b00d6eb33/sikr/models/__init__.py -------------------------------------------------------------------------------- /sikr/models/entries.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from sqlalchemy import Column, Integer, String, ForeignKey, Table, DateTime 4 | from sqlalchemy.orm import relationship 5 | from sqlalchemy.sql import func 6 | 7 | from sikr.db.connector import Base 8 | from sikr.db.mixins import SikrModelMixin 9 | from sikr.models.users import User 10 | 11 | 12 | class Group(Base, SikrModelMixin): 13 | name = Column(String) 14 | #allowed_users = ManyToManyField(User, related_name='allowed_categories') 15 | 16 | #UserCategory = Category.allowed_users.get_through_model() 17 | 18 | 19 | class Entry(Base, SikrModelMixin): 20 | name = Column(String) 21 | description = Column(String) 22 | #allowed_users = ManyToManyField(User, related_name='allowed_items') 23 | pub_date = Column(DateTime(timezone=True), server_default=func.now()) 24 | mod_date = Column(DateTime(timezone=True), onupdate=func.now()) 25 | #tags = pw.CharField(null=True) 26 | #category = pw.ForeignKeyField(Category) 27 | 28 | #UserItem = Item.allowed_users.get_through_model() 29 | -------------------------------------------------------------------------------- /sikr/models/users.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey, Table 2 | from sqlalchemy.orm import relationship 3 | 4 | from sikr.db.connector import Base 5 | from sikr.db.mixins import SikrModelMixin 6 | 7 | 8 | user_group_table = Table('sikr_user_group_m2m', Base.metadata, 9 | Column('sikr_user', Integer, ForeignKey('sikr_user.id')), 10 | Column('sikr_usergroup', Integer, ForeignKey('sikr_usergroup.id')) 11 | ) 12 | 13 | 14 | class UserGroup(Base, SikrModelMixin): 15 | 16 | """ 17 | Basic model to group users. 18 | """ 19 | name = Column(String) 20 | users = relationship("User", 21 | secondary=user_group_table, 22 | backref="groups") 23 | 24 | def __repr__(self): 25 | """String representation of the object.""" 26 | return f"" 44 | -------------------------------------------------------------------------------- /sikr/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sikrvault/sikr/715df0227f8f888a9b2486326f50151b00d6eb33/sikr/resources/__init__.py -------------------------------------------------------------------------------- /sikr/resources/auth/basicauth.py: -------------------------------------------------------------------------------- 1 | import falcon 2 | import logging 3 | import json 4 | 5 | from sikre.models.models import User 6 | 7 | 8 | class LoginResource(object): 9 | 10 | """ 11 | The login resource handles the login from all the 12 | """ 13 | def __init__(self): 14 | self.logger = logging.getLogger('thingsapp.' + __name__) 15 | 16 | def on_get(self, request, response): 17 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 18 | "The GET method is not allowed in this endpoint.") 19 | 20 | def on_post(self, request, response, **kwargs): 21 | 22 | """ 23 | This method will check that the email and the token match, then logs 24 | in the user. 25 | """ 26 | print(request.get_param('email')) 27 | try: 28 | user = User.get(username=request.get_param('login-email')) 29 | valid = user.check_password(request.body['login-password']) 30 | print("WHATEVER: " + request) 31 | if valid: 32 | pass 33 | response.status = falcon.HTTP_200 34 | response.body = 'Logged in' 35 | except: 36 | raise falcon.HTTPError(falcon.HTTP_500) 37 | 38 | 39 | def on_put(self, request, response): 40 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 41 | "The GET method is not allowed in this endpoint.") 42 | 43 | def on_update(self, request, response): 44 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 45 | "The UPDATE method is not allowed in this endpoint.") 46 | 47 | def on_delete(self, request, response): 48 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 49 | "The DELETE method is not allowed in this endpoint.") 50 | 51 | def on_options(self, request, response): 52 | ''' 53 | Handles all OPTIONS requests. 54 | Returns status code 200. 55 | ''' 56 | resp.status = falcon.HTTP_200 57 | 58 | 59 | class LogoutResource(object): 60 | 61 | """ 62 | The login resource handles the login from all the 63 | """ 64 | def on_get(self, request, response): 65 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 66 | "The GET method is not allowed in this endpoint.") 67 | 68 | def on_post(self, request, response): 69 | 70 | """ 71 | This method will check that the email and the token match, then logs 72 | in the user. 73 | """ 74 | 75 | response.status = falcon.HTTP_200 76 | response.body = 'whatever, man' 77 | 78 | def on_put(self, request, response): 79 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 80 | "The GET method is not allowed in this endpoint.") 81 | 82 | def on_update(self, request, response): 83 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 84 | "The UPDATE method is not allowed in this endpoint.") 85 | 86 | def on_delete(self, request, response): 87 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 88 | "The DELETE method is not allowed in this endpoint.") 89 | 90 | class ForgotPasswordResource(object): 91 | 92 | """ 93 | The login resource handles the login from all the 94 | """ 95 | def on_get(self, request, response): 96 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 97 | "The GET method is not allowed in this endpoint.") 98 | 99 | def on_post(self, request, response, provider): 100 | 101 | """ 102 | This method will check that the email and the token match, then logs 103 | in the user. 104 | """ 105 | 106 | response.status = falcon.HTTP_200 107 | response.body = 'whatever, man' 108 | 109 | def on_put(self, request, response): 110 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 111 | "The GET method is not allowed in this endpoint.") 112 | 113 | def on_update(self, request, response): 114 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 115 | "The UPDATE method is not allowed in this endpoint.") 116 | 117 | def on_delete(self, request, response): 118 | raise falcon.HTTPError(falcon.HTTP_405, "Client error", 119 | "The DELETE method is not allowed in this endpoint.") 120 | -------------------------------------------------------------------------------- /sikr/resources/auth/decorators.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import falcon 4 | 5 | from sikre import settings 6 | from sikre.resources.auth import utils 7 | from sikre.utils.logs import logger 8 | 9 | 10 | def login_required(req, res, resource, params): 11 | 12 | """Check the token to validate login state 13 | Check the JWT token sent by the frontend to see if the following conditions 14 | are NOT met: 15 | - Issuer host doesn't match the one specified in the settings file 16 | - Expiry timestamp is lower than the current timestamp 17 | - Issued timestamp is lower than the current timestamp minus SESSION_EXPIRES 18 | :returns: Redirect to the LOGIN_URL or HTTP 200 19 | """ 20 | if req.auth: 21 | logger.debug("The user has a token in the header") 22 | payload = utils.parse_token(req) 23 | current_time = datetime.datetime.now() 24 | issue_time = current_time - datetime.timedelta(hours=settings.SESSION_EXPIRES) 25 | if payload['iss'] != settings.SITE_DOMAIN or \ 26 | payload['exp'] <= int(current_time.timestamp()) or \ 27 | payload['iat'] <= int(issue_time.timestamp()): 28 | 29 | logger.debug("JWT token expired or malformed") 30 | raise falcon.HTTPError(falcon.HTTP_401, title="Credentials expired", 31 | description="Your crendentials have expired. Please login again.") 32 | 33 | else: 34 | res.status = falcon.HTTP_200 35 | else: 36 | logger.debug("No JWT token found") 37 | raise falcon.HTTPError(falcon.HTTP_401, title="Credentials not found", 38 | description="You don't have the credentials to access this resource") 39 | -------------------------------------------------------------------------------- /sikr/resources/auth/facebook.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.parse import parse_qsl 3 | 4 | import falcon 5 | import requests 6 | 7 | from sikre import settings 8 | from sikre.models.users import User 9 | from sikre.resources.auth import utils 10 | from sikre.utils.logs import logger 11 | 12 | 13 | class FacebookAuth(object): 14 | 15 | def on_post(self, req, res): 16 | 17 | """Create the JWT token for the user 18 | """ 19 | access_token_url = 'https://graph.facebook.com/oauth/access_token' 20 | graph_api_url = 'https://graph.facebook.com/me' 21 | 22 | # Read the incoming data 23 | stream = req.stream.read() 24 | data = json.loads(stream.decode('utf-8')) 25 | logger.debug("Facebook OAuth: Incoming data read successfully") 26 | 27 | params = { 28 | 'client_id': data['clientId'], 29 | 'redirect_uri': data['redirectUri'] + '/', 30 | 'client_secret': settings.FACEBOOK_SECRET, 31 | 'code': data['code'] 32 | } 33 | logger.debug("Facebook OAuth: Built the code response correctly") 34 | 35 | # Step 1. Exchange authorization code for access token. 36 | r = requests.get(access_token_url, params=params) 37 | access_token = dict(parse_qsl(r.text)) 38 | logger.debug("Facebook OAuth: Auth code exchange for token success") 39 | 40 | # Step 2. Retrieve information about the current user. 41 | r = requests.get(graph_api_url, params=access_token) 42 | profile = json.loads(r.text) 43 | logger.debug("Facebook OAuth: Retrieve user information success") 44 | 45 | # Step 3. (optional) Link accounts. 46 | if req.auth: 47 | payload = utils.parse_token(req) 48 | try: 49 | user = User.select().where( 50 | (User.facebook == profile['id']) | 51 | (User.id == payload['sub']) | 52 | (User.email == profile['email']) 53 | ).get() 54 | # Set the facebook code again. This is a failsafe. 55 | user.facebook = profile['id'] 56 | user.save() 57 | logger.debug("Facebook OAuth: Account {0} already exists".format(profile["id"])) 58 | except User.DoesNotExist: 59 | logger.debug("Facebook OAuth: User does not exist") 60 | user = User.create(facebook=profile['id'], username=profile['name'], email=profile["email"]) 61 | user.save() 62 | logger.debug("Facebook OAuth: Created user {0}".format(profile["name"])) 63 | else: 64 | try: 65 | user = User.select().where( 66 | (User.facebook == profile['id']) | 67 | (User.email == profile['email']) 68 | ).get() 69 | # Set the github code again. This is a failsafe. 70 | user.facebook = profile['id'] 71 | user.save() 72 | except User.DoesNotExist: 73 | logger.debug("Facebook OAuth: User does not exist") 74 | user = User.create(facebook=profile['id'], username=profile['name'], email=profile["email"]) 75 | user.save() 76 | logger.debug("Facebook OAuth: Created user {0}".format(profile["name"])) 77 | token = utils.create_jwt_token(user) 78 | res.body = json.dumps({"token": token}) 79 | res.status = falcon.HTTP_200 80 | 81 | def on_options(self, req, res): 82 | 83 | """Acknowledge the OPTIONS method. 84 | """ 85 | res.status = falcon.HTTP_200 86 | 87 | def on_get(self, req, res): 88 | raise falcon.HTTPError(falcon.HTTP_405, 89 | title="Client error", 90 | description=req.method + " method not allowed.", 91 | href=settings.__docs__) 92 | 93 | def on_put(self, req, res): 94 | raise falcon.HTTPError(falcon.HTTP_405, 95 | title="Client error", 96 | description=req.method + " method not allowed.", 97 | href=settings.__docs__) 98 | 99 | def on_update(self, req, res): 100 | raise falcon.HTTPError(falcon.HTTP_405, 101 | title="Client error", 102 | description=req.method + " method not allowed.", 103 | href=settings.__docs__) 104 | 105 | def on_delete(self, req, res): 106 | raise falcon.HTTPError(falcon.HTTP_405, 107 | title="Client error", 108 | description=req.method + " method not allowed.", 109 | href=settings.__docs__) 110 | -------------------------------------------------------------------------------- /sikr/resources/auth/github.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.parse import parse_qsl 3 | 4 | import falcon 5 | import requests 6 | 7 | from sikre import settings 8 | from sikre.models.users import User 9 | from sikre.resources.auth import utils 10 | from sikre.utils.logs import logger 11 | 12 | 13 | class GithubAuth(object): 14 | 15 | def on_post(self, req, res): 16 | 17 | """Create the JWT token for the user 18 | """ 19 | access_token_url = 'https://github.com/login/oauth/access_token' 20 | users_api_url = 'https://api.github.com/user' 21 | 22 | # Read the incoming data 23 | stream = req.stream.read() 24 | data = json.loads(stream.decode('utf-8')) 25 | logger.debug("GitHub OAuth: Incoming data read successfully") 26 | 27 | params = { 28 | 'client_id': data['clientId'], 29 | 'redirect_uri': data['redirectUri'], 30 | 'client_secret': settings.GITHUB_SECRET, 31 | 'code': data['code'] 32 | } 33 | logger.debug("GitHub OAuth: Built the code response correctly") 34 | 35 | # Step 1. Exchange authorization code for access token. 36 | r = requests.get(access_token_url, params=params) 37 | access_token = dict(parse_qsl(r.text)) 38 | headers = {'User-Agent': 'Satellizer'} 39 | logger.debug("GitHub OAuth: Auth code exchange for token success") 40 | 41 | # Step 2. Retrieve information about the current user. 42 | r = requests.get(users_api_url, params=access_token, headers=headers) 43 | profile = json.loads(r.text) 44 | logger.debug("GitHub OAuth: Retrieve user information success") 45 | logger.debug("GitHub OAuth: Profile: {}".format(profile)) 46 | 47 | # Step 3. (optional) Link accounts. 48 | if req.auth: 49 | payload = utils.parse_token(req) 50 | try: 51 | user = User.select().where( 52 | (User.github == profile['id']) | 53 | (User.id == payload['sub']) | 54 | (User.email == profile['email']) 55 | ).get() 56 | # Set the github code again. This is a failsafe. 57 | user.github = profile['id'] 58 | user.save() 59 | logger.debug("GitHub OAuth: Account {0} already exists".format(profile["id"])) 60 | except User.DoesNotExist: 61 | logger.debug("GitHub OAuth: User does not exist") 62 | user = User.create(github=profile['id'], username=profile['name'], email=profile["email"]) 63 | user.save() 64 | logger.debug("GitHub OAuth: Created user {0}".format(profile["name"])) 65 | else: 66 | try: 67 | user = User.select().where( 68 | (User.github == profile['id']) | 69 | (User.email == profile['email']) 70 | ).get() 71 | # Set the github code again. This is a failsafe. 72 | user.github = profile['id'] 73 | user.save() 74 | except User.DoesNotExist: 75 | logger.debug("GitHub OAuth: User does not exist") 76 | user = User.create(github=profile['id'], username=profile['name'], email=profile["email"]) 77 | user.save() 78 | logger.debug("GitHub OAuth: Created user {0}".format(profile["name"])) 79 | token = utils.create_jwt_token(user) 80 | res.body = json.dumps({"token": token}) 81 | res.status = falcon.HTTP_200 82 | 83 | def on_options(self, req, res): 84 | 85 | """Acknowledge the OPTIONS method. 86 | """ 87 | res.status = falcon.HTTP_200 88 | 89 | def on_get(self, req, res): 90 | raise falcon.HTTPError(falcon.HTTP_405, 91 | title="Client error", 92 | description=req.method + " method not allowed.", 93 | href=settings.__docs__) 94 | 95 | def on_put(self, req, res): 96 | raise falcon.HTTPError(falcon.HTTP_405, 97 | title="Client error", 98 | description=req.method + " method not allowed.", 99 | href=settings.__docs__) 100 | 101 | def on_update(self, req, res): 102 | raise falcon.HTTPError(falcon.HTTP_405, 103 | title="Client error", 104 | description=req.method + " method not allowed.", 105 | href=settings.__docs__) 106 | 107 | def on_delete(self, req, res): 108 | raise falcon.HTTPError(falcon.HTTP_405, 109 | title="Client error", 110 | description=req.method + " method not allowed.", 111 | href=settings.__docs__) 112 | -------------------------------------------------------------------------------- /sikr/resources/auth/google.py: -------------------------------------------------------------------------------- 1 | import json 2 | # from urllib.parse import parse_qsl 3 | 4 | import falcon 5 | import requests 6 | 7 | from sikre import settings 8 | from sikre.models.users import User 9 | from sikre.models.shares import ShareToken 10 | from sikre.resources.auth import utils 11 | from sikre.utils.logs import logger 12 | 13 | 14 | class GoogleAuth(object): 15 | 16 | def on_post(self, req, res): 17 | access_token_url = 'https://accounts.google.com/o/oauth2/token' 18 | people_api_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' 19 | 20 | # Read the incoming data 21 | stream = req.stream.read() 22 | data = json.loads(stream.decode('utf-8')) 23 | logger.debug("Google OAuth: Incoming data read successfully") 24 | 25 | # See if the user has a share token 26 | share_token = req.get_param("share_token", required=False) 27 | logger.debug("Google OAuth: User carries a share token") 28 | 29 | payload = { 30 | 'client_id': data['clientId'], 31 | 'redirect_uri': data['redirectUri'], 32 | 'client_secret': settings.GOOGLEPLUS_SECRET, 33 | 'code': data['code'], 34 | 'grant_type': 'authorization_code' 35 | } 36 | logger.debug("Google OAuth: Built the code response correctly") 37 | 38 | # Step 1. Exchange authorization code for access token. 39 | r = requests.post(access_token_url, data=payload) 40 | token = json.loads(r.text) 41 | headers = {'Authorization': 'Bearer {0}'.format(token['access_token'])} 42 | logger.debug("Google OAuth: Auth code exchange for token success") 43 | 44 | # Step 2. Retrieve information about the current user. 45 | r = requests.get(people_api_url, headers=headers) 46 | profile = json.loads(r.text) 47 | logger.debug("Google OAuth: Retrieve user information success") 48 | 49 | try: 50 | user = User.select().where(User.google == profile['sub']).get() 51 | if user: 52 | logger.debug("Google OAuth: Account {0} already exists".format(profile["sub"])) 53 | except User.DoesNotExist: 54 | logger.debug("Google OAuth: User does not exist") 55 | user = User.create(google=profile['sub'], username=profile['name'], email=profile['email']) 56 | user.save() 57 | logger.debug("Google OAuth: Created user {0}".format(profile["name"])) 58 | 59 | token = utils.create_jwt_token(user) 60 | 61 | # if share_token: 62 | # try: 63 | # token = ShareToken.get(token=share_token) 64 | # if token.is_valid(): 65 | # if token.resource == 0: 66 | 67 | # except: 68 | # logger.error("Token does not exist") 69 | 70 | 71 | res.body = json.dumps({"token": token}) 72 | res.status = falcon.HTTP_200 73 | return 74 | 75 | def on_options(self, req, res): 76 | 77 | """Acknowledge the OPTIONS method. 78 | """ 79 | res.status = falcon.HTTP_200 80 | 81 | def on_get(self, req, res): 82 | raise falcon.HTTPError(falcon.HTTP_405, 83 | title="Client error", 84 | description=req.method + " method not allowed.", 85 | href=settings.__docs__) 86 | 87 | def on_put(self, req, res): 88 | raise falcon.HTTPError(falcon.HTTP_405, 89 | title="Client error", 90 | description=req.method + " method not allowed.", 91 | href=settings.__docs__) 92 | 93 | def on_update(self, req, res): 94 | raise falcon.HTTPError(falcon.HTTP_405, 95 | title="Client error", 96 | description=req.method + " method not allowed.", 97 | href=settings.__docs__) 98 | 99 | def on_delete(self, req, res): 100 | raise falcon.HTTPError(falcon.HTTP_405, 101 | title="Client error", 102 | description=req.method + " method not allowed.", 103 | href=settings.__docs__) 104 | # class GoogleAuth(object): 105 | # access_token_url = 'https://accounts.google.com/o/oauth2/token' 106 | # people_api_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' 107 | 108 | # payload = dict(client_id=request.json['clientId'], 109 | # redirect_uri=request.json['redirectUri'], 110 | # client_secret=app.config['GOOGLE_SECRET'], 111 | # code=request.json['code'], 112 | # grant_type='authorization_code') 113 | 114 | # # Step 1. Exchange authorization code for access token. 115 | # r = requests.post(access_token_url, data=payload) 116 | # token = json.loads(r.text) 117 | # headers = {'Authorization': 'Bearer {0}'.format(token['access_token'])} 118 | 119 | # # Step 2. Retrieve information about the current user. 120 | # r = requests.get(people_api_url, headers=headers) 121 | # profile = json.loads(r.text) 122 | 123 | # user = User.query.filter_by(google=profile['sub']).first() 124 | # if user: 125 | # token = create_jwt_token(user) 126 | # return jsonify(token=token) 127 | # u = User(google=profile['sub'], 128 | # first_name=profile['given_name'], 129 | # last_name=profile['family_name']) 130 | # db.session.add(u) 131 | # db.session.commit() 132 | # token = create_jwt_token(u) 133 | # return jsonify(token=token) 134 | -------------------------------------------------------------------------------- /sikr/resources/auth/linkedin.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.parse import parse_qsl, urlencode 3 | 4 | import falcon 5 | import requests 6 | from requests_oauthlib import OAuth1Session 7 | 8 | from sikre import settings 9 | from sikre.models.users import User 10 | from sikre.resources.auth import utils 11 | from sikre.utils.logs import logger 12 | 13 | 14 | class LinkedinAuth(object): 15 | 16 | def on_post(self, req, res): 17 | 18 | @app.route('/auth/linkedin', methods=['POST']) 19 | def linkedin(): 20 | access_token_url = 'https://www.linkedin.com/uas/oauth2/accessToken' 21 | people_api_url = 'https://api.linkedin.com/v1/people/~:(id,first-name,last-name,email-address)' 22 | 23 | payload = dict(client_id=request.json['clientId'], 24 | redirect_uri=request.json['redirectUri'], 25 | client_secret=app.config['LINKEDIN_SECRET'], 26 | code=request.json['code'], 27 | grant_type='authorization_code') 28 | 29 | # Step 1. Exchange authorization code for access token. 30 | r = requests.post(access_token_url, data=payload) 31 | access_token = json.loads(r.text) 32 | params = dict(oauth2_access_token=access_token['access_token'], 33 | format='json') 34 | 35 | # Step 2. Retrieve information about the current user. 36 | r = requests.get(people_api_url, params=params) 37 | profile = json.loads(r.text) 38 | 39 | user = User.query.filter_by(linkedin=profile['id']).first() 40 | if user: 41 | token = create_token(user) 42 | return jsonify(token=token) 43 | u = User(linkedin=profile['id'], 44 | display_name=profile['firstName'] + ' ' + profile['lastName']) 45 | db.session.add(u) 46 | db.session.commit() 47 | token = create_token(u) 48 | return jsonify(token=token) 49 | 50 | def on_options(self, req, res): 51 | 52 | """Acknowledge the OPTIONS method. 53 | """ 54 | res.status = falcon.HTTP_200 55 | 56 | def on_get(self, req, res): 57 | raise falcon.HTTPError(falcon.HTTP_405, 58 | title="Client error", 59 | description=req.method + " method not allowed.", 60 | href=settings.__docs__) 61 | 62 | def on_put(self, req, res): 63 | raise falcon.HTTPError(falcon.HTTP_405, 64 | title="Client error", 65 | description=req.method + " method not allowed.", 66 | href=settings.__docs__) 67 | 68 | def on_update(self, req, res): 69 | raise falcon.HTTPError(falcon.HTTP_405, 70 | title="Client error", 71 | description=req.method + " method not allowed.", 72 | href=settings.__docs__) 73 | 74 | def on_delete(self, req, res): 75 | raise falcon.HTTPError(falcon.HTTP_405, 76 | title="Client error", 77 | description=req.method + " method not allowed.", 78 | href=settings.__docs__) 79 | -------------------------------------------------------------------------------- /sikr/resources/auth/twitter.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.parse import parse_qsl, urlencode 3 | 4 | import falcon 5 | import requests 6 | from requests_oauthlib import OAuth1 7 | 8 | from sikre import settings 9 | from sikre.models.users import User 10 | from sikre.resources.auth import utils 11 | from sikre.utils.logs import logger 12 | 13 | 14 | class TwitterAuth(object): 15 | 16 | def on_get(self, req, res): 17 | 18 | """Create Twitter JWT token 19 | """ 20 | request_token_url = 'https://api.twitter.com/oauth/request_token' 21 | access_token_url = 'https://api.twitter.com/oauth/access_token' 22 | authenticate_url = 'https://api.twitter.com/oauth/authenticate' 23 | 24 | if req.get_param('oauth_token') and req.get_param('oauth_verifier'): 25 | auth = OAuth1(settings.TWITTER_KEY, 26 | client_secret=settings.TWITTER_SECRET, 27 | resource_owner_key=req.get_param('oauth_token'), 28 | verifier=req.get_param('oauth_verifier')) 29 | logger.debug("Twitter OAuth: Got auth session.") 30 | r = requests.post(access_token_url, auth=auth) 31 | profile = dict(parse_qsl(r.text)) 32 | logger.debug("Twitter OAuth: User profile retrieved") 33 | 34 | try: 35 | user = User.select().where(User.twitter == profile['user_id'] | 36 | User.username == profile['screen_name']).get() 37 | except: 38 | user = User.create(twitter=profile['user_id'], 39 | username=profile['screen_name']) 40 | user.save() 41 | 42 | token = utils.create_jwt_token(user) 43 | res.body = json.dumps({"token": token}) 44 | res.status = falcon.HTTP_200 45 | else: 46 | oauth = OAuth1(settings.TWITTER_KEY, 47 | client_secret=settings.TWITTER_SECRET, 48 | callback_uri=settings.TWITTER_CALLBACK_URI) 49 | logger.debug("Twitter OAuth: Got auth session.") 50 | r = requests.post(request_token_url, auth=oauth) 51 | oauth_token = dict(parse_qsl(r.text)) 52 | logger.debug("Twitter OAuth: User profile retrieved") 53 | qs = urlencode(dict(oauth_token=oauth_token['oauth_token'])) 54 | 55 | # Falcon doesn't support redirects, so we have to fake it 56 | # this implementation has been taken from werkzeug 57 | final_url = authenticate_url + '?' + qs 58 | res.body = ( 59 | '\n' 60 | 'Redirecting...\n' 61 | '

Redirecting...

\n' 62 | '

You should be redirected automatically to target URL: ' 63 | '{0}. If not click the link.'.format(final_url) 64 | ) 65 | res.location = final_url 66 | res.status = falcon.HTTP_301 67 | 68 | def on_options(self, req, res): 69 | 70 | """Acknowledge the OPTIONS method. 71 | """ 72 | res.status = falcon.HTTP_200 73 | 74 | def on_post(self, req, res): 75 | raise falcon.HTTPError(falcon.HTTP_405, 76 | title="Client error", 77 | description=req.method + " method not allowed.", 78 | href=settings.__docs__) 79 | 80 | def on_put(self, req, res): 81 | raise falcon.HTTPError(falcon.HTTP_405, 82 | title="Client error", 83 | description=req.method + " method not allowed.", 84 | href=settings.__docs__) 85 | 86 | def on_update(self, req, res): 87 | raise falcon.HTTPError(falcon.HTTP_405, 88 | title="Client error", 89 | description=req.method + " method not allowed.", 90 | href=settings.__docs__) 91 | 92 | def on_delete(self, req, res): 93 | raise falcon.HTTPError(falcon.HTTP_405, 94 | title="Client error", 95 | description=req.method + " method not allowed.", 96 | href=settings.__docs__) 97 | -------------------------------------------------------------------------------- /sikr/resources/auth/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | import jwt 4 | 5 | from sikre import settings 6 | 7 | 8 | def create_jwt_token(user): 9 | payload = { 10 | 'iss': settings.SITE_DOMAIN, 11 | 'sub': user.id, 12 | 'iat': datetime.now(), 13 | 'exp': datetime.now() + timedelta(hours=settings.SESSION_EXPIRES) 14 | } 15 | token = jwt.encode(payload, settings.SECRET) 16 | return token.decode('unicode_escape') 17 | 18 | 19 | def parse_token(req): 20 | token = req.auth.split()[1] 21 | return jwt.decode(token, settings.SECRET) 22 | -------------------------------------------------------------------------------- /sikr/resources/categories.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014-2016 Oscar Carballal Prego 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy 5 | # of the License at http:#www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import json 14 | 15 | import falcon 16 | 17 | from sikr import settings 18 | from sikr.utils.logs import logger 19 | from sikr.models.users import User 20 | from sikr.models.items import Category 21 | from sikr.resources.auth.decorators import login_required 22 | from sikr.resources.auth.utils import parse_token 23 | 24 | 25 | class Categories(object): 26 | """Show all the groups that the current user has read permission. 27 | 28 | This resource will send the Categories that belong to the user in the 29 | matter of ID and NAME 30 | """ 31 | 32 | @falcon.before(login_required) 33 | def on_get(self, req, res): 34 | # Parse token and get user id 35 | user_id = parse_token(req)['sub'] 36 | 37 | try: 38 | # Get the user 39 | user = User.get(User.id == int(user_id)) 40 | 41 | groups = list(user.allowed_categories 42 | .select(Category.id, Category.name) 43 | .dicts()) 44 | res.status = falcon.HTTP_200 45 | res.body = json.dumps(groups) 46 | except Exception as e: 47 | print(e) 48 | error_msg = ("Unable to get the groups. Please try again later") 49 | raise falcon.HTTPServiceUnavailable(title="{0} failed".format( 50 | req.method), 51 | description=error_msg, 52 | retry_after=30, 53 | href=settings.__docs__) 54 | 55 | @falcon.before(login_required) 56 | def on_post(self, req, res): 57 | try: 58 | # Parse token and get user id 59 | user_id = parse_token(req)['sub'] 60 | # Get the user 61 | user = User.get(User.id == int(user_id)) 62 | except Exception as e: 63 | logger.error("Can't verify user") 64 | raise falcon.HTTPBadRequest(title="Bad request", 65 | description=e, 66 | href=settings.__docs__) 67 | 68 | try: 69 | raw_json = req.stream.read() 70 | logger.debug("Got incoming JSON data") 71 | except Exception as e: 72 | logger.error("Can't read incoming data stream") 73 | raise falcon.HTTPBadRequest(title="Bad request", 74 | description=e, 75 | href=settings.__docs__) 76 | 77 | try: 78 | result_json = json.loads(raw_json.decode("utf-8"), 79 | encoding='utf-8') 80 | except ValueError: 81 | raise falcon.HTTPError(falcon.HTTP_400, 82 | 'Malformed JSON', 83 | 'Could not decode the request body. The ' 84 | 'JSON was incorrect.') 85 | 86 | try: 87 | new_category = Category.create(name=result_json['name'] or '') 88 | new_category.save() 89 | new_category.allowed_users.add(user) 90 | except Exception as e: 91 | raise falcon.HTTPInternalServerError(title="Error while saving the group", 92 | description=e, 93 | href=settings.__docs__) 94 | 95 | def on_options(self, req, res): 96 | """Acknowledge the OPTIONS method.""" 97 | res.status = falcon.HTTP_200 98 | 99 | def on_put(self, req, res): 100 | raise falcon.HTTPError(falcon.HTTP_405, 101 | title="Client error", 102 | description="{0} method not allowed.".format(req.method), 103 | href=settings.__docs__) 104 | 105 | def on_update(self, req, res): 106 | raise falcon.HTTPError(falcon.HTTP_405, 107 | title="Client error", 108 | description="{0} method not allowed.".format(req.method), 109 | href=settings.__docs__) 110 | 111 | def on_delete(self, req, res): 112 | raise falcon.HTTPError(falcon.HTTP_405, 113 | title="Client error", 114 | description="{0} method not allowed.".format(req.method), 115 | href=settings.__docs__) 116 | 117 | 118 | class DetailCategory(object): 119 | """Show details of a specific group or add/delete a group.""" 120 | 121 | @falcon.before(login_required) 122 | def on_get(self, req, res, id): 123 | user_id = parse_token(req)['sub'] 124 | try: 125 | user = User.get(User.id == int(user_id)) 126 | group = Category.get(Category.id == int(id)) 127 | if user not in group.allowed_users: 128 | raise falcon.HTTPForbidden(title="Permission denied", 129 | description="You don't have access to this resource", 130 | href=settings.__docs__) 131 | res.status = falcon.HTTP_200 132 | res.body = json.dumps(group) 133 | logger.debug("Items request succesful") 134 | except Exception as e: 135 | print(e) 136 | error_msg = ("Unable to get the group. Please try again later.") 137 | raise falcon.HTTPServiceUnavailable(req.method + " failed", 138 | description=error_msg, 139 | retry_after=30, 140 | href=settings.__docs__) 141 | 142 | @falcon.before(login_required) 143 | def on_put(self, req, res, id): 144 | try: 145 | # Parse token and get user id 146 | user_id = parse_token(req)['sub'] 147 | # Get the user 148 | user = User.get(User.id == int(user_id)) 149 | except Exception as e: 150 | logger.error("Can't verify user") 151 | raise falcon.HTTPBadRequest(title="Bad request", 152 | description=e, 153 | href=settings.__docs__) 154 | try: 155 | raw_json = req.stream.read() 156 | logger.debug("Got incoming JSON data") 157 | except Exception as e: 158 | logger.error("Can't read incoming data stream") 159 | raise falcon.HTTPBadRequest(title="Bad request", 160 | description=e, 161 | href=settings.__docs__) 162 | try: 163 | result_json = json.loads(raw_json.decode("utf-8"), encoding='utf-8') 164 | except ValueError: 165 | raise falcon.HTTPError(falcon.HTTP_400, 166 | 'Malformed JSON', 167 | 'Could not decode the request body. The ' 168 | 'JSON was incorrect.') 169 | try: 170 | category = Category.get(Category.id == int(id)) 171 | if user not in category.allowed_users: 172 | raise falcon.HTTPForbidden(title="Permission denied", 173 | description="You don't have access to this resource", 174 | href=settings.__docs__) 175 | category.name = result_json["name"] 176 | category.save() 177 | res.status = falcon.HTTP_200 178 | res.body = json.dumps({"message": "Category updated"}) 179 | except Exception as e: 180 | print(e) 181 | error_msg = ("Unable to get the category. Please try again later.") 182 | raise falcon.HTTPServiceUnavailable(req.method + " failed", 183 | description=error_msg, 184 | retry_after=30, 185 | href=settings.__docs__) 186 | 187 | @falcon.before(login_required) 188 | def on_delete(self, req, res, id): 189 | try: 190 | # Parse token and get user id 191 | user_id = parse_token(req)['sub'] 192 | # Get the user 193 | user = User.get(User.id == int(user_id)) 194 | except Exception as e: 195 | logger.error("Can't verify user") 196 | raise falcon.HTTPBadRequest(title="Bad request", 197 | description=e, 198 | href=settings.__docs__) 199 | try: 200 | category = Category.get(Category.id == int(id)) 201 | category.delete_instance(recursive=True) 202 | 203 | res.status = falcon.HTTP_200 204 | res.body = json.dumps({"status": "Deletion successful"}) 205 | 206 | except Exception as e: 207 | print(e) 208 | error_msg = ("Unable to delete category. Please try again later.") 209 | raise falcon.HTTPServiceUnavailable(title="{0} failed".format(req.method), 210 | description=error_msg, 211 | retry_after=30, 212 | href=settings.__docs__) 213 | 214 | def on_options(self, req, res, id): 215 | 216 | """Acknowledge the OPTIONS method. 217 | """ 218 | res.status = falcon.HTTP_200 219 | 220 | def on_post(self, req, res, id): 221 | raise falcon.HTTPError(falcon.HTTP_405, 222 | title="Client error", 223 | description="{0} method not allowed.".format(req.method), 224 | href=settings.__docs__) 225 | 226 | def on_update(self, req, res, id): 227 | raise falcon.HTTPError(falcon.HTTP_405, 228 | title="Client error", 229 | description="{0} method not allowed.".format(req.method), 230 | href=settings.__docs__) 231 | -------------------------------------------------------------------------------- /sikr/resources/items.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import falcon 4 | 5 | from sikr import settings 6 | from sikr.utils.logs import logger 7 | from sikr.models.users import User 8 | from sikr.models.items import Category, Item 9 | from sikr.models.services import Service 10 | from sikr.resources.auth.decorators import login_required 11 | from sikr.resources.auth.utils import parse_token 12 | 13 | 14 | class Items(object): 15 | 16 | @falcon.before(login_required) 17 | def on_get(self, req, res): 18 | """Get the items that belong to that user. 19 | 20 | This method contains two behaviours, one returns 21 | Handle the GET request, returning a list of the items that the user 22 | has access to. 23 | 24 | First we create an empty dictionary and query the database to get 25 | all the item objects. After that, we iterate over the objects to 26 | populate the dictionary. In the end we return a 200 code to the browser 27 | and return the results dictionary wrapped in a list like the REsT 28 | standard says. 29 | """ 30 | payload = {} 31 | # Parse token and get user id 32 | user_id = parse_token(req)['sub'] 33 | 34 | try: 35 | # Get the user 36 | user = User.get(User.id == int(user_id)) 37 | # See if we have to filter by category 38 | filter_category = req.get_param("category", required=False) 39 | if filter_category: 40 | # Get the category 41 | category = (Category.select(Category.name, Category.id) 42 | .where(Category.id == int(filter_category)) 43 | .get()) 44 | payload["category_name"] = str(category.name) 45 | payload["category_id"] = int(category.id) 46 | items = list(user.allowed_items 47 | .select(Item.name, Item.description, Item.id) 48 | .where(Item.category == int(filter_category)) 49 | .dicts()) 50 | logger.debug("Got items filtered by category and user") 51 | else: 52 | payload["category_name"] = "All" 53 | items = list(user.allowed_items 54 | .select(Item.name, Item.description, Item.id) 55 | .dicts()) 56 | logger.debug("Got all items") 57 | for item in items: 58 | services = list(user.allowed_services 59 | .select(Service.id, Service.name) 60 | .where(Service.item == item["id"]) 61 | .dicts()) 62 | item["services"] = services 63 | payload["items"] = items 64 | res.status = falcon.HTTP_200 65 | res.body = json.dumps(payload) 66 | logger.debug("Items request succesful") 67 | except Exception as e: 68 | print(e) 69 | logger.error(e) 70 | error_msg = ("Unable to get the items. Please try again later") 71 | raise falcon.HTTPServiceUnavailable(title=req.method + " failed", 72 | description=error_msg, 73 | retry_after=30, 74 | href=settings.__docs__) 75 | 76 | @falcon.before(login_required) 77 | def on_post(self, req, res): 78 | 79 | """Save a new item 80 | """ 81 | try: 82 | # Parse token and get user id 83 | user_id = parse_token(req)['sub'] 84 | # Get the user 85 | user = User.get(User.id == int(user_id)) 86 | logger.debug("Got user data") 87 | except Exception as e: 88 | logger.error("Can't verify user") 89 | raise falcon.HTTPBadRequest(title="Bad request", 90 | description=e, 91 | href=settings.__docs__) 92 | 93 | try: 94 | raw_json = req.stream.read() 95 | logger.debug("Got incoming JSON data") 96 | except Exception as e: 97 | logger.error("Can't read incoming data stream") 98 | raise falcon.HTTPBadRequest(title="Bad request", 99 | description=e, 100 | href=settings.__docs__) 101 | 102 | try: 103 | result_json = json.loads(raw_json.decode("utf-8"), encoding='utf-8') 104 | logger.debug("Parsed JSON data") 105 | except ValueError: 106 | raise falcon.HTTPError(falcon.HTTP_400, 107 | 'Malformed JSON', 108 | 'Could not decode the request body. The ' 109 | 'JSON was incorrect.') 110 | 111 | try: 112 | new_item = Item.create(name=result_json.get('name'), 113 | description=result_json.get("description", ''), 114 | category=result_json.get("category"), 115 | tags=result_json.get("tags", '')) 116 | new_item.save() 117 | new_item.allowed_users.add(user) 118 | logger.debug("Saved new item into the database") 119 | except Exception as e: 120 | raise falcon.HTTPInternalServerError(title="Error while saving the item", 121 | description=e, 122 | href=settings.__docs__) 123 | 124 | def on_options(self, req, res): 125 | 126 | """Acknowledge the OPTIONS method. 127 | """ 128 | res.status = falcon.HTTP_200 129 | 130 | def on_put(self, req, res): 131 | raise falcon.HTTPError(falcon.HTTP_405, 132 | title="Client error", 133 | description=req.method + " method not allowed.", 134 | href=settings.__docs__) 135 | 136 | def on_update(self, req, res): 137 | raise falcon.HTTPError(falcon.HTTP_405, 138 | title="Client error", 139 | description=req.method + " method not allowed.", 140 | href=settings.__docs__) 141 | 142 | def on_delete(self, req, res): 143 | raise falcon.HTTPError(falcon.HTTP_405, 144 | title="Client error", 145 | description=req.method + " method not allowed.", 146 | href=settings.__docs__) 147 | 148 | 149 | class DetailItem(object): 150 | 151 | """Show details of a specific category or add/delete a category 152 | """ 153 | @falcon.before(login_required) 154 | def on_get(self, req, res, id): 155 | user_id = parse_token(req)['sub'] 156 | try: 157 | user = User.get(User.id == int(user_id)) 158 | item = Item.get(Item.id == int(id)) 159 | if user not in item.allowed_users: 160 | raise falcon.HTTPForbidden(title="Permission denied", 161 | description="You don't have access to this resource", 162 | href=settings.__docs__) 163 | res.status = falcon.HTTP_200 164 | res.body = json.dumps(item) 165 | logger.debug("Items request succesful") 166 | except Exception as e: 167 | print(e) 168 | error_msg = ("Unable to get the item. Please try again later.") 169 | raise falcon.HTTPServiceUnavailable(req.method + " failed", 170 | description=error_msg, 171 | retry_after=30, 172 | href=settings.__docs__) 173 | 174 | @falcon.before(login_required) 175 | def on_put(self, req, res, id): 176 | try: 177 | # Parse token and get user id 178 | user_id = parse_token(req)['sub'] 179 | # Get the user 180 | user = User.get(User.id == int(user_id)) 181 | except Exception as e: 182 | logger.error("Can't verify user") 183 | raise falcon.HTTPBadRequest(title="Bad request", 184 | description=e, 185 | href=settings.__docs__) 186 | try: 187 | raw_json = req.stream.read() 188 | logger.debug("Got incoming JSON data") 189 | except Exception as e: 190 | logger.error("Can't read incoming data stream") 191 | raise falcon.HTTPBadRequest(title="Bad request", 192 | description=e, 193 | href=settings.__docs__) 194 | try: 195 | result_json = json.loads(raw_json.decode("utf-8"), encoding='utf-8') 196 | except ValueError: 197 | raise falcon.HTTPError(falcon.HTTP_400, 198 | 'Malformed JSON', 199 | 'Could not decode the request body. The ' 200 | 'JSON was incorrect.') 201 | try: 202 | item = Item.get(Item.id == int(id)) 203 | if user not in item.allowed_users: 204 | raise falcon.HTTPForbidden(title="Permission denied", 205 | description="You don't have access to this resource", 206 | href=settings.__docs__) 207 | item.name = result_json.get("name", item.name) 208 | item.description = result_json.get("description", item.description) 209 | item.category = result_json.get("category", item.category) 210 | item.tags = result_json.get("tags", item.tags) 211 | item.save() 212 | res.status = falcon.HTTP_200 213 | res.body = json.dumps({"message": "Item updated"}) 214 | except Exception as e: 215 | print(e) 216 | error_msg = ("Unable to get the item. Please try again later.") 217 | raise falcon.HTTPServiceUnavailable(req.method + " failed", 218 | description=error_msg, 219 | retry_after=30, 220 | href=settings.__docs__) 221 | 222 | @falcon.before(login_required) 223 | def on_delete(self, req, res, id): 224 | try: 225 | # Parse token and get user id 226 | user_id = parse_token(req)['sub'] 227 | # Get the user 228 | user = User.get(User.id == int(user_id)) 229 | except Exception as e: 230 | logger.error("Can't verify user") 231 | raise falcon.HTTPBadRequest(title="Bad request", 232 | description=e, 233 | href=settings.__docs__) 234 | try: 235 | item = Item.get(Item.id == int(id)) 236 | if user not in item.allowed_users: 237 | raise falcon.HTTPForbidden(title="Permission denied", 238 | description="You don't have access to this resource", 239 | href=settings.__docs__) 240 | item.delete_instance() 241 | res.status = falcon.HTTP_200 242 | res.body = json.dumps({"message": "Deletion successful"}) 243 | 244 | except Exception as e: 245 | print(e) 246 | error_msg = ("Unable to delete category. Please try again later.") 247 | raise falcon.HTTPServiceUnavailable(title="{0} failed".format(req.method), 248 | description=error_msg, 249 | retry_after=30, 250 | href=settings.__docs__) 251 | 252 | def on_options(self, req, res, id): 253 | 254 | """Acknowledge the OPTIONS method. 255 | """ 256 | res.status = falcon.HTTP_200 257 | 258 | def on_post(self, req, res, id): 259 | raise falcon.HTTPError(falcon.HTTP_405, 260 | title="Client error", 261 | description=req.method + " method not allowed.", 262 | href=settings.__docs__) 263 | 264 | def on_update(self, req, res): 265 | raise falcon.HTTPError(falcon.HTTP_405, 266 | title="Client error", 267 | description=req.method + " method not allowed.", 268 | href=settings.__docs__) 269 | -------------------------------------------------------------------------------- /sikr/resources/main.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | import json 3 | 4 | import falcon 5 | 6 | from sikr import settings 7 | 8 | 9 | class APIInfo(object): 10 | 11 | """Show the main information about the API like endpoints, version, etc. 12 | """ 13 | 14 | def on_get(self, req, res): 15 | payload = { 16 | "version": { 17 | "api_version": settings.__version__, 18 | "api_codename": settings.__codename__, 19 | "api_status": settings.__status__, 20 | "documentation": settings.__docs__ 21 | }, 22 | "date": str(datetime.utcnow().replace(tzinfo=timezone.utc)), 23 | } 24 | res.status = falcon.HTTP_200 25 | res.body = json.dumps(payload) 26 | 27 | def on_options(self, req, res): 28 | res.status = falcon.HTTP_200 29 | 30 | def on_post(self, req, res): 31 | raise falcon.HTTPError(falcon.HTTP_405, 32 | title="Client error", 33 | description="{0} method not allowed.".format(req.method), 34 | href=settings.__docs__) 35 | 36 | def on_put(self, req, res): 37 | raise falcon.HTTPError(falcon.HTTP_405, 38 | title="Client error", 39 | description="{0} method not allowed.".format(req.method), 40 | href=settings.__docs__) 41 | 42 | def on_update(self, req, res): 43 | raise falcon.HTTPError(falcon.HTTP_405, 44 | title="Client error", 45 | description="{0} method not allowed.".format(req.method), 46 | href=settings.__docs__) 47 | 48 | def on_delete(self, req, res): 49 | raise falcon.HTTPError(falcon.HTTP_405, 50 | title="Client error", 51 | description="{0} method not allowed.".format(req.method), 52 | href=settings.__docs__) 53 | -------------------------------------------------------------------------------- /sikr/resources/services.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import falcon 4 | 5 | from sikr import settings 6 | from sikr.utils.logs import logger 7 | from sikr.models.users import User 8 | from sikr.models.services import Service 9 | from sikr.resources.auth.decorators import login_required 10 | from sikr.resources.auth.utils import parse_token 11 | 12 | 13 | class Services(object): 14 | 15 | @falcon.before(login_required) 16 | def on_get(self, req, res): 17 | """ 18 | """ 19 | # Parse token and get user id 20 | user_id = parse_token(req)['sub'] 21 | # See if we have to filter by item 22 | filter_item = req.get_param("item", required=False) 23 | 24 | try: 25 | # Get the user 26 | user = User.get(User.id == int(user_id)) 27 | if filter_item: 28 | services = list(user.allowed_services 29 | .select(Service.id, Service.name, 30 | Service.username, Service.password, 31 | Service.url, Service.port, Service.extra, 32 | Service.ssh_title, Service.ssh_public, 33 | Service.ssh_private, Service.ssl_title, 34 | Service.ssl_filename, Service.other) 35 | .where(Service.item == int(filter_item)) 36 | .dicts()) 37 | logger.debug("Got services filtered by item") 38 | else: 39 | services = list(user.allowed_services 40 | .select(Service.id, Service.name, 41 | Service.username, Service.password, 42 | Service.url, Service.port, Service.extra, 43 | Service.ssh_title, Service.ssh_public, 44 | Service.ssh_private, Service.ssl_title, 45 | Service.ssl_filename, Service.other) 46 | .dicts()) 47 | logger.debug("Got all the items") 48 | res.status = falcon.HTTP_200 49 | res.body = json.dumps(services) 50 | except Exception as e: 51 | logger.error(e) 52 | error_msg = ("Unable to get the services. Please try again later") 53 | raise falcon.HTTPServiceUnavailable(title=req.method + " failed", 54 | description=error_msg, 55 | retry_after=30, 56 | href=settings.__docs__) 57 | 58 | @falcon.before(login_required) 59 | def on_post(self, req, res): 60 | try: 61 | # Parse token and get user id 62 | user_id = parse_token(req)['sub'] 63 | # Get the user 64 | user = User.get(User.id == int(user_id)) 65 | except Exception as e: 66 | logger.error("Can't verify user") 67 | raise falcon.HTTPBadRequest(title="Bad request", 68 | description=e, 69 | href=settings.__docs__) 70 | 71 | try: 72 | raw_json = req.stream.read() 73 | logger.debug("Got incoming JSON data") 74 | except Exception as e: 75 | logger.error("Can't read incoming data stream") 76 | raise falcon.HTTPBadRequest(title="Bad request", 77 | description=e, 78 | href=settings.__docs__) 79 | 80 | try: 81 | result_json = json.loads(raw_json.decode("utf-8"), encoding='utf-8') 82 | logger.debug(result_json) 83 | except ValueError: 84 | raise falcon.HTTPError(falcon.HTTP_400, 85 | 'Malformed JSON', 86 | 'Could not decode the request body. The ' 87 | 'JSON was incorrect.') 88 | 89 | try: 90 | new_service = Service.create(name=result_json.get("name"), 91 | item=result_json.get("item"), 92 | username=result_json.get("username", ''), 93 | password=result_json.get("password", ''), 94 | url=result_json.get("url", ''), 95 | port=result_json.get("port", 0), 96 | extra=result_json.get("extra", ''), 97 | ssh_title=result_json.get("ssh_title", ''), 98 | ssh_public=result_json.get("ssh_public", ''), 99 | ssh_private=result_json.get("ssh_private", ''), 100 | ssl_title=result_json.get("ssl_title", ''), 101 | ssl_filename=result_json.get("ssh_title", ''), 102 | other=result_json.get("other", '')) 103 | new_service.save() 104 | new_service.allowed_users.add(user) 105 | except Exception as e: 106 | raise falcon.HTTPInternalServerError(title="Error while saving the item", 107 | description=e, 108 | href=settings.__docs__) 109 | 110 | def on_options(self, req, res): 111 | 112 | """Acknowledge the OPTIONS method. 113 | """ 114 | res.status = falcon.HTTP_200 115 | 116 | def on_put(self, req, res, pk): 117 | raise falcon.HTTPError(falcon.HTTP_405, 118 | title="Client error", 119 | description="{0} method not allowed.".format(req.method), 120 | href=settings.__docs__) 121 | 122 | def on_update(self, req, res): 123 | raise falcon.HTTPError(falcon.HTTP_405, 124 | title="Client error", 125 | description="{0} method not allowed.".format(req.method), 126 | href=settings.__docs__) 127 | 128 | def on_delete(self, req, res): 129 | raise falcon.HTTPError(falcon.HTTP_405, 130 | title="Client error", 131 | description="{0} method not allowed.".format(req.method), 132 | href=settings.__docs__) 133 | 134 | 135 | class DetailService(object): 136 | 137 | """ 138 | This resource handles the /services/ url. 139 | """ 140 | @falcon.before(login_required) 141 | def on_get(self, req, res, id): 142 | try: 143 | # Parse token and get user id 144 | user_id = parse_token(req)['sub'] 145 | # Get the user 146 | user = User.get(User.id == int(user_id)) 147 | except Exception as e: 148 | logger.error("Can't verify user") 149 | raise falcon.HTTPBadRequest(title="Bad request", 150 | description=e, 151 | href=settings.__docs__) 152 | try: 153 | service_obj = Service.get(Service.id == id) 154 | service = list(service_obj.select(Service.id, Service.name, 155 | Service.username, Service.password, 156 | Service.url, Service.port, Service.extra, 157 | Service.ssh_title, Service.ssh_public, 158 | Service.ssh_private, Service.ssl_title, 159 | Service.ssl_filename, Service.other) 160 | .where(Service.id == id) 161 | .dicts()) 162 | if user not in service_obj.allowed_users: 163 | raise falcon.HTTPForbidden(title="Permission denied", 164 | description="You don't have access to this resource", 165 | href=settings.__docs__) 166 | 167 | res.status = falcon.HTTP_200 168 | res.body = json.dumps(service) 169 | except Exception as e: 170 | print(e) 171 | error_msg = ("Unable to get the items. Please try again later") 172 | raise falcon.HTTPServiceUnavailable(title="{0} failed".format(req.method), 173 | description=error_msg, 174 | retry_after=30, 175 | href=settings.__docs__) 176 | 177 | @falcon.before(login_required) 178 | def on_put(self, req, res, id): 179 | raise falcon.HTTPError(falcon.HTTP_405, 180 | title="Client error", 181 | description="{0} method not allowed.".format(req.method), 182 | href=settings.__docs__) 183 | 184 | @falcon.before(login_required) 185 | def on_delete(self, req, res, id): 186 | try: 187 | # Parse token and get user id 188 | user_id = parse_token(req)['sub'] 189 | # Get the user 190 | user = User.get(User.id == int(user_id)) 191 | except Exception as e: 192 | logger.error("Can't verify user") 193 | raise falcon.HTTPBadRequest(title="Bad request", 194 | description=e, 195 | href=settings.__docs__) 196 | try: 197 | service = Service.get(Service.id == id) 198 | if user not in service.allowed_users: 199 | raise falcon.HTTPForbidden(title="Permission denied", 200 | description="You don't have access to this resource", 201 | href=settings.__docs__) 202 | service.delete_instance() 203 | res.status = falcon.HTTP_200 204 | res.body = json.dumps({"message": "Deletion successful"}) 205 | 206 | except Exception as e: 207 | print(e) 208 | error_msg = ("Unable to delete service. Please try again later.") 209 | raise falcon.HTTPServiceUnavailable(title="{0} failed".format(req.method), 210 | description=error_msg, 211 | retry_after=30, 212 | href=settings.__docs__) 213 | 214 | def on_options(self, req, res, id): 215 | 216 | """Acknowledge the OPTIONS method. 217 | """ 218 | res.status = falcon.HTTP_200 219 | 220 | def on_update(self, req, res, id): 221 | raise falcon.HTTPError(falcon.HTTP_405, 222 | title="Client error", 223 | description="{0} method not allowed.".format(req.method), 224 | href=settings.__docs__) 225 | 226 | @falcon.before(login_required) 227 | def on_post(self, req, res, id): 228 | pass 229 | -------------------------------------------------------------------------------- /sikr/resources/sharing.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import falcon 4 | 5 | from sikr import settings 6 | from sikr.utils.logs import logger 7 | from sikr.models.users import User 8 | from sikr.models.services import Service 9 | from sikr.models.shares import ShareToken 10 | from sikr.resources.auth.decorators import login_required 11 | from sikr.resources.auth.utils import parse_token 12 | from sikr.utils.tokens import generate_token 13 | 14 | 15 | class Share(object): 16 | 17 | """Share a object from the platform with someone 18 | """ 19 | @falcon.before(login_required) 20 | def on_post(self, req, res): 21 | try: 22 | # Parse token and get user id 23 | user_id = parse_token(req)['sub'] 24 | # Get the user 25 | user = User.get(User.id == int(user_id)) 26 | except Exception as e: 27 | logger.error("Can't verify user") 28 | raise falcon.HTTPBadRequest(title="Bad request", 29 | description=e, 30 | href=settings.__docs__) 31 | 32 | try: 33 | raw_json = req.stream.read() 34 | logger.debug("Got incoming JSON data") 35 | except Exception as e: 36 | logger.error("Can't read incoming data stream") 37 | raise falcon.HTTPBadRequest(title="Bad request", 38 | description=e, 39 | href=settings.__docs__) 40 | 41 | try: 42 | result_json = json.loads(raw_json.decode("utf-8"), encoding='utf-8') 43 | logger.debug(result_json) 44 | except ValueError: 45 | raise falcon.HTTPError(falcon.HTTP_400, 46 | 'Malformed JSON', 47 | 'Could not decode the request body. The ' 48 | 'JSON was incorrect.') 49 | 50 | try: 51 | new_share = ShareToken(user=user, token=generate_token(), 52 | resource=int(result_json.get())) 53 | except: 54 | pass 55 | 56 | def on_options(self, req, res): 57 | 58 | """Acknowledge the OPTIONS method. 59 | """ 60 | res.status = falcon.HTTP_200 61 | 62 | def on_get(self, req, res, pk): 63 | raise falcon.HTTPError(falcon.HTTP_405, 64 | title="Client error", 65 | description="{0} method not allowed.".format(req.method), 66 | href=settings.__docs__) 67 | 68 | def on_put(self, req, res, pk): 69 | raise falcon.HTTPError(falcon.HTTP_405, 70 | title="Client error", 71 | description="{0} method not allowed.".format(req.method), 72 | href=settings.__docs__) 73 | 74 | def on_update(self, req, res): 75 | raise falcon.HTTPError(falcon.HTTP_405, 76 | title="Client error", 77 | description="{0} method not allowed.".format(req.method), 78 | href=settings.__docs__) 79 | 80 | def on_delete(self, req, res): 81 | raise falcon.HTTPError(falcon.HTTP_405, 82 | title="Client error", 83 | description="{0} method not allowed.".format(req.method), 84 | href=settings.__docs__) 85 | -------------------------------------------------------------------------------- /sikr/settings/__init__.py: -------------------------------------------------------------------------------- 1 | """Settings selector. 2 | 3 | This module decides which settings module to load depending on the DEBUG and 4 | STAGING settings. These two setings can be defined from the local environment 5 | using values of 0 (False) and 1 (True). 6 | """ 7 | 8 | import os 9 | 10 | DEBUG = bool(int(os.getenv('SIKR_IS_DEBUG', True))) 11 | STAGING = bool(int(os.getenv('SIKR_IS_STAGING', False))) 12 | 13 | __version__ = '0.1.0' 14 | __codename__ = 'Kaneda' 15 | __status__ = 'alpha' 16 | __docs__ = 'http://docs.sikr.io' 17 | 18 | if DEBUG and not STAGING: 19 | from .development import * 20 | elif STAGING: 21 | from .staging import * 22 | elif not DEBUG and not STAGING: 23 | from .production import * 24 | -------------------------------------------------------------------------------- /sikr/settings/development.py: -------------------------------------------------------------------------------- 1 | """Main development settings.""" 2 | 3 | import os 4 | 5 | # Add the current directory to the python path 6 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 7 | 8 | # Specify here the database settings. We support MySQL, PostgreSQL and SQLite 9 | DATABASE = { 10 | 'ENGINE': 'postgresql', # 'postgresql', 'sqlite' or 'mysql'. Any other defaults to sqlite 11 | 'NAME': 'sikr_test', # Filename for SQLite or DB name for PostgreSQL/MySQL 12 | 'HOST': '172.17.0.2', # Not needed for SQLite. Server IP. Default: localhost 13 | 'PORT': '', # Not needed for SQLite. PostgreSQL default: 5432 14 | 'USER': 'postgres', # Not needed for SQLite. User that has access to the DB 15 | 'PASSWORD': '4J2bPbMT5jLe5Re9', # Not needed for SQLite. Password for the user 16 | } 17 | 18 | # Restrict the extensions allowed for the uploaded files, we do other checks 19 | # on the views, but this is the first level 20 | ALLOWED_EXTENSIONS = [ 21 | 'cer', 'crt', 'pfx', 'key', 'pem', 'arm', 'crt', 'pub' 22 | ] 23 | 24 | # Select the default version of the API, this will load specific parts of your 25 | # logic in the app. 26 | DEFAULT_API = 'v1' 27 | 28 | # Site domain, you usually want this to be your frontend url. This is used for 29 | # login verification between other things like CORS 30 | CORS_ACTIVE = True 31 | SITE_DOMAIN = 'https://sikr.io' 32 | LOGIN_URL = 'http://sikr.io/login.html' 33 | 34 | # This rewrites the response "Server" header, so you can hide your server name 35 | # for protection 36 | SERVER_NAME = "sikr.io" 37 | 38 | # How long the user session will last (in hours). Default: 168 (7 days) 39 | SESSION_EXPIRES = 168 40 | 41 | # Service tokens, this are usually the "client secret" or private API keys 42 | # that you need to finish the OAuth validation. Remember NOT to commit back 43 | # this values! They should remain known to you only! 44 | GITHUB_SECRET = '' 45 | FACEBOOK_SECRET = '' 46 | LINKEDIN_SECRET = '' 47 | GOOGLEPLUS_SECRET = '' 48 | TWITTER_KEY = '' 49 | TWITTER_SECRET = '' # Twitter consumer secret 50 | TWITTER_CALLBACK_URI = '' 51 | 52 | # Main server token Make it unique and keep it away from strangers! This token 53 | # is used in authentication and part of the storage encryption. This token 54 | # is an example. **You MUST replace it!** 55 | SECRET = '-&3whmt0f&h#zvyc@yk4bs3g6biu9l&a%0l=5u*q2+rz(sypdk' 56 | 57 | # Email SMTP settings. 58 | DEFAULT_EMAIL_FROM = 'noreply@sikr.io' 59 | SMTP_SERVER = '' 60 | SMTP_PORT = 587 61 | SMTP_USER = '' 62 | SMTP_PASSWORD = '' 63 | SMTP_TLS = True 64 | 65 | # Logging settings. This is a standard python logging configuration. The levels 66 | # are supposed to change depending on the settings file, to avoid clogging the 67 | # logs with useless information. 68 | LOGFILE = 'sikr.log' 69 | LOG_CONFIG = { 70 | "version": 1, 71 | 'formatters': { 72 | 'standard': { 73 | 'format': "[%(asctime)s] %(levelname)s [%(filename)s->%(funcName)s:%(lineno)s] %(message)s", 74 | 'datefmt': "%Y/%m/%d %H:%M:%S" 75 | }, 76 | }, 77 | 'handlers': { 78 | 'logfile': { 79 | 'level': 'DEBUG', 80 | 'class': 'logging.handlers.RotatingFileHandler', 81 | 'filename': os.path.join(BASE_DIR, LOGFILE), 82 | 'maxBytes': 10485760, # 10MB per file 83 | 'backupCount': 2, # Store up to three files 84 | 'formatter': 'standard', 85 | }, 86 | }, 87 | 'loggers': { 88 | 'sikr': { 89 | 'handlers': ["logfile", ], 90 | 'level': 'DEBUG', 91 | }, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /sikr/settings/production.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sikrvault/sikr/715df0227f8f888a9b2486326f50151b00d6eb33/sikr/settings/production.py -------------------------------------------------------------------------------- /sikr/settings/staging.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sikrvault/sikr/715df0227f8f888a9b2486326f50151b00d6eb33/sikr/settings/staging.py -------------------------------------------------------------------------------- /sikr/utils/checks.py: -------------------------------------------------------------------------------- 1 | """Main system checks of the platform. 2 | 3 | In this module are located the functions to perform the system cheks to 4 | guarantee that the platform will run correctly. 5 | """ 6 | 7 | import sys 8 | 9 | 10 | def check_python(): 11 | """Check that the python version is no less than 3.6.x.""" 12 | if sys.version_info <= (3, 6): 13 | sys.stdout.write("\nSorry, requires Python 3.6.x or better.\n") 14 | sys.exit(1) 15 | -------------------------------------------------------------------------------- /sikr/utils/cryptofunctions.py: -------------------------------------------------------------------------------- 1 | """Main cryptographic functions of the platform. 2 | 3 | These are the fucntion that encrypt and decrypt all the content from the 4 | backend side of the platform. 5 | """ 6 | 7 | import base64 8 | import hashlib 9 | from Crypto import Random 10 | from Crypto.Cipher import AES 11 | 12 | 13 | class AESCipher(object): 14 | 15 | def __init__(self, key): 16 | """Declare main variables like byte size (BS) and key.""" 17 | self.bs = 32 18 | self.key = hashlib.sha256(key.encode()).digest() 19 | 20 | def encrypt(self, raw): 21 | """Encrypt content using AES.""" 22 | raw = self._pad(raw) 23 | iv = Random.new().read(AES.block_size) 24 | cipher = AES.new(self.key, AES.MODE_CBC, iv) 25 | return base64.b64encode(iv + cipher.encrypt(raw)) 26 | 27 | def decrypt(self, enc): 28 | """Decrypt content.""" 29 | enc = base64.b64decode(enc) 30 | iv = enc[:AES.block_size] 31 | cipher = AES.new(self.key, AES.MODE_CBC, iv) 32 | return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8') 33 | 34 | def _pad(self, s): 35 | """Pad the text if it doesn't match the byte size.""" 36 | return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs) 37 | 38 | @staticmethod 39 | def _unpad(s): 40 | """Unpad the content for decryption.""" 41 | return s[:-ord(s[len(s) - 1:])] 42 | -------------------------------------------------------------------------------- /sikr/utils/email.py: -------------------------------------------------------------------------------- 1 | """Email sender. 2 | 3 | Basic module to send emails from the platform using the standard Python SMTP 4 | mechanishm. 5 | """ 6 | 7 | from email.mime.text import MIMEText 8 | from datetime import date 9 | import smtplib 10 | from urllib.parse import urlparse 11 | 12 | from sikre import settings 13 | from sikre.utils.logs import logger 14 | 15 | from_addr = settings.DEFAULT_EMAIL_FROM 16 | site_domain = urlparse(settings.SITE_DOMAIN).netloc 17 | EMAIL_SPACE = ", " 18 | 19 | 20 | def send_email(subject='', to_address=[], from_address=from_addr, content=''): 21 | """Send an email to a specified user or users. 22 | 23 | This is a basic wrapper around python's STMPLIB library that allows us to 24 | send emails to the users in case it's necessary. Any failure of this 25 | script is considered fatal. 26 | """ 27 | try: 28 | msg = MIMEText(content) 29 | msg['Subject'] = "[{0}] {1} {2}".format(site_domain, subject, 30 | date.today().strftime("%Y%m%d")) 31 | msg['To'] = EMAIL_SPACE.join(to_address) 32 | msg['From'] = from_address 33 | logger.debug("All parameters set") 34 | mail = smtplib.SMTP(settings.SMTP_SERVER, settings.SMTP_PORT) 35 | logger.debug("Instantiated the SMTP") 36 | if settings.SMTP_TLS: 37 | mail.starttls() 38 | logger.debug("Started SMTP TLS connection") 39 | mail.login(settings.SMTP_USER, settings.SMTP_PASSWORD) 40 | logger.debug("Login success") 41 | mail.sendmail(from_addr, to_address, msg.as_string()) 42 | logger.debug("Sent email") 43 | mail.quit() 44 | except Exception as e: 45 | logger.error("Email send failed. Error: {0}".format(e)) 46 | -------------------------------------------------------------------------------- /sikr/utils/logs.py: -------------------------------------------------------------------------------- 1 | """Log activator for the platform. 2 | 3 | This module activates the logging mechanism of the platform. It must be 4 | imported in all modules that need to declare logs. 5 | """ 6 | 7 | import logging 8 | import logging.config 9 | 10 | from sikr import settings 11 | 12 | logging.config.dictConfig(settings.LOG_CONFIG) 13 | logger = logging.getLogger("sikr") 14 | -------------------------------------------------------------------------------- /sikr/utils/tokens.py: -------------------------------------------------------------------------------- 1 | """Invitation token generator. 2 | 3 | This module generrate the invitation tokens based off a uuid4 web-safe 4 | function. 5 | """ 6 | 7 | import uuid 8 | 9 | from sikre.models.shares import ShareToken 10 | 11 | 12 | def generate_token(): 13 | """Generate a unique token. 14 | 15 | Generate a new token based of the HEX version of a UUID and check if it 16 | already exists. If that's the case generate a new one. 17 | """ 18 | duplicated = True 19 | 20 | while duplicated: 21 | try: 22 | token = uuid.uuid4().hex 23 | share_token = ShareToken.get(token=token) 24 | duplicated = True 25 | except ShareToken.DoesNotExist: 26 | duplicated = False 27 | return token 28 | --------------------------------------------------------------------------------