p3lib 1.1.65__py3-none-any.whl → 1.1.67__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.
- p3lib/bokeh_auth.py +111 -42
- p3lib/bokeh_gui.py +14 -4
- {p3lib-1.1.65.dist-info → p3lib-1.1.67.dist-info}/METADATA +1 -1
- {p3lib-1.1.65.dist-info → p3lib-1.1.67.dist-info}/RECORD +7 -7
- {p3lib-1.1.65.dist-info → p3lib-1.1.67.dist-info}/LICENSE +0 -0
- {p3lib-1.1.65.dist-info → p3lib-1.1.67.dist-info}/WHEEL +0 -0
- {p3lib-1.1.65.dist-info → p3lib-1.1.67.dist-info}/top_level.txt +0 -0
p3lib/bokeh_auth.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
This file is a modified version of the Bokeh authorisation example code.
|
4
4
|
Many thanks to bokeh for this.
|
5
5
|
|
6
|
-
|
6
|
+
This contains a mix of pep8 and camel case method names ...
|
7
7
|
|
8
8
|
'''
|
9
9
|
import os
|
@@ -13,33 +13,41 @@ import tornado
|
|
13
13
|
from tornado.web import RequestHandler
|
14
14
|
from argon2 import PasswordHasher
|
15
15
|
from argon2.exceptions import VerificationError
|
16
|
+
from datetime import datetime
|
17
|
+
|
18
|
+
# could also define get_login_url function (but must give up LoginHandler)
|
19
|
+
login_url = "/login"
|
16
20
|
|
17
21
|
CRED_FILE_KEY = "CRED_FILE"
|
18
22
|
LOGIN_HTML_FILE_KEY = "LOGIN_HTML_FILE"
|
23
|
+
ACCESS_LOG_FILE = "ACCESS_LOG_FILE"
|
19
24
|
|
20
25
|
# could define get_user_async instead
|
21
26
|
def get_user(request_handler):
|
27
|
+
# Record the get request
|
28
|
+
LoginHandler.RecordGet(request_handler.request.remote_ip)
|
22
29
|
user = request_handler.get_cookie("user")
|
30
|
+
# Record the user making the request
|
31
|
+
LoginHandler.SaveInfoAccessLogMessage(f"USER={user}")
|
23
32
|
return user
|
24
33
|
|
25
|
-
# could also define get_login_url function (but must give up LoginHandler)
|
26
|
-
login_url = "/login"
|
27
|
-
|
28
34
|
def GetAuthAttrFile():
|
29
35
|
"""@brief Get the file that is used to pass parameters (credentials file and login.html file) to the tornado server.
|
30
36
|
There must be a better way of passing the credentials file to the tornado login handler than this..."""
|
31
37
|
jsonFile = os.path.join( tempfile.gettempdir(), f"bokeh_auth_attr_{os.getpid()}.json")
|
32
38
|
return jsonFile
|
33
39
|
|
34
|
-
def SetBokehAuthAttrs(credentialsJsonFile, loginHTMLFile):
|
40
|
+
def SetBokehAuthAttrs(credentialsJsonFile, loginHTMLFile, accessLogFile=None):
|
35
41
|
"""@brief Set the attributes used to login to the bokeh server. By default
|
36
42
|
no login is required to the bokeh server.
|
37
43
|
@param credentialsJsonFile The file that stores the username and hashed passwords for the server.
|
38
|
-
@param loginHTMLFile The HTML file for the page presented to the user when logging into the bokeh server.
|
44
|
+
@param loginHTMLFile The HTML file for the page presented to the user when logging into the bokeh server.
|
45
|
+
@param accessLogFile The log file to record access to. If left as None then no logging occurs."""
|
39
46
|
jsonFile = GetAuthAttrFile()
|
40
47
|
with open(jsonFile, 'w') as fd:
|
41
48
|
cfgDict={CRED_FILE_KEY: credentialsJsonFile}
|
42
49
|
cfgDict[LOGIN_HTML_FILE_KEY]=loginHTMLFile
|
50
|
+
cfgDict[ACCESS_LOG_FILE]=accessLogFile
|
43
51
|
json.dump(cfgDict, fd, ensure_ascii=False, indent=4)
|
44
52
|
|
45
53
|
def _getCredDict():
|
@@ -60,10 +68,73 @@ def GetLoginHTMLFile():
|
|
60
68
|
"""@return The html file for the login page."""
|
61
69
|
credDict = _getCredDict()
|
62
70
|
return credDict[LOGIN_HTML_FILE_KEY]
|
63
|
-
|
71
|
+
|
72
|
+
def GetAccessLogFile():
|
73
|
+
"""@return Get the access log file.."""
|
74
|
+
credDict = _getCredDict()
|
75
|
+
return credDict[ACCESS_LOG_FILE]
|
76
|
+
|
64
77
|
# optional login page for login_url
|
65
78
|
class LoginHandler(RequestHandler):
|
66
79
|
|
80
|
+
@staticmethod
|
81
|
+
def RecordGet(remoteIP):
|
82
|
+
"""@brief Record an HHTP get on the login page.
|
83
|
+
@param remoteIP The IP address of the client."""
|
84
|
+
msg = f"HTTP GET from {remoteIP}"
|
85
|
+
LoginHandler.SaveInfoAccessLogMessage(msg)
|
86
|
+
try:
|
87
|
+
# We import here so that the p3lib module will import even if ip2geotools
|
88
|
+
# is not available as pip install ip2geotools adds in ~ 70 python modules !!!
|
89
|
+
from ip2geotools.databases.noncommercial import DbIpCity
|
90
|
+
response = DbIpCity.get(remoteIP, api_key='free')
|
91
|
+
LoginHandler.SaveInfoAccessLogMessage(f"HTTP GET country = {response.country}")
|
92
|
+
LoginHandler.SaveInfoAccessLogMessage(f"HTTP GET region = {response.region}")
|
93
|
+
LoginHandler.SaveInfoAccessLogMessage(f"HTTP GET city = {response.city}")
|
94
|
+
LoginHandler.SaveInfoAccessLogMessage(f"HTTP GET latitude = {response.latitude}")
|
95
|
+
LoginHandler.SaveInfoAccessLogMessage(f"HTTP GET longitude = {response.longitude}")
|
96
|
+
except:
|
97
|
+
pass
|
98
|
+
|
99
|
+
def _recordLoginAttempt(self, username, password):
|
100
|
+
"""@brief Record an attempt to login to the server.
|
101
|
+
@param username The username entered.
|
102
|
+
@param password The password entered."""
|
103
|
+
pw = "*"*len(password)
|
104
|
+
LoginHandler.SaveInfoAccessLogMessage(f"Login attempt from {self.request.remote_ip}: username = {username}, password={pw}")
|
105
|
+
|
106
|
+
def _recordLoginSuccess(self, username, password):
|
107
|
+
"""@brief Record a successful login to the server.
|
108
|
+
@param username The username entered.
|
109
|
+
@param password The password entered."""
|
110
|
+
pw = "*"*len(password)
|
111
|
+
LoginHandler.SaveInfoAccessLogMessage(f"Login success from {self.request.remote_ip}: username = {username}, password={pw}")
|
112
|
+
|
113
|
+
@staticmethod
|
114
|
+
def SaveInfoAccessLogMessage(msg):
|
115
|
+
"""@brief Save an info level access log message.
|
116
|
+
@param msg The message to save to the access log."""
|
117
|
+
LoginHandler.SaveAccessLogMessage("INFO: "+str(msg))
|
118
|
+
|
119
|
+
@staticmethod
|
120
|
+
def SaveAccessLogMessage(msg):
|
121
|
+
"""@brief Save an access log message.
|
122
|
+
@param msg The message to save to the access log."""
|
123
|
+
now = datetime.now()
|
124
|
+
accessLogFile = GetAccessLogFile()
|
125
|
+
if accessLogFile and len(accessLogFile) > 0:
|
126
|
+
try:
|
127
|
+
if not os.path.isfile(accessLogFile):
|
128
|
+
with open(accessLogFile, 'w'):
|
129
|
+
pass
|
130
|
+
|
131
|
+
with open(accessLogFile, 'a') as fd:
|
132
|
+
line = now.isoformat() + ": " + str(msg)+"\n"
|
133
|
+
fd.write(line)
|
134
|
+
|
135
|
+
except:
|
136
|
+
pass
|
137
|
+
|
67
138
|
def get(self):
|
68
139
|
try:
|
69
140
|
errormessage = self.get_argument("error")
|
@@ -75,11 +146,13 @@ class LoginHandler(RequestHandler):
|
|
75
146
|
def check_permission(self, username, password):
|
76
147
|
"""@brief Check if we the username and password are valid
|
77
148
|
@return True if the username and password are valid."""
|
149
|
+
self._recordLoginAttempt(username, password)
|
78
150
|
valid = False
|
79
151
|
credentialsJsonFile = GetCredentialsFile()
|
80
152
|
ch = CredentialsHasher(credentialsJsonFile)
|
81
153
|
if ch.verify(username, password):
|
82
154
|
valid = True
|
155
|
+
self._recordLoginSuccess(username, password)
|
83
156
|
return valid
|
84
157
|
|
85
158
|
def post(self):
|
@@ -96,8 +169,10 @@ class LoginHandler(RequestHandler):
|
|
96
169
|
def set_current_user(self, user):
|
97
170
|
if user:
|
98
171
|
self.set_cookie("user", tornado.escape.json_encode(user))
|
172
|
+
LoginHandler.SaveInfoAccessLogMessage(f"Set user cookie: user={user}")
|
99
173
|
else:
|
100
174
|
self.clear_cookie("user")
|
175
|
+
LoginHandler.SaveInfoAccessLogMessage("Cleared user cookie")
|
101
176
|
|
102
177
|
# optional logout_url, available as curdoc().session_context.logout_url
|
103
178
|
logout_url = "/logout"
|
@@ -115,32 +190,32 @@ class CredentialsHasherExeption(Exception):
|
|
115
190
|
|
116
191
|
class CredentialsHasher(object):
|
117
192
|
"""@brief Responsible for storing hashed credentials to a local file.
|
118
|
-
There are issues storing hashed credentials and so this is not
|
119
|
-
recommended for high security systems but is aimed at providing
|
193
|
+
There are issues storing hashed credentials and so this is not
|
194
|
+
recommended for high security systems but is aimed at providing
|
120
195
|
a simple credentials storage solution for Bokeh servers."""
|
121
|
-
|
196
|
+
|
122
197
|
def __init__(self, credentialsJsonFile):
|
123
|
-
"""@brief Construct an object that can be used to generate a credentials has file and check
|
124
|
-
credentials entered by a user.
|
198
|
+
"""@brief Construct an object that can be used to generate a credentials has file and check
|
199
|
+
credentials entered by a user.
|
125
200
|
@param credentialsJsonFile A file that contains the hashed (via argon2) login credentials."""
|
126
201
|
self._credentialsJsonFile = credentialsJsonFile
|
127
202
|
self._passwordHasher = PasswordHasher()
|
128
203
|
self._credDict = self._getCredDict()
|
129
|
-
|
204
|
+
|
130
205
|
def _getCredDict(self):
|
131
206
|
"""@brief Get a dictionary containing the current credentials.
|
132
|
-
@return A dict containing the credentials.
|
207
|
+
@return A dict containing the credentials.
|
133
208
|
value = username
|
134
209
|
key = hashed password."""
|
135
210
|
credDict = {}
|
136
211
|
# If the hash file exists
|
137
212
|
if os.path.isfile(self._credentialsJsonFile):
|
138
|
-
# Add the hash a a line in the file
|
213
|
+
# Add the hash a a line in the file
|
139
214
|
with open(self._credentialsJsonFile, 'r') as fd:
|
140
215
|
contents = fd.read()
|
141
216
|
credDict = json.loads(contents)
|
142
217
|
return credDict
|
143
|
-
|
218
|
+
|
144
219
|
def isUsernameAvailable(self, username):
|
145
220
|
"""@brief Determine if the username is not already used.
|
146
221
|
@param username The login username.
|
@@ -149,12 +224,12 @@ class CredentialsHasher(object):
|
|
149
224
|
if username in self._credDict:
|
150
225
|
usernameAvailable = False
|
151
226
|
return usernameAvailable
|
152
|
-
|
227
|
+
|
153
228
|
def _saveCredentials(self):
|
154
229
|
"""@brief Save the cr3edentials to the file."""
|
155
230
|
with open(self._credentialsJsonFile, 'w', encoding='utf-8') as f:
|
156
231
|
json.dump(self._credDict, f, ensure_ascii=False, indent=4)
|
157
|
-
|
232
|
+
|
158
233
|
def add(self, username, password):
|
159
234
|
"""@brief Add credential to the stored hashes.
|
160
235
|
@param username The login username.
|
@@ -163,22 +238,22 @@ class CredentialsHasher(object):
|
|
163
238
|
hash = self._passwordHasher.hash(password)
|
164
239
|
self._credDict[username] = hash
|
165
240
|
self._saveCredentials()
|
166
|
-
|
241
|
+
|
167
242
|
else:
|
168
243
|
raise CredentialsHasherExeption(f"{username} username is already in use.")
|
169
|
-
|
244
|
+
|
170
245
|
def remove(self, username):
|
171
246
|
"""@brief Remove a user from the stored hashes.
|
172
247
|
If the username is not present then this method will return without an error.
|
173
248
|
@param username The login username.
|
174
249
|
@return True if the username/password was removed"""
|
175
|
-
removed = False
|
250
|
+
removed = False
|
176
251
|
if username in self._credDict:
|
177
252
|
del self._credDict[username]
|
178
253
|
self._saveCredentials()
|
179
254
|
removed = True
|
180
255
|
return removed
|
181
|
-
|
256
|
+
|
182
257
|
def verify(self, username, password):
|
183
258
|
"""@brief Check the credentials are valid and stored in the hash file.
|
184
259
|
@param username The login username.
|
@@ -190,25 +265,25 @@ class CredentialsHasher(object):
|
|
190
265
|
try:
|
191
266
|
self._passwordHasher.verify(storedHash, password)
|
192
267
|
validCredential = True
|
193
|
-
|
268
|
+
|
194
269
|
except VerificationError:
|
195
|
-
pass
|
196
|
-
|
270
|
+
pass
|
271
|
+
|
197
272
|
return validCredential
|
198
|
-
|
273
|
+
|
199
274
|
def getCredentialCount(self):
|
200
275
|
"""@brief Get the number of credentials that are stored.
|
201
276
|
@return The number of credentials stored."""
|
202
|
-
return len(self._credDict.keys())
|
203
|
-
|
277
|
+
return len(self._credDict.keys())
|
278
|
+
|
204
279
|
def getUsernameList(self):
|
205
280
|
"""@brief Get a list of usernames.
|
206
281
|
@return A list of usernames."""
|
207
|
-
return list(self._credDict.keys())
|
282
|
+
return list(self._credDict.keys())
|
208
283
|
|
209
284
|
class CredentialsManager(object):
|
210
285
|
"""@brief Responsible for allowing the user to add and remove credentials to a a local file."""
|
211
|
-
|
286
|
+
|
212
287
|
def __init__(self, uio, credentialsJsonFile):
|
213
288
|
"""@brief Constructor.
|
214
289
|
@param uio A UIO instance that allows user input output.
|
@@ -216,7 +291,7 @@ class CredentialsManager(object):
|
|
216
291
|
self._uio = uio
|
217
292
|
self._credentialsJsonFile = credentialsJsonFile
|
218
293
|
self.credentialsHasher = CredentialsHasher(self._credentialsJsonFile)
|
219
|
-
|
294
|
+
|
220
295
|
def _add(self):
|
221
296
|
"""@brief Add a username/password to the list of credentials."""
|
222
297
|
self._uio.info('Add a username/password')
|
@@ -226,7 +301,7 @@ class CredentialsManager(object):
|
|
226
301
|
self.credentialsHasher.add(username, password)
|
227
302
|
else:
|
228
303
|
self._uio.error(f"{username} is already in use.")
|
229
|
-
|
304
|
+
|
230
305
|
def _delete(self):
|
231
306
|
"""@brief Delete a username/password from the list of credentials."""
|
232
307
|
self._uio.info('Delete a username/password')
|
@@ -236,10 +311,10 @@ class CredentialsManager(object):
|
|
236
311
|
self._uio.info(f"Removed {username}")
|
237
312
|
else:
|
238
313
|
self._uio.error(f"Failed to remove {username}.")
|
239
|
-
|
314
|
+
|
240
315
|
else:
|
241
316
|
self._uio.error(f"{username} not found.")
|
242
|
-
|
317
|
+
|
243
318
|
def _check(self):
|
244
319
|
"""@brief Check a username/password from the list of credentials."""
|
245
320
|
self._uio.info('Check a username/password')
|
@@ -249,14 +324,14 @@ class CredentialsManager(object):
|
|
249
324
|
self._uio.info(f"The username and password match.")
|
250
325
|
else:
|
251
326
|
self._uio.error(f"The username and password do not match.")
|
252
|
-
|
327
|
+
|
253
328
|
def _showUsernames(self):
|
254
329
|
"""@brief Show the user a list of the usernames stored."""
|
255
330
|
table = [["USERNAME"]]
|
256
331
|
for username in self.credentialsHasher.getUsernameList():
|
257
332
|
table.append([username])
|
258
333
|
self._uio.showTable(table)
|
259
|
-
|
334
|
+
|
260
335
|
def manage(self):
|
261
336
|
"""@brief Allow the user to add and remove user credentials from a local file."""
|
262
337
|
while True:
|
@@ -280,9 +355,3 @@ class CredentialsManager(object):
|
|
280
355
|
return
|
281
356
|
else:
|
282
357
|
self._uio.error(f"{response} is an invalid response.")
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
p3lib/bokeh_gui.py
CHANGED
@@ -760,12 +760,19 @@ class MultiAppServer(object):
|
|
760
760
|
@return The TCP port or -1 if no port is available."""
|
761
761
|
return SingleAppServer.GetNextUnusedPort(basePort=basePort, maxPort=maxPort, bindAddress=bindAddress)
|
762
762
|
|
763
|
-
def __init__(self,
|
763
|
+
def __init__(self,
|
764
|
+
address="0.0.0.0",
|
765
|
+
bokehPort=0,
|
766
|
+
wsOrigin="*:*",
|
767
|
+
credentialsJsonFile=None,
|
768
|
+
loginHTMLFile="login.html",
|
769
|
+
accessLogFile=None):
|
764
770
|
"""@Constructor
|
765
771
|
@param address The address of the bokeh server.
|
766
772
|
@param bokehPort The TCP port to run the server on. If left at the default
|
767
773
|
of 0 then a spare TCP port will be used.
|
768
774
|
@param credentialsJsonFile A file that contains the json formatted hashed (via argon2) login credentials.
|
775
|
+
@param accessLogFile The log file to record access to. If left as None then no logging occurs.
|
769
776
|
"""
|
770
777
|
if bokehPort == 0:
|
771
778
|
bokehPort = MultiAppServer.GetNextUnusedPort()
|
@@ -774,6 +781,7 @@ class MultiAppServer(object):
|
|
774
781
|
os.environ[MultiAppServer.BOKEH_ALLOW_WS_ORIGIN]=wsOrigin
|
775
782
|
self._credentialsJsonFile = credentialsJsonFile
|
776
783
|
self._loginHTMLFile = loginHTMLFile
|
784
|
+
self._accessLogFile = accessLogFile
|
777
785
|
|
778
786
|
def getServerPort(self):
|
779
787
|
"""@return The bokeh server port."""
|
@@ -803,19 +811,21 @@ class MultiAppServer(object):
|
|
803
811
|
evtLoop = asyncio.new_event_loop()
|
804
812
|
asyncio.set_event_loop(evtLoop)
|
805
813
|
if self._credentialsJsonFile:
|
806
|
-
SetBokehAuthAttrs(self._credentialsJsonFile,
|
814
|
+
SetBokehAuthAttrs(self._credentialsJsonFile,
|
815
|
+
self._loginHTMLFile,
|
816
|
+
accessLogFile=self._accessLogFile)
|
807
817
|
# We don't check the credentials hash file exists as this should have been
|
808
818
|
# done at a higher level. We assume that web server authoristion is required.
|
809
819
|
selfPath = os.path.dirname(os.path.abspath(__file__))
|
810
820
|
authFile = os.path.join(selfPath, "bokeh_auth.py")
|
811
821
|
authModule = AuthModule(authFile)
|
812
|
-
self._server = Server(appDict,
|
822
|
+
self._server = Server(appDict,
|
813
823
|
address=self._address,
|
814
824
|
port=self._bokehPort,
|
815
825
|
auth_provider=authModule)
|
816
826
|
|
817
827
|
else:
|
818
|
-
self._server = Server(appDict,
|
828
|
+
self._server = Server(appDict,
|
819
829
|
address=self._address,
|
820
830
|
port=self._bokehPort)
|
821
831
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: p3lib
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.67
|
4
4
|
Summary: A group of python modules for networking, plotting data, config storage, automating boot scripts, ssh access and user input output.
|
5
5
|
Home-page: https://github.com/pjaos/p3lib
|
6
6
|
Author: Paul Austen
|
@@ -1,7 +1,7 @@
|
|
1
1
|
p3lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
p3lib/ate.py,sha256=_BiqMUYNAlp4O8MkP_PAUe62Bzd8dzV4Ipv62OFS6Ok,4759
|
3
|
-
p3lib/bokeh_auth.py,sha256=
|
4
|
-
p3lib/bokeh_gui.py,sha256=
|
3
|
+
p3lib/bokeh_auth.py,sha256=XqdCLOalHL3dkyrMPqQoVQBSziVnqlfjB9YAWhZUd_U,14769
|
4
|
+
p3lib/bokeh_gui.py,sha256=MNapLctSncZ9YyKGzNcUICMwpI5_h7a7bH3QdMd2UxI,40393
|
5
5
|
p3lib/boot_manager.py,sha256=-kbfYbFpO-ktKv_heUgYdvvIQrntfCQ7pBcPWqS3C0s,12551
|
6
6
|
p3lib/conduit.py,sha256=jPkjdtyCx2I6SFqcEo8y2g7rgnZ-jNY7oCuYIETzT5Q,6046
|
7
7
|
p3lib/database_if.py,sha256=mgTdSeCTQs1V10E7KuyK3xRkdaU3RXrqYpMQvosrA_M,11897
|
@@ -13,8 +13,8 @@ p3lib/netplotly.py,sha256=PMDx-w1jtRVW6Od5u_kuKbBxNpTS_Y88mMF60puMxLM,9363
|
|
13
13
|
p3lib/pconfig.py,sha256=_ri9w3aauHXZp8u2YLYHBVroFR_iCqaTCwj_MRa3rHo,30153
|
14
14
|
p3lib/ssh.py,sha256=YE1ddgiEt9EeBM8evuxCV8Gnj4jz1Sv52vEKidvjrJA,38369
|
15
15
|
p3lib/uio.py,sha256=hMarPnYXnqVF23HUIeDfzREo7TMdBjrupXMY_ffuCbI,23133
|
16
|
-
p3lib-1.1.
|
17
|
-
p3lib-1.1.
|
18
|
-
p3lib-1.1.
|
19
|
-
p3lib-1.1.
|
20
|
-
p3lib-1.1.
|
16
|
+
p3lib-1.1.67.dist-info/LICENSE,sha256=igqTy5u0kVWM1n-NUZMvAlinY6lVjAXKoag0okkS8V8,1067
|
17
|
+
p3lib-1.1.67.dist-info/METADATA,sha256=3SZVksgoZrtT2NwfrYK0Q2VOq6Yh_j31A9pYB7042y8,920
|
18
|
+
p3lib-1.1.67.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
19
|
+
p3lib-1.1.67.dist-info/top_level.txt,sha256=SDCpXYh-19yCFp4Z8ZK4B-3J4NvTCJElZ42NPgcR6-U,6
|
20
|
+
p3lib-1.1.67.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|