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 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
- !!! This contains a mix of pep8 and camel case method names !!!
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, address="0.0.0.0", bokehPort=0, wsOrigin="*:*", credentialsJsonFile=None, loginHTMLFile="login.html"):
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, self._loginHTMLFile)
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.65
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=auo-OPYQYKhwQW_-_9flJX1wBFsXRn3FoN0TOV6w1s4,11496
4
- p3lib/bokeh_gui.py,sha256=A9I7jLts6BeeaRw45vRq5_xGv6rPMMZHzFBJzpZTXm0,40028
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.65.dist-info/LICENSE,sha256=igqTy5u0kVWM1n-NUZMvAlinY6lVjAXKoag0okkS8V8,1067
17
- p3lib-1.1.65.dist-info/METADATA,sha256=UJoqf3nq2nMnc4ARO3HFoKWP3mf9Flv8P39PWXeVQb0,920
18
- p3lib-1.1.65.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
19
- p3lib-1.1.65.dist-info/top_level.txt,sha256=SDCpXYh-19yCFp4Z8ZK4B-3J4NvTCJElZ42NPgcR6-U,6
20
- p3lib-1.1.65.dist-info/RECORD,,
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