tina4-python 0.2.122__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tina4_python/Auth.py +222 -0
- tina4_python/Constant.py +43 -0
- tina4_python/Database.py +591 -0
- tina4_python/DatabaseResult.py +107 -0
- tina4_python/DatabaseTypes.py +15 -0
- tina4_python/Debug.py +126 -0
- tina4_python/Env.py +37 -0
- tina4_python/Localization.py +42 -0
- tina4_python/Messages.py +30 -0
- tina4_python/MiddleWare.py +90 -0
- tina4_python/Migration.py +107 -0
- tina4_python/ORM.py +639 -0
- tina4_python/Queue.py +615 -0
- tina4_python/Request.py +19 -0
- tina4_python/Response.py +121 -0
- tina4_python/Router.py +423 -0
- tina4_python/Session.py +342 -0
- tina4_python/ShellColors.py +20 -0
- tina4_python/Swagger.py +228 -0
- tina4_python/Template.py +107 -0
- tina4_python/Webserver.py +429 -0
- tina4_python/Websocket.py +49 -0
- tina4_python/__init__.py +392 -0
- tina4_python/messages.pot +83 -0
- tina4_python/public/css/readme.md +0 -0
- tina4_python/public/favicon.ico +0 -0
- tina4_python/public/images/403.png +0 -0
- tina4_python/public/images/404.png +0 -0
- tina4_python/public/images/500.png +0 -0
- tina4_python/public/images/logo.png +0 -0
- tina4_python/public/images/readme.md +0 -0
- tina4_python/public/js/readme.md +0 -0
- tina4_python/public/js/reconnecting-websocket.js +365 -0
- tina4_python/public/js/tina4helper.js +397 -0
- tina4_python/public/swagger/index.html +90 -0
- tina4_python/public/swagger/oauth2-redirect.html +63 -0
- tina4_python/templates/errors/403.twig +10 -0
- tina4_python/templates/errors/404.twig +10 -0
- tina4_python/templates/errors/500.twig +11 -0
- tina4_python/templates/readme.md +1 -0
- tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- tina4_python/translations/en/LC_MESSAGES/messages.po +80 -0
- tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- tina4_python/translations/fr/LC_MESSAGES/messages.po +84 -0
- tina4_python-0.2.122.dist-info/METADATA +465 -0
- tina4_python-0.2.122.dist-info/RECORD +47 -0
- tina4_python-0.2.122.dist-info/WHEEL +4 -0
tina4_python/Auth.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Tina4 - This is not a 4ramework.
|
|
3
|
+
# Copy-right 2007 - current Tina4
|
|
4
|
+
# License: MIT https://opensource.org/licenses/MIT
|
|
5
|
+
#
|
|
6
|
+
# flake8: noqa: E501
|
|
7
|
+
import datetime
|
|
8
|
+
import os
|
|
9
|
+
import jwt
|
|
10
|
+
import bcrypt
|
|
11
|
+
from json import JSONEncoder
|
|
12
|
+
from cryptography import x509
|
|
13
|
+
from cryptography.x509 import NameOID
|
|
14
|
+
from cryptography.hazmat.primitives import serialization, hashes
|
|
15
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
16
|
+
from cryptography.hazmat.backends import default_backend
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuthJSONSerializer(JSONEncoder):
|
|
20
|
+
def default(self, o):
|
|
21
|
+
if isinstance(o, datetime.datetime):
|
|
22
|
+
return o.isoformat()
|
|
23
|
+
else:
|
|
24
|
+
raise TypeError(f'Object of type {o.__class__.__name__} '
|
|
25
|
+
f'is not JSON serializable')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Auth:
|
|
29
|
+
secret = None
|
|
30
|
+
private_key = None
|
|
31
|
+
root_path = None
|
|
32
|
+
loaded_private_key = None
|
|
33
|
+
loaded_public_key = None
|
|
34
|
+
|
|
35
|
+
def hash_password(self, text):
|
|
36
|
+
"""
|
|
37
|
+
Generates a Bcrypt password hash
|
|
38
|
+
:param text:
|
|
39
|
+
:return:
|
|
40
|
+
"""
|
|
41
|
+
password_bytes = text.encode('utf-8')
|
|
42
|
+
# Generate a salt
|
|
43
|
+
salt = bcrypt.gensalt()
|
|
44
|
+
# Hash the password
|
|
45
|
+
return bcrypt.hashpw(password_bytes, salt).decode('utf-8')
|
|
46
|
+
|
|
47
|
+
def check_password(self, password_hash, text):
|
|
48
|
+
"""
|
|
49
|
+
Checks a Bcrypt password hash
|
|
50
|
+
:param password_hash:
|
|
51
|
+
:param text:
|
|
52
|
+
:return:
|
|
53
|
+
"""
|
|
54
|
+
password_bytes = text.encode('utf-8')
|
|
55
|
+
return bcrypt.checkpw(password_bytes, password_hash.encode('utf-8'))
|
|
56
|
+
|
|
57
|
+
def load_private_key(self):
|
|
58
|
+
"""
|
|
59
|
+
Loads the private key from the secrets folder
|
|
60
|
+
:return:
|
|
61
|
+
"""
|
|
62
|
+
if self.loaded_private_key:
|
|
63
|
+
return self.loaded_private_key
|
|
64
|
+
with open(self.private_key, "rb") as f:
|
|
65
|
+
private_key = serialization.load_pem_private_key(
|
|
66
|
+
f.read(), b"{self.secret}", backend=default_backend()
|
|
67
|
+
)
|
|
68
|
+
self.loaded_private_key = private_key
|
|
69
|
+
return private_key
|
|
70
|
+
|
|
71
|
+
def load_public_key(self):
|
|
72
|
+
"""
|
|
73
|
+
Loads the public key from the secrets folder
|
|
74
|
+
:return:
|
|
75
|
+
"""
|
|
76
|
+
if self.loaded_public_key:
|
|
77
|
+
return self.loaded_public_key
|
|
78
|
+
with open(self.public_key, "rb") as f:
|
|
79
|
+
public_key = serialization.load_pem_public_key(
|
|
80
|
+
f.read(), backend=default_backend()
|
|
81
|
+
)
|
|
82
|
+
self.loaded_public_key = public_key
|
|
83
|
+
return public_key
|
|
84
|
+
|
|
85
|
+
def __init__(self, root_path):
|
|
86
|
+
self.root_path = root_path
|
|
87
|
+
self.secret = os.environ.get("SECRET", None)
|
|
88
|
+
self.private_key = root_path + os.sep + "secrets" + os.sep + "private.key"
|
|
89
|
+
self.public_key = root_path + os.sep + "secrets" + os.sep + "public.key"
|
|
90
|
+
self.self_signed = root_path + os.sep + "secrets" + os.sep + "domain.cert"
|
|
91
|
+
|
|
92
|
+
# check if we have a secrets folder
|
|
93
|
+
if not os.path.exists(root_path + os.sep + "secrets"):
|
|
94
|
+
os.makedirs(root_path + os.sep + "secrets")
|
|
95
|
+
|
|
96
|
+
if not os.path.isfile(self.private_key):
|
|
97
|
+
private_key = rsa.generate_private_key(
|
|
98
|
+
public_exponent=65537,
|
|
99
|
+
key_size=2048,
|
|
100
|
+
)
|
|
101
|
+
with open(self.private_key, "wb") as f:
|
|
102
|
+
f.write(private_key.private_bytes(
|
|
103
|
+
encoding=serialization.Encoding.PEM,
|
|
104
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
105
|
+
encryption_algorithm=serialization.BestAvailableEncryption(b"{self.secret}"),
|
|
106
|
+
))
|
|
107
|
+
else:
|
|
108
|
+
private_key = self.load_private_key()
|
|
109
|
+
|
|
110
|
+
if not os.path.isfile(self.public_key):
|
|
111
|
+
public_key = private_key.public_key()
|
|
112
|
+
public_pem = public_key.public_bytes(
|
|
113
|
+
encoding=serialization.Encoding.PEM,
|
|
114
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
115
|
+
)
|
|
116
|
+
with open(self.public_key, 'wb') as f:
|
|
117
|
+
f.write(public_pem)
|
|
118
|
+
|
|
119
|
+
if not os.path.isfile(self.self_signed):
|
|
120
|
+
subject = issuer = x509.Name([
|
|
121
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, os.environ.get('COUNTRY', 'ZA')),
|
|
122
|
+
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, os.environ.get('STATE', 'WESTERN CAPE')),
|
|
123
|
+
x509.NameAttribute(NameOID.LOCALITY_NAME, os.environ.get('CITY', 'CAPE TOWN')),
|
|
124
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, os.environ.get('ORGANIZATION', 'Tina4')),
|
|
125
|
+
x509.NameAttribute(NameOID.COMMON_NAME, os.environ.get('DOMAIN_NAME', 'localhost'))
|
|
126
|
+
])
|
|
127
|
+
cert = (x509.CertificateBuilder().subject_name(
|
|
128
|
+
subject
|
|
129
|
+
).issuer_name(
|
|
130
|
+
issuer
|
|
131
|
+
).public_key(
|
|
132
|
+
private_key.public_key()
|
|
133
|
+
).serial_number(
|
|
134
|
+
x509.random_serial_number()
|
|
135
|
+
).not_valid_before(
|
|
136
|
+
datetime.datetime.now(datetime.timezone.utc)
|
|
137
|
+
).not_valid_after(
|
|
138
|
+
datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=99999)
|
|
139
|
+
).add_extension(
|
|
140
|
+
x509.SubjectAlternativeName([x509.DNSName("localhost")]),
|
|
141
|
+
critical=False,
|
|
142
|
+
).sign(private_key, hashes.SHA256()))
|
|
143
|
+
|
|
144
|
+
with open(self.self_signed, "wb") as f:
|
|
145
|
+
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
|
146
|
+
|
|
147
|
+
def get_token(self, payload_data, expiry_minutes=0):
|
|
148
|
+
"""
|
|
149
|
+
Returns a token to be used as bearer
|
|
150
|
+
:param payload_data:
|
|
151
|
+
:param expiry_minutes:
|
|
152
|
+
:return:
|
|
153
|
+
"""
|
|
154
|
+
private_key = self.load_private_key()
|
|
155
|
+
now = datetime.datetime.now()
|
|
156
|
+
|
|
157
|
+
if "expires" not in payload_data:
|
|
158
|
+
token_limit_minutes = int(os.environ.get("TINA4_TOKEN_LIMIT", 2))
|
|
159
|
+
if expiry_minutes != 0:
|
|
160
|
+
token_limit_minutes = expiry_minutes
|
|
161
|
+
expiry_time = now + datetime.timedelta(minutes=token_limit_minutes)
|
|
162
|
+
payload_data["expires"] = expiry_time.isoformat()
|
|
163
|
+
|
|
164
|
+
token = jwt.encode(
|
|
165
|
+
payload=payload_data,
|
|
166
|
+
key=private_key,
|
|
167
|
+
algorithm='RS256',
|
|
168
|
+
json_encoder=AuthJSONSerializer
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return token
|
|
172
|
+
|
|
173
|
+
def get_payload(self, token):
|
|
174
|
+
"""
|
|
175
|
+
Gets the payload of the token to be evaluated
|
|
176
|
+
:param token:
|
|
177
|
+
:return:
|
|
178
|
+
"""
|
|
179
|
+
public_key = self.load_public_key()
|
|
180
|
+
try:
|
|
181
|
+
payload = jwt.decode(token, key=public_key, algorithms=['RS256'])
|
|
182
|
+
except jwt.exceptions.InvalidSignatureError:
|
|
183
|
+
payload = None
|
|
184
|
+
|
|
185
|
+
return payload
|
|
186
|
+
|
|
187
|
+
def validate(self, token):
|
|
188
|
+
"""
|
|
189
|
+
Validates the token
|
|
190
|
+
:param token:
|
|
191
|
+
:return:
|
|
192
|
+
"""
|
|
193
|
+
# first check for API_KEY = token, simplest form
|
|
194
|
+
if os.environ.get("API_KEY", None) is not None:
|
|
195
|
+
if token == os.environ.get("API_KEY"):
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
public_key = self.load_public_key()
|
|
199
|
+
try:
|
|
200
|
+
payload = jwt.decode(token, key=public_key, algorithms=['RS256'])
|
|
201
|
+
if "expires" not in payload:
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
if "expires" in payload:
|
|
205
|
+
now = datetime.datetime.now()
|
|
206
|
+
expiry_time = datetime.datetime.fromisoformat(payload["expires"])
|
|
207
|
+
if now > expiry_time:
|
|
208
|
+
return False
|
|
209
|
+
else:
|
|
210
|
+
return True
|
|
211
|
+
except Exception:
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
def valid(self, token):
|
|
217
|
+
"""
|
|
218
|
+
Validates the token
|
|
219
|
+
:param token:
|
|
220
|
+
:return:
|
|
221
|
+
"""
|
|
222
|
+
return self.validate(token)
|
tina4_python/Constant.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Tina4 - This is not a 4ramework.
|
|
3
|
+
# Copy-right 2007 - current Tina4
|
|
4
|
+
# License: MIT https://opensource.org/licenses/MIT
|
|
5
|
+
#
|
|
6
|
+
# flake8: noqa: E501
|
|
7
|
+
TINA4_LOG_INFO = "INFO"
|
|
8
|
+
TINA4_LOG_WARNING = "WARN"
|
|
9
|
+
TINA4_LOG_DEBUG = "DEBUG"
|
|
10
|
+
TINA4_LOG_ERROR = "ERROR"
|
|
11
|
+
TINA4_LOG_ALL = "ALL"
|
|
12
|
+
|
|
13
|
+
TINA4_GET = "GET"
|
|
14
|
+
TINA4_POST = "POST"
|
|
15
|
+
TINA4_ANY = "ANY"
|
|
16
|
+
TINA4_PATCH = "PATCH"
|
|
17
|
+
TINA4_PUT = "PUT"
|
|
18
|
+
TINA4_DELETE = "DELETE"
|
|
19
|
+
TINA4_OPTIONS = "OPTIONS"
|
|
20
|
+
|
|
21
|
+
HTTP_OK = 200
|
|
22
|
+
HTTP_CREATED = 201
|
|
23
|
+
HTTP_ACCEPTED = 202
|
|
24
|
+
HTTP_NO_CONTENT = 204
|
|
25
|
+
HTTP_PARTIAL_CONTENT = 206
|
|
26
|
+
HTTP_BAD_REQUEST = 400
|
|
27
|
+
HTTP_UNAUTHORIZED = 401
|
|
28
|
+
HTTP_FORBIDDEN = 403
|
|
29
|
+
HTTP_NOT_FOUND = 404
|
|
30
|
+
HTTP_REDIRECT = 302
|
|
31
|
+
HTTP_SERVER_ERROR = 500
|
|
32
|
+
|
|
33
|
+
LOOKUP_HTTP_CODE = {HTTP_OK: "OK", HTTP_CREATED: "Created", HTTP_ACCEPTED: "Accepted", HTTP_BAD_REQUEST: "Bad Request",
|
|
34
|
+
HTTP_PARTIAL_CONTENT: "Partial Content", HTTP_UNAUTHORIZED: "Unauthorized",
|
|
35
|
+
HTTP_FORBIDDEN: "Forbidden", HTTP_NOT_FOUND: "Not Found", HTTP_NO_CONTENT: "No Content",
|
|
36
|
+
HTTP_REDIRECT: "Redirect",
|
|
37
|
+
HTTP_SERVER_ERROR: "Internal Server Error"}
|
|
38
|
+
|
|
39
|
+
TEXT_HTML = "text/html"
|
|
40
|
+
TEXT_CSS = "text/css"
|
|
41
|
+
TEXT_PLAIN = "text/plain"
|
|
42
|
+
APPLICATION_JSON = "application/json"
|
|
43
|
+
APPLICATION_XML = "application/xml"
|