pywebexec 1.7.2__py3-none-any.whl → 1.7.4__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.
pywebexec/pywebexec.py CHANGED
@@ -33,10 +33,12 @@ if os.environ.get('PYWEBEXEC_LDAP_SERVER'):
33
33
  app = Flask(__name__)
34
34
  app.secret_key = os.urandom(24) # Secret key for session management
35
35
  app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Add SameSite attribute to session cookies
36
+ app.config['SESSION_COOKIE_SECURE'] = True
37
+ app.config['SESSION_COOKIE_HTTPONLY'] = True
36
38
  auth = HTTPBasicAuth()
37
39
 
38
40
  app.config['LDAP_SERVER'] = os.environ.get('PYWEBEXEC_LDAP_SERVER')
39
- app.config['LDAP_USER_ID'] = os.environ.get('PYWEBEXEC_LDAP_USER_ID', "uid")
41
+ app.config['LDAP_USER_ID'] = os.environ.get('PYWEBEXEC_LDAP_USER_ID', "uid") # sAMAccountName
40
42
  app.config['LDAP_GROUPS'] = os.environ.get('PYWEBEXEC_LDAP_GROUPS')
41
43
  app.config['LDAP_BASE_DN'] = os.environ.get('PYWEBEXEC_LDAP_BASE_DN')
42
44
  app.config['LDAP_BIND_DN'] = os.environ.get('PYWEBEXEC_LDAP_BIND_DN')
@@ -112,7 +114,7 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
112
114
  from cryptography.hazmat.backends import default_backend
113
115
  from cryptography.hazmat.primitives import serialization
114
116
  from cryptography.hazmat.primitives.asymmetric import rsa
115
-
117
+
116
118
  # Generate our key
117
119
  if key is None:
118
120
  key = rsa.generate_private_key(
@@ -120,16 +122,16 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
120
122
  key_size=2048,
121
123
  backend=default_backend(),
122
124
  )
123
-
125
+
124
126
  name = x509.Name([
125
127
  x509.NameAttribute(NameOID.COMMON_NAME, hostname)
126
128
  ])
127
-
128
- # best practice seem to be to include the hostname in the SAN, which *SHOULD* mean COMMON_NAME is ignored.
129
+
130
+ # best practice seem to be to include the hostname in the SAN, which *SHOULD* mean COMMON_NAME is ignored.
129
131
  alt_names = [x509.DNSName(hostname)]
130
132
  alt_names.append(x509.DNSName("localhost"))
131
-
132
- # allow addressing by IP, for when you don't have real DNS (common in most testing scenarios
133
+
134
+ # allow addressing by IP, for when you don't have real DNS (common in most testing scenarios
133
135
  if ip_addresses:
134
136
  for addr in ip_addresses:
135
137
  # openssl wants DNSnames for ips...
@@ -138,7 +140,7 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
138
140
  # note: older versions of cryptography do not understand ip_address objects
139
141
  alt_names.append(x509.IPAddress(ipaddress.ip_address(addr)))
140
142
  san = x509.SubjectAlternativeName(alt_names)
141
-
143
+
142
144
  # path_len=0 means this cert can only sign itself, not other certs.
143
145
  basic_contraints = x509.BasicConstraints(ca=True, path_length=0)
144
146
  now = datetime.now(timezone.utc)
@@ -225,7 +227,7 @@ def get_last_line(file_path, cols=None, rows=None, maxsize=2048):
225
227
  except OSError:
226
228
  fd.seek(0)
227
229
  return get_visible_output(fd.read(), cols, rows)
228
-
230
+
229
231
 
230
232
  def start_gunicorn(daemonized=False, baselog=None):
231
233
  check_processes()
@@ -304,7 +306,7 @@ def daemon_d(action, pidfilepath, silent=False, hostname=None, args=None):
304
306
  def start_term():
305
307
  os.environ["PYWEBEXEC"] = " (shared)"
306
308
  os.chdir(CWD)
307
- start_time = datetime.now().isoformat()
309
+ start_time = datetime.now(timezone.utc).isoformat()
308
310
  user = pwd.getpwuid(os.getuid())[0]
309
311
  print(f"Starting terminal session for {user} : {term_command_id}")
310
312
  update_command_status(term_command_id, {
@@ -316,7 +318,7 @@ def start_term():
316
318
  })
317
319
  output_file_path = get_output_file_path(term_command_id)
318
320
  res = script(output_file_path)
319
- end_time = datetime.now().isoformat()
321
+ end_time = datetime.now(timezone.utc).isoformat()
320
322
  update_command_status(term_command_id, {
321
323
  'status': 'success',
322
324
  'end_time': end_time,
@@ -488,7 +490,7 @@ def script(output_file):
488
490
  sigwinch_passthrough(None, None)
489
491
  signal.signal(signal.SIGWINCH, sigwinch_passthrough)
490
492
  p.interact()
491
-
493
+
492
494
 
493
495
  def run_command(fromip, user, command, params, command_id):
494
496
  # app.logger.info(f'{fromip} run_command {command_id} {user}: {command} {params}')
@@ -593,6 +595,7 @@ def read_commands():
593
595
  'start_time': status.get('start_time', 'N/A'),
594
596
  'end_time': status.get('end_time', 'N/A'),
595
597
  'command': command,
598
+ 'user': status.get('user'),
596
599
  'exit_code': status.get('exit_code', 'N/A'),
597
600
  'last_output_line': status.get('last_output_line'),
598
601
  })
@@ -662,7 +665,7 @@ def check_authentication():
662
665
  if request.args.get('token') == token:
663
666
  return
664
667
  return jsonify({'error': 'Forbidden'}), 403
665
-
668
+
666
669
  if not app.config['USER'] and not app.config['LDAP_SERVER']:
667
670
  return
668
671
 
@@ -687,30 +690,30 @@ def verify_ldap(username, password):
687
690
  tls_configuration = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2) if app.config['LDAP_SERVER'].startswith("ldaps:") else None
688
691
  server = Server(app.config['LDAP_SERVER'], tls=tls_configuration, get_info=ALL)
689
692
  user_filter = f"({app.config['LDAP_USER_ID']}={username})"
693
+ group_filter = ""
694
+ if app.config["LDAP_GROUPS"]:
695
+ group_filter = "".join(f"(memberOf={group})" for group in app.config['LDAP_GROUPS'].split(" "))
696
+ group_filter = f"(|{group_filter})"
697
+ ldap_filter = f"(&(objectClass=person){user_filter}{group_filter})"
690
698
  try:
691
699
  # Bind with the bind DN and password
692
700
  conn = Connection(server, user=app.config['LDAP_BIND_DN'], password=app.config['LDAP_BIND_PASSWORD'], authentication=SIMPLE, auto_bind=True, read_only=True)
693
701
  try:
694
- conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=user_filter, search_scope=SUBTREE)
702
+ conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=ldap_filter, search_scope=SUBTREE)
695
703
  if len(conn.entries) == 0:
696
- print(f"User {username} not found in LDAP.")
704
+ print(f"User {username} not found in LDAP in allowed groups.")
697
705
  return False
698
706
  user_dn = conn.entries[0].entry_dn
699
707
  finally:
700
708
  conn.unbind()
701
-
709
+
702
710
  # Bind with the user DN and password to verify credentials
703
711
  conn = Connection(server, user=user_dn, password=password, authentication=SIMPLE, auto_bind=True, read_only=True)
704
712
  try:
705
- if not app.config['LDAP_GROUPS'] and conn.result["result"] == 0:
713
+ if conn.result["result"] == 0:
706
714
  return True
707
- group_filter = "".join([f'({group})' for group in app.config['LDAP_GROUPS'].split(",")])
708
- group_filter = f"(&{group_filter}(|(member={user_dn})(uniqueMember={user_dn})))"
709
- conn.search(search_base=app.config['LDAP_BASE_DN'], search_filter=group_filter, search_scope=SUBTREE)
710
- result = len(conn.entries) > 0
711
- if not result:
712
- print(f"User {username} is not a member of groups {app.config['LDAP_GROUPS']}.")
713
- return result
715
+ print(f"{username}: Password mismatch")
716
+ return False
714
717
  finally:
715
718
  conn.unbind()
716
719
  except Exception as e:
@@ -117,7 +117,7 @@ async function fetchOutput(url) {
117
117
  try {
118
118
  const response = await fetch(url);
119
119
  if (!response.ok) {
120
- document.getElementById('dimmer').style.display = 'none';
120
+ document.getElementById('dimmer').style.display = 'block';
121
121
  return;
122
122
  }
123
123
  const data = await response.json();
@@ -151,6 +151,7 @@ async function fetchOutput(url) {
151
151
  toggleButton.style.display = 'block';
152
152
  setCommandStatus(data.status)
153
153
  }
154
+ document.getElementById('dimmer').style.display = 'none';
154
155
  }
155
156
  } catch (error) {
156
157
  document.getElementById('dimmer').style.display = 'block';
@@ -186,7 +186,7 @@ async function fetchCommands(hide=false) {
186
186
  <td>
187
187
  ${command.command.startsWith('term') ? '' : command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}', event)">Stop</button>` : `<button onclick="relaunchCommand('${command.command_id}', event)">Run</button>`}
188
188
  </td>
189
- <td class="system-font">${command.command.replace(/^\.\//, '')}</td>
189
+ <td class="system-font" title="${command.user == '-' ? '' : command.user}">${command.command.replace(/^\.\//, '')}</td>
190
190
  <td class="monospace outcol">
191
191
  <button class="popup-button" onclick="openPopup('${command.command_id}', event)"></button>
192
192
  ${command.last_output_line || ''}
@@ -361,7 +361,7 @@ async function stopCommand(command_id, event) {
361
361
  function formatTime(time) {
362
362
  if (!time || time === 'N/A') return 'N/A';
363
363
  const date = new Date(time);
364
- return date.toISOString().slice(0, 16).replace('T', ' ');
364
+ return date.toLocaleString().slice(0, 16).replace('T', ' ');
365
365
  }
366
366
 
367
367
  function formatDuration(startTime, endTime) {
pywebexec/version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.7.2'
16
- __version_tuple__ = version_tuple = (1, 7, 2)
15
+ __version__ = version = '1.7.4'
16
+ __version_tuple__ = version_tuple = (1, 7, 4)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 1.7.2
3
+ Version: 1.7.4
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -153,13 +153,14 @@ $ pywebexec -u myuser [-P mypass]
153
153
  Generated password is given if no `--pasword` option
154
154
 
155
155
  * ldap(s) password check / group member
156
+ ldap server must accept memberOf attribute for group members
156
157
  ```shell
157
- $ export PYWEBEXEC_LDAP_SERVER=ldap://ldap.forumsys.com:389
158
+ $ export PYWEBEXEC_LDAP_SERVER=ldaps://ldap.mydomain.com:389
158
159
  $ export PYWEBEXEC_LDAP_BIND_DN="cn=read-only-admin,dc=example,dc=com"
159
160
  $ export PYWEBEXEC_LDAP_BIND_PASSWORD="password"
160
- $ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,ou=scientists"
161
- $ export PYWEBEXEC_LDAP_USER_ID="uid"
162
161
  $ export PYWEBEXEC_LDAP_BASE_DN="dc=example,dc=com"
162
+ $ export PYWEBEXEC_LDAP_USER_ID="uid" # sAMAccountName for AD
163
+ $ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,dc=example,dc=com ou=scientists,dc=example,dc=com"
163
164
  $ pywebexec
164
165
  ```
165
166
  ## HTTPS server
@@ -1,6 +1,6 @@
1
1
  pywebexec/__init__.py,sha256=4spIsVaF8RJt8S58AG_wWoORRNkws9Iwqprj27C3ljM,99
2
- pywebexec/pywebexec.py,sha256=gJsI7kob3IyB6DVoWWSyEHOcxpWlsZz-b7jQUyePA-A,33555
3
- pywebexec/version.py,sha256=Qj1rYkRQ0hSV6NX03bgvPhjsn6HWrQ8maBXi446C5ZM,411
2
+ pywebexec/pywebexec.py,sha256=VBR6KmRiHNCd0ZvGEu_5sTqzVrUEPBhM8Hi6knS1QcA,33526
3
+ pywebexec/version.py,sha256=RXCVfb--6hzsLq9eHm4YOs9d-j0v74-HRGs2DFWnpjQ,411
4
4
  pywebexec/static/css/Consolas NF.ttf,sha256=DJEOzF0eqZ-kxu3Gs_VE8X0NJqiobBzmxWDGpdgGRxI,1313900
5
5
  pywebexec/static/css/style.css,sha256=3s7QgbCh4wb7kfZ7Pjo-B6o3lDIBogZ-3j6AfaPdpzU,8209
6
6
  pywebexec/static/css/xterm.css,sha256=uo5phWaUiJgcz0DAzv46uoByLLbJLeetYosL1xf68rY,5559
@@ -21,8 +21,8 @@ pywebexec/static/images/resume.svg,sha256=99LP1Ya2JXakRCO9kW8JMuT_4a_CannF65Eiuw
21
21
  pywebexec/static/images/running.svg,sha256=fBCYwYb2O9K4N3waC2nURP25NRwZlqR4PbDZy6JQMww,610
22
22
  pywebexec/static/images/success.svg,sha256=NVwezvVMplt46ElW798vqGfrL21Mw_DWHUp_qiD_FU8,489
23
23
  pywebexec/static/js/commands.js,sha256=h2fkd9qpypLBxvhEEbay23nwuqUwcKJA0vHugcyL8pU,7961
24
- pywebexec/static/js/popup.js,sha256=UPKtjPLIR5KaAV2i1iWUGwcCTX1tT3vnOIMc8VKQqag,9311
25
- pywebexec/static/js/script.js,sha256=W5NfzFYdyfQpALx_bjeppQFVWxvZGMoVTTq0VOnITxw,18147
24
+ pywebexec/static/js/popup.js,sha256=oVkwnOKHae11omBiTZs-00NVeS9HGNEiUo71JSwOqn4,9382
25
+ pywebexec/static/js/script.js,sha256=hT50gLRs-GRQoxDNJN__X3UQxk003M1ZPlvmzj8BZtc,18201
26
26
  pywebexec/static/js/xterm/LICENSE,sha256=EU1P4eXTull-_T9I80VuwnJXubB-zLzUl3xpEYj2T1M,1083
27
27
  pywebexec/static/js/xterm/addon-canvas.js,sha256=ez6QTVvsmLVNJmdJlM-ZQ5bErwlxAQ_9DUmDIptl2TM,94607
28
28
  pywebexec/static/js/xterm/addon-canvas.js.map,sha256=ECBA4B-BqUpdFeRzlsEWLSQnudnhLP-yPQJ8_hKquMo,379537
@@ -35,9 +35,9 @@ pywebexec/static/js/xterm/xterm.js.map,sha256=Y7O2Pb-fIS7Z8AC1D5s04_aiW_Jf1f4mCf
35
35
  pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  pywebexec/templates/index.html,sha256=2fEN8cggHBEd8-RamDFpnekVJtIbRembFSw0-1YEptc,2979
37
37
  pywebexec/templates/popup.html,sha256=GT2jY7oOxpCaBaRl924QJWdFBmfSOP952T13d37R_pY,1506
38
- pywebexec-1.7.2.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
39
- pywebexec-1.7.2.dist-info/METADATA,sha256=NudV7MvLxsTsIZzpTjzNuLJNijAmoK0C9o5VDIg0oRI,8000
40
- pywebexec-1.7.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
41
- pywebexec-1.7.2.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
42
- pywebexec-1.7.2.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
43
- pywebexec-1.7.2.dist-info/RECORD,,
38
+ pywebexec-1.7.4.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
39
+ pywebexec-1.7.4.dist-info/METADATA,sha256=Cui8J6P2uQXqzD43UuHSypY2oQCrp3palZ34daD_Gfg,8122
40
+ pywebexec-1.7.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
41
+ pywebexec-1.7.4.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
42
+ pywebexec-1.7.4.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
43
+ pywebexec-1.7.4.dist-info/RECORD,,