1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-10-06 11:33:43 +03:00

400 Commits

Author SHA1 Message Date
Adolfo Gómez García
a1cd0dc047 Added a couple of tests on using certificates with ldap (due to gnu-tls or openssl possible usage) 2023-07-22 02:21:48 +02:00
Adolfo Gómez García
bd7faf7867 Small comment on ticket length for tunnel 2023-07-18 21:22:58 +02:00
Adolfo Gómez García
708c54878f Fixed restricted mode from ticket launcher 2023-07-13 18:46:17 +02:00
Adolfo Gómez García
18c0d0cb9a Small fix 2023-07-13 15:43:43 +02:00
Adolfo Gómez García
53c8d0fbc9 comment to proxmox client 2023-07-13 14:58:46 +02:00
Adolfo Gómez García
03b068a64e Fixed VGPus check on proxmox 2023-07-13 14:57:37 +02:00
Adolfo Gómez García
667deb7011 Fixed ciphers for ENS compat 2023-07-05 15:24:14 +02:00
Adolfo Gómez García
a9eae0e2a3 small fix to error pages 2023-07-04 00:47:38 +02:00
Adolfo Gómez García
3d1f046c1a Fixed default error messages 2023-07-04 00:42:15 +02:00
Adolfo Gómez García
78af8b262f Fixed adding machine to AD group (now, python ldap seems to require a list of byte strings) 2023-06-29 14:47:56 +02:00
Adolfo Gómez García
cba5fca237 libldap < 2.5 does not have PROTOCOL_TLS1.3 constant defined, fixed 2023-06-27 15:22:59 +02:00
Adolfo Gómez García
7d8d575e2e Added LDAP ciphers changing (GNU-TLS ciphers)
Removed password of configuration being sent to Admin Interface when editing config (not a real problem, but it is not used anyway
Updated default settings
2023-06-26 18:15:38 +02:00
Adolfo Gómez García
9b4827453c securing ldap connections a bit more 2023-06-24 19:03:40 +02:00
Adolfo Gómez García
3073ac0cc0 Reduced the valid certificates to secure ones on ldaps connections 2023-06-24 17:48:28 +02:00
Adolfo Gómez García
35f27cbbd9 updated tranlations 2023-06-23 16:27:31 +02:00
Adolfo Gómez García
f1ed1f5cf4 Updated compiled gui to show "remember" button on MFA 2023-06-23 15:24:14 +02:00
Adolfo Gómez García
09e9d70e08 update pomf for guacamole auth module for uds 2023-06-21 22:13:14 +02:00
Adolfo Gómez García
d6691908b3 Clean VERY OLD commentary that was not true since a long ago.. :) 2023-06-21 15:07:07 +02:00
Adolfo Gómez García
f2fa4de21d Typo fix (repeated import) 2023-06-21 15:05:36 +02:00
Adolfo Gómez García
3110d070e6 Fixed ticket and federated login not logged on auth.log 2023-06-21 15:00:14 +02:00
Adolfo Gómez García
16e0064d56 Improving support for VGPUs on proxmox 2023-06-20 19:32:39 +02:00
Adolfo Gómez García
20e44615e4 fixed processes runner for tunnel when not using uvloop as even loop (was not using ANY event loop!!!). Thanks @Future998 for helping! :) 2023-06-13 21:41:07 +02:00
Adolfo Gómez García
cae457a85e upgraded regexldap field processor to admit:
* : as a "prependable" to the attribute value
* +, to join several fields (must be fields with 0 o 1 elements only) into an single value
2023-06-13 17:04:50 +02:00
Adolfo Gómez García
b3a7718942 PRoxmox Works 2023-06-09 14:46:49 +02:00
Adolfo Gómez García
35c375e3bd Fixed "required" on checkboxes admin dashboard 2023-06-08 17:09:08 +02:00
Adolfo Gómez García
2b21b22fe8 Updated Translations 2023-06-08 04:18:02 +02:00
Adolfo Gómez García
2f246d49b9 Small fix for checking if "re-notify" preconnect 2023-06-07 23:28:55 +02:00
Adolfo Gómez García
1c01e9aba0 added recall preconnect if we have the username 2023-06-06 15:04:49 +02:00
Adolfo Gómez García
d135e4e1e2 simple comment fix¡ 2023-06-05 15:22:37 +02:00
Adolfo Gómez García
756ac04d90 added new ldaputil method to get rootds 2023-06-02 16:39:36 +02:00
Adolfo Gómez García
1d58ffb3b3 added gpu availability check on proxmox 2023-05-30 17:53:28 +02:00
Adolfo Gómez García
0363ac3a6a added /rfx /rfx:gfx to macos by default 2023-05-23 03:15:34 +02:00
Adolfo Gómez García
45a4dec18f Fix tunnel removal procedure on Mac Launcher 2023-05-23 02:25:35 +02:00
Adolfo Gómez García
23a9465ae1 removed micro on mac rdp (not working on most cases...) 2023-05-22 13:21:31 +02:00
Adolfo Gómez García
084e0cc2a0 fixed bad ssl handshake management 2023-05-21 16:49:04 +02:00
Adolfo Gómez García
2c77d361d7 backported 4.0 version improvements 2023-05-21 16:23:18 +02:00
Adolfo Gómez García
392cb6e406 updated tranlations 2023-05-17 14:36:45 +02:00
Adolfo Gómez García
4df4892111 removed DEPRECATED from simple ldap 2023-05-17 14:26:29 +02:00
Adolfo Gómez García
8e4615de19 re-fixed mac tls version for newer python 2023-05-17 02:01:09 +02:00
Adolfo Gómez García
d8ad7ddd22 moved tools imports to top level 2023-05-17 01:46:00 +02:00
Adolfo Gómez García
4d26df9580 upgrading and linting tunnel 2023-05-16 01:09:32 +02:00
Adolfo Gómez García
ddf07eb68b fixed tunnel timeout check BEFORE transport connected 2023-05-12 15:39:25 +02:00
admin
ba28ab78ed added fixes for ssl support on mac os 2023-05-12 14:11:29 +02:00
Adolfo Gómez García
e42ab76088 removed unused imports 2023-05-11 21:23:26 +02:00
Adolfo Gómez García
d72723d6f2 Improved connection tunnel timeout 2023-05-11 21:22:56 +02:00
Adolfo Gómez García
8feef1d3f9 Fixed installer permissions fod UDSClient 2023-05-11 16:36:26 +02:00
Adolfo Gómez García
0c2ee7906b Fixed installer permissions fod UDSClient 2023-05-11 16:35:34 +02:00
Adolfo Gómez García
8891da5987 Fixed notify action 2023-05-10 23:22:56 +02:00
Adolfo Gómez García
8c9b326c3c added keep_listening parameter for future and initial_payload (fix) 2023-05-10 16:45:39 +02:00
Adolfo Gómez García
db70f02df0 added keep_listening parameter for future and initial_payload (fix) 2023-05-09 18:24:55 +02:00
Adolfo Gómez García
f502f4ceb9 added keep_listening parameter for future and initial_payload (fix) 2023-05-09 14:49:36 +02:00
Adolfo Gómez García
a2bfcd3d5a fixed "time" parameter from ticket (missing on check) 2023-05-09 14:21:13 +02:00
Adolfo Gómez García
0f41544830 added keep_listening parameter for future and initial_payload 2023-05-09 14:00:35 +02:00
Adolfo Gómez García
eaa05ead0b fixed homepage and copyright info 2023-05-05 20:05:51 +02:00
Adolfo Gómez García
2d6a381321 small typo fix 2023-05-05 00:43:24 +02:00
Adolfo Gómez García
84b0bd1de2 fixed tunnel info on connect 2023-05-04 15:33:19 +02:00
Adolfo Gómez García
0aeb9b923f fixed tunnel ouput 2023-05-04 15:20:24 +02:00
Adolfo Gómez García
55ed118ae9 fixed tunnel ouput 2023-05-04 15:18:31 +02:00
Adolfo Gómez García
c4690a25bb added TLS version to log 2023-05-04 15:12:25 +02:00
Adolfo Gómez García
902f838178 Fixed timeout on test command and added sample min_tls_version to config 2023-05-04 14:45:58 +02:00
Adolfo Gómez García
36a4f9a68b recovered mic for xfreerdp on mac 2023-05-03 21:59:09 +02:00
Adolfo Gómez García
59141a9f03 fixed igel init script (error setting perms to 777) 2023-05-03 15:28:24 +02:00
Adolfo Gómez García
7a6c5966d9 fixed igel init script 2023-05-03 15:07:17 +02:00
Adolfo Gómez García
48aec57256 recovered "azuread" logging compat 2023-04-25 13:08:19 +02:00
Adolfo Gómez García
e81982dd41 make clients prior to 3.5 not supported in any case 2023-04-24 14:17:28 +02:00
Adolfo Gómez García
c8982cf677 Removed ceil creating function on sqlite 2023-04-21 00:46:06 +02:00
Adolfo Gómez García
9b4d1139d1 Converted username to lowercase for internal db 2023-04-20 13:31:55 +02:00
Adolfo Gómez García
4756437d9f upgraded guacamole auth component 2023-04-18 10:45:11 +02:00
Adolfo Gómez García
9e61d142e2 upgraded guacamole auth component 2023-04-18 10:43:59 +02:00
Adolfo Gómez García
d98be68d96 fixed UDS client removing http references 2023-04-17 17:28:13 +02:00
Adolfo Gómez García
4c759c3367 removed http support from uds client 2023-04-17 13:24:38 +02:00
Adolfo Gómez García
e70146fad6 removed http support 2023-04-17 13:15:43 +02:00
Adolfo Gómez García
c7e1f36cb3 added exception logging on getTransportData and forced TLS to 1.3 on UDS tunnel 2023-04-15 13:52:18 +02:00
Adolfo Gómez García
f78053fc0c Fixed Tuneled Spice transport and resigned everything 2023-04-13 13:27:57 +02:00
Adolfo Gómez García
e15746b4a4 Fixed SPICE tunnel 2023-04-13 13:11:38 +02:00
Adolfo Gómez García
14dd5aca64 fixed minor bugs on install and actor comms 2023-04-10 18:06:00 +02:00
Adolfo Gómez García
7bf4859399 Minor security enhacements (moved some vars to settings' 2023-04-10 13:51:48 +02:00
Adolfo Gómez García
846f9225f1 Moved all requests calls through secureRequestsSession call 2023-04-07 01:45:53 +02:00
Adolfo Gómez García
09c44ac0b6 adding secure context to requests (so we can manipulate ssl context better and centralized) 2023-04-07 01:08:48 +02:00
Adolfo Gómez García
9db8e8d7ec refix 2023-04-06 17:29:04 +02:00
Adolfo Gómez García
15bc3a0b6f Small improvements to security utils module 2023-04-06 17:18:42 +02:00
Adolfo Gómez García
321255a1b0 Changed redirection from redirect to permanent redirect on HTTP use 2023-04-04 18:55:05 +02:00
Adolfo Gómez García
59d578f292 Addoed some logs to UDS operations and fixed udsactor ssl verification 2023-04-04 14:43:28 +02:00
Adolfo Gómez García
ea343659ff Small ldaputil fix (moved newctx to last ssl instruction before connect) 2023-04-04 01:51:46 +02:00
Adolfo Gómez García
636b72a471 Small fix to requests sessions context creation 2023-04-03 22:53:32 +02:00
Adolfo Gómez García
31104c3fc2 Fixed actor client minimum version and ciphers 2023-04-03 22:46:59 +02:00
Adolfo Gómez García
9d9a764a81 Fixed Actor Server Ciphers 2023-04-03 22:43:46 +02:00
Adolfo Gómez García
8aa04c6a9c Several security improvements:
* Reduced supported actor lists ciphers to only one (AES256-GCM)
* Removed actor client support for protocols previous to 1.3.
   Only user cipher ECDHE-RSA-AES256-GCM-SHA384
2023-04-03 15:35:38 +02:00
Adolfo Gómez García
1380cbde3e added sequre requests to actors 2023-04-03 15:13:42 +02:00
Adolfo Gómez García
0ac4fe60a8 Refactoring cert to security 2023-04-03 14:38:23 +02:00
Adolfo Gómez García
9d5e983847 removed support for tls1.0&tls1.1 for ldap client on UDS, backported from 4.0 2023-04-03 14:04:35 +02:00
Adolfo Gómez García
c11ea77f22 Small type check fix 2023-04-02 04:32:53 +02:00
Adolfo Gómez García
6cbb497902 Minor fixes (type related and comments) 2023-04-01 13:29:28 +02:00
Adolfo Gómez García
8fc9495d5e removed support for TLS1.0 and TLS1.1 2023-03-31 16:31:02 +02:00
Adolfo Gómez García
d43167707c Removed TLS1.0 & TLS1.1 support 2023-03-31 16:22:02 +02:00
Adolfo Gómez García
3fc86482dc Some minor typing fixes 2023-03-31 15:41:59 +02:00
Adolfo Gómez García
7eaf0c8126 updated tranlations 2023-03-30 16:00:16 +02:00
Adolfo Gómez García
df0e1bde96 added Email "justUsername" and now redirects to https is by default True, as a previos step forward removing Service 2023-03-30 15:15:06 +02:00
Adolfo Gómez García
14a8f1f5e1 fixed return button of error page 2023-03-29 16:18:40 +02:00
Adolfo Gómez García
8f132e7524 fixing up SMS mfa and related 2023-03-29 16:11:01 +02:00
Adolfo Gómez García
ab7b4c78ef Fixed SMS auth 2023-03-29 14:22:17 +02:00
Adolfo Gómez García
f5af2b12d2 Improved counter mechanics 2023-03-29 01:37:12 +02:00
Adolfo Gómez García
f11da32f0d backported fixes for uds tunnel (timeout, stats accounting and stop mechanics) 2023-03-29 00:04:45 +02:00
Adolfo Gómez García
55b8763f72 updated testing sample keys 2023-03-28 23:15:53 +02:00
Adolfo Gómez García
5694420f89 small comment fix to tickets 2023-03-28 17:53:43 +02:00
Adolfo Gómez García
76f7b36508 Removed duplicated default dipher on uds actor server part 2023-03-27 14:18:35 +02:00
Adolfo Gómez García
2269f8c770 mergin 2023-03-25 17:50:10 +01:00
Adolfo Gómez García
6f4d84a08e added csrftoken from cookie 2023-03-25 17:45:10 +01:00
Adolfo Gómez García
b983d5d409 More typing fixes 2023-03-25 13:43:54 +01:00
Adolfo Gómez García
562e9201c8 Added typing correction on 3.6 version 2023-03-25 13:19:28 +01:00
Adolfo Gómez García
e2814f2674 small text fix 2023-03-24 15:05:14 +01:00
Adolfo Gómez García
ef9a0ce0b2 Added time limit for pool removal 2023-03-24 15:01:00 +01:00
Adolfo Gómez García
77bc47671e Added support for checking certificate is from private key 2023-03-23 01:51:51 +01:00
Adolfo Gómez García
f7886abfbc fixed simpleldap mfa support 2023-03-22 02:47:17 +01:00
Adolfo Gómez García
5c9dd741d3 Added certificate check support for all ldap based auths 2023-03-22 02:33:27 +01:00
Adolfo Gómez García
0ba381dbc4 small addong to certs 2023-03-21 16:39:59 +01:00
Adolfo Gómez García
8abe2ad31b Fixed ssh help strings 2023-03-21 15:02:21 +01:00
Adolfo Gómez García
4386c5567a fixed Client to correctly handle versions upgrades 2023-03-17 14:09:08 +01:00
Adolfo Gómez García
5da71a4f6e added upload/download to ssh 2023-03-16 16:24:09 +01:00
Adolfo Gómez García
6bb4c3bd5e Added Support for SSH through HTML5 2023-03-16 15:18:20 +01:00
Adolfo Gómez García
b9a01e686f updated translations 2023-03-15 04:11:43 +01:00
Adolfo Gómez García
011223ec05 upgraded admin interface 2023-03-14 13:15:43 +01:00
Adolfo Gómez García
a12aa1f3d4 fixed auth label re 2023-03-13 18:05:18 +01:00
Adolfo Gómez García
666b982c50 fixed authenticator label not allowing dot in name 2023-03-13 18:00:47 +01:00
Adolfo Gómez García
cf1048afcb Now Ciphers can be pushed by uds server to UDS Actor, so we can addapt Actor ciphers without reinstalling if needed 2023-03-08 21:48:48 +01:00
Adolfo Gómez García
7985f44389 HTTPS UDS Actor server supports only TLSv1.3, and upgraded certificate key length to 4096 bits 2023-03-08 15:47:38 +01:00
Adolfo Gómez García
4517b781cf now onwards uds client 3.5 will not be compatible with UDS 2023-03-08 14:24:10 +01:00
Adolfo Gómez García
9de5387fd6 Make verify certificate default on dropdown 2023-03-08 11:02:09 +01:00
Adolfo Gómez García
304f5dd686 Removed old UDS client versions support. Nonsense 2023-03-08 10:06:30 +01:00
Adolfo Gómez García
2699c090f8 added certificate verify to ldaps 2023-03-08 02:10:14 +01:00
Adolfo Gómez García
43e1353154 added ssl verify support to ldaps 2023-03-08 01:18:34 +01:00
Adolfo Gómez García
ba5be7e2fb Fixed check time for openstack 2023-03-03 15:43:31 +01:00
Adolfo Gómez García
62a401f9b4 fixed translations 2023-03-01 16:09:13 +01:00
Adolfo Gómez García
ddc9d5a434 Added Translations and message edition to Email MFA 2023-03-01 14:46:30 +01:00
Adolfo Gómez García
8f2b9bf136 Fixed MFAs 2023-02-28 14:00:55 +01:00
Adolfo Gómez García
f4d15e0fca Fixed MFA cache time. 2023-02-28 13:27:56 +01:00
Adolfo Gómez García
002321c339 fixed openstack, for working with compute_legacy 2023-02-26 21:44:13 +01:00
Adolfo Gómez García
bcdbfa67d7 small fix for pylance complain 2023-02-26 21:27:07 +01:00
Adolfo Gómez García
e8022389fb Updated translations 2023-02-26 21:21:08 +01:00
Adolfo Gómez García
12c59f66e2 Simplified TOTP MFA 2023-02-24 14:57:52 +01:00
Adolfo Gómez García
2541642160 Small string fixes 2023-02-24 14:51:13 +01:00
Adolfo Gómez García
56d7619e9f TOPT time is got from DBSERVER 2023-02-24 14:40:07 +01:00
Adolfo Gómez García
a7ae7d3771 Several Minor MFA fixes 2023-02-24 14:25:59 +01:00
Adolfo Gómez García
e47e0ee69f Updated translations 2023-02-23 21:59:29 +01:00
Adolfo Gómez García
9d6a74faa3 updated admin interface 2023-02-23 21:54:56 +01:00
Adolfo Gómez García
56ab199856 Fixed translations 2023-02-23 21:39:35 +01:00
Adolfo Gómez García
9be108dbd5 added reset related data to user, to clean up mfa data for example 2023-02-23 19:37:24 +01:00
Adolfo Gómez García
165bd89829 Fixed parameters order for coherence 2023-02-23 16:56:42 +01:00
Adolfo Gómez García
52096b1eff Added Google Auth MFA 2023-02-23 16:44:47 +01:00
Adolfo Gómez García
ac49786492 Fixed "resetData" whe user is deleted from db, so mfa storage can be freed 2023-02-23 15:10:04 +01:00
Adolfo Gómez García
b14581c522 Added reseting tries to mfa 2023-02-23 03:25:42 +01:00
Adolfo Gómez García
98954b5e3b Minor format fixes 2023-02-23 03:01:32 +01:00
Adolfo Gómez García
762c0e5392 Some minor MFA fixes 2023-02-23 02:57:37 +01:00
Adolfo Gómez García
0aaa734030 Fixed MFA stuff 2023-02-22 15:15:31 +01:00
Adolfo Gómez García
2d48320ac8 Small typing fix for openstack_client 2023-02-21 17:17:45 +01:00
Adolfo Gómez García
9192a0a822 small auth rest fix 2023-02-20 18:21:07 +01:00
Adolfo Gómez García
5907985719 Removed support of tunnel for proxied SPICE connection (not supported) 2023-02-15 16:58:10 +01:00
Adolfo Gómez García
cf0f6a0cce Spice cleans 2023-02-14 14:46:26 +01:00
Adolfo Gómez García
c8df5de095 Fixed spice upgrades 2023-02-14 14:41:56 +01:00
Adolfo Gómez García
d2d13d1089 Added support for service provided ca and spice proxy overriding 2023-02-14 14:30:04 +01:00
Adolfo Gómez García
978c39edd1 Fixed spice for ProxMox 2023-02-10 14:06:15 +01:00
Adolfo Gómez García
fe91fa635b Better solution for default changing values 2023-02-07 18:47:55 +01:00
Adolfo Gómez García
a75bc4a4b5 fixed reports dates 2023-02-07 18:25:55 +01:00
Adolfo Gómez García
8aa94fd0c5 updated translations 2023-02-07 16:42:58 +01:00
Adolfo Gómez García
0db41e1a14 added spicde to proxmox 2023-02-07 16:37:26 +01:00
Adolfo Gómez García
4f12602db3 small guacamole dispatcher fix. Backported to 3.6 2023-02-02 12:35:37 +01:00
Adolfo Gómez García
1fbc4b9bfd Fixed & updated patch from @Future998 2023-01-31 14:18:27 +01:00
Adolfo Gómez García
d8fce3bb73 Backported pr from @Future998 to enable OpenNebula Spice support. Fix from merge. 2023-01-31 14:04:57 +01:00
Adolfo Gómez García
e94b558ae9 Backported pr from @Future998 to enable OpenNebula Spice support 2023-01-31 13:47:21 +01:00
Adolfo Gómez García
58c4c4e68f Fixed date field in the system cleaner log 2023-01-26 12:21:11 +01:00
Adolfo Gómez García
5f8854335e fixed proxy detection && tests 2023-01-19 13:59:32 +01:00
Adolfo Gómez García
f2f7edbd67 Fixed IP detection 2023-01-18 23:53:52 +01:00
Adolfo Gómez García
56cf0049db Merge remote-tracking branch 'origin/v3.5' into v3.6 2023-01-17 12:54:04 +01:00
Adolfo Gómez García
9478a86b02 Ignored "errored" machines on vm list 2023-01-17 12:53:25 +01:00
Adolfo Gómez García
e15ba650f9 limited ip size length 2023-01-17 12:37:31 +01:00
Adolfo Gómez García
86e8c759ec limited src ip and hostname size 2023-01-17 12:35:41 +01:00
Adolfo Gómez García
48cd8240e9 fixed field name error on auth beforeSave 2023-01-16 12:26:47 +01:00
Adolfo Gómez García
75005399aa extended actor_v3 log in case of blocking uds actor accesses 2023-01-11 14:51:56 +01:00
Adolfo Gómez García
da02ad0c7c Backport fix time reporting of connection fix 2023-01-09 14:27:15 +01:00
Adolfo Gómez García
0a15f7bdce backported 4.0 version 2023-01-05 23:48:36 +01:00
Adolfo Gómez García
2189267358 backported tunnel improvements 2023-01-05 18:17:04 +01:00
Adolfo Gómez García
1a9517675d some fixes backported from 4.0 2023-01-04 23:48:08 +01:00
Adolfo Gómez García
aa97309db9 backported final fixes 2022-12-22 15:24:28 +01:00
Adolfo Gómez García
3e947e1d82 backported final fixes 2022-12-22 15:16:01 +01:00
Adolfo Gómez García
dcedb268dd backport udstunnel fix 2022-12-21 21:05:46 +01:00
Adolfo Gómez García
645f61c8e6 backport from 4.0 tunnel server 2022-12-21 20:40:38 +01:00
Adolfo Gómez García
d462ecbb32 backport small fixes from dev version 2022-12-21 14:28:54 +01:00
Adolfo Gómez García
c1e4c5b81a fixed udstunnel connection stop event bein the same for all connections 2022-12-20 23:39:51 +01:00
Adolfo Gómez García
d707771fe5 small tunnel fixes 2022-12-20 18:31:25 +01:00
Adolfo Gómez García
544fb21a01 small tunnel fixes 2022-12-20 15:04:26 +01:00
Adolfo Gómez García
159aa3c6ec fixed uds tunnel args 2022-12-19 18:02:38 +01:00
Adolfo Gómez García
8cc17464c3 removed -r parameter 2022-12-19 14:26:29 +01:00
Adolfo Gómez García
adeb6b2a46 updates from 4.0 backported 2022-12-19 01:25:29 +01:00
Adolfo Gómez García
b7962a24f1 updated translations 2022-12-16 12:52:21 +01:00
Adolfo Gómez García
94cab0c204 fixed Enforced misspeling :) 2022-12-16 12:47:21 +01:00
Adolfo Gómez García
fa3e6cb5a2 backport of 4.0 improvements 2022-12-14 16:25:57 +01:00
Adolfo Gómez García
2a9900d2eb Fixed empty label not being saved on re match enforcement :) 2022-12-13 15:03:29 +01:00
Adolfo Gómez García
ccf75e7104 enforced label to be same as url pattern chars 2022-12-13 14:09:17 +01:00
Adolfo Gómez García
c208814dd0 fixed label and autenticator labels to replace spaces by underscores 2022-12-13 13:43:08 +01:00
Adolfo Gómez García
6081122311 Fixed meta pool transport priority 2022-12-13 13:15:57 +01:00
Adolfo Gómez García
3d421ac38c Fixed transport priority on meta pools 2022-12-13 13:13:45 +01:00
Adolfo Gómez García
d22f606f49 Added calendars new colums with rules, pools with access and pools with rules 2022-12-12 14:23:55 +01:00
Adolfo Gómez García
859f09aa0b Backport of 4.0 crypto manager
For enhacements on user password hashing
2022-12-11 13:01:53 +01:00
Adolfo Gómez García
dba2526ffb Backported 4.0 tunnel server 2022-12-10 21:45:07 +01:00
Adolfo Gómez García
d17dae8bdd Changed to thread_time to better calc 2022-12-06 20:05:20 +01:00
Adolfo Gómez García
c6b9817749 changed timeit.default_counter for time.perf_counter_ns 2022-12-06 20:00:08 +01:00
Adolfo Gómez García
b34f09e58e updated cache decorator & added stats 2022-12-06 16:30:45 +01:00
Adolfo Gómez García
053fcfd3e3 updated cache decorators 2022-12-06 09:43:33 +01:00
Adolfo Gómez García
36e3c7eea9 added sample to SMS MFA on comments 2022-12-01 02:09:22 +01:00
Adolfo Gómez García
6772455111 fixed recipe to remove update info (non existing) and adding a signature to it 2022-11-29 13:44:10 +01:00
Adolfo Gómez García
b69efb5426 fixed build process for clients with new app image builder (1.1.0) 2022-11-29 13:28:02 +01:00
Adolfo Gómez García
5c4141f9a2 more usefull info for debugging 2022-11-23 18:26:14 +01:00
Adolfo Gómez García
d8e8ddd1bb added logging environment variables if debug level is DEBUG 2022-11-23 17:05:02 +01:00
Adolfo Gómez García
c8770e5f4f updated translations 2022-11-23 16:37:43 +01:00
Adolfo Gómez García
f2e3a2468a Fixed stats counters weekly 2022-11-15 19:43:47 +01:00
Adolfo Gómez García
106c28cb34 removed table conversion to avoid problems with galera cluster 2022-11-15 15:00:36 +01:00
Adolfo Gómez García
2165798408 updated translations 2022-11-15 14:35:30 +01:00
Adolfo Gómez García
271351b3d6 updated translations 2022-11-15 14:21:36 +01:00
Adolfo Gómez García
d0d6892858 removed a couple invalid imports 2022-11-15 14:08:51 +01:00
Adolfo Gómez García
80e53615c5 added custom parameters for rdp client 2022-11-15 14:01:04 +01:00
Adolfo Gómez García
899156843e removing convert InnoDB stats table to MyIsam (will broke Galera compat) 2022-11-15 13:35:52 +01:00
Adolfo Gómez García
2b917839c7 fixed stats_counters query 2022-11-14 14:11:00 +01:00
Adolfo Gómez García
adec02dc3f fixed stats_counters on system 2022-11-13 03:46:30 +01:00
Adolfo Gómez García
d96a639a3d added dependency "xset" for rpm and fixed exceptions when some programs were not found 2022-11-10 14:02:40 +01:00
Adolfo Gómez García
721d32c972 Fixing up stats counters accumulators 2022-11-09 23:02:41 +01:00
Adolfo Gómez García
1cf2e2cd76 Adding stats counter acummulators 2022-11-09 21:48:38 +01:00
Adolfo Gómez García
40364cdcce Renamed Interval type 2022-11-02 21:57:54 +01:00
Adolfo Gómez García
39fd5dbf3f set stats_counters get_grouped to interval None by default 2022-11-02 21:45:15 +01:00
Adolfo Gómez García
396e0f0c38 updated stats_counters db to include interval_type for values 2022-11-02 21:38:01 +01:00
Adolfo Gómez García
fc6224dada Enhacements to stats counters 2022-11-02 19:47:31 +01:00
Adolfo Gómez García
1d8d1fe2c9 Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-11-02 19:20:50 +01:00
Adolfo Gómez García
0923a3fbca removed nonsense indexes from stats 2022-11-02 18:53:41 +01:00
Adolfo Gómez García
43458cbf99 Reduced stats 2022-11-02 15:02:05 +01:00
Adolfo Gómez García
114247e407 Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-11-02 02:57:00 +01:00
Adolfo Gómez García
ec2645b0a2 fixed stats_counters 2022-11-02 02:56:47 +01:00
Adolfo Gómez García
dd08257fb9 Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-10-28 14:42:35 +02:00
Adolfo Gómez García
9d0df6cfae small fix for client detecti 2022-10-28 14:42:09 +02:00
Adolfo Gómez García
7bd0d571e6 increased security by encrypting with own key, different on each instalation 2022-10-27 14:46:34 +02:00
Adolfo Gómez García
ad269b3c28 added initial export command for relevant UDS entities 2022-10-26 18:32:52 +02:00
Adolfo Gómez García
f3dd5753a3 fixed mfa_data name on db 2022-10-26 16:40:04 +02:00
Adolfo Gómez García
13336b966e updating delayed task 2022-10-21 00:56:12 +02:00
Adolfo Gómez García
a76989d885 fixed not opening html5 2022-10-19 15:14:52 +02:00
Adolfo Gómez García
5f0e5a5dfe Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-10-19 14:19:46 +02:00
Adolfo Gómez García
cfbce5aef5 fixed caching calendars 2022-10-19 14:19:30 +02:00
Adolfo Gómez García
d2cb4356f0 Added user interface default value 2022-10-17 13:51:35 +02:00
Adolfo Gómez García
4f4f1f24fd fixes for transports 2022-10-16 18:46:56 +02:00
Adolfo Gómez García
65d38d8722 updated translations 2022-10-14 19:51:58 +02:00
Adolfo Gómez García
b16cea984c Updated mfa string 2022-10-14 19:07:41 +02:00
Adolfo Gómez García
7769351d42 adding spice support for proxmox 2022-10-14 02:07:12 +02:00
Adolfo Gómez García
bf635a5e9a small html fixes 2022-10-14 00:28:18 +02:00
Adolfo Gómez García
ae2ffccbc3 Added ask credentials dialog 2022-10-13 20:02:02 +02:00
Adolfo Gómez García
a005bf1ca0 fixed incorrect import 2022-10-13 15:05:32 +02:00
Adolfo Gómez García
4de443395d Updated translations 2022-10-13 14:49:38 +02:00
Adolfo Gómez García
9f2bc5417f Fixed choiceField bug & MFA table 2022-10-13 14:47:37 +02:00
Adolfo Gómez García
c6d1bf450c Fixed choicefield generator for strings (was generating "name" instead of "text") 2022-10-05 23:52:28 +02:00
Adolfo Gómez García
cf21936f41 Added report for audit log for administration 2022-10-05 23:05:36 +02:00
Adolfo Gómez García
5d9c8ee53f better audit log 2022-10-05 19:35:45 +02:00
Adolfo Gómez García
7d3bfb5d3b replaced "-" with ":" for checking if a save field is optional so we can provide the default value" 2022-10-05 19:16:30 +02:00
Adolfo Gómez García
b474e63924 updated translations 2022-10-05 18:06:48 +02:00
Adolfo Gómez García
d48747abff Added administration audit and fixed some translations 2022-10-05 17:54:07 +02:00
Adolfo Gómez García
8b3ad295cc Added MAC controled by uds for proxmox 2022-09-28 15:33:54 +02:00
Adolfo Gómez García
aa677353ad fixed tree command 2022-09-19 14:23:44 +02:00
Adolfo Gómez García
9c6c4078b1 Fixed showConfig 2022-09-19 14:04:53 +02:00
Adolfo Gómez García
9fba2b45ad Added "ERROR" user services on report with log 2022-09-18 15:09:17 +02:00
Adolfo Gómez García
71582fc415 fixed tree yaml generation 2022-09-16 23:27:12 +02:00
Adolfo Gómez García
0d1d38c18a added showconfig in yaml 2022-09-16 22:34:40 +02:00
Adolfo Gómez García
4ec8841a57 added tree command to allow an full overview of uds data 2022-09-16 18:45:37 +02:00
Adolfo Gómez García
8c6390733c added showconfig command 2022-09-16 00:53:56 +02:00
Adolfo Gómez García
98f56ee58b restored deleted line by mistake on auth 2022-09-15 13:06:13 +02:00
Adolfo Gómez García
1c01c35a87 Renamed config value 2022-09-14 12:09:06 +02:00
Adolfo Gómez García
673d1b6813 Added "Ultimate Security". When enabled, UDS will not cache encrypted credentials on server, so no credential can be redirected 2022-09-13 16:14:54 +02:00
Adolfo Gómez García
1ba12bb82d Updated translations 2022-09-12 15:04:33 +02:00
Adolfo Gómez García
f90f108869 Fixed UserInterface new guiField acceptance of values 2022-09-12 12:37:21 +02:00
Adolfo Gómez García
88c3f9077b small cosmetic fix 2022-09-08 12:20:21 +02:00
Adolfo Gómez García
2a01df542d Added "allow reset" and "allow release" to metapool. Automatically enabled is ALL member pools allows. 2022-09-08 12:15:18 +02:00
Adolfo Gómez García
2733444355 Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-09-05 12:53:31 +02:00
Adolfo Gómez García
cf6820aa2b Fixed security 2022-09-05 12:48:54 +02:00
Adolfo Gómez García
6692e5ce6d Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-09-02 16:45:25 +02:00
Adolfo Gómez García
1a85f60f4f Fixed "Database error" from OpenGnsys to include some more helpfull information 2022-09-02 16:45:11 +02:00
Adolfo Gómez García
38b3318704 updated translations 2022-08-31 15:22:15 +02:00
Adolfo Gómez García
ccec281e0d Fixed text of maxServices 2022-08-31 15:09:55 +02:00
Adolfo Gómez García
230187d9ee small fix on service unmarshall 2022-08-31 13:52:11 +02:00
Adolfo Gómez García
092bb83001 Added "maxServices" to OpenGnsys to limit number of possible services provided by a single UDS Service 2022-08-31 12:45:33 +02:00
Adolfo Gómez García
ac62aed420 upgrading cache updater to take into account maxDeployed to stop creating cache services 2022-08-30 21:53:03 +02:00
Adolfo Gómez García
e16be78ad5 Fixed remove or cancel detecting "hanged" canceling operations 2022-08-29 15:20:09 +02:00
Adolfo Gómez García
28319b216f updated compat level to 10 2022-08-28 19:23:04 +02:00
Adolfo Gómez García
739b0c7f81 fixed logout absolute url building on logout 2022-08-24 11:08:56 +02:00
Adolfo Gómez García
e5e8ad5fbd Adding radius challenge MFA provided by Daniel Torregrosa (Thanks!) 2022-08-23 15:22:48 +02:00
Adolfo Gómez García
86ebd7766e Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-08-18 13:56:32 +02:00
Adolfo Gómez García
83394f0d34 Fixed XEN/XCP-NG network moving on service 2022-08-18 13:56:07 +02:00
Adolfo Gómez García
4f0ea76666 Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-08-17 15:06:49 +02:00
Adolfo Gómez García
c34fc41f56 unmanaged fix 2022-08-17 14:55:33 +02:00
Adolfo Gómez García
18e9cab9ef fixed local log 2022-08-17 14:33:44 +02:00
Adolfo Gómez García
6053e34d1d Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-08-17 14:12:52 +02:00
Adolfo Gómez García
90aa455586 fixed unmanaged 2022-08-17 14:12:13 +02:00
Adolfo Gómez García
11041ff44f Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-08-14 21:52:40 +02:00
Adolfo Gómez García
bc2328a239 fixing up sqlite 2022-08-14 21:52:24 +02:00
Adolfo Gómez García
98826504d6 fixing up sqlite 2022-08-14 21:52:06 +02:00
Adolfo Gómez García
3a990e19a6 Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-08-06 20:19:36 +02:00
Adolfo Gómez García
d9d3bc452c fixed login/logout 2022-08-06 20:19:23 +02:00
Adolfo Gómez García
8a150439ae Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-08-06 19:29:43 +02:00
Adolfo Gómez García
08f14bff57 Fixing up unmanaged actor 2022-08-05 13:33:57 +02:00
Adolfo Gómez García
653bff420f Fixed logout notification 2022-08-05 13:19:47 +02:00
Adolfo Gómez García
73a3c89e04 Fixed logout notification 2022-08-05 13:05:52 +02:00
Adolfo Gómez García
e79753748e Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-08-04 21:56:39 +02:00
Adolfo Gómez García
adaabf9d83 Fixing up unmanaged actor 2022-08-04 21:37:33 +02:00
Adolfo Gómez García
a8a9b24596 exit_url is now relative by default 2022-08-04 15:07:52 +02:00
Adolfo Gómez García
f24c77f20a removed mic redirect on mac py default for xfreerdp (2.8 is "crashing"?) 2022-08-01 14:37:39 +02:00
Adolfo Gómez García
d2fa5e38d0 small fix to remove "remember_device" if not set 2022-07-29 16:59:33 +02:00
Adolfo Gómez García
ada5374db5 fixed showing MFA on list 2022-07-29 16:42:24 +02:00
Adolfo Gómez García
93ba05f6cb Fixes to MFAs 2022-07-29 16:20:14 +02:00
Adolfo Gómez García
94cf5582e2 Added RH-based unmanaged actor 2022-07-26 13:33:09 +02:00
Adolfo Gómez García
afcfffbd29 Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-07-15 10:26:47 +02:00
Adolfo Gómez García
3cfbdc86e0 Small cryptomanager typing fix 2022-07-15 10:26:02 +02:00
Adolfo Gómez García
d1329849f3 Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-07-14 12:49:29 +02:00
Adolfo Gómez García
ba759b3652 Fixed Proxmox MAC generation for internal DB (Case sensitive...) 2022-07-14 12:49:14 +02:00
Adolfo Gómez García
1e3478314b Reformating 2022-07-14 12:48:54 +02:00
Adolfo Gómez García
f5d2776478 Adde "custom html" support for MFA input code page 2022-07-06 17:41:09 +02:00
Adolfo Gómez García
0496117fc1 Fixing up mfa to include request on more methods 2022-07-06 14:34:42 +02:00
Adolfo Gómez García
89864b11c2 Fixed window upen 2022-07-06 13:20:10 +02:00
Adolfo Gómez García
fcdf599e18 Fixed HTML5 window opening & MFA 2022-07-06 13:17:35 +02:00
Adolfo Gómez García
05b6bebf36 bumping version to 3.6 2022-07-05 15:25:58 +02:00
Adolfo Gómez García
cdbc8d7ba1 bumping to v3.6 2022-07-05 15:20:44 +02:00
Adolfo Gómez García
072a722b09 Added udsactor-unamanged for rpm and bumped version to 3.6 2022-07-05 15:03:41 +02:00
Adolfo Gómez García
2d2e2d7b1f Upgrading version to next intermediary release 2022-07-05 14:52:17 +02:00
Adolfo Gómez García
f4da75cea9 Adding MFA support to existing auths 2022-07-04 22:10:06 +02:00
Adolfo Gómez García
1c65722d24 added mfaData to admin 2022-07-04 21:29:41 +02:00
Adolfo Gómez García
8783db925f fixed rest of MFA 2022-07-02 00:17:23 +02:00
Adolfo Gómez García
5e61871091 Added network to MFA and added initGui suppor for "providers" 2022-07-01 20:23:13 +02:00
Adolfo Gómez García
80b26446f6 translations 2022-06-30 16:45:13 +02:00
Adolfo Gómez García
a0ac50d9c2 small label fixes 2022-06-30 16:24:46 +02:00
Adolfo Gómez García
6094f55182 small MFA fixes for generic SMS 2022-06-29 23:17:52 +02:00
Adolfo Gómez García
11d9c77a79 Tested correct working of generic SMS sending using HTTP 2022-06-29 23:14:26 +02:00
Adolfo Gómez García
76e67b1f63 Fixing up MFA 2022-06-29 22:05:45 +02:00
Adolfo Gómez García
64fc61a2d6 Added generic SMS using HTTP server 2022-06-28 20:47:47 +02:00
Adolfo Gómez García
57b19757b9 fixed MFA 2022-06-28 16:40:35 +02:00
Adolfo Gómez García
aec2f5b57f Added "not tested" generic SMS sending using an HTTP server 2022-06-28 14:50:39 +02:00
Adolfo Gómez García
77e021a371 Fixed auth mfaIdentifier to provide userName 2022-06-27 21:30:59 +02:00
Adolfo Gómez García
4db98684d3 refactorized 2022-06-24 13:27:45 +02:00
Adolfo Gómez García
a948d5eeb1 Added email MFA 2022-06-24 13:26:39 +02:00
Adolfo Gómez García
c7e6857492 If user has already been authorized, no mfa is allowed 2022-06-24 11:28:46 +02:00
Adolfo Gómez García
aaa4216862 Fixed MFA & Added remember me 2022-06-23 20:24:56 +02:00
Adolfo Gómez García
098396be87 Updared admin interface 2022-06-23 16:46:19 +02:00
Adolfo Gómez García
d02c693202 Fixed mfas rest path 2022-06-23 16:42:46 +02:00
Adolfo Gómez García
cb11a26fbe updated mfa icon 2022-06-23 16:23:27 +02:00
Adolfo Gómez García
43934d425f added timeout value 2022-06-23 15:56:14 +02:00
Adolfo Gómez García
5b499de983 Initial MFA done 2022-06-23 15:14:39 +02:00
Adolfo Gómez García
00d9f5759d Merge remote-tracking branch 'origin/v3.5' into v3.5-mfa 2022-06-23 14:05:25 +02:00
Adolfo Gómez García
c6a40ac182 fixed global logout on federated auth 2022-06-23 12:51:41 +02:00
Adolfo Gómez García
7d9ffca559 Fixex internal db with sqlite 2022-06-23 12:23:22 +02:00
Adolfo Gómez García
ec02f63cac advancing on MFA implementation 2022-06-23 12:16:08 +02:00
Adolfo Gómez García
0de655d14f Adding MFA authorization page 2022-06-22 23:39:11 +02:00
Adolfo Gómez García
68e327847b Created migrations 2022-06-22 21:40:43 +02:00
Adolfo Gómez García
81ea07f0a0 Created migrations 2022-06-22 21:40:23 +02:00
Adolfo Gómez García
d7540c3305 Adding MFA 2022-06-22 17:04:18 +02:00
Adolfo Gómez García
f43b9c7bfd Fixed small actor network card check and removed required of network for interface select 2022-06-20 19:42:36 +02:00
Adolfo Gómez García
972c48ddee Merge branch 'v3.5' of github.com:dkmstr/openuds into v3.5 2022-06-17 22:27:37 +02:00
Adolfo Gómez García
118e642700 Fixed frame over buttons ons unmanaged setup 2022-06-17 22:27:23 +02:00
Adolfo Gómez
dfa441871b Fixed logger on Windows (import mistake) 2022-06-17 22:07:25 +02:00
Adolfo Gómez
18c5e3a242 Fixed logger on Windows (import mistake) 2022-06-17 21:59:24 +02:00
Adolfo Gómez García
3a4d571a6c Fixed actor tools changes for typeinfo 2022-06-17 13:54:52 +02:00
Adolfo Gómez García
3cc42e1e73 Adding udsuser to preconnect 2022-06-16 12:52:27 +02:00
Adolfo Gómez García
ffe9baa9a5 Adding udsuser to preconnect 2022-06-16 12:49:28 +02:00
Adolfo Gómez García
0b05009d3f Adding support for several network cards ond unmanaged 2022-06-14 16:51:37 +02:00
Adolfo Gómez García
b34b12ec9f Fixed RDP Transport with RDS Sessions 2022-06-13 11:24:44 +02:00
Adolfo Gómez García
fb70524cb3 fixed sampling points 2022-06-07 21:03:49 +02:00
Adolfo Gómez García
4c66401e4f Fixes to reports 2022-06-07 15:38:57 +02:00
Adolfo Gómez García
364ebd6f3a Fixed several reports 2022-06-06 22:29:42 +02:00
Adolfo Gómez García
493cbbb4e7 fixed samplingPoints 2022-06-06 21:42:04 +02:00
Adolfo Gómez García
5277a74c1c Backport of 4.0 report fixes 2022-06-06 21:26:29 +02:00
Adolfo Gómez García
1e01339b93 Fixed language change on admin 2022-06-06 19:03:47 +02:00
Adolfo Gómez García
9343f7c263 Added CERTIFICATE_BUNDLE_PATH possible variable on environment to check certificates 2022-06-03 13:44:00 +02:00
Adolfo Gómez García
7775964d62 Added never cache to indes 2022-05-26 15:57:14 +02:00
Adolfo Gómez García
a207e8f65f Fixed csrf_field name 2022-05-26 15:36:56 +02:00
Adolfo Gómez García
0a0f2771ae Updated error page logic 2022-05-26 15:00:50 +02:00
Adolfo Gómez García
ceb5fd9bde updated csrf info 2022-05-20 09:03:45 +02:00
Adolfo Gómez García
7bfa6a6c4f Updated admin interface 2022-05-19 09:13:28 +02:00
Adolfo Gómez García
858b79614b Added improved safeHTML method to frontend 2022-05-19 09:11:10 +02:00
Adolfo Gómez García
45b47ce702 Updated user interface 2022-05-17 16:42:06 +02:00
Adolfo Gómez García
dd98ba5653 Remove clear of session on login (nonsense)0 2022-05-10 15:31:29 +02:00
Adolfo Gómez García
0fe5b32224 Fixed RDP usb redir 2022-04-29 14:59:16 +02:00
Adolfo Gómez García
a0adc1ded3 redirect to logout 2022-04-25 14:29:40 +02:00
Adolfo Gómez García
4f5cc505d3 redirect to logout 2022-04-25 14:28:18 +02:00
Adolfo Gómez García
8bac68b55b Fixed locked on machines_multi 2022-04-25 14:23:46 +02:00
Adolfo Gómez García
b5412e70fd Fixed Lock of service multi 2022-04-25 14:18:46 +02:00
Adolfo Gómez García
75cd3c4845 Chanced a couple of declarations 2022-04-24 17:13:38 +02:00
Adolfo Gómez García
e8c45b568d Fixing up some typos 2022-04-24 16:52:45 +02:00
Adolfo Gómez García
540a2b83be Added brand to configjs so we can use it on a future 2022-04-12 22:30:32 +02:00
Adolfo Gómez García
aa4d157c30 Fixed request session timeout 2022-04-12 21:36:17 +02:00
Adolfo Gómez García
69ca93586a Fixed transport & groups deletion 2022-04-12 14:34:06 +02:00
Adolfo Gómez García
cf283bba0f Fixed calendar action delete all groups 2022-04-08 15:29:13 +02:00
Adolfo Gómez García
9abaada7cb Fixed perms 2022-04-06 21:32:16 +02:00
Adolfo Gómez García
b359892454 images 2022-04-06 20:19:11 +02:00
Adolfo Gómez García
927a86c835 Added USB redirection policy for windows 2022-04-06 14:21:52 +02:00
Adolfo Gómez García
2b5aa9c9a4 Fixed address passing to tunnel 2022-04-04 21:12:54 +02:00
Adolfo Gómez García
b3047e366d Fixed 3.5 tunnel DOS attacks tolerance 2022-03-30 15:11:00 +02:00
Adolfo Gómez García
d2ef6e3704 Restored timeout 2022-03-29 22:12:43 +02:00
Adolfo Gómez García
5fb4461934 Remove external timeout 2022-03-29 22:01:10 +02:00
Adolfo Gómez García
2f5f87e122 added timeout to oppened tunnel to avoid possible DOS 2022-03-29 13:08:34 +02:00
Adolfo Gómez García
d9be83863c increaded backlog 2022-03-28 13:59:37 +02:00
305 changed files with 39672 additions and 20033 deletions

View File

@@ -1 +1 @@
3.5.0
3.6.0

View File

@@ -1,4 +0,0 @@
Linux:
python3-prctl (recommended, but not required in fact)
python3-pyqt5

View File

@@ -11,6 +11,9 @@ dpkg-buildpackage -b
cat udsactor-template.spec |
sed -e s/"version 0.0.0"/"version ${VERSION}"/g |
sed -e s/"release 1"/"release ${RELEASE}"/g > udsactor-$VERSION.spec
cat udsactor-unmanaged-template.spec |
sed -e s/"version 0.0.0"/"version ${VERSION}"/g |
sed -e s/"release 1"/"release ${RELEASE}"/g > udsactor-unmanaged-$VERSION.spec
# Now fix dependencies for opensuse
# Note that, although on opensuse the library is "libXss1" on newer,
@@ -22,7 +25,7 @@ cat udsactor-template.spec |
# sed -e s/"libXScrnSaver"/"libXss1"/g > udsactor-opensuse-$VERSION.spec
#for pkg in udsactor-$VERSION.spec udsactor-opensuse-$VERSION.spec; do
for pkg in udsactor-$VERSION.spec; do
for pkg in udsactor-*$VERSION.spec; do
rm -rf rpm
for folder in SOURCES BUILD RPMS SPECS SRPMS; do

View File

@@ -1,3 +1,9 @@
udsactor (3.6.0) stable; urgency=medium
* Upgraded to 3.6.0 release
-- Adolfo Gómez García <agomez@virtualcable.es> Fri, 1 Jul 2022 14:00:00 +0200
udsactor (3.5.0) stable; urgency=medium
* Upgraded to 3.5.0 release

View File

@@ -1,3 +1,3 @@
udsactor-unmanaged_3.5.0_all.deb admin optional
udsactor_3.5.0_all.deb admin optional
udsactor_3.5.0_amd64.buildinfo admin optional
udsactor-unmanaged_3.6.0_all.deb admin optional
udsactor_3.6.0_all.deb admin optional
udsactor_3.6.0_amd64.buildinfo admin optional

View File

@@ -11,7 +11,7 @@ Release: %{release}
Summary: Actor for Universal Desktop Services (UDS) Broker
License: BSD3
Group: Admin
Requires: python3-six python3-requests python3-qt5 libXScrnSaver
Requires: python3-six python3-requests python3-qt5 libXScrnSaver xset
Vendor: Virtual Cable S.L.U.
URL: http://www.udsenterprise.com
Provides: udsactor

View File

@@ -0,0 +1,70 @@
%define _topdir %(echo $PWD)/rpm
%define name udsactor-unmanaged
%define version 0.0.0
%define release 1
%define buildroot %{_topdir}/%{name}-%{version}-%{release}-root
BuildRoot: %{buildroot}
Name: %{name}
Version: %{version}
Release: %{release}
Summary: Actor for Universal Desktop Services (UDS) Broker
License: BSD3
Group: Admin
Requires: python3-six python3-requests python3-qt5 libXScrnSaver
Vendor: Virtual Cable S.L.U.
URL: http://www.udsenterprise.com
Provides: udsactor
%define _rpmdir ../
%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm
%install
curdir=`pwd`
cd ../..
make DESTDIR=$RPM_BUILD_ROOT DISTRO=rh install-udsactor-unmanaged
cd $curdir
%clean
rm -rf $RPM_BUILD_ROOT
curdir=`pwd`
cd ../..
make DESTDIR=$RPM_BUILD_ROOT DISTRO=rh clean
cd $curdir
%post
systemctl enable udsactor.service > /dev/null 2>&1
%preun
systemctl disable udsactor.service > /dev/null 2>&1
systemctl stop udsactor.service > /dev/null 2>&1
%postun
# $1 == 0 on uninstall, == 1 on upgrade for preun and postun (just a reminder for me... :) )
if [ $1 -eq 0 ]; then
rm -rf /etc/udsactor
rm /var/log/udsactor.log
fi
# And, posibly, the .pyc leaved behind on /usr/share/UDSActor
rm -rf /usr/share/UDSActor > /dev/null 2>&1
%description
This package provides the required components to allow this unmanaged machine to work on an environment managed by UDS Broker.
%files
%defattr(-,root,root)
/etc/udsactor
/etc/xdg/autostart/UDSActorTool.desktop
/etc/systemd/system/udsactor.service
/usr/bin/UDSActorTool-startup
/usr/bin/udsactor
/usr/bin/udsvapp
/usr/bin/UDSActorTool
/usr/sbin/UDSActorConfig
/usr/sbin/UDSActorConfig-pkexec
/usr/share/UDSActor/*
/usr/share/applications/UDS_Actor_Configuration.desktop
/usr/share/autostart/UDSActorTool.desktop
/usr/share/polkit-1/actions/org.openuds.pkexec.UDSActorConfig.policy

View File

@@ -67,7 +67,7 @@ if __name__ == "__main__":
# Note: Signals are only checked on python code execution, so we create a timer to force call back to python
timer = QTimer(qApp)
timer.start(1000)
timer.timeout.connect(lambda *a: None) # timeout can be connected to a callable
timer.timeout.connect(lambda *a: None) # type: ignore # timeout can be connected to a callable
qApp.exec()

View File

@@ -40,6 +40,7 @@ import PyQt5 # pylint: disable=unused-import
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
import udsactor
import udsactor.tools
from ui.setup_dialog_unmanaged_ui import Ui_UdsActorSetupDialog
@@ -49,6 +50,7 @@ if typing.TYPE_CHECKING:
logger = logging.getLogger('actor')
class UDSConfigDialog(QDialog):
_host: str = ''
_config: udsactor.types.ActorConfigurationType
@@ -60,65 +62,99 @@ class UDSConfigDialog(QDialog):
self.ui = Ui_UdsActorSetupDialog()
self.ui.setupUi(self)
self.ui.host.setText(self._config.host)
self.ui.validateCertificate.setCurrentIndex(1 if self._config.validateCertificate else 0)
self.ui.validateCertificate.setCurrentIndex(
1 if self._config.validateCertificate else 0
)
self.ui.logLevelComboBox.setCurrentIndex(self._config.log_level)
self.ui.serviceToken.setText(self._config.master_token or '')
self.ui.restrictNet.setText(self._config.restrict_net or '')
self.ui.testButton.setEnabled(bool(self._config.master_token and self._config.host))
self.ui.testButton.setEnabled(
bool(self._config.master_token and self._config.host)
)
@property
def api(self) -> udsactor.rest.UDSServerApi:
return udsactor.rest.UDSServerApi(self.ui.host.text(), self.ui.validateCertificate.currentIndex() == 1)
return udsactor.rest.UDSServerApi(
self.ui.host.text(), self.ui.validateCertificate.currentIndex() == 1
)
def finish(self) -> None:
self.close()
def configChanged(self, text: str) -> None:
self.ui.testButton.setEnabled(self.ui.host.text() == self._config.host and self.ui.serviceToken.text() == self._config.master_token)
self.ui.testButton.setEnabled(
self.ui.host.text() == self._config.host
and self.ui.serviceToken.text() == self._config.master_token
and self.ui.restrictNet.text() == self._config.restrict_net
)
def testUDSServer(self) -> None:
if not self._config.master_token or not self._config.host:
self.ui.testButton.setEnabled(False)
return
try:
api = udsactor.rest.UDSServerApi(self._config.host, self._config.validateCertificate)
api = udsactor.rest.UDSServerApi(
self._config.host, self._config.validateCertificate
)
if not api.test(self._config.master_token, udsactor.types.UNMANAGED):
QMessageBox.information(
self,
'UDS Test',
'Service token seems to be invalid . Please, check token validity.',
QMessageBox.Ok
QMessageBox.Ok,
)
else:
QMessageBox.information(
self,
'UDS Test',
'Configuration for {} seems to be correct.'.format(self._config.host),
QMessageBox.Ok
'Configuration for {} seems to be correct.'.format(
self._config.host
),
QMessageBox.Ok,
)
except Exception:
QMessageBox.information(
self,
'UDS Test',
'Configured host {} seems to be inaccesible.'.format(self._config.host),
QMessageBox.Ok
QMessageBox.Ok,
)
def saveConfig(self) -> None:
# Ensure restrict_net is empty or a valid subnet
restrictNet = self.ui.restrictNet.text().strip()
if restrictNet:
try:
subnet = udsactor.tools.strToNoIPV4Network(restrictNet)
if not subnet:
raise Exception('Invalid subnet')
except Exception:
QMessageBox.information(
self,
'Invalid subnet',
'Invalid subnet {}. Please, check it.'.format(restrictNet),
QMessageBox.Ok,
)
return
# Store parameters on register for later use, notify user of registration
self._config = udsactor.types.ActorConfigurationType(
actorType=udsactor.types.UNMANAGED,
host=self.ui.host.text(),
validateCertificate=self.ui.validateCertificate.currentIndex() == 1,
master_token=self.ui.serviceToken.text(),
log_level=self.ui.logLevelComboBox.currentIndex()
master_token=self.ui.serviceToken.text().strip(),
restrict_net=restrictNet,
log_level=self.ui.logLevelComboBox.currentIndex(),
)
udsactor.platform.store.writeConfig(self._config)
# Enables test button
self.ui.testButton.setEnabled(True)
# Informs the user
QMessageBox.information(self, 'UDS Configuration', 'Configuration saved.', QMessageBox.Ok)
QMessageBox.information(
self, 'UDS Configuration', 'Configuration saved.', QMessageBox.Ok
)
if __name__ == "__main__":
@@ -127,9 +163,9 @@ if __name__ == "__main__":
os.environ['QT_X11_NO_MITSHM'] = '1'
app = QApplication(sys.argv)
if udsactor.platform.operations.checkPermissions() is False:
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok)
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok) # type: ignore
sys.exit(1)
if len(sys.argv) > 2:

View File

@@ -10,8 +10,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>595</width>
<height>220</height>
<width>601</width>
<height>243</height>
</rect>
</property>
<property name="sizePolicy">
@@ -55,7 +55,7 @@
<property name="geometry">
<rect>
<x>10</x>
<y>180</y>
<y>210</y>
<width>181</width>
<height>23</height>
</rect>
@@ -83,7 +83,7 @@
<property name="geometry">
<rect>
<x>410</x>
<y>180</y>
<y>210</y>
<width>171</width>
<height>23</height>
</rect>
@@ -117,7 +117,7 @@
<property name="geometry">
<rect>
<x>210</x>
<y>180</y>
<y>210</y>
<width>181</width>
<height>23</height>
</rect>
@@ -144,7 +144,7 @@
<x>10</x>
<y>10</y>
<width>571</width>
<height>161</height>
<height>191</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
@@ -214,21 +214,21 @@
<item row="2" column="1">
<widget class="QLineEdit" name="serviceToken">
<property name="toolTip">
<string>UDS user with administration rights (Will not be stored on template)</string>
<string>UDS Service Token</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Administrator user on UDS Server.&lt;/p&gt;&lt;p&gt;Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Token of the service on UDS platform&lt;/p&gt;&lt;p&gt;This token can be obtainend from the service configuration on UDS.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_loglevel">
<property name="text">
<string>Log Level</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QComboBox" name="logLevelComboBox">
<property name="currentIndex">
<number>1</number>
@@ -258,6 +258,23 @@
</item>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_restrictNet">
<property name="text">
<string>Restrict Net</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="restrictNet">
<property name="toolTip">
<string>Restrict valid detection of network interfaces to this network.</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Restrics valid detection of network interfaces.&lt;/p&gt;&lt;p&gt;Note: Use this field only in case of several network interfaces, so UDS knows which one is the interface where the user will be connected..&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
<zorder>label_host</zorder>
<zorder>host</zorder>
@@ -267,6 +284,8 @@
<zorder>label_security</zorder>
<zorder>label_loglevel</zorder>
<zorder>logLevelComboBox</zorder>
<zorder>label_restrictNet</zorder>
<zorder>restrictNet</zorder>
</widget>
</widget>
<resources>
@@ -353,6 +372,22 @@
</hint>
</hints>
</connection>
<connection>
<sender>restrictNet</sender>
<signal>textChanged(QString)</signal>
<receiver>UdsActorSetupDialog</receiver>
<slot>configChanged()</slot>
<hints>
<hint type="sourcelabel">
<x>341</x>
<y>139</y>
</hint>
<hint type="destinationlabel">
<x>295</x>
<y>121</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>finish()</slot>

View File

@@ -224,6 +224,9 @@
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select the security for communications with UDS Broker.&lt;/p&gt;&lt;p&gt;The recommended method of communication is &lt;span style=&quot; font-weight:600;&quot;&gt;Use SSL&lt;/span&gt;, but selection needs to be acording to your broker configuration.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>Ignore certificate</string>

View File

@@ -35,4 +35,4 @@ from . import platform
__title__ = 'udsactor'
__author__ = 'Adolfo Gómez <dkmaster@dkmon.com>'
__license__ = "BSD 3-clause"
__copyright__ = "Copyright 2014-2020 VirtualCable S.L.U."
__copyright__ = "Copyright 2014-2022 VirtualCable S.L.U."

View File

@@ -185,7 +185,8 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
try:
# Notify loging and mark it
self._loginInfo = self.api.login(platform.operations.getCurrentUser(), platform.operations.getSessionType())
user, sessionType = platform.operations.getCurrentUser(), platform.operations.getSessionType()
self._loginInfo = self.api.login(user, sessionType)
if self._loginInfo.max_idle:
platform.operations.initIdleDuration(self._loginInfo.max_idle)
@@ -197,8 +198,11 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
time.sleep(1.3) # Sleeps between loop iterations
self.api.logout(user + self._extraLogoff, sessionType)
logger.info('Notified logout for %s (%s)', user, sessionType) # Log logout
# Clean up login info
self._loginInfo = None
self.api.logout(platform.operations.getCurrentUser() + self._extraLogoff)
except Exception as e:
logger.error('Error on client loop: %s', e)
@@ -235,7 +239,7 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
pixmap: 'QPixmap' = self._qApp.primaryScreen().grabWindow(0) # type: ignore
ba = QByteArray()
buffer = QBuffer(ba)
buffer.open(QIODevice.WriteOnly)
buffer.open(QIODevice.OpenModeFlag.WriteOnly)
pixmap.save(buffer, 'PNG')
buffer.close()
scrBase64 = bytes(ba.toBase64()).decode() # type: ignore # there are problems with Pylance and connects on PyQt5... :)

View File

@@ -1,7 +1,10 @@
from .. import types
# Default certificate, will be overwritten by the first call to Broker, it's needed to wake up the server part of the actor
# at the beginning, but will be replaced by the real certificate.
defaultCertificate = types.CertificateInfoType(
private_key='-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFHTBPBgkqhkiG9w0BBQ0wQjApBgkqhkiG9w0BBQwwHAQIfG2+iMYJBswCAggA\nMAwGCCqGSIb3DQIJBQAwFQYJKwYBBAGXVQECBAhCusU5R8ulZQSCBMgheyZ81Qkq\n+TcbPeBlUGCFllSUOo7xQ/OuwYSmzLx8LpN0hQNv4azF6MYH+I8eMSPd3A547yW3\nJE4GjIBfRvcq2X1UZ2FQfECU9UP0ShPuPrVhIh6ZZklmlRjbIF8hGfSzXAuafQb+\n4wXXsofahi/SPgqK1Gw65nRiMcoeRZchJkx8pBgKVWED6Cbh6aAkeqkVKPnsebiV\n6kE+0C7+hgNUbyRd46R+/5NXzPjg4ItfSak+PLzQ1KeRv4Cu6DdzRKJ4V9/MlNdU\nNNEkSVSEaRn4sv+eByU4uxBMaSmD1tLc/A7OmaAeRpIQvls3Zcf2+V0+anAtjbjd\n6eIb2nceey+dKFm4ewlR4mXuzj1QowRTHceOIkvKIrOODxdy9M5hNBZ7VLum29tY\nRhqtmEH2BZZJ8SpM2SsEZzPxqJFiVZbvpeOKjxlMyn1dFWn1rP8uMnfuMKqBaj5D\nd5clOPlwebYw5UpM6Vvawu4nGqxECTSWcfNlDYO5U/0Fsm9+JIrJ7Buukgv2+rhs\nD/6oUK9NB8AW9qnDr7UxbC/ujhkKQG3woaZlPbiMs5WQaS+DrTg4N49wPzS0h+ME\nF8ZzuPnd6+sMGQioCIrQAZ08rk54oCijBhFh8/EQhQKGsMFw2swi9t6+FVU5Bvil\nlhmBd3LA5EuQ5y1X0jRL/+GDiUiZw1gOJP8d/XzhUJL9AmamdqJ6/rAU7lUTNWkM\ndzmFonUO2Mh2zgEEudHsTOH8udZ2l64LIHc6fCkDmM8QzghjrEFyci6R8333DSSM\nwbM0MvyTLM7TTqZUD60EgD+Ihyr/wJcBZY7GVn7hTq7ee14zeI+dZFmTMYOnt0mA\ngof19t0naPPZU+zyl/ambNF5mmSkGOAl4IBHNvPt5ztEVbNpwW3DHbmdYW71Ax+z\nCDlr4iKZahv21o1PCesPV2IlaHZFD6aBRt0DxzMqtq9cpWsI1g7aEaAjRbSvqhMY\npUeqFXz/GfR9rjRkufr48//ll0/Q/Ogx7m1TjQ6mAEQrklI7pa2W0u3H0BpSZSis\nR6ST3ulE+wfsp8cau6q2er+BSsDhBjSn9FeCUjHzY56u9ud/kb6/jLEdgxNpj0na\n3WVqCCCL/dAFSWznBmdracZsRMXapXInHCiiOEkXXbXIXvRKiTPJXdN+w2/U2j2B\nwXZuazVSpmM+xAZTAS9dtBUQJo+5px9b6P09uagvTA32ezbpPXf+hSfmTdUwbmAY\nrmE9SW85tzX+cD17loygBBRrjOr4uQy/s/9FqLx8bM73jly05rdOmX28ECKwEA05\n8aCFkfqrl9J9doVapaUlywpJVPFtE6W6tCF+ULMfb16vEjT1du1+epEnbGGLRQxg\n3aFLyKlvFaNvR38fiQFUGtBgGOaBN3rhGpbMwjch3oReXv9X/4UCL6sVIiOH2H3c\nVSZdC3O5g6CMVe4zckUe1k9mLDb5524IHDFfptZ6Bw+uzrqIy3GHW8dJF2AK471b\nMUnCojTpdbFHaUs2u/rNKVUyY+vLf8hkyP+znBUoPxSJtty53EWNukxjjsxx0lx3\niZGqN72lXlXuSFZAIxi307+xxE21cbzDsMidyJkbKKGm/F4BOKvX9jWmAyYmBG6A\n1L3yNRouFWsYDwYAX2nZ1is=\n-----END ENCRYPTED PRIVATE KEY-----\n',
server_certificate='-----BEGIN CERTIFICATE-----\nMIIDcTCCAlkCBDfnXU8wDQYJKoZIhvcNAQELBQAwfTELMAkGA1UEBhMCRVMxDzAN\nBgNVBAgMBk1hZHJpZDEPMA0GA1UEBwwGTWFkcmlkMREwDwYDVQQKDAhVRFMgQ2Vy\ndDERMA8GA1UECwwIVURTIENlcnQxEjAQBgNVBAMMCTEyNy4wLjAuMTESMBAGA1Ud\nEQwJMTI3LjAuMC4xMB4XDTIwMDIxNzExNTkzMloXDTMwMDIxNDExNTkzMlowfTEL\nMAkGA1UEBhMCRVMxDzANBgNVBAgMBk1hZHJpZDEPMA0GA1UEBwwGTWFkcmlkMREw\nDwYDVQQKDAhVRFMgQ2VydDERMA8GA1UECwwIVURTIENlcnQxEjAQBgNVBAMMCTEy\nNy4wLjAuMTESMBAGA1UdEQwJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA2e1cW7YtRpNLazR3f/LqLv8OB0rKh8cUPH4wuQhbBTkee8Wu\n5eMSadRCIyRbKj4b8dtVfI9QW0SrmhGuMx1KCh3CsYd9XsWiKbGkiRBHIDOn5pkF\n6PUayDJ8KjnGbfnZjp0AmxXP4r1OO8jUPqzKS9Ubf5PgwcwdFiUKVfVPwGwctwt5\nt9YpSRONw0rTsCjVHvO2dd9h6EopskLCWxpN8l9kNLwLM/6t0IqVKmn5/IYPKKN2\nCX8a7IXpxwoiUs4sBZYhUMBWikB1hKQRSYafp1Xvc5PeTFXTFqGANnqz0NoZ8tqL\n8qjQUN/PCdtzhfcP5RgT2g1qyS2RBCMYH7Zs0wIDAQABMA0GCSqGSIb3DQEBCwUA\nA4IBAQCUt+qlLA1N9VXMwDQAYG4Kt6/UlMHCXAajHQQGtjdyGJ4++m7EIjI96hMU\n3Cx2gp2ggR3JGnuSR+DdBvPl5iGku7J8KV0JiJg30gTY8JuUIy/PMLZWloYKrBHV\nlin2GujQ4OsIt3dbr4XtcKW1Wd7L6fBzHlq7Xyxh+gcTzTvTmq67Q9XKlBWsegMf\nv4FKy0lfcSFK3vTzswQtuTontG4TqLiT/4AnMt3D0cTQ6b6KoZwUUX/TDNhau06d\nQ4Ilz8X61ka+4HBkFSR5ahP9noCVhwO329h+6epO141E5Tep3OLc/GCF4oaKOlMR\nfqxf5f2bghU0fxmtEoNJTZkBsN1S\n-----END CERTIFICATE-----\n',
password='Pw7qbatz5u-y-Z5ora2D2ZuBCm95AHnKRcpze53k8tw'
password='Pw7qbatz5u-y-Z5ora2D2ZuBCm95AHnKRcpze53k8tw',
ciphers=''
)

View File

@@ -37,9 +37,9 @@ import requests
from ..log import logger
# For avoid proxy on localhost connections
NO_PROXY = {
'http': None,
'https': None,
NO_PROXY: typing.Dict[str, str] = {
'http': '',
'https': '',
}
class UDSActorClientPool:

View File

@@ -42,7 +42,7 @@ class LocalProvider(handler.Handler):
return result._asdict()
def post_logout(self) -> typing.Any:
self._service.logout(self._params['username'])
self._service.logout(self._params['username'], self._params['session_type'])
return 'ok'
def post_ping(self) -> typing.Any:

View File

@@ -38,6 +38,7 @@ from ..log import logger
if typing.TYPE_CHECKING:
from ..service import CommonService
class PublicProvider(handler.Handler):
def post_logout(self) -> typing.Any:
logger.debug('Sending LOGOFF to clients')
@@ -51,7 +52,9 @@ class PublicProvider(handler.Handler):
logger.debug('Sending MESSAGE to clients')
if 'message' not in self._params:
raise Exception('Invalid message parameters')
self._service._clientsPool.message(self._params['message']) # pylint: disable=protected-access
self._service._clientsPool.message(
self._params['message']
) # pylint: disable=protected-access
return 'ok'
def post_script(self) -> typing.Any:
@@ -60,7 +63,9 @@ class PublicProvider(handler.Handler):
raise Exception('Invalid script parameters')
if self._params.get('user', False):
logger.debug('Sending SCRIPT to client')
self._service._clientsPool.executeScript(self._params['script']) # pylint: disable=protected-access
self._service._clientsPool.executeScript(
self._params['script']
) # pylint: disable=protected-access
else:
# Execute script at server space, that is, here
# as a parallel thread
@@ -72,14 +77,22 @@ class PublicProvider(handler.Handler):
logger.debug('Received Pre connection')
if 'user' not in self._params or 'protocol' not in self._params:
raise Exception('Invalid preConnect parameters')
return self._service.preConnect(self._params['user'], self._params['protocol'], self._params.get('ip', 'unknown'), self._params.get('hostname', 'unknown'))
return self._service.preConnect(
self._params['user'],
self._params['protocol'],
self._params.get('ip', 'unknown'),
self._params.get('hostname', 'unknown'),
self._params.get('udsuser', 'unknown'),
)
def get_information(self) -> typing.Any:
# Return something useful? :)
return 'UDS Actor Secure Server'
def get_screenshot(self) -> typing.Any:
return self._service._clientsPool.screenshot() # pylint: disable=protected-access
return (
self._service._clientsPool.screenshot()
) # pylint: disable=protected-access
def get_uuid(self) -> typing.Any:
if self._service.isManaged():

View File

@@ -42,11 +42,18 @@ from .. import rest
from .public import PublicProvider
from .local import LocalProvider
# a couple of 1.2 ciphers + 1.3 ciphers (implicit)
DEFAULT_CIPHERS = (
'ECDHE-RSA-AES128-GCM-SHA256'
':ECDHE-RSA-AES256-GCM-SHA384'
)
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from ..service import CommonService
from .handler import Handler
class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.0'
server_version = 'UDS Actor Server'
@@ -54,7 +61,12 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
_service: typing.Optional['CommonService'] = None
def sendJsonResponse(self, result: typing.Optional[typing.Any] = None, error: typing.Optional[str] = None, code: int = 200) -> None:
def sendJsonResponse(
self,
result: typing.Optional[typing.Any] = None,
error: typing.Optional[str] = None,
code: int = 200,
) -> None:
data = json.dumps({'result': result, 'error': error})
self.send_response(code)
self.send_header('Content-type', 'application/json')
@@ -71,11 +83,13 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
# Very simple path & params splitter
path = self.path.split('?')[0][1:].split('/')
logger.debug('Path: %s, params: %s', path, params)
logger.debug('Path: %s, ip: %s, params: %s', path, self.client_address, params)
handlerType: typing.Optional[typing.Type['Handler']] = None
if len(path) == 3 and path[0] == 'actor' and path[1] == self._service._secret: # pylint: disable=protected-access
if (
len(path) == 3 and path[0] == 'actor' and path[1] == self._service._secret
): # pylint: disable=protected-access
# public method
handlerType = PublicProvider
elif len(path) == 2 and path[0] == 'ui':
@@ -88,12 +102,18 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
return
try:
result = getattr(handlerType(self._service, method, params), method + '_' + path[-1])() # last part of path is method
result = getattr(
handlerType(self._service, method, params), method + '_' + path[-1]
)() # last part of path is method
except AttributeError:
self.sendJsonResponse(error='Method not found', code=404)
return
except Exception as e:
logger.error('Got exception executing {} {}: {}'.format(method, '/'.join(path), str(e)))
logger.error(
'Got exception executing {} {}: {}'.format(
method, '/'.join(path), str(e)
)
)
self.sendJsonResponse(error=str(e), code=500)
return
@@ -101,7 +121,10 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None:
try:
params = {v.split('=')[0]: v.split('=')[1] for v in self.path.split('?')[1].split('&')}
params = {
v.split('=')[0]: v.split('=')[1]
for v in self.path.split('?')[1].split('&')
}
except Exception:
params = {}
@@ -113,7 +136,9 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
content = self.rfile.read(length)
params: typing.MutableMapping[str, str] = json.loads(content)
except Exception as e:
logger.error('Got exception executing POST {}: {}'.format(self.path, str(e)))
logger.error(
'Got exception executing POST {}: {}'.format(self.path, str(e))
)
self.sendJsonResponse(error='Invalid parameters', code=400)
return
@@ -125,6 +150,7 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, format, *args): # pylint: disable=redefined-builtin
logger.debug(format, *args)
class HTTPServerThread(threading.Thread):
_server: typing.Optional[http.server.HTTPServer]
_service: 'CommonService'
@@ -153,13 +179,22 @@ class HTTPServerThread(threading.Thread):
def run(self):
HTTPServerHandler._service = self._service # pylint: disable=protected-access
self._certFile, password = certs.saveCertificate(self._service._certificate) # pylint: disable=protected-access
self._certFile, password = certs.saveCertificate(
self._service._certificate
) # pylint: disable=protected-access
self._server = http.server.HTTPServer(('0.0.0.0', rest.LISTEN_PORT), HTTPServerHandler)
self._server = http.server.HTTPServer(
('0.0.0.0', rest.LISTEN_PORT), HTTPServerHandler
)
# self._server.socket = ssl.wrap_socket(self._server.socket, certfile=self.certFile, server_side=True)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.options = ssl.CERT_NONE
# Disable TLSv1.0 and TLSv1.1, use only TLSv1.3 or TLSv1.2 with allowed ciphers
context.minimum_version = ssl.TLSVersion.TLSv1_2
# If a configures ciphers are provided, use them, otherwise use the default ones
context.set_ciphers(self._service._certificate.ciphers or DEFAULT_CIPHERS)
context.load_cert_chain(certfile=self._certFile, password=password)
self._server.socket = context.wrap_socket(self._server.socket, server_side=True)

View File

@@ -41,10 +41,11 @@ import typing
from .. import types
from udsactor.log import logger
from .renamer import rename
from . import xss
def _getMacAddr(ifname: str) -> typing.Optional[str]:
'''
Returns the mac address of an interface
@@ -106,6 +107,7 @@ def _getIpAndMac(ifname: str) -> typing.Tuple[typing.Optional[str], typing.Optio
return (ip, mac)
def checkPermissions() -> bool:
return True
return os.getuid() == 0 # getuid only available on linux. Expect "complaioins" if edited from Windows
def getComputerName() -> str:
@@ -137,14 +139,19 @@ def reboot(flags: int = 0):
'''
Simple reboot using os command
'''
subprocess.call(['/sbin/shutdown', 'now', '-r'])
try:
subprocess.call(['/sbin/shutdown', 'now', '-r'])
except Exception as e:
logger.error('Error rebooting: %s', e)
def loggoff() -> None:
'''
Right now restarts the machine...
'''
subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']])
try:
subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']])
except Exception as e:
logger.error('Error killing user processes: %s', e)
# subprocess.call(['/sbin/shutdown', 'now', '-r'])
# subprocess.call(['/usr/bin/systemctl', 'reboot', '-i'])

View File

@@ -53,9 +53,10 @@ def readConfig() -> types.ActorConfigurationType:
return types.ActorConfigurationType(
actorType=uds.get('type', types.MANAGED),
host=uds.get('host', ''),
validateCertificate=uds.getboolean('validate', fallback=False),
validateCertificate=uds.getboolean('validate', fallback=True),
master_token=uds.get('master_token', None),
own_token=uds.get('own_token', None),
restrict_net=uds.get('restrict_net', None),
pre_command=uds.get('pre_command', None),
runonce_command=uds.get('runonce_command', None),
post_command=uds.get('post_command', None),
@@ -78,6 +79,7 @@ def writeConfig(config: types.ActorConfigurationType) -> None:
writeIfValue(config.actorType, 'type')
writeIfValue(config.master_token, 'master_token')
writeIfValue(config.own_token, 'own_token')
writeIfValue(config.restrict_net, 'restrict_net')
writeIfValue(config.pre_command, 'pre_command')
writeIfValue(config.post_command, 'post_command')
writeIfValue(config.runonce_command, 'runonce_command')

View File

@@ -33,6 +33,10 @@ import ctypes
import ctypes.util
import subprocess
from udsactor.log import logger
xlib = None
xss = None
display = None
@@ -107,9 +111,12 @@ def _ensureInitialized():
def initIdleDuration(atLeastSeconds: int) -> None:
_ensureInitialized()
if atLeastSeconds:
subprocess.call(['/usr/bin/xset', 's', '{}'.format(atLeastSeconds + 30)])
# And now reset it
subprocess.call(['/usr/bin/xset', 's', 'reset'])
try:
subprocess.call(['/usr/bin/xset', 's', '{}'.format(atLeastSeconds + 30)])
# And now reset it
subprocess.call(['/usr/bin/xset', 's', 'reset'])
except Exception as e:
logger.error('Error setting screensaver time: %s', e)
def getIdleDuration() -> float:

View File

@@ -31,10 +31,12 @@
# pylint: disable=invalid-name
import warnings
import json
import ssl
import logging
import typing
import requests
import requests.adapters
from . import types
from .version import VERSION
@@ -82,6 +84,7 @@ NO_PROXY = {
UDS_BASE_URL = 'https://{}/uds/rest/'
#
# Basic UDS Api
#
@@ -93,6 +96,7 @@ class UDSApi: # pylint: disable=too-few-public-methods
_host: str
_validateCert: bool
_url: str
_session: 'requests.Session'
def __init__(self, host: str, validateCert: bool) -> None:
self._host = host
@@ -106,6 +110,28 @@ class UDSApi: # pylint: disable=too-few-public-methods
except Exception:
pass
context = (
ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
if validateCert
else ssl._create_unverified_context(purpose=ssl.Purpose.SERVER_AUTH, check_hostname=False)
)
# Disable SSLv2, SSLv3, TLSv1, TLSv1.1, TLSv1.2
context.minimum_version = ssl.TLSVersion.TLSv1_3
# Configure session security
class UDSHTTPAdapter(requests.adapters.HTTPAdapter):
def init_poolmanager(self, *args, **kwargs) -> None:
kwargs["ssl_context"] = context
return super().init_poolmanager(*args, **kwargs)
def cert_verify(self, conn, url, verify, cert): # pylint: disable=unused-argument
# Overridden to do nothing
return super().cert_verify(conn, url, validateCert, cert)
self._session = requests.Session()
self._session.mount("https://", UDSHTTPAdapter())
@property
def _headers(self) -> typing.MutableMapping[str, str]:
return {
@@ -125,13 +151,13 @@ class UDSApi: # pylint: disable=too-few-public-methods
) -> typing.Any:
headers = headers or self._headers
try:
result = requests.post(
result = self._session.post(
self._apiURL(method),
data=json.dumps(payLoad),
headers=headers,
verify=self._validateCert,
# verify=self._validateCert, Not needed, already in session
timeout=TIMEOUT,
proxies=NO_PROXY
proxies=NO_PROXY # type: ignore
if disableProxy
else None, # if not proxies wanted, enforce it
)
@@ -162,10 +188,10 @@ class UDSServerApi(UDSApi):
def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]:
try:
result = requests.get(
result = self._session.get(
self._url + 'auth/auths',
headers=self._headers,
verify=self._validateCert,
# verify=self._validateCert,
timeout=4,
)
if result.ok:
@@ -178,7 +204,7 @@ class UDSServerApi(UDSApi):
priority=v['priority'],
isCustom=v['isCustom'],
)
except Exception:
except Exception as e:
pass
def register( # pylint: disable=too-many-arguments, too-many-locals
@@ -213,22 +239,22 @@ class UDSServerApi(UDSApi):
# First, try to login
authInfo = {'auth': auth, 'username': username, 'password': password}
headers = self._headers
result = requests.post(
result = self._session.post(
self._url + 'auth/login',
data=json.dumps(authInfo),
headers=headers,
verify=self._validateCert,
# verify=self._validateCert,
)
if not result.ok or result.json()['result'] == 'error':
raise Exception() # Invalid credentials
headers['X-Auth-Token'] = result.json()['token']
result = requests.post(
result = self._session.post(
self._apiURL('register'),
data=json.dumps(data),
headers=headers,
verify=self._validateCert,
# verify=self._validateCert,
)
if result.ok:
return result.json()['result']
@@ -282,6 +308,7 @@ class UDSServerApi(UDSApi):
private_key=result['private_key'],
server_certificate=result['server_certificate'],
password=result['password'],
ciphers=result.get('ciphers', ''),
)
def notifyIpChange(
@@ -294,6 +321,7 @@ class UDSServerApi(UDSApi):
private_key=result['private_key'],
server_certificate=result['server_certificate'],
password=result['password'],
ciphers=result.get('ciphers', ''),
)
def notifyUnmanagedCallback(
@@ -315,6 +343,7 @@ class UDSServerApi(UDSApi):
private_key=result['private_key'],
server_certificate=result['server_certificate'],
password=result['password'],
ciphers=result.get('ciphers', ''),
)
def login(
@@ -351,19 +380,21 @@ class UDSServerApi(UDSApi):
actor_type: typing.Optional[str],
token: str,
username: str,
sessionType: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
secret: typing.Optional[str],
) -> None:
) -> typing.Optional[str]:
if not token:
return
return None
payload = {
'type': actor_type or types.MANAGED,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
'token': token,
'username': username,
'session_type': sessionType,
'secret': secret or '',
}
self._doPost('logout', payload)
return self._doPost('logout', payload) # Can be 'ok' or 'notified'
def log(self, own_token: str, level: int, message: str) -> None:
if not own_token:
@@ -418,8 +449,8 @@ class UDSClientApi(UDSApi):
max_idle=result['max_idle'],
)
def logout(self, username: str) -> None:
payLoad = {'username': username}
def logout(self, username: str, sessionType: typing.Optional[str]) -> None:
payLoad = {'username': username, 'session_type': sessionType or UNKNOWN}
self.post('logout', payLoad)
def ping(self) -> bool:

View File

@@ -39,6 +39,7 @@ import typing
from . import platform
from . import rest
from . import types
from . import tools
from .log import logger, DEBUG, INFO, ERROR, FATAL
from .http import clients_pool, server, cert
@@ -55,6 +56,7 @@ from .http import clients_pool, server, cert
# else:
# logger.setLevel(20000)
class CommonService: # pylint: disable=too-many-instance-attributes
_isAlive: bool = True
_rebootRequested: bool = False
@@ -75,7 +77,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.debug('Executing command on {}: {}'.format(section, cmdLine))
res = subprocess.check_call(cmdLine, shell=True)
except Exception as e:
logger.error('Got exception executing: {} - {} - {}'.format(section, cmdLine, e))
logger.error(
'Got exception executing: {} - {} - {}'.format(section, cmdLine, e)
)
return False
logger.debug('Result of executing cmd for {} was {}'.format(section, res))
return True
@@ -86,7 +90,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._api = rest.UDSServerApi(self._cfg.host, self._cfg.validateCertificate)
self._secret = secrets.token_urlsafe(33)
self._clientsPool = clients_pool.UDSActorClientPool()
self._certificate = cert.defaultCertificate # For being used on "unmanaged" hosts only
self._certificate = (
cert.defaultCertificate
) # For being used on "unmanaged" hosts only, and prior to first login
self._http = None
# Initialzies loglevel and serviceLogger
@@ -112,16 +118,24 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._http.start()
def isManaged(self) -> bool:
return self._cfg.actorType != types.UNMANAGED # Only "unmanaged" hosts are unmanaged, the rest are "managed"
return (
self._cfg.actorType != types.UNMANAGED
) # Only "unmanaged" hosts are unmanaged, the rest are "managed"
def serviceInterfaceInfo(self, interfaces: typing.Optional[typing.List[types.InterfaceInfoType]] = None) -> typing.Optional[types.InterfaceInfoType]:
def serviceInterfaceInfo(
self, interfaces: typing.Optional[typing.List[types.InterfaceInfoType]] = None
) -> typing.Optional[types.InterfaceInfoType]:
"""
returns the inteface with unique_id mac or first interface or None if no interfaces...
"""
interfaces = interfaces or self._interfaces # Emty interfaces is like "no ip change" because cannot be notified
interfaces = (
interfaces or self._interfaces
) # Emty interfaces is like "no ip change" because cannot be notified
if self._cfg.config and interfaces:
try:
return next(x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id)
return next(
x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id
)
except StopIteration:
return interfaces[0]
@@ -152,7 +166,12 @@ class CommonService: # pylint: disable=too-many-instance-attributes
while self._isAlive:
counter -= 1
try:
self._certificate = self._api.ready(self._cfg.own_token, self._secret, srvInterface.ip, rest.LISTEN_PORT)
self._certificate = self._api.ready(
self._cfg.own_token,
self._secret,
srvInterface.ip,
rest.LISTEN_PORT,
)
except rest.RESTConnectionError as e:
if not logged: # Only log connection problems ONCE
logged = True
@@ -168,7 +187,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Success or any error that is not recoverable (retunerd by UDS). if Error, service will be cleaned in a while.
break
else:
logger.error('Could not locate IP address!!!. (Not registered with UDS)')
logger.error(
'Could not locate IP address!!!. (Not registered with UDS)'
)
# Do not continue if not alive...
if not self._isAlive:
@@ -176,7 +197,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Cleans sensible data
if self._cfg.config:
self._cfg = self._cfg._replace(config=self._cfg.config._replace(os=None), data=None)
self._cfg = self._cfg._replace(
config=self._cfg.config._replace(os=None), data=None
)
platform.store.writeConfig(self._cfg)
logger.info('Service ready')
@@ -195,10 +218,10 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._cfg = self._cfg._replace(runonce_command=None)
platform.store.writeConfig(self._cfg)
if self.execute(runOnce, "runOnce"):
# If runonce is present, will not do anythin more
# So we have to ensure that, when runonce command is finished, reboots the machine.
# That is, the COMMAND itself has to restart the machine!
return False # If the command fails, continue with the rest of the operations...
# If runonce is present, will not do anythin more
# So we have to ensure that, when runonce command is finished, reboots the machine.
# That is, the COMMAND itself has to restart the machine!
return False # If the command fails, continue with the rest of the operations...
# Retry configuration while not stop service, config in case of error 10 times, reboot vm
counter = 10
@@ -208,9 +231,20 @@ class CommonService: # pylint: disable=too-many-instance-attributes
if self._cfg.config and self._cfg.config.os:
osData = self._cfg.config.os
if osData.action == 'rename':
self.rename(osData.name, osData.username, osData.password, osData.new_password)
self.rename(
osData.name,
osData.username,
osData.password,
osData.new_password,
)
elif osData.action == 'rename_ad':
self.joinDomain(osData.name, osData.ad or '', osData.ou or '', osData.username or '', osData.password or '')
self.joinDomain(
osData.name,
osData.ad or '',
osData.ou or '',
osData.username or '',
osData.password or '',
)
if self._rebootRequested:
try:
@@ -234,7 +268,12 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self.getInterfaces() # Ensure we have interfaces
if self._cfg.master_token:
try:
self._certificate = self._api.notifyUnmanagedCallback(self._cfg.master_token, self._secret, self._interfaces, rest.LISTEN_PORT)
self._certificate = self._api.notifyUnmanagedCallback(
self._cfg.master_token,
self._secret,
self._interfaces,
rest.LISTEN_PORT,
)
except Exception as e:
logger.error('Couuld not notify unmanaged callback: %s', e)
@@ -245,13 +284,17 @@ class CommonService: # pylint: disable=too-many-instance-attributes
return
while self._isAlive:
self._interfaces = list(platform.operations.getNetworkInfo())
self._interfaces = tools.validNetworkCards(
self._cfg.restrict_net, platform.operations.getNetworkInfo()
)
if self._interfaces:
break
self.doWait(5000)
def initialize(self) -> bool:
if self._initialized or not self._cfg.host or not self._isAlive: # Not configured or not running
if (
self._initialized or not self._cfg.host or not self._isAlive
): # Not configured or not running
return False
self._initialized = True
@@ -268,9 +311,15 @@ class CommonService: # pylint: disable=too-many-instance-attributes
try:
# If master token is present, initialize and get configuration data
if self._cfg.master_token:
initResult: types.InitializationResultType = self._api.initialize(self._cfg.master_token, self._interfaces, self._cfg.actorType)
initResult: types.InitializationResultType = self._api.initialize(
self._cfg.master_token, self._interfaces, self._cfg.actorType
)
if not initResult.own_token: # Not managed
logger.debug('This host is not managed by UDS Broker (ids: {})'.format(self._interfaces))
logger.debug(
'This host is not managed by UDS Broker (ids: {})'.format(
self._interfaces
)
)
return False
# Only removes master token for managed machines (will need it on next client execution)
@@ -279,9 +328,8 @@ class CommonService: # pylint: disable=too-many-instance-attributes
master_token=master_token,
own_token=initResult.own_token,
config=types.ActorDataConfigurationType(
unique_id=initResult.unique_id,
os=initResult.os
)
unique_id=initResult.unique_id, os=initResult.os
),
)
# On first successfull initialization request, master token will dissapear for managed hosts so it will be no more available (not needed anyway)
@@ -294,10 +342,16 @@ class CommonService: # pylint: disable=too-many-instance-attributes
break # Initial configuration done..
except rest.RESTConnectionError as e:
logger.info('Trying to inititialize connection with broker (last error: {})'.format(e))
logger.info(
'Trying to inititialize connection with broker (last error: {})'.format(
e
)
)
self.doWait(5000) # Wait a bit and retry
except rest.RESTError as e: # Invalid key?
logger.error('Error validating with broker. (Invalid token?): {}'.format(e))
except rest.RESTError as e: # Invalid key?
logger.error(
'Error validating with broker. (Invalid token?): {}'.format(e)
)
return False
except Exception:
logger.exception()
@@ -307,7 +361,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
def uninitialize(self):
self._initialized = False
self._cfg = self._cfg._replace(own_token=None) # Ensures assigned token is cleared
self._cfg = self._cfg._replace(
own_token=None
) # Ensures assigned token is cleared
def finish(self) -> None:
if self._http:
@@ -321,8 +377,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._cfg.actorType,
self._cfg.own_token,
'',
'',
self._interfaces,
self._secret
self._secret,
)
except Exception as e:
logger.error('Error notifying final logout to UDS: %s', e)
@@ -334,19 +391,33 @@ class CommonService: # pylint: disable=too-many-instance-attributes
return # Unamanaged hosts does not changes ips. (The full initialize-login-logout process is done in a row, so at login the IP is correct)
try:
if not self._cfg.own_token or not self._cfg.config or not self._cfg.config.unique_id:
if (
not self._cfg.own_token
or not self._cfg.config
or not self._cfg.config.unique_id
):
# Not enouth data do check
return
currentInterfaces = list(platform.operations.getNetworkInfo())
currentInterfaces = tools.validNetworkCards(
self._cfg.restrict_net, platform.operations.getNetworkInfo()
)
old = self.serviceInterfaceInfo()
new = self.serviceInterfaceInfo(currentInterfaces)
if not new or not old:
raise Exception('No ip currently available for {}'.format(self._cfg.config.unique_id))
raise Exception(
'No ip currently available for {}'.format(
self._cfg.config.unique_id
)
)
if old.ip != new.ip:
self._certificate = self._api.notifyIpChange(self._cfg.own_token, self._secret, new.ip, rest.LISTEN_PORT)
self._certificate = self._api.notifyIpChange(
self._cfg.own_token, self._secret, new.ip, rest.LISTEN_PORT
)
# Now store new addresses & interfaces...
self._interfaces = currentInterfaces
logger.info('Ip changed from {} to {}. Notified to UDS'.format(old.ip, new.ip))
logger.info(
'Ip changed from {} to {}. Notified to UDS'.format(old.ip, new.ip)
)
# Stop the running HTTP Thread and start a new one, with new generated cert
self.startHttpServer()
except Exception as e:
@@ -354,12 +425,12 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.warn('Checking ips failed: {}'.format(e))
def rename(
self,
name: str,
userName: typing.Optional[str] = None,
oldPassword: typing.Optional[str] = None,
newPassword: typing.Optional[str] = None
) -> None:
self,
name: str,
userName: typing.Optional[str] = None,
oldPassword: typing.Optional[str] = None,
newPassword: typing.Optional[str] = None,
) -> None:
'''
Invoked when broker requests a rename action
default does nothing
@@ -370,10 +441,14 @@ class CommonService: # pylint: disable=too-many-instance-attributes
if userName and newPassword:
logger.info('Setting password for configured user')
try:
platform.operations.changeUserPassword(userName, oldPassword or '', newPassword)
platform.operations.changeUserPassword(
userName, oldPassword or '', newPassword
)
except Exception as e:
# Logs error, but continue renaming computer
logger.error('Could not change password for user {}: {}'.format(userName, e))
logger.error(
'Could not change password for user {}: {}'.format(userName, e)
)
if hostName.lower() == name.lower():
logger.info('Computer name is already {}'.format(hostName))
@@ -390,7 +465,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Now check if every registered client is already there (if logged in OFC)
if self._loggedIn and not self._clientsPool.ping():
self.logout('client_unavailable')
self.logout('client_unavailable', '')
except Exception as e:
logger.error('Exception on main service loop: %s', e)
@@ -398,13 +473,8 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Methods that can be overriden by linux & windows Actor
# ******************************************************
def joinDomain( # pylint: disable=unused-argument, too-many-arguments
self,
name: str,
domain: str,
ou: str,
account: str,
password: str
) -> None:
self, name: str, domain: str, ou: str, account: str, password: str
) -> None:
'''
Invoked when broker requests a "domain" action
default does nothing
@@ -412,19 +482,24 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.debug('Base join invoked: {} on {}, {}'.format(name, domain, ou))
# Client notifications
def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
result = types.LoginResultInfoType(ip='', hostname='', dead_line=None, max_idle=None)
self._loggedIn = True
def login(
self, username: str, sessionType: typing.Optional[str] = None
) -> types.LoginResultInfoType:
result = types.LoginResultInfoType(
ip='', hostname='', dead_line=None, max_idle=None
)
master_token = None
secret = None
# If unmanaged, do initialization now, because we don't know before this
# Also, even if not initialized, get a "login" notification token
if not self.isManaged():
self.initialize()
self._initialized = (
self.initialize()
) # Maybe it's a local login by an unmanaged host.... On real login, will execute initilize again
# Unamanaged, need the master token
master_token = self._cfg.master_token
secret = self._secret
# Own token will not be set if UDS did not assigned the initialized VM to an user
# In that case, take master token (if machine is Unamanaged version)
token = self._cfg.own_token or master_token
@@ -435,33 +510,43 @@ class CommonService: # pylint: disable=too-many-instance-attributes
username,
sessionType or '',
self._interfaces,
secret
secret,
)
script = platform.store.invokeScriptOnLogin()
if script:
script += f'{username} {sessionType or "unknown"} {self._cfg.actorType}'
self.execute(script, 'Logon')
if result.logged_in:
logger.debug('Login successful')
self._loggedIn = True
script = platform.store.invokeScriptOnLogin()
if script:
logger.info('Executing script on login: {}'.format(script))
script += f'{username} {sessionType or "unknown"} {self._cfg.actorType}'
self.execute(script, 'Logon')
return result
def logout(self, username: str) -> None:
self._loggedIn = False
def logout(self, username: str, sessionType: typing.Optional[str]) -> None:
master_token = self._cfg.master_token
# Own token will not be set if UDS did not assigned the initialized VM to an user
# In that case, take master token (if machine is Unamanaged version)
token = self._cfg.own_token or master_token
if token:
self._api.logout(
self._cfg.actorType,
token,
username,
self._interfaces,
self._secret
)
# If logout is not processed (that is, not ok result), the logout has not been processed
if (
self._api.logout(
self._cfg.actorType,
token,
username,
sessionType or '',
self._interfaces,
self._secret,
)
!= 'ok'
):
logger.info('Logout from %s ignored as required by uds broker', username)
return
self._loggedIn = False
self.onLogout(username)
if not self.isManaged():
@@ -488,13 +573,25 @@ class CommonService: # pylint: disable=too-many-instance-attributes
'''
logger.info('Service stopped')
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str) -> str: # pylint: disable=unused-argument
def preConnect(
self, userName: str, protocol: str, ip: str, hostname: str, udsUserName: str
) -> str:
'''
Invoked when received a PRE Connection request via REST
Base preconnect executes the preconnect command
'''
if self._cfg.pre_command:
self.execute(self._cfg.pre_command + ' {} {} {} {}'.format(userName.replace('"', '%22'), protocol, ip, hostname), 'preConnect')
self.execute(
self._cfg.pre_command
+ ' {} {} {} {} {}'.format(
userName.replace('"', '%22'),
protocol,
ip,
hostname,
udsUserName.replace('"', '%22'),
),
'preConnect',
)
return 'ok'

View File

@@ -28,20 +28,58 @@
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
# pylint: disable=invalid-name
import threading
import ipaddress
import typing
if typing.TYPE_CHECKING:
from udsactor.types import InterfaceInfoType
from udsactor.log import logger
class ScriptExecutorThread(threading.Thread):
def __init__(self, script: str) -> None:
super(ScriptExecutorThread, self).__init__()
self.script = script
def run(self) -> None:
from udsactor.log import logger
try:
logger.debug('Executing script: {}'.format(self.script))
exec(self.script, globals(), None) # pylint: disable=exec-used
except Exception as e:
logger.error('Error executing script: {}'.format(e))
logger.exception()
# Convert "X.X.X.X/X" to ipaddress.IPv4Network
def strToNoIPV4Network(net: typing.Optional[str]) -> typing.Optional[ipaddress.IPv4Network]:
if not net: # Empty or None
return None
try:
return ipaddress.IPv4Interface(net).network
except Exception:
return None
def validNetworkCards(
net: typing.Optional[str], cards: typing.Iterable['InterfaceInfoType']
) -> typing.List['InterfaceInfoType']:
try:
subnet = strToNoIPV4Network(net)
except Exception as e:
subnet = None
if subnet is None:
return list(cards)
def isValid(ip: str, subnet: ipaddress.IPv4Network) -> bool:
if not ip:
return False
try:
return ipaddress.IPv4Address(ip) in subnet
except Exception:
return False
return [c for c in cards if isValid(c.ip, subnet)]

View File

@@ -35,6 +35,7 @@ class ActorConfigurationType(typing.NamedTuple):
actorType: typing.Optional[str] = None
master_token: typing.Optional[str] = None
own_token: typing.Optional[str] = None
restrict_net: typing.Optional[str] = None
pre_command: typing.Optional[str] = None
runonce_command: typing.Optional[str] = None
@@ -57,7 +58,12 @@ class LoginResultInfoType(typing.NamedTuple):
dead_line: typing.Optional[int]
max_idle: typing.Optional[int] # Not provided by broker
@property
def logged_in(self) -> bool:
return self.hostname != '' or self.ip != ''
class CertificateInfoType(typing.NamedTuple):
private_key: str
server_certificate: str
password: str
ciphers: str

View File

@@ -1 +1 @@
VERSION = '3.5.0'
VERSION = '3.6.0'

View File

@@ -35,7 +35,6 @@ import tempfile
import typing
import servicemanager
from udsactor import service # pylint: disable=import-error
# Valid logging levels, from UDS Broker (uds.core.utils.log).
from .. import loglevel

View File

@@ -139,7 +139,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
logger.info('Using multiple step join because configuration requests to do so')
self.multiStepJoin(name, domain, ou, account, password)
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str) -> str:
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str, udsUserName: str) -> str:
logger.debug('Pre connect invoked')
if protocol == 'rdp': # If connection is not using rdp, skip adding user
@@ -168,7 +168,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
self._user = None
logger.debug('User {} already in group'.format(userName))
return super().preConnect(userName, protocol, ip, hostname)
return super().preConnect(userName, protocol, ip, hostname, udsUserName)
def ovLogon(self, username: str, password: str) -> str:
"""

View File

@@ -2,9 +2,10 @@
# Form implementation generated from reading ui file 'setup-dialog.ui'
#
# Created by: PyQt5 UI code generator 5.13.2
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
@@ -191,6 +192,7 @@ class Ui_UdsActorSetupDialog(object):
self.retranslateUi(UdsActorSetupDialog)
self.tabWidget.setCurrentIndex(0)
self.validateCertificate.setCurrentIndex(1)
self.logLevelComboBox.setCurrentIndex(1)
self.closeButton.clicked.connect(UdsActorSetupDialog.finish)
self.registerButton.clicked.connect(UdsActorSetupDialog.registerWithUDS)

View File

@@ -2,9 +2,10 @@
# Form implementation generated from reading ui file 'setup-dialog-unmanaged.ui'
#
# Created by: PyQt5 UI code generator 5.13.2
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
@@ -14,7 +15,7 @@ class Ui_UdsActorSetupDialog(object):
def setupUi(self, UdsActorSetupDialog):
UdsActorSetupDialog.setObjectName("UdsActorSetupDialog")
UdsActorSetupDialog.setWindowModality(QtCore.Qt.WindowModal)
UdsActorSetupDialog.resize(595, 220)
UdsActorSetupDialog.resize(601, 243)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -34,12 +35,12 @@ class Ui_UdsActorSetupDialog(object):
UdsActorSetupDialog.setModal(True)
self.saveButton = QtWidgets.QPushButton(UdsActorSetupDialog)
self.saveButton.setEnabled(True)
self.saveButton.setGeometry(QtCore.QRect(10, 180, 181, 23))
self.saveButton.setGeometry(QtCore.QRect(10, 210, 181, 23))
self.saveButton.setMinimumSize(QtCore.QSize(181, 0))
self.saveButton.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
self.saveButton.setObjectName("saveButton")
self.closeButton = QtWidgets.QPushButton(UdsActorSetupDialog)
self.closeButton.setGeometry(QtCore.QRect(410, 180, 171, 23))
self.closeButton.setGeometry(QtCore.QRect(410, 210, 171, 23))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -49,11 +50,11 @@ class Ui_UdsActorSetupDialog(object):
self.closeButton.setObjectName("closeButton")
self.testButton = QtWidgets.QPushButton(UdsActorSetupDialog)
self.testButton.setEnabled(False)
self.testButton.setGeometry(QtCore.QRect(210, 180, 181, 23))
self.testButton.setGeometry(QtCore.QRect(210, 210, 181, 23))
self.testButton.setMinimumSize(QtCore.QSize(181, 0))
self.testButton.setObjectName("testButton")
self.layoutWidget = QtWidgets.QWidget(UdsActorSetupDialog)
self.layoutWidget.setGeometry(QtCore.QRect(10, 10, 571, 161))
self.layoutWidget.setGeometry(QtCore.QRect(10, 10, 571, 191))
self.layoutWidget.setObjectName("layoutWidget")
self.formLayout = QtWidgets.QFormLayout(self.layoutWidget)
self.formLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
@@ -84,7 +85,7 @@ class Ui_UdsActorSetupDialog(object):
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.serviceToken)
self.label_loglevel = QtWidgets.QLabel(self.layoutWidget)
self.label_loglevel.setObjectName("label_loglevel")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_loglevel)
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_loglevel)
self.logLevelComboBox = QtWidgets.QComboBox(self.layoutWidget)
self.logLevelComboBox.setFrame(True)
self.logLevelComboBox.setObjectName("logLevelComboBox")
@@ -96,7 +97,13 @@ class Ui_UdsActorSetupDialog(object):
self.logLevelComboBox.setItemText(2, "ERROR")
self.logLevelComboBox.addItem("")
self.logLevelComboBox.setItemText(3, "FATAL")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.logLevelComboBox)
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.logLevelComboBox)
self.label_restrictNet = QtWidgets.QLabel(self.layoutWidget)
self.label_restrictNet.setObjectName("label_restrictNet")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_restrictNet)
self.restrictNet = QtWidgets.QLineEdit(self.layoutWidget)
self.restrictNet.setObjectName("restrictNet")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.restrictNet)
self.label_host.raise_()
self.host.raise_()
self.label_serviceToken.raise_()
@@ -105,6 +112,8 @@ class Ui_UdsActorSetupDialog(object):
self.label_security.raise_()
self.label_loglevel.raise_()
self.logLevelComboBox.raise_()
self.label_restrictNet.raise_()
self.restrictNet.raise_()
self.retranslateUi(UdsActorSetupDialog)
self.logLevelComboBox.setCurrentIndex(1)
@@ -113,6 +122,7 @@ class Ui_UdsActorSetupDialog(object):
self.saveButton.clicked.connect(UdsActorSetupDialog.saveConfig)
self.host.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
self.serviceToken.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
self.restrictNet.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
QtCore.QMetaObject.connectSlotsByName(UdsActorSetupDialog)
def retranslateUi(self, UdsActorSetupDialog):
@@ -136,7 +146,10 @@ class Ui_UdsActorSetupDialog(object):
self.host.setToolTip(_translate("UdsActorSetupDialog", "Uds Broker Server Addres. Use IP or FQDN"))
self.host.setWhatsThis(_translate("UdsActorSetupDialog", "Enter here the UDS Broker Addres using either its IP address or its FQDN address"))
self.label_serviceToken.setText(_translate("UdsActorSetupDialog", "Service Token"))
self.serviceToken.setToolTip(_translate("UdsActorSetupDialog", "UDS user with administration rights (Will not be stored on template)"))
self.serviceToken.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Administrator user on UDS Server.</p><p>Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.</p></body></html>"))
self.serviceToken.setToolTip(_translate("UdsActorSetupDialog", "UDS Service Token"))
self.serviceToken.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Token of the service on UDS platform</p><p>This token can be obtainend from the service configuration on UDS.</p></body></html>"))
self.label_loglevel.setText(_translate("UdsActorSetupDialog", "Log Level"))
self.label_restrictNet.setText(_translate("UdsActorSetupDialog", "Restrict Net"))
self.restrictNet.setToolTip(_translate("UdsActorSetupDialog", "Restrict valid detection of network interfaces to this network."))
self.restrictNet.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Restrics valid detection of network interfaces.</p><p>Note: Use this field only in case of several network interfaces, so UDS knows which one is the interface where the user will be connected..</p></body></html>"))
from ui import uds_rc

View File

@@ -2,7 +2,7 @@
# Resource object code
#
# Created by: The Resource Compiler for PyQt5 (Qt v5.13.2)
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
#
# WARNING! All changes made in this file will be lost!
@@ -175,7 +175,7 @@ qt_resource_struct_v2 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x6e\x86\x31\xef\xa3\
\x00\x00\x01\x81\xce\x8a\xac\xf2\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]

View File

@@ -6,3 +6,4 @@
/UDSClient*.AppImage
/appimage*
/UDSClient.desktop
*.zsync

View File

@@ -64,9 +64,9 @@ ifeq ($(DISTRO),i686)
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g | sed -e s/amd64/i386/g | sed -e s/x86_64/i686/g > appimage.recipe
endif
# Ensure all working folders are "clean"
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir appimage-build AppDir
appimage-builder --recipe appimage.recipe
appimage-builder --recipe appimage.recipe --appdir /tmp/UDSClientDir
# Now create dist and move appimage
rm -rf $(DESTDIR)
mkdir -p $(DESTDIR)
@@ -79,7 +79,7 @@ endif
tar czvf ../udsclient3-$(DISTRO)-$(VERSION).tar.gz -C $(DESTDIR) .
# cleanup
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir appimage-build AppDir
build-igel:
rm -rf $(DESTDIR)

View File

@@ -12,9 +12,6 @@ cat udsclient-template.spec |
sed -e s/"version 0.0.0"/"version ${VERSION}"/g |
sed -e s/"release 1"/"release ${RELEASE}"/g > udsclient-$VERSION.spec
cat appimage-udsclient.recipe |
sed -e s/"version: 0.0.0"/"version: ${VERSION}"/g > appimage.recipe
# Now fix dependencies for opensuse
# Note: Right now, opensuse & rh seems to have same dependencies, only 1 package needed
# cat udsclient-template.spec |

View File

@@ -1,3 +1,9 @@
udsclient3 (3.6.0) stable; urgency=medium
* Upgraded to 3.6.0 release
-- Adolfo Gómez García <agomez@virtualcable.es> Fri, 1 Jul 2022 14:12:10 +0200
udsclient3 (3.5.0) stable; urgency=medium
* Upgraded to 3.5.0 release

View File

@@ -1 +1 @@
9
10

View File

@@ -1,10 +1,10 @@
Source: udsclient3
Section: admin
Priority: optional
Maintainer: Adolfo Gómez García <agomez@virtualcable.es>
Maintainer: Adolfo Gómez García <agomez@virtualcable.net>
Build-Depends: debhelper (>= 7), po-debconf
Standards-Version: 3.9.2
Homepage: http://www.virtualcable.es
Homepage: http://www.udsenterprise.com
Package: udsclient3
Section: admin

View File

@@ -5,9 +5,9 @@ Source: http://github.com/dkmstr/openuds/client-py3
Files: *
Copyright: (c) 2014-2022, Virtual Cable S.L.U.
License: 3-BSD
License: BSD-3-clause
License: 3-BSD
License: BSD-3-clause
All rights reserved.
.
Redistribution and use in source and binary forms, with or without
@@ -35,4 +35,4 @@ License: 3-BSD
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,2 +1,2 @@
udsclient3_3.5.0_all.deb admin optional
udsclient3_3.5.0_amd64.buildinfo admin optional
udsclient3_3.6.0_all.deb admin optional
udsclient3_3.6.0_amd64.buildinfo admin optional

View File

@@ -1,2 +1,4 @@
#!/bin/sh
cp /UDSClient/UDSClient.desktop /usr/share/applications.mime
chmod 755 /UDSClient/UDSClient

View File

@@ -9,6 +9,7 @@ fi
echo "Installing UDSClient Portable..."
cp UDSClient-0.0.0-x86_64.AppImage /usr/bin
chmod 755 /usr/bin/UDSClient-0.0.0-x86_64.AppImage
cp UDSClient.desktop /usr/share/applications
update-desktop-database

View File

@@ -1,3 +1,5 @@
UDSClient is the client connector needed to get acccess to services managed by UDS Broker.
For raspberry Pi, AppImage does not works with 1.1.0 (works with 1.0.3)
Please, visit http://www.udsenterprise.com for more information

View File

@@ -1,18 +1,17 @@
version: 1
script:
# Remove any previous build
- rm -rf /tmp/UDSClientDir | true
- rm -rf $TARGET_APPDIR | true
# Make usr and icons dirs
- mkdir -p /tmp/UDSClientDir/usr/src
- mkdir -p $TARGET_APPDIR/usr/src
# Copy the python application code into the UDSClientDir
- cp ../src/UDS*.py /tmp/UDSClientDir/usr/src
- cp -r ../src/uds /tmp/UDSClientDir/usr/src
- cp ../src/UDS*.py $TARGET_APPDIR/usr/src
- cp -r ../src/uds $TARGET_APPDIR/usr/src
# Remove __pycache__ and .mypy if exists
- rm /tmp/UDSClientDir/usr/src/.mypy_cache -rf 2>&1 > /dev/null
- rm /tmp/UDSClientDir/usr/src/uds/.mypy_cache -rf 2>&1 > /dev/null
- rm /tmp/UDSClientDir/usr/src/__pycache__ -rf 2>&1 > /dev/null
- rm /tmp/UDSClientDir/usr/src/uds/__pycache__ -rf 2>&1 > /dev/null
- rm $TARGET_APPDIR/usr/src/.mypy_cache -rf 2>&1 > /dev/null
- rm $TARGET_APPDIR/usr/src/uds/.mypy_cache -rf 2>&1 > /dev/null
- rm $TARGET_APPDIR/usr/src/__pycache__ -rf 2>&1 > /dev/null
- rm $TARGET_APPDIR/usr/src/uds/__pycache__ -rf 2>&1 > /dev/null
AppDir:
# On /tmp, that is an ext4 filesystem. On btrfs squashfs complains with "Unrecognised xattr prefix btrfs.compression"
path: /tmp/UDSClientDir
@@ -57,6 +56,6 @@ AppDir:
PYTHONPATH: '${APPDIR}/usr/lib/python3.9/site-packages'
AppImage:
update-information: None
sign-key: None
# update-information: None
sign-key: 592AF43A64B8559137FA2458AA4ECFEE784E6BA7
arch: x86_64

View File

@@ -41,11 +41,11 @@ import typing
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QSettings
from uds.rest import RestApi, RetryException, InvalidVersion, UDSException
from uds.rest import RestApi, RetryException, InvalidVersion
# Just to ensure there are available on runtime
from uds.forward import forward as ssh_forward # type: ignore
from uds.tunnel import forward as tunnel_forwards # type: ignore
from uds.forward import forward as ssh_forward # type: ignore # pylint: disable=unused-import
from uds.tunnel import forward as tunnel_forwards # type: ignore # pylint: disable=unused-import
from uds.log import logger
from uds import tools
@@ -55,7 +55,6 @@ from UDSWindow import Ui_MainWindow
class UDSClient(QtWidgets.QMainWindow):
ticket: str = ''
scrambler: str = ''
withError = False
@@ -149,8 +148,10 @@ class UDSClient(QtWidgets.QMainWindow):
webbrowser.open(e.downloadUrl)
self.closeWindow()
return
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
self.showError(e)
self.closeWindow()
return
self.getTransportData()
@@ -166,7 +167,9 @@ class UDSClient(QtWidgets.QMainWindow):
# self.hide()
self.closeWindow()
exec(script, globals(), {'parent': self, 'sp': params})
exec(
script, globals(), {'parent': self, 'sp': params}
) # pylint: disable=exec-used
# Execute the waiting tasks...
threading.Thread(target=endScript).start()
@@ -175,7 +178,8 @@ class UDSClient(QtWidgets.QMainWindow):
self.ui.info.setText(str(e) + ', retrying access...')
# Retry operation in ten seconds
QtCore.QTimer.singleShot(10000, self.getTransportData)
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.exception('Error getting transport data')
self.showError(e)
def start(self):
@@ -192,27 +196,27 @@ def endScript():
try:
# Remove early stage files...
tools.unlinkFiles(early=True)
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.debug('Unlinking files on early stage: %s', e)
# After running script, wait for stuff
try:
logger.debug('Wating for tasks to finish...')
tools.waitForTasks()
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.debug('Watiting for tasks to finish: %s', e)
try:
logger.debug('Unlinking files')
tools.unlinkFiles(early=False)
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.debug('Unlinking files on later stage: %s', e)
# Removing
try:
logger.debug('Executing threads before exit')
tools.execBeforeExit()
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.debug('execBeforeExit: %s', e)
logger.debug('endScript done')
@@ -303,7 +307,7 @@ def minimal(api: RestApi, ticket: str, scrambler: str):
+ '\n\nPlease, retry again in a while.',
QtWidgets.QMessageBox.Ok,
)
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
# logger.exception('Got exception on getTransportData')
QtWidgets.QMessageBox.critical(
None, # type: ignore
@@ -350,31 +354,38 @@ def main(args: typing.List[str]):
sys.exit(0)
logger.debug('URI: %s', uri)
if uri[:6] != 'uds://' and uri[:7] != 'udss://':
raise Exception()
# Shows error if using http (uds:// ) version, not supported anymore
if uri[:6] == 'uds://':
QtWidgets.QMessageBox.critical(
None, # type: ignore
'Notice',
f'UDS Client Version {VERSION} does not support HTTP protocol Anymore.',
QtWidgets.QMessageBox.Ok,
)
sys.exit(1)
if uri[:7] != 'udss://':
raise Exception('Not supported protocol') # Just shows "about" dialog
ssl = uri[3] == 's'
host, ticket, scrambler = uri.split('//')[1].split('/') # type: ignore
logger.debug(
'ssl:%s, host:%s, ticket:%s, scrambler:%s',
ssl,
'host:%s, ticket:%s, scrambler:%s',
host,
ticket,
scrambler,
)
except Exception:
except Exception: # pylint: disable=broad-except
logger.debug('Detected execution without valid URI, exiting')
QtWidgets.QMessageBox.critical(
None, # type: ignore
'Notice',
'UDS Client Version {}'.format(VERSION),
f'UDS Client Version {VERSION}',
QtWidgets.QMessageBox.Ok,
)
sys.exit(1)
# Setup REST api endpoint
api = RestApi(
'{}://{}/uds/rest/client'.format(['http', 'https'][ssl], host), sslError
f'https://{host}/uds/rest/client', sslError
)
try:
@@ -392,7 +403,7 @@ def main(args: typing.List[str]):
exitVal = app.exec()
logger.debug('Execution finished correctly')
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.exception('Got an exception executing client:')
exitVal = 128
QtWidgets.QMessageBox.critical(
@@ -402,5 +413,6 @@ def main(args: typing.List[str]):
logger.debug('Exiting')
sys.exit(exitVal)
if __name__ == "__main__":
main(sys.argv)

View File

@@ -11,6 +11,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui
SCRIPT_NAME = 'UDSClientLauncher'
class UdsApplication(QtWidgets.QApplication):
path: str
tunnels: typing.List[subprocess.Popen]
@@ -22,6 +23,10 @@ class UdsApplication(QtWidgets.QApplication):
self.lastWindowClosed.connect(self.closeTunnels) # type: ignore
def cleanTunnels(self) -> None:
'''
Removes all finished tunnels from the list
'''
def isRunning(p: subprocess.Popen):
try:
if p.poll() is None:
@@ -30,13 +35,13 @@ class UdsApplication(QtWidgets.QApplication):
logger.debug('Got error polling subprocess: %s', e)
return False
for k in [i for i, tunnel in enumerate(self.tunnels) if not isRunning(tunnel)]:
try:
del self.tunnels[k]
except Exception as e:
logger.debug('Error closing tunnel: %s', e)
# Remove references to finished tunnels, they will be garbage collected
self.tunnels = [tunnel for tunnel in self.tunnels if isRunning(tunnel)]
def closeTunnels(self) -> None:
'''
Finishes all running tunnels
'''
logger.debug('Closing remaining tunnels')
for tunnel in self.tunnels:
logger.debug('Checking %s - "%s"', tunnel, tunnel.poll())
@@ -45,7 +50,7 @@ class UdsApplication(QtWidgets.QApplication):
tunnel.kill()
def event(self, evnt: QtCore.QEvent) -> bool:
if evnt.type() == QtCore.QEvent.FileOpen:
if evnt.type() == QtCore.QEvent.Type.FileOpen:
fe = typing.cast(QtGui.QFileOpenEvent, evnt)
logger.debug('Got url: %s', fe.url().url())
fe.accept()
@@ -70,6 +75,6 @@ def main(args: typing.List[str]):
sys.exit(app.exec())
if __name__ == "__main__":
main(args=sys.argv)

View File

@@ -72,4 +72,4 @@ if __name__ == "__main__":
ui = Ui_MacLauncher()
ui.setupUi(MacLauncher)
MacLauncher.show()
sys.exit(app.exec())
sys.exit(app.exec_())

View File

@@ -2467,9 +2467,9 @@ qt_resource_struct_v2 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x70\xc4\x82\x24\xd0\
\x00\x00\x01\x81\xce\x8a\xaf\xd2\
\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x08\xf1\
\x00\x00\x01\x70\xc4\x82\x24\xd0\
\x00\x00\x01\x81\xce\x8a\xaf\xd2\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]

View File

@@ -90,4 +90,4 @@ if __name__ == "__main__":
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec())
sys.exit(app.exec_())

View File

@@ -29,13 +29,11 @@
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
VERSION = '3.5.0'
VERSION = '3.6.0'
__title__ = 'udclient'
__version__ = VERSION
__build__ = 0x010760
__author__ = 'Adolfo Gómez'
__build__ = 0x010712
__author__ = 'Adolfo Gómez <dkmaster@dkmon.com>'
__license__ = "BSD 3-clause"
__copyright__ = "Copyright 2014-2017 VirtualCable S.L.U."
__copyright__ = "Copyright 2014-2022 VirtualCable S.L.U."

View File

@@ -216,7 +216,7 @@ class ForwardThread(threading.Thread):
class SubHandler(Handler):
chain_host = self.redirectHost
chain_port = self.redirectPort
ssh_transport = self.client.get_transport()
ssh_transport = self.client.get_transport() # type: ignore
event = self.stopEvent
thread = self

View File

@@ -32,6 +32,7 @@
import logging
import os
import os.path
import platform
import sys
import tempfile
@@ -61,3 +62,38 @@ except Exception:
logging.basicConfig(format='%(levelname)s %(asctime)s %(message)s', level=LOGLEVEL)
logger = logging.getLogger('udsclient')
if DEBUG:
# Include as much as platform info as possible
logger.debug('Platform info:')
logger.debug(' Platform: %s', platform.platform())
logger.debug(' Node: %s', platform.node())
logger.debug(' System: %s', platform.system())
logger.debug(' Release: %s', platform.release())
logger.debug(' Version: %s', platform.version())
logger.debug(' Machine: %s', platform.machine())
logger.debug(' Processor: %s', platform.processor())
logger.debug(' Architecture: %s', platform.architecture())
logger.debug(' Python version: %s', platform.python_version())
logger.debug(' Python implementation: %s', platform.python_implementation())
logger.debug(' Python compiler: %s', platform.python_compiler())
logger.debug(' Python build: %s', platform.python_build())
# Also environment variables and any useful info
logger.debug('Log level set to DEBUG')
logger.debug('Environment variables:')
for k, v in os.environ.items():
logger.debug(' %s=%s', k, v)
# usefull info for debugging
logger.debug('Python path: %s', sys.path)
logger.debug('Python executable: %s', sys.executable)
logger.debug('Python version: %s', sys.version)
logger.debug('Python version info: %s', sys.version_info)
logger.debug('Python prefix: %s', sys.prefix)
logger.debug('Python base prefix: %s', sys.base_prefix)
logger.debug('Python executable: %s', sys.executable)
logger.debug('Python argv: %s', sys.argv)
logger.debug('Python modules path: %s', sys.path)
logger.debug('Python modules path (site): %s', sys.path_importer_cache)
logger.debug('Python modules path (site): %s', sys.path_hooks)

View File

@@ -51,6 +51,19 @@ from .log import logger
# Server before this version uses "unsigned" scripts
OLD_METHOD_VERSION = '2.4.0'
SECURE_CIPHERS = (
'TLS_AES_256_GCM_SHA384'
':TLS_CHACHA20_POLY1305_SHA256'
':TLS_AES_128_GCM_SHA256'
':ECDHE-RSA-AES256-GCM-SHA384'
':ECDHE-RSA-AES128-GCM-SHA256'
':ECDHE-RSA-CHACHA20-POLY1305'
':ECDHE-ECDSA-AES128-GCM-SHA256'
':ECDHE-ECDSA-AES256-GCM-SHA384'
':ECDHE-ECDSA-AES128-SHA256'
':ECDHE-ECDSA-CHACHA20-POLY1305'
)
# Callback for error on cert
# parameters are hostname, serial
# If returns True, ignores error
@@ -72,7 +85,6 @@ class InvalidVersion(UDSException):
super().__init__(downloadUrl)
self.downloadUrl = downloadUrl
class RestApi:
_restApiUrl: str # base Rest API URL
@@ -127,6 +139,8 @@ class RestApi:
raise InvalidVersion(downloadUrl)
return self._serverVersion
except InvalidVersion:
raise
except Exception as e:
raise UDSException(e)
@@ -150,10 +164,7 @@ class RestApi:
params = None
if self._serverVersion <= OLD_METHOD_VERSION:
script = bz2.decompress(base64.b64decode(data['result']))
# This fixes uds 2.2 "write" string on binary streams on some transport
script = script.replace(b'stdin.write("', b'stdin.write(b"')
script = script.replace(b'version)', b'version.decode("utf-8"))')
raise Exception('Server version is too old. Please, update it')
else:
res = data['result']
# We have three elements on result:
@@ -185,6 +196,10 @@ class RestApi:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
# Disable SSLv2, SSLv3, TLSv1, TLSv1.1
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.set_ciphers(SECURE_CIPHERS)
# If we have the certificates file, we use it
if tools.getCaCertsFile() is not None:
ctx.load_verify_locations(tools.getCaCertsFile())

View File

@@ -44,11 +44,17 @@ import typing
import certifi
# For signature checking
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import utils, padding
try:
import psutil
except ImportError:
psutil = None
from .log import logger
_unlinkFiles: typing.List[typing.Tuple[str, bool]] = []
@@ -76,9 +82,7 @@ nVgtClKcDDlSaBsO875WDR0CAwEAAQ==
def saveTempFile(content: str, filename: typing.Optional[str] = None) -> str:
if filename is None:
filename = ''.join(
random.choice(string.ascii_lowercase + string.digits) for _ in range(16)
)
filename = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
filename = filename + '.uds'
filename = os.path.join(tempfile.gettempdir(), filename)
@@ -108,9 +112,7 @@ def testServer(host: str, port: typing.Union[str, int], timeOut: int = 4) -> boo
return True
def findApp(
appName: str, extraPath: typing.Optional[str] = None
) -> typing.Optional[str]:
def findApp(appName: str, extraPath: typing.Optional[str] = None) -> typing.Optional[str]:
searchPath = os.environ['PATH'].split(os.pathsep)
if extraPath:
searchPath += list(extraPath)
@@ -139,9 +141,7 @@ def addFileToUnlink(filename: str, early: bool = False) -> None:
'''
Adds a file to the wait-and-unlink list
'''
logger.debug(
'Added file %s to unlink on %s stage', filename, 'early' if early else 'later'
)
logger.debug('Added file %s to unlink on %s stage', filename, 'early' if early else 'later')
_unlinkFiles.append((filename, early))
@@ -195,9 +195,7 @@ def waitForTasks() -> None:
psutil.process_iter(attrs=('ppid',)),
)
)
logger.debug(
'Waiting for subprocesses... %s, %s', task.pid, subProcesses
)
logger.debug('Waiting for subprocesses... %s, %s', task.pid, subProcesses)
for i in subProcesses:
logger.debug('Found %s', i)
i.wait()
@@ -224,18 +222,11 @@ def verifySignature(script: bytes, signature: bytes) -> bool:
param: signature String signature to be verified
return: Boolean. True if the signature is valid; False otherwise.
'''
# For signature checking
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import utils, padding
public_key = serialization.load_pem_public_key(
data=PUBLIC_KEY, backend=default_backend()
)
public_key = serialization.load_pem_public_key(data=PUBLIC_KEY, backend=default_backend())
try:
public_key.verify(
base64.b64decode(signature), script, padding.PKCS1v15(), hashes.SHA256()
public_key.verify( # type: ignore
base64.b64decode(signature), script, padding.PKCS1v15(), hashes.SHA256() # type: ignore
)
except Exception: # InvalidSignature
return False
@@ -247,6 +238,10 @@ def verifySignature(script: bytes, signature: bytes) -> bool:
def getCaCertsFile() -> typing.Optional[str]:
# First, try certifi...
# If environment contains CERTIFICATE_BUNDLE_PATH, use it
if 'CERTIFICATE_BUNDLE_PATH' in os.environ:
return os.environ['CERTIFICATE_BUNDLE_PATH']
try:
if os.path.exists(certifi.where()):
return certifi.where()
@@ -257,9 +252,17 @@ def getCaCertsFile() -> typing.Optional[str]:
# Check if "standard" paths are valid for linux systems
if 'linux' in sys.platform:
for path in ('/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/certs/ca-certificates.crt', '/etc/ssl/ca-bundle.pem'):
for path in (
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/certs/ca-certificates.crt',
'/etc/ssl/ca-bundle.pem',
):
if os.path.exists(path):
logger.info('Found certifi path: %s', path)
return path
return None
def isMac() -> bool:
return 'darwin' in sys.platform

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2021 Virtual Cable S.L.U.
# Copyright (c) 2022 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -32,8 +32,6 @@ import socket
import socketserver
import ssl
import threading
import time
import random
import threading
import select
import typing
@@ -49,8 +47,10 @@ LISTEN_ADDRESS = '0.0.0.0' if DEBUG else '127.0.0.1'
# ForwarServer states
TUNNEL_LISTENING, TUNNEL_OPENING, TUNNEL_PROCESSING, TUNNEL_ERROR = 0, 1, 2, 3
logger = logging.getLogger(__name__)
PayLoadType = typing.Optional[typing.Tuple[typing.Optional[bytes], typing.Optional[bytes]]]
class ForwardServer(socketserver.ThreadingTCPServer):
daemon_threads = True
@@ -60,9 +60,9 @@ class ForwardServer(socketserver.ThreadingTCPServer):
ticket: str
stop_flag: threading.Event
can_stop: bool
timeout: int
timer: typing.Optional[threading.Timer]
check_certificate: bool
keep_listening: bool
current_connections: int
status: int
@@ -73,30 +73,29 @@ class ForwardServer(socketserver.ThreadingTCPServer):
timeout: int = 0,
local_port: int = 0,
check_certificate: bool = True,
keep_listening: bool = False,
) -> None:
local_port = local_port or random.randrange(33000, 53000)
super().__init__(
server_address=(LISTEN_ADDRESS, local_port), RequestHandlerClass=Handler
)
self.remote = remote
self.ticket = ticket
# Negative values for timeout, means "accept always connections"
# "but if no connection is stablished on timeout (positive)"
# "stop the listener"
self.timeout = int(time.time()) + timeout if timeout > 0 else 0
# Note that this is for backwards compatibility, better use "keep_listening"
if timeout < 0:
keep_listening = True
timeout = abs(timeout)
super().__init__(server_address=(LISTEN_ADDRESS, local_port), RequestHandlerClass=Handler)
self.remote = remote
self.ticket = ticket
self.check_certificate = check_certificate
self.keep_listening = keep_listening
self.stop_flag = threading.Event() # False initial
self.current_connections = 0
self.status = TUNNEL_LISTENING
self.can_stop = False
timeout = abs(timeout) or 60
self.timer = threading.Timer(
abs(timeout), ForwardServer.__checkStarted, args=(self,)
)
timeout = timeout or 60
self.timer = threading.Timer(abs(timeout), ForwardServer.__checkStarted, args=(self,))
self.timer.start()
def stop(self) -> None:
@@ -120,10 +119,13 @@ class ForwardServer(socketserver.ThreadingTCPServer):
# Do not "recompress" data, use only "base protocol" compression
context.options |= ssl.OP_NO_COMPRESSION
# Macs with default installed python, does not support mininum tls version set to TLSv1.3
# USe "brew" version instead, or uncomment next line and comment the next one
# context.minimum_version = ssl.TLSVersion.TLSv1_2 if tools.isMac() else ssl.TLSVersion.TLSv1_3
context.minimum_version = ssl.TLSVersion.TLSv1_3
if tools.getCaCertsFile() is not None:
context.load_verify_locations(
tools.getCaCertsFile()
) # Load certifi certificates
context.load_verify_locations(tools.getCaCertsFile()) # Load certifi certificates
# If ignore remote certificate
if self.check_certificate is False:
@@ -148,18 +150,20 @@ class ForwardServer(socketserver.ThreadingTCPServer):
logger.debug('Tunnel is available!')
return True
except Exception as e:
logger.error(
'Error connecting to tunnel server %s: %s', self.server_address, e
)
logger.error('Error connecting to tunnel server %s: %s', self.server_address, e)
return False
@property
def stoppable(self) -> bool:
logger.debug('Is stoppable: %s', self.can_stop)
return self.can_stop or (self.timeout != 0 and int(time.time()) > self.timeout)
return self.can_stop
@staticmethod
def __checkStarted(fs: 'ForwardServer') -> None:
# As soon as the timer is fired, the server can be stopped
# This means that:
# * If not connections are stablished, the server will be stopped
# * If no "keep_listening" is set, the server will not allow any new connections
logger.debug('New connection limit reached')
fs.timer = None
fs.can_stop = True
@@ -175,8 +179,8 @@ class Handler(socketserver.BaseRequestHandler):
def handle(self) -> None:
self.server.status = TUNNEL_OPENING
# If server processing is over time
if self.server.stoppable:
# If server processing is over time, and don't allow more connections
if self.server.stoppable and not self.server.keep_listening:
self.server.status = TUNNEL_ERROR
logger.info('Rejected timedout connection')
self.request.close() # End connection without processing it
@@ -194,11 +198,10 @@ class Handler(socketserver.BaseRequestHandler):
data = ssl_socket.recv(2)
if data != b'OK':
data += ssl_socket.recv(128)
raise Exception(
f'Error received: {data.decode(errors="ignore")}'
) # Notify error
raise Exception(f'Error received: {data.decode(errors="ignore")}') # Notify error
# All is fine, now we can tunnel data
self.process(remote=ssl_socket)
except Exception as e:
logger.error(f'Error connecting to {self.server.remote!s}: {e!s}')
@@ -235,10 +238,9 @@ class Handler(socketserver.BaseRequestHandler):
def _run(server: ForwardServer) -> None:
logger.debug(
'Starting forwarder: %s -> %s, timeout: %d',
'Starting forwarder: %s -> %s',
server.server_address,
server.remote,
server.timeout,
)
server.serve_forever()
logger.debug('Stoped forwarder %s -> %s', server.server_address, server.remote)
@@ -250,14 +252,15 @@ def forward(
timeout: int = 0,
local_port: int = 0,
check_certificate=True,
keep_listening=True,
) -> ForwardServer:
fs = ForwardServer(
remote=remote,
ticket=ticket,
timeout=timeout,
local_port=local_port,
check_certificate=check_certificate,
keep_listening=keep_listening,
)
# Starts a new thread
threading.Thread(target=_run, args=(fs,)).start()
@@ -272,18 +275,26 @@ if __name__ == "__main__":
log.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'%(levelname)s - %(message)s'
) # Basic log format, nice for syslog
formatter = logging.Formatter('%(levelname)s - %(message)s') # Basic log format, nice for syslog
handler.setFormatter(formatter)
log.addHandler(handler)
ticket = 'mffqg7q4s61fvx0ck2pe0zke6k0c5ipb34clhbkbs4dasb4g'
fs = forward(
('172.27.0.1', 7777),
('demoaslan.udsenterprise.com', 11443),
ticket,
local_port=49999,
local_port=0,
timeout=-20,
check_certificate=False,
)
print('Listening on port', fs.server_address)
import socket
# Open a socket to local fs.server_address and send some random data
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(fs.server_address)
s.sendall(b'Hello world!')
data = s.recv(1024)
print('Received', repr(data))
fs.stop()

View File

@@ -7,9 +7,9 @@
<groupId>org.openuds.server</groupId>
<artifactId>guacamole-auth-uds</artifactId>
<packaging>jar</packaging>
<version>2.5.0</version>
<version>4.0.0</version>
<name>UDS Integration Extension for Apache Guacamole</name>
<url>https://github.com/dkmstr/openuds</url>
<url>https://github.com/VirtualCable/openuds</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -18,11 +18,11 @@
<build>
<plugins>
<!-- Compile using Java 1.8 -->
<!-- Compile using Java 1.8, as guacamole-client -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
@@ -38,7 +38,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<version>3.1.2</version>
<executions>
<execution>
<id>unpack-dependencies</id>
@@ -70,15 +70,15 @@
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<version>1.1.1</version>
<scope>provided</scope>
<version>1.1.1</version>
</dependency>
<!-- Guacamole extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>1.2.0</version>
<version>1.5.2</version>
<scope>provided</scope>
</dependency>
@@ -86,7 +86,7 @@
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>3.0</version>
<version>5.1.0</version>
</dependency>
</dependencies>

View File

@@ -55,7 +55,7 @@ public class UDSModule extends AbstractModule {
* If the guacamole.properties file cannot be read.
*/
public UDSModule() throws GuacamoleException {
this.environment = new LocalEnvironment();
this.environment = LocalEnvironment.getInstance();
}
@Override

View File

@@ -30,13 +30,8 @@ package org.openuds.guacamole.config;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.URIGuacamoleProperty;

View File

@@ -149,6 +149,32 @@ SECRET_KEY = 's5ky!7b5f#s35!e38xv%e-+iey6yi-#630x)kk3kk5_j8rie2*'
# This is a very long string, an RSA KEY (this can be changed, but if u loose it, all encription will be lost)
RSA_KEY = '-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQC0qe1GlriQbHFYdKYRPBFDSS8Ne/TEKI2mtPKJf36XZTy6rIyH\nvUpT1gMScVjHjOISLNJQqktyv0G+ZGzLDmfkCUBev6JBlFwNeX3Dv/97Q0BsEzJX\noYHiDANUkuB30ukmGvG0sg1v4ccl+xs2Su6pFSc5bGINBcQ5tO0ZI6Q1nQIDAQAB\nAoGBAKA7Octqb+T/mQOX6ZXNjY38wXOXJb44LXHWeGnEnvUNf/Aci0L0epCidfUM\nfG33oKX4BMwwTVxHDrsa/HaXn0FZtbQeBVywZqMqWpkfL/Ho8XJ8Rsq8OfElrwek\nOCPXgxMzQYxoNHw8V97k5qhfupQ+h878BseN367xSyQ8plahAkEAuPgAi6aobwZ5\nFZhx/+6rmQ8sM8FOuzzm6bclrvfuRAUFa9+kMM2K48NAneAtLPphofqI8wDPCYgQ\nTl7O96GXVQJBAPoKtWIMuBHJXKCdUNOISmeEvEzJMPKduvyqnUYv17tM0JTV0uzO\nuDpJoNIwVPq5c3LJaORKeCZnt3dBrdH1FSkCQQC3DK+1hIvhvB0uUvxWlIL7aTmM\nSny47Y9zsc04N6JzbCiuVdeueGs/9eXHl6f9gBgI7eCD48QAocfJVygphqA1AkEA\nrvzZjcIK+9+pJHqUO0XxlFrPkQloaRK77uHUaW9IEjui6dZu4+2T/q7SjubmQgWR\nZy7Pap03UuFZA2wCoqJbaQJAUG0FVrnyUORUnMQvdDjAWps2sXoPvA8sbQY1W8dh\nR2k4TCFl2wD7LutvsdgdkiH0gWdh5tc1c4dRmSX1eQ27nA==\n-----END RSA PRIVATE KEY-----'
# Trusted cyphers
SECURE_CIPHERS = (
'AES-256-GCM-SHA384'
':CHACHA20-POLY1305-SHA256'
':AES-128-GCM-SHA256'
':ECDHE-RSA-AES256-GCM-SHA384'
':ECDHE-RSA-AES128-GCM-SHA256'
':ECDHE-RSA-CHACHA20-POLY1305'
':ECDHE-ECDSA-AES128-GCM-SHA256'
':ECDHE-ECDSA-AES256-GCM-SHA384'
':ECDHE-ECDSA-AES128-SHA256'
':ECDHE-ECDSA-CHACHA20-POLY1305'
)
# Min TLS version
SECURE_MIN_TLS_VERSION = '1.2'
# LDAP CIFHER SUITE can be enforced here. Use GNU TLS cipher suite names in this case
# i.e.:
# * NORMAL
# * NORMAL:-VERS-TLS-ALL:+VERS-TLS1.2:+VERS-TLS1.3
# * PFS
# * SECURE256
# If omitted, defaults to PFS:-VERS-TLS-ALL:+VERS-TLS1.2:+VERS-TLS1.3:-AES-128-CBC:-AES-256-CBC:-DHE-RSA
# Example:
LDAP_CIPHER_SUITE = 'PFS:-VERS-TLS-ALL:+VERS-TLS1.2:+VERS-TLS1.3:-AES-128-CBC:-AES-256-CBC:-DHE-RSA'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',

View File

@@ -43,6 +43,8 @@ from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from uds.core import VERSION, VERSION_STAMP
from . import log
from .handlers import (
Handler,
HandlerError,
@@ -61,8 +63,6 @@ if typing.TYPE_CHECKING:
logger = logging.getLogger(__name__)
__all__ = ['Handler', 'Dispatcher']
AUTH_TOKEN_HEADER = 'X-Auth-Token'
@@ -82,9 +82,6 @@ class Dispatcher(View):
"""
Processes the REST request and routes it wherever it needs to be routed
"""
# Remove session from request, so response middleware do nothing with this
del request.session
# Now we extract method and possible variables from path
path: typing.List[str] = kwargs['arguments'].split('/')
del kwargs['arguments']
@@ -149,6 +146,8 @@ class Dispatcher(View):
except processors.ParametersException as e:
logger.debug('Path: %s', full_path)
logger.debug('Error: %s', e)
log.log_operation(handler, 500, log.ERROR)
return http.HttpResponseServerError(
'Invalid parameters invoking {0}: {1}'.format(full_path, e),
content_type="text/plain",
@@ -158,6 +157,8 @@ class Dispatcher(View):
for n in ['get', 'post', 'put', 'delete']:
if hasattr(handler, n):
allowedMethods.append(n)
log.log_operation(handler, 405, log.ERROR)
return http.HttpResponseNotAllowed(
allowedMethods, content_type="text/plain"
)
@@ -168,6 +169,8 @@ class Dispatcher(View):
except Exception:
logger.exception('error accessing attribute')
logger.debug('Getting attribute %s for %s', http_method, full_path)
log.log_operation(handler, 500, log.ERROR)
return http.HttpResponseServerError(
'Unexcepected error', content_type="text/plain"
)
@@ -182,20 +185,29 @@ class Dispatcher(View):
response['UDS-Version'] = f'{VERSION};{VERSION_STAMP}'
for k, val in handler.headers().items():
response[k] = val
log.log_operation(handler, response.status_code, log.INFO)
return response
except RequestError as e:
log.log_operation(handler, 400, log.ERROR)
return http.HttpResponseBadRequest(str(e), content_type="text/plain")
except ResponseError as e:
log.log_operation(handler, 500, log.ERROR)
return http.HttpResponseServerError(str(e), content_type="text/plain")
except NotSupportedError as e:
return http.HttpResponseBadRequest(str(e), content_type="text/plain")
log.log_operation(handler, 501, log.ERROR)
return http.HttpResponseBadRequest(str(e), content_type="text/plain", status=501)
except AccessDenied as e:
log.log_operation(handler, 403, log.ERROR)
return http.HttpResponseForbidden(str(e), content_type="text/plain")
except NotFound as e:
log.log_operation(handler, 404, log.ERROR)
return http.HttpResponseNotFound(str(e), content_type="text/plain")
except HandlerError as e:
log.log_operation(handler, 500, log.ERROR)
return http.HttpResponseBadRequest(str(e), content_type="text/plain")
except Exception as e:
log.log_operation(handler, 500, log.ERROR)
logger.exception('Error processing request')
return http.HttpResponseServerError(str(e), content_type="text/plain")
@@ -240,7 +252,9 @@ class Dispatcher(View):
# Dinamycally import children of this package.
package = 'methods'
pkgpath = os.path.join(os.path.dirname(sys.modules[__name__].__file__), package)
pkgpath = os.path.join(
os.path.dirname(typing.cast(str, sys.modules[__name__].__file__)), package
)
for _, name, _ in pkgutil.iter_modules([pkgpath]):
# __import__(__name__ + '.' + package + '.' + name, globals(), locals(), [], 0)
importlib.import_module(

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved.
@@ -28,7 +27,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
import logging
@@ -42,6 +41,9 @@ from uds.core.util import net
from uds.models import Authenticator, User
from uds.core.managers import cryptoManager
from . import log
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core.util.request import ExtendedHttpRequestWithUser
@@ -127,8 +129,8 @@ class Handler:
self,
request: 'ExtendedHttpRequestWithUser',
path: str,
operation: str,
params: typing.Any,
method: str,
params: typing.MutableMapping[str, typing.Any],
*args: str,
**kwargs
):
@@ -147,7 +149,7 @@ class Handler:
self._request = request
self._path = path
self._operation = operation
self._operation = method
self._params = params
self._args = args
self._kwargs = kwargs
@@ -178,6 +180,7 @@ class Handler:
else:
self._user = User() # Empty user for non authenticated handlers
def headers(self) -> typing.Dict[str, str]:
"""
Returns the headers of the REST request (all)

107
server/src/uds/REST/log.py Normal file
View File

@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2022 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
from uds import models
from uds.core.util.log import (
REST,
OWNER_TYPE_AUDIT,
DEBUG,
INFO,
WARNING,
ERROR,
CRITICAL,
)
if typing.TYPE_CHECKING:
from .handlers import Handler
# This structct allows us to perform the following:
# If path has ".../providers/[uuid]/..." we will replace uuid with "provider nanme" sourrounded by []
# If path has ".../services/[uuid]/..." we will replace uuid with "service name" sourrounded by []
# If path has ".../users/[uuid]/..." we will replace uuid with "user name" sourrounded by []
# If path has ".../groups/[uuid]/..." we will replace uuid with "group name" sourrounded by []
UUID_REPLACER = (
('providers', models.Provider),
('services', models.Service),
('users', models.User),
('groups', models.Group),
)
def replacePath(path: str) -> str:
"""Replaces uuids in path with names
All paths are in the form .../type/uuid/...
"""
for type, model in UUID_REPLACER:
if f'/{type}/' in path:
try:
uuid = path.split(f'/{type}/')[1].split('/')[0]
name = model.objects.get(uuid=uuid).name # type: ignore
path = path.replace(uuid, f'[{name}]')
except Exception:
pass
return path
def log_operation(
handler: typing.Optional['Handler'], response_code: int, level: int = INFO
):
"""
Logs a request
"""
if not handler:
return # Nothing to log
path = handler._request.path
# If a common request, and no error, we don't log it because it's useless and a waste of resources
if response_code < 400 and any(
x in path for x in ('overview', 'tableinfo', 'gui', 'types', 'system')
):
return
path = replacePath(path)
username = handler._request.user.pretty_name if handler._request.user else 'Unknown'
# Global log is used without owner nor type
models.Log.objects.create(
owner_id=0,
owner_type=OWNER_TYPE_AUDIT,
created=models.getSqlDatetime(),
level=level,
source=REST,
data=f'{handler._request.ip} {username}: [{handler._request.method}/{response_code}] {path}'[
:255
],
)

View File

@@ -141,7 +141,7 @@ class Actor(Handler):
except Exception:
return Actor.result({})
def get(self): # pylint: disable=too-many-return-statements
def get(self) -> typing.Any: # pylint: disable=too-many-return-statements
"""
Processes get requests
"""
@@ -186,7 +186,7 @@ class Actor(Handler):
raise RequestError('Invalid request')
# Must be invoked as '/rest/actor/UUID/[message], with message data in post body
def post(self): # pylint: disable=too-many-branches
def post(self) -> typing.Any: # pylint: disable=too-many-branches
"""
Processes post requests
"""

View File

@@ -44,7 +44,7 @@ from uds.models import (
# from uds.core import VERSION
from uds.core.managers import userServiceManager
from uds.core import osmanagers
from uds.core.util import log, certs
from uds.core.util import log, security
from uds.core.util.state import State
from uds.core.util.cache import Cache
from uds.core.util.config import GlobalConfig
@@ -54,6 +54,7 @@ from ..handlers import Handler, AccessDenied, RequestError
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core import services
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
@@ -66,26 +67,27 @@ class BlockAccess(Exception):
# Helpers
def fixIdsList(idsList: typing.List[str]) -> typing.List[str]:
return [i.upper() for i in idsList] + [i.lower() for i in idsList]
def checkBlockedIp(ip: str) -> None:
def checkBlockedIp(request: 'ExtendedHttpRequest') -> None:
if GlobalConfig.BLOCK_ACTOR_FAILURES.getBool() is False:
return
cache = Cache('actorv3')
fails = cache.get(ip) or 0
fails = cache.get(request.ip) or 0
if fails > ALLOWED_FAILS:
logger.info(
'Access to actor from %s is blocked for %s seconds since last fail',
ip,
GlobalConfig.LOGIN_BLOCK.getInt(),
)
err = f'DENIED Access to actor from {request.ip}. Blocked for {GlobalConfig.LOGIN_BLOCK.getInt()} seconds since last fail.'
# if request.ip_proxy is not request.ip, notify so administrator can figure out what is going on
if request.ip_proxy != request.ip:
err += f' Proxied ip is present: {request.ip_proxy}.'
logger.warning(err)
raise BlockAccess()
def incFailedIp(ip: str) -> None:
def incFailedIp(request: 'ExtendedHttpRequest') -> None:
cache = Cache('actorv3')
fails = (cache.get(ip) or 0) + 1
cache.put(ip, fails, GlobalConfig.LOGIN_BLOCK.getInt())
fails = cache.get(request.ip, 0) + 1
cache.put(request.ip, fails, GlobalConfig.LOGIN_BLOCK.getInt())
class ActorV3Action(Handler):
@@ -113,6 +115,7 @@ class ActorV3Action(Handler):
try:
return UserService.objects.get(uuid=self._params['token'])
except UserService.DoesNotExist:
logger.error('User service not found (params: %s)', self._params)
raise BlockAccess()
def action(self) -> typing.MutableMapping[str, typing.Any]:
@@ -120,13 +123,13 @@ class ActorV3Action(Handler):
def post(self) -> typing.MutableMapping[str, typing.Any]:
try:
checkBlockedIp(self._request.ip) # pylint: disable=protected-access
checkBlockedIp(self._request)
result = self.action()
logger.debug('Action result: %s', result)
return result
except (BlockAccess, KeyError):
# For blocking attacks
incFailedIp(self._request.ip) # pylint: disable=protected-access
incFailedIp(self._request)
except Exception as e:
logger.exception('Posting %s: %s', self.__class__, e)
@@ -181,6 +184,7 @@ class Register(ActorV3Action):
actorToken.log_level = self._params['log_level']
actorToken.stamp = getSqlDatetime()
actorToken.save()
logger.info('Registered actor %s', self._params)
except Exception:
actorToken = ActorToken.objects.create(
username=self._user.pretty_name,
@@ -263,22 +267,15 @@ class Initialize(ActorV3Action):
# Valid actor token, now validate access allowed. That is, look for a valid mac from the ones provided.
try:
# Enforce lowecase ids for sqlite
idsList = [i.lower() for i in idsList]
# ensure idsLists has upper and lower versions for case sensitive databases
idsList = fixIdsList(idsList)
# Set full filter
dbFilter = dbFilter.filter(
unique_id__in=idsList,
state__in=[State.USABLE, State.PREPARING],
)
userService: UserService = next(
iter(
dbFilter.filter(
unique_id__in=idsList,
state__in=[State.USABLE, State.PREPARING],
)
)
)
userService: UserService = next(iter(dbFilter))
except Exception as e:
logger.info('Unmanaged host request: %s, %s', self._params, e)
return ActorV3Action.actorResult(
@@ -353,7 +350,7 @@ class BaseReadyChange(ActorV3Action):
userServiceManager().notifyReadyFromOsManager(userService, '')
# Generates a certificate and send it to client.
privateKey, cert, password = certs.selfSignedCert(self._params['ip'])
privateKey, cert, password = security.selfSignedCert(self._params['ip'])
# Store certificate with userService
userService.setProperty('cert', cert)
userService.setProperty('priv', privateKey)
@@ -426,7 +423,7 @@ class Version(ActorV3Action):
class LoginLogout(ActorV3Action):
name = 'notused' # Not really important, this is not a "leaf" class and will not be directly available
def notifyService(self, isLogin: bool):
def notifyService(self, isLogin: bool) -> None:
try:
# If unmanaged, use Service locator
service: 'services.Service' = Service.objects.get(
@@ -440,8 +437,8 @@ class LoginLogout(ActorV3Action):
x['mac'] for x in self._params['id']
][:10]
# Enforce lowercase for idList
idsList = [x.lower() for x in idsList]
# ensure idsLists has upper and lower versions for case sensitive databases
idsList = fixIdsList(idsList)
validId: typing.Optional[str] = service.getValidId(idsList)
@@ -453,15 +450,17 @@ class LoginLogout(ActorV3Action):
# idInfo = service.recoverIdInfo(validId)
# Notify Service that someone logged in/out
is_remote = self._params.get('session_type', '')[:4] in ('xrdp', 'RDP-')
if isLogin:
# Try to guess if this is a remote session
is_remote = self._params.get('session_type', '')[:4] in ('xrdp', 'RDP-')
service.processLogin(validId, remote_login=is_remote)
else:
service.processLogout(validId)
service.processLogout(validId, remote_login=is_remote)
# All right, service notified...
except Exception:
# All right, service notified..
except Exception as e :
# Log error and continue
logger.error('Error notifying service: %s (%s)', e, self._params)
raise BlockAccess()
@@ -569,6 +568,7 @@ class Logout(LoginLogout):
if isManaged:
raise
self.notifyService(isLogin=False) # Logout notification
return ActorV3Action.actorResult('notified') # Result is that we have not processed the logout in fact, but notified the service
return ActorV3Action.actorResult('ok')
@@ -653,8 +653,8 @@ class Unmanaged(ActorV3Action):
][:10]
validId: typing.Optional[str] = service.getValidId(idsList)
# enforce lowercase idsList
idsList = [i.lower() for i in idsList]
# ensure idsLists has upper and lower versions for case sensitive databases
idsList = fixIdsList(idsList)
# Check if there is already an assigned user service
# To notify it logout
@@ -688,7 +688,7 @@ class Unmanaged(ActorV3Action):
ip = self._params['id'][0]['ip'] # Get first IP if no valid ip found
# Generates a certificate and send it to client.
privateKey, certificate, password = certs.selfSignedCert(ip)
privateKey, certificate, password = security.selfSignedCert(ip)
cert: typing.Dict[str, str] = {
'private_key': privateKey,
'server_certificate': certificate,
@@ -734,8 +734,8 @@ class Notify(ActorV3Action):
try:
# Check block manually
checkBlockedIp(self._request.ip) # pylint: disable=protected-access
if 'action' == 'login':
checkBlockedIp(self._request) # pylint: disable=protected-access
if self._params['action'] == 'login':
Login.action(typing.cast(Login, self))
else:
Logout.action(typing.cast(Logout, self))
@@ -743,6 +743,6 @@ class Notify(ActorV3Action):
return ActorV3Action.actorResult('ok')
except UserService.DoesNotExist:
# For blocking attacks
incFailedIp(self._request.ip) # pylint: disable=protected-access
incFailedIp(self._request) # pylint: disable=protected-access
raise AccessDenied('Access denied')

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2019 Virtual Cable S.L.
# Copyright (c) 2014-2022 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -30,16 +30,18 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import re
import logging
import typing
from django.utils.translation import ugettext, ugettext_lazy as _
from uds.models import Authenticator
from uds.models import Authenticator, MFA
from uds.core import auths
from uds.REST import NotFound
from uds.REST.model import ModelHandler
from uds.core.util import permissions
from uds.core.util.model import processUuid
from uds.core.ui import gui
from .users_groups import Users, Groups
@@ -58,7 +60,7 @@ class Authenticators(ModelHandler):
# Custom get method "search" that requires authenticator id
custom_methods = [('search', True)]
detail = {'users': Users, 'groups': Groups}
save_fields = ['name', 'comments', 'tags', 'priority', 'small_name', 'visible']
save_fields = ['name', 'comments', 'tags', 'priority', 'small_name', 'visible', 'mfa_id:'] # mfa_id is optional, and defaults to '' (no mfa)
table_title = _('Authenticators')
table_fields = [
@@ -70,6 +72,7 @@ class Authenticators(ModelHandler):
{'visible': {'title': _('Visible'), 'type': 'callback', 'width': '3em'}},
{'small_name': {'title': _('Label')}},
{'users_count': {'title': _('Users'), 'type': 'numeric', 'width': '5em'}},
{'mfa_name': {'title': _('MFA'),}},
{'tags': {'title': _('tags'), 'visible': False}},
]
@@ -87,16 +90,17 @@ class Authenticators(ModelHandler):
'passwordLabel': _(type_.passwordLabel),
'canCreateUsers': type_.createUser != auths.Authenticator.createUser, # type: ignore
'isExternal': type_.isExternalSource,
'supportsMFA': type_.providesMfa(),
}
# Not of my type
return {}
def getGui(self, type_: str) -> typing.List[typing.Any]:
try:
tgui = auths.factory().lookup(type_)
if tgui:
authType = auths.factory().lookup(type_)
if authType:
g = self.addDefaultFields(
tgui.guiDescription(),
authType.guiDescription(),
['name', 'comments', 'tags', 'priority', 'small_name'],
)
self.addField(
@@ -106,16 +110,39 @@ class Authenticators(ModelHandler):
'value': True,
'label': ugettext('Visible'),
'tooltip': ugettext(
'If active, transport will be visible for users'
'If active, authenticator will be visible for users'
),
'type': gui.InputField.CHECKBOX_TYPE,
'order': 107,
'tab': ugettext('Display'),
'tab': gui.DISPLAY_TAB,
},
)
# If supports mfa, add MFA provider selector field
if authType.providesMfa():
self.addField(
g,
{
'name': 'mfa_id',
'values': [gui.choiceItem('', _('None'))]
+ gui.sortedChoices(
[
gui.choiceItem(v.uuid, v.name) # type: ignore
for v in MFA.objects.all()
]
),
'label': ugettext('MFA Provider'),
'tooltip': ugettext(
'MFA provider to use for this authenticator'
),
'type': gui.InputField.CHOICE_TYPE,
'order': 108,
'tab': gui.MFA_TAB,
},
)
return g
raise Exception() # Not found
except Exception:
except Exception as e:
logger.info('Type not found: %s', e)
raise NotFound('type not found')
def item_as_dict(self, item: Authenticator) -> typing.Dict[str, typing.Any]:
@@ -127,6 +154,8 @@ class Authenticators(ModelHandler):
'tags': [tag.tag for tag in item.tags.all()],
'comments': item.comments,
'priority': item.priority,
'mfa_id': item.mfa.uuid if item.mfa else '',
'mfa_name': item.mfa.name if item.mfa else '', # For overview
'visible': item.visible,
'small_name': item.small_name,
'users_count': item.users.count(),
@@ -182,6 +211,29 @@ class Authenticators(ModelHandler):
return self.success()
return res[1]
def beforeSave(
self, fields: typing.Dict[str, typing.Any]
) -> None: # pylint: disable=too-many-branches,too-many-statements
logger.debug(self._params)
if fields.get('mfa_id'):
try:
mfa = MFA.objects.get(
uuid=processUuid(fields['mfa_id'])
)
fields['mfa_id'] = mfa.id
except MFA.DoesNotExist:
pass # will set field to null
else:
fields['mfa_id'] = None
fields['small_name'] = fields['small_name'].strip().replace(' ', '-')
# And ensure small_name chars are valid [ a-zA-Z0-9:-.]+
if fields['small_name'] and not re.match(r'^[a-zA-Z0-9:.-]+$', fields['small_name']):
raise self.invalidRequestException(
_('Label must contain only letters, numbers, or symbols: - : .')
)
def deleteItem(self, item: Authenticator):
# For every user, remove assigned services (mark them for removal)

View File

@@ -68,6 +68,9 @@ class Calendars(ModelHandler):
},
{'comments': {'title': _('Comments')}},
{'modified': {'title': _('Modified'), 'type': 'datetime'}},
{'number_rules': {'title': _('Rules')}},
{'number_access': {'title': _('Pools with Accesses')}},
{'number_actions': {'title': _('Pools with Actions')}},
{'tags': {'title': _('tags'), 'visible': False}},
]
@@ -78,6 +81,10 @@ class Calendars(ModelHandler):
'tags': [tag.tag for tag in item.tags.all()],
'comments': item.comments,
'modified': item.modified,
'number_rules': item.rules.count(),
'number_access': item.calendaraccess_set.all().values('service_pool').distinct().count(),
'number_actions': item.calendaraction_set.all().values('service_pool').distinct().count(),
'permission': permissions.getEffectivePermission(self._user, item),
}

View File

@@ -46,11 +46,14 @@ from uds.core.util.config import GlobalConfig
from uds.core.services.exceptions import ServiceNotReadyError
from uds.core import VERSION as UDS_VERSION
if typing.TYPE_CHECKING:
from uds.models import UserService
logger = logging.getLogger(__name__)
CLIENT_VERSION = UDS_VERSION
REQUIRED_CLIENT_VERSION = '3.5.0'
REQUIRED_CLIENT_VERSION = '3.6.0'
# Enclosed methods under /client path
@@ -102,7 +105,7 @@ class Client(Handler):
"""
return Client.result(_('Correct'))
def get(self): # pylint: disable=too-many-locals
def get(self) -> typing.Any: # pylint: disable=too-many-locals
"""
Processes get requests
"""
@@ -122,6 +125,7 @@ class Client(Handler):
if len(self._args) == 1: # Simple test
return Client.result(_('Correct'))
userService: typing.Optional['UserService'] = None
try:
(
ticket,
@@ -130,6 +134,11 @@ class Client(Handler):
self._args
) # If more than 2 args, got an error. pylint: disable=unbalanced-tuple-unpacking
hostname = self._params['hostname'] # Or if hostname is not included...
version = self._params.get('version', '0.0.0')
if version < '3.6.0':
return Client.result(error='Client version not supported.\n Please, upgrade it.')
srcIp = self._request.ip
# Ip is optional,
@@ -180,9 +189,6 @@ class Client(Handler):
)
password = cryptoManager().symDecrpyt(data['password'], scrambler)
# Set "accesedByClient"
userService.setProperty('accessedByClient', '1')
# userService.setConnectionSource(srcIp, hostname) # Store where we are accessing from so we can notify Service
if not ip:
raise ServiceNotReadyError()
@@ -218,10 +224,6 @@ class Client(Handler):
}
)
except ServiceNotReadyError as e:
# Set that client has accesed userService
if e.userService:
e.userService.setProperty('accessedByClient', '1')
# Refresh ticket and make this retrayable
TicketStore.revalidate(
ticket, 20
@@ -232,3 +234,7 @@ class Client(Handler):
except Exception as e:
logger.exception("Exception")
return Client.result(error=str(e))
finally:
if userService:
userService.setProperty('accessedByClient', '1')

View File

@@ -39,61 +39,26 @@ from uds.REST import Handler
logger = logging.getLogger(__name__)
# Pair of section/value removed from current UDS version
REMOVED = {
'UDS': (
'allowPreferencesAccess',
'customHtmlLogin',
'UDS Theme',
'UDS Theme Enhaced',
'css',
'allowPreferencesAccess',
'loginUrl',
'maxLoginTries',
'loginBlockTime',
),
'Cluster': ('Destination CPU Load', 'Migration CPU Load', 'Migration Free Memory'),
'IPAUTH': ('autoLogin',),
'VMWare': ('minUsableDatastoreGB', 'maxRetriesOnError'),
'HyperV': ('minUsableDatastoreGB',),
'Security': ('adminIdleTime', 'userSessionLength'),
}
# Enclosed methods under /config path
class Config(Handler):
needs_admin = True # By default, staff is lower level needed
def get(self):
def get(self) -> typing.Any:
cfg: CfgConfig.Value
res: typing.Dict[str, typing.Dict[str, typing.Any]] = {}
addCrypt = self.is_admin()
for cfg in CfgConfig.enumerate():
# Skip removed configuration values, even if they are in database
logger.debug('Key: %s, val: %s', cfg.section(), cfg.key())
if cfg.key() in REMOVED.get(cfg.section(), ()):
continue
if cfg.isCrypted() is True and addCrypt is False:
continue
# add section if it do not exists
if cfg.section() not in res:
res[cfg.section()] = {}
res[cfg.section()][cfg.key()] = {
'value': cfg.get(),
'crypt': cfg.isCrypted(),
'longText': cfg.isLongText(),
'type': cfg.getType(),
'params': cfg.getParams(),
configs = CfgConfig.getConfigValues(self.is_admin())
# Remove values from cryptes keys
return {
section: {
key: vals if not vals['crypt'] else {**vals, 'value': '********'}
for key, vals in secDict.items()
}
logger.debug('Configuration: %s', res)
return res
for section, secDict in configs.items()
}
def put(self):
for section, secDict in self._params.items():
for key, vals in secDict.items():
logger.info('Updating config value %s.%s to %s by %s', section, key, vals['value'], self._user.name)
CfgConfig.update(section, key, vals['value'])
return 'done'

View File

@@ -65,9 +65,9 @@ class Login(Handler):
@staticmethod
def result(
result: str = 'error',
token: str = None,
scrambler: str = None,
error: str = None,
token: typing.Optional[str] = None,
scrambler: typing.Optional[str] = None,
error: typing.Optional[str] = None,
) -> typing.MutableMapping[str, typing.Any]:
res = {
'result': result,
@@ -229,7 +229,7 @@ class Auths(Handler):
path = 'auth'
authenticated = False # By default, all handlers needs authentication
def auths(self):
def auths(self) -> typing.Iterator[typing.Dict[str, typing.Any]]:
paramAll: bool = self._params.get('all', 'false') == 'true'
auth: Authenticator
for auth in Authenticator.objects.all():

View File

@@ -160,7 +160,7 @@ class MetaPools(ModelHandler):
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
for v in Image.objects.all()
]
),
@@ -175,7 +175,7 @@ class MetaPools(ModelHandler):
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
for v in ServicePoolGroup.objects.all()
]
),

View File

@@ -256,7 +256,7 @@ class MetaAssignedService(DetailHandler):
user: User = User.objects.get(uuid=processUuid(fields['user_id']))
logStr = 'Changing ownership of service from {} to {} by {}'.format(
service.user.pretty_name, user.pretty_name, self._user.pretty_name
service.user.pretty_name, user.pretty_name, self._user.pretty_name # type: ignore
)
# If there is another service that has this same owner, raise an exception

View File

@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@itemor: Adolfo Gómez, dkmaster at dkmon dot com
'''
import logging
import typing
from django.utils.translation import gettext_lazy as _, gettext
from uds import models
from uds.core import mfas
from uds.core.ui import gui
from uds.core.util import permissions
from uds.REST.model import ModelHandler
logger = logging.getLogger(__name__)
# Enclosed methods under /item path
class MFA(ModelHandler):
model = models.MFA
save_fields = ['name', 'comments', 'tags', 'remember_device', 'validity']
table_title = _('Multi Factor Authentication')
table_fields = [
{'name': {'title': _('Name'), 'visible': True, 'type': 'iconType'}},
{'type_name': {'title': _('Type')}},
{'comments': {'title': _('Comments')}},
{'tags': {'title': _('tags'), 'visible': False}},
]
def enum_types(self) -> typing.Iterable[typing.Type[mfas.MFA]]:
return mfas.factory().providers().values()
def getGui(self, type_: str) -> typing.List[typing.Any]:
mfa = mfas.factory().lookup(type_)
if not mfa:
raise self.invalidItemException()
localGui = self.addDefaultFields(
mfa.guiDescription(), ['name', 'comments', 'tags']
)
self.addField(
localGui,
{
'name': 'remember_device',
'value': '0',
'minValue': '0',
'label': gettext('Device Caching'),
'tooltip': gettext(
'Time in hours to cache device so MFA is not required again. User based.'
),
'type': gui.InputField.NUMERIC_TYPE,
'order': 111,
},
)
self.addField(
localGui,
{
'name': 'validity',
'value': '5',
'minValue': '0',
'label': gettext('MFA code validity'),
'tooltip': gettext(
'Time in minutes to allow MFA code to be used.'
),
'type': gui.InputField.NUMERIC_TYPE,
'order': 112,
},
)
return localGui
def item_as_dict(self, item: models.MFA) -> typing.Dict[str, typing.Any]:
type_ = item.getType()
return {
'id': item.uuid,
'name': item.name,
'remember_device': item.remember_device,
'validity': item.validity,
'tags': [tag.tag for tag in item.tags.all()],
'comments': item.comments,
'type': type_.type(),
'type_name': type_.name(),
'permission': permissions.getEffectivePermission(self._user, item),
}

View File

@@ -92,10 +92,10 @@ class Permissions(Handler):
{
'id': perm.uuid,
'type': kind,
'auth': entity.manager.uuid,
'auth_name': entity.manager.name,
'entity_id': entity.uuid,
'entity_name': entity.name,
'auth': entity.manager.uuid, # type: ignore
'auth_name': entity.manager.name, # type: ignore
'entity_id': entity.uuid, # type: ignore
'entity_name': entity.name, # type: ignore
'perm': perm.permission,
'perm_name': perm.permission_as_string,
}
@@ -103,7 +103,7 @@ class Permissions(Handler):
return sorted(res, key=lambda v: v['auth_name'] + v['entity_name'])
def get(self):
def get(self) -> typing.Any:
"""
Processes get requests
"""

View File

@@ -308,7 +308,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
'values': [gui.choiceItem(-1, '')]
+ gui.sortedChoices(
[
gui.choiceItem(v.uuid, v.name)
gui.choiceItem(v.uuid, v.name) # type: ignore
for v in models.Proxy.objects.all()
]
),

View File

@@ -93,7 +93,7 @@ class ServicesPoolGroups(ModelHandler):
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
for v in Image.objects.all()
]
),

View File

@@ -233,8 +233,8 @@ class ServicesPools(ModelHandler):
'name': item.name,
'short_name': item.short_name,
'tags': [tag.tag for tag in item.tags.all()],
'parent': item.service.name,
'parent_type': item.service.data_type,
'parent': item.service.name, # type: ignore
'parent_type': item.service.data_type, # type: ignore
'comments': item.comments,
'state': state,
'thumb': item.image.thumb64
@@ -242,8 +242,8 @@ class ServicesPools(ModelHandler):
else DEFAULT_THUMB_BASE64,
'account': item.account.name if item.account is not None else '',
'account_id': item.account.uuid if item.account is not None else None,
'service_id': item.service.uuid,
'provider_id': item.service.provider.uuid,
'service_id': item.service.uuid, # type: ignore
'provider_id': item.service.provider.uuid, # type: ignore
'image_id': item.image.uuid if item.image is not None else None,
'initial_srvs': item.initial_srvs,
'cache_l1_srvs': item.cache_l1_srvs,
@@ -297,7 +297,7 @@ class ServicesPools(ModelHandler):
val['tags'] = [tag.tag for tag in item.tags.all()]
val['restrained'] = restrained
val['permission'] = permissions.getEffectivePermission(self._user, item)
val['info'] = Services.serviceInfo(item.service)
val['info'] = Services.serviceInfo(item.service) # type: ignore
val['pool_group_id'] = poolGroupId
val['pool_group_name'] = poolGroupName
val['pool_group_thumb'] = poolGroupThumb
@@ -325,7 +325,7 @@ class ServicesPools(ModelHandler):
'values': [gui.choiceItem('', '')]
+ gui.sortedChoices(
[
gui.choiceItem(v.uuid, v.provider.name + '\\' + v.name)
gui.choiceItem(v.uuid, v.provider.name + '\\' + v.name) # type: ignore
for v in Service.objects.all()
]
),
@@ -339,7 +339,7 @@ class ServicesPools(ModelHandler):
'name': 'osmanager_id',
'values': [gui.choiceItem(-1, '')]
+ gui.sortedChoices(
[gui.choiceItem(v.uuid, v.name) for v in OSManager.objects.all()]
[gui.choiceItem(v.uuid, v.name) for v in OSManager.objects.all()] # type: ignore
),
'label': ugettext('OS Manager'),
'tooltip': ugettext('OS Manager used as base of this service pool'),
@@ -394,7 +394,7 @@ class ServicesPools(ModelHandler):
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
for v in Image.objects.all()
]
),
@@ -409,7 +409,7 @@ class ServicesPools(ModelHandler):
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
for v in ServicePoolGroup.objects.all()
]
),
@@ -493,7 +493,7 @@ class ServicesPools(ModelHandler):
'name': 'account_id',
'values': [gui.choiceItem(-1, '')]
+ gui.sortedChoices(
[gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()]
[gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()] # type: ignore
),
'label': ugettext('Accounting'),
'tooltip': ugettext('Account associated to this service pool'),
@@ -659,7 +659,7 @@ class ServicesPools(ModelHandler):
# Returns the action list based on current element, for calendar
def actionsList(self, item: ServicePool) -> typing.Any:
validActions: typing.Tuple[typing.Dict, ...] = ()
itemInfo = item.service.getType()
itemInfo = item.service.getType() # type: ignore
if itemInfo.usesCache is True:
validActions += (
CALENDAR_ACTION_INITIAL,
@@ -691,7 +691,7 @@ class ServicesPools(ModelHandler):
return validActions
def listAssignables(self, item: ServicePool) -> typing.Any:
service = item.service.getInstance()
service = item.service.getInstance() # type: ignore
return [gui.choiceItem(i[0], i[1]) for i in service.listAssignables()]
def createFromAssignable(self, item: ServicePool) -> typing.Any:

View File

@@ -77,8 +77,8 @@ class ServicesUsage(DetailHandler):
'friendly_name': item.friendly_name,
'owner': owner,
'owner_info': owner_info,
'service': item.deployed_service.service.name,
'service_id': item.deployed_service.service.uuid,
'service': item.deployed_service.service.name, # type: ignore
'service_id': item.deployed_service.service.uuid, # type: ignore
'pool': item.deployed_service.name,
'pool_id': item.deployed_service.uuid,
'ip': props.get('ip', _('unknown')),

View File

@@ -53,8 +53,8 @@ if typing.TYPE_CHECKING:
cache = Cache('StatsDispatcher')
# Enclosed methods under /stats path
POINTS = 150
SINCE = 30 # Days, if higer values used, ensure mysql/mariadb has a bigger sort buffer
POINTS = 70
SINCE = 7 # Days, if higer values used, ensure mysql/mariadb has a bigger sort buffer
USE_MAX = True
CACHE_TIME = SINCE * 24 * 3600 // POINTS
@@ -112,7 +112,7 @@ class System(Handler):
needs_admin = False
needs_staff = True
def get(self):
def get(self) -> typing.Any:
logger.debug('args: %s', self._args)
# Only allow admin user for global stats
if len(self._args) == 1:

View File

@@ -58,10 +58,11 @@ VALID_PARAMS = (
'transport', # Admited to be backwards compatible, but not used. Will be removed on a future release.
'force',
'userIp',
'time',
)
# Enclosed methods under /actor path
# Enclosed methods under /tickets path
class Tickets(Handler):
"""
Processes tickets access requests.
@@ -127,7 +128,7 @@ class Tickets(Handler):
# Must be invoked as '/rest/ticket/create, with "username", ("authId" or ("authSmallName" or "authTag"), "groups" (array) and optionally "time" (in seconds) as paramteres
def put(
self,
): # pylint: disable=too-many-locals,too-many-branches,too-many-statements
) -> typing.Any:
"""
Processes put requests, currently only under "create"
"""
@@ -170,7 +171,7 @@ class Tickets(Handler):
groupIds: typing.List[str] = []
for groupName in tools.asList(self._params['groups']):
try:
groupIds.append(auth.groups.get(name=groupName).uuid)
groupIds.append(auth.groups.get(name=groupName).uuid or '')
except Exception:
logger.info(
'Group %s from ticket does not exists on auth %s, forced creation: %s',
@@ -184,6 +185,7 @@ class Tickets(Handler):
name=groupName,
comments='Autocreated form ticket by using force paratemeter',
).uuid
or ''
)
if not groupIds: # No valid group in groups names
@@ -224,7 +226,7 @@ class Tickets(Handler):
# For metapool, transport is ignored..
servicePoolId = 'M' + pool.uuid
servicePoolId = 'M' + pool.uuid # type: ignore
transportId = 'meta'
except models.MetaPool.DoesNotExist:
@@ -240,7 +242,7 @@ class Tickets(Handler):
):
pool.assignedGroups.add(auth.groups.get(uuid=addGrp))
servicePoolId = 'F' + pool.uuid
servicePoolId = 'F' + pool.uuid # type: ignore
except models.Authenticator.DoesNotExist:
return Tickets.result(error='Authenticator does not exists')

View File

@@ -30,6 +30,7 @@
'''
@itemor: Adolfo Gómez, dkmaster at dkmon dot com
'''
import re
import logging
import typing
@@ -109,7 +110,7 @@ class Transports(ModelHandler):
'value': [],
'values': sorted(
[{'id': x.uuid, 'text': x.name} for x in Network.objects.all()],
key=lambda x: x['text'].lower(),
key=lambda x: x['text'].lower(), # type: ignore
),
'label': ugettext('Networks'),
'tooltip': ugettext(
@@ -147,7 +148,7 @@ class Transports(ModelHandler):
'values': [
{'id': x.uuid, 'text': x.name}
for x in ServicePool.objects.all().order_by('name')
if transport.protocol in x.service.getType().allowedProtocols
if transport.protocol in x.service.getType().allowedProtocols # type: ignore
],
'label': ugettext('Service Pools'),
'tooltip': ugettext('Currently assigned services pools'),
@@ -199,6 +200,13 @@ class Transports(ModelHandler):
def beforeSave(self, fields: typing.Dict[str, typing.Any]) -> None:
fields['allowed_oss'] = ','.join(fields['allowed_oss'])
# If label has spaces, replace them with underscores
fields['label'] = fields['label'].strip().replace(' ', '-')
# And ensure small_name chars are valid [ a-zA-Z0-9:-]+
if fields['label'] and not re.match(r'^[a-zA-Z0-9:-]+$', fields['label']):
raise self.invalidRequestException(
_('Label must contain only letters, numbers, ":" and "-"')
)
def afterSave(self, item: Transport) -> None:
try:

View File

@@ -208,7 +208,7 @@ class AssignedService(DetailHandler):
user = models.User.objects.get(uuid=processUuid(fields['user_id']))
logStr = 'Changing ownership of service from {} to {} by {}'.format(
userService.user.pretty_name, user.pretty_name, self._user.pretty_name
userService.user.pretty_name, user.pretty_name, self._user.pretty_name # type: ignore
)
# If there is another service that has this same owner, raise an exception

View File

@@ -78,23 +78,23 @@ def getPoolsForGroups(groups):
class Users(DetailHandler):
custom_methods = ['servicesPools', 'userServices']
custom_methods = ['servicesPools', 'userServices', 'cleanRelated']
@staticmethod
def uuid_to_id(iterator):
for v in iterator:
v['id'] = v['uuid']
del v['uuid']
yield v
def getItems(self, parent: Authenticator, item: typing.Optional[str]):
# processes item to change uuid key for id
def uuid_to_id(iterable: typing.Iterable[typing.MutableMapping[str, typing.Any]]):
for v in iterable:
v['id'] = v['uuid']
del v['uuid']
yield v
def getItems(self, parent, item):
logger.debug(item)
# Extract authenticator
try:
if item is None:
values = list(
Users.uuid_to_id(
parent.users.all().values(
uuid_to_id(
(i for i in parent.users.all().values(
'uuid',
'name',
'real_name',
@@ -104,7 +104,8 @@ class Users(DetailHandler):
'is_admin',
'last_access',
'parent',
)
'mfa_data',
))
)
)
for res in values:
@@ -127,6 +128,7 @@ class Users(DetailHandler):
'is_admin',
'last_access',
'parent',
'mfa_data',
),
)
res['id'] = u.uuid
@@ -153,7 +155,7 @@ class Users(DetailHandler):
except Exception:
return _('Current users')
def getFields(self, parent):
def getFields(self, parent: Authenticator):
return [
{
'name': {
@@ -179,7 +181,7 @@ class Users(DetailHandler):
def getRowStyle(self, parent):
return {'field': 'state', 'prefix': 'row-state-'}
def getLogs(self, parent, item):
def getLogs(self, parent: Authenticator, item):
user = None
try:
user = parent.users.get(uuid=processUuid(item))
@@ -188,7 +190,7 @@ class Users(DetailHandler):
return log.getLogs(user)
def saveItem(self, parent, item):
def saveItem(self, parent: Authenticator, item: typing.Optional[str]) -> None:
logger.debug('Saving user %s / %s', parent, item)
valid_fields = [
'name',
@@ -198,12 +200,16 @@ class Users(DetailHandler):
'staff_member',
'is_admin',
]
if self._params.get('name', '') == '':
if self._params.get('name', '').strip() == '':
raise RequestError(_('Username cannot be empty'))
if 'password' in self._params:
valid_fields.append('password')
self._params['password'] = cryptoManager().hash(self._params['password'])
if 'mfa_data' in self._params:
valid_fields.append('mfa_data')
self._params['mfa_data'] = self._params['mfa_data'].strip()
fields = self.readFieldsFromParams(valid_fields)
if not self._user.is_admin:
@@ -224,9 +230,8 @@ class Users(DetailHandler):
user.__dict__.update(fields)
logger.debug('User parent: %s', user.parent)
if auth.isExternalSource is False and (
user.parent is None or user.parent == ''
):
# If internal auth, threat it "special"
if auth.isExternalSource is False and not user.parent:
groups = self.readFieldsFromParams(['groups'])['groups']
logger.debug('Groups: %s', groups)
logger.debug('Got Groups %s', parent.groups.filter(uuid__in=groups))
@@ -248,9 +253,7 @@ class Users(DetailHandler):
logger.exception('Saving user')
raise self.invalidRequestException()
return self.getItems(parent, user.uuid)
def deleteItem(self, parent, item):
def deleteItem(self, parent: Authenticator, item):
try:
user = parent.users.get(uuid=processUuid(item))
if not self._user.is_admin and (user.is_admin or user.staff_member):
@@ -279,7 +282,7 @@ class Users(DetailHandler):
return 'deleted'
def servicesPools(self, parent, item):
def servicesPools(self, parent: Authenticator, item: str) -> typing.List[typing.Dict]:
uuid = processUuid(item)
user = parent.users.get(uuid=processUuid(uuid))
res = []
@@ -301,7 +304,7 @@ class Users(DetailHandler):
return res
def userServices(self, parent, item):
def userServices(self, parent: Authenticator, item: str) -> typing.List[typing.Dict]:
uuid = processUuid(item)
user = parent.users.get(uuid=processUuid(uuid))
res = []
@@ -313,13 +316,19 @@ class Users(DetailHandler):
res.append(v)
return res
def cleanRelated(self, parent: Authenticator, item: str) -> typing.Dict:
uuid = processUuid(item)
user = parent.users.get(uuid=processUuid(uuid))
user.cleanRelated()
return {'status': 'ok'}
class Groups(DetailHandler):
custom_methods = ['servicesPools', 'users']
def getItems(self, parent, item):
def getItems(self, parent: Authenticator, item):
try:
multi = False
if item is None:
@@ -346,16 +355,16 @@ class Groups(DetailHandler):
if multi or not i:
return res
# Add pools field if 1 item only
res = res[0]
result = res[0]
if i.is_meta:
res[
result[
'pools'
] = (
[]
) # Meta groups do not have "assigned "pools, they get it from groups interaction
else:
res['pools'] = [v.uuid for v in i.deployedServices.all()]
return res
result['pools'] = [v.uuid for v in i.deployedServices.all()]
return result
except Exception:
logger.exception('REST groups')
raise self.invalidItemException()
@@ -393,7 +402,7 @@ class Groups(DetailHandler):
},
]
def getTypes(self, parent, forType):
def getTypes(self, parent: Authenticator, forType):
tDct = {
'group': {'name': _('Group'), 'description': _('UDS Group')},
'meta': {'name': _('Meta group'), 'description': _('UDS Meta Group')},
@@ -412,11 +421,11 @@ class Groups(DetailHandler):
return types
try:
return types[forType]
return next(filter(lambda x: x['type'] == forType, types))
except Exception:
raise self.invalidRequestException()
def saveItem(self, parent, item):
def saveItem(self, parent: Authenticator, item: typing.Optional[str]) -> None:
group = None # Avoid warning on reference before assignment
try:
is_meta = self._params['type'] == 'meta'
@@ -431,7 +440,7 @@ class Groups(DetailHandler):
fields = self.readFieldsFromParams(valid_fields)
is_pattern = fields.get('name', '').find('pat:') == 0
auth = parent.getInstance()
if item is None: # Create new
if not item: # Create new
if not is_meta and not is_pattern:
auth.createGroup(
fields
@@ -476,9 +485,7 @@ class Groups(DetailHandler):
logger.exception('Saving group')
raise self.invalidRequestException()
return self.getItems(parent, group.uuid)
def deleteItem(self, parent, item):
def deleteItem(self, parent: Authenticator, item: str) -> None:
try:
group = parent.groups.get(uuid=item)
@@ -486,12 +493,12 @@ class Groups(DetailHandler):
except Exception:
raise self.invalidItemException()
return 'deleted'
def servicesPools(self, parent, item):
def servicesPools(
self, parent: Authenticator, item: str
) -> typing.List[typing.Mapping[str, typing.Any]]:
uuid = processUuid(item)
group = parent.groups.get(uuid=processUuid(uuid))
res = []
res: typing.List[typing.Mapping[str, typing.Any]] = []
for i in getPoolsForGroups((group,)):
res.append(
{
@@ -509,7 +516,9 @@ class Groups(DetailHandler):
return res
def users(self, parent, item):
def users(
self, parent: Authenticator, item: str
) -> typing.List[typing.Mapping[str, typing.Any]]:
uuid = processUuid(item)
group = parent.groups.get(uuid=processUuid(uuid))
@@ -522,7 +531,7 @@ class Groups(DetailHandler):
'last_access': user.last_access,
}
res = []
res: typing.List[typing.Mapping[str, typing.Any]] = []
if group.is_meta:
# Get all users for everygroup and
groups = getGroupsFromMeta((group,))

View File

@@ -114,8 +114,8 @@ class BaseModelHandler(Handler):
'values': field.get('values', []),
},
}
if 'tab' in field:
v['gui']['tab'] = field['tab']
if field.get('tab', None):
v['gui']['tab'] = _(field['tab'])
gui.append(v)
return gui
@@ -268,7 +268,11 @@ class BaseModelHandler(Handler):
args: typing.Dict[str, str] = {}
try:
for key in fldList:
args[key] = self._params[key]
if ':' in key: # optional field? get default if not present
k, default = key.split(':')[:2]
args[k] = self._params.get(k, default)
else:
args[key] = self._params[key]
# del self._params[key]
except KeyError as e:
raise RequestError('needed parameter not found in data {0}'.format(e))
@@ -675,7 +679,9 @@ class ModelHandler(BaseModelHandler):
detail: typing.ClassVar[
typing.Optional[typing.Dict[str, typing.Type[DetailHandler]]]
] = None # Dictionary containing detail routing
# Put needed fields
# Fields that are going to be saved directly
# If a field is in the form "field:default" and field is not present in the request, default will be used
# Note that these fields has to be present in the model, and they can be "edited" in the beforeSave method
save_fields: typing.ClassVar[typing.List[str]] = []
# Put removable fields before updating
remove_fields: typing.ClassVar[typing.List[str]] = []

View File

@@ -106,8 +106,11 @@ class ContentProcessor:
if isinstance(obj, (list, tuple, types.GeneratorType)):
return [ContentProcessor.procesForRender(v) for v in obj]
if isinstance(obj, (datetime.datetime, datetime.date)):
if isinstance(obj, (datetime.datetime,)):
return int(time.mktime(obj.timetuple()))
if isinstance(obj, (datetime.date,)):
return '{}-{:02d}-{:02d}'.format(obj.year, obj.month, obj.day)
if isinstance(obj, bytes):
return obj.decode('utf-8')

View File

@@ -71,6 +71,7 @@ class UDSAppConfig(AppConfig):
# pylint: disable=unused-import
from . import services # to make sure that the packages are initialized at this point
from . import auths # To make sure that the packages are initialized at this point
from . import mfas # To make sure mfas are loaded on memory
from . import osmanagers # To make sure that packages are initialized at this point
from . import transports # To make sure that packages are initialized at this point
from . import dispatchers # Ensure all dischatchers all also available
@@ -85,7 +86,7 @@ default_app_config = 'uds.UDSAppConfig'
@receiver(connection_created)
def extend_sqlite(connection=None, **kwargs):
if connection.vendor == "sqlite":
if connection and connection.vendor == "sqlite":
logger.debug('Connection vendor is sqlite, extending methods')
cursor = connection.cursor()
cursor.execute('PRAGMA synchronous=OFF')
@@ -94,4 +95,4 @@ def extend_sqlite(connection=None, **kwargs):
cursor.execute('PRAGMA journal_mode=WAL')
connection.connection.create_function("MIN", 2, min)
connection.connection.create_function("MAX", 2, max)
connection.connection.create_function("CEIL", 1, math.ceil)

View File

@@ -31,6 +31,7 @@
import logging
from django.http import HttpResponse
from django.middleware import csrf
from django.shortcuts import render
from django.template import RequestContext, loader
from django.utils.translation import ugettext as _
@@ -41,10 +42,22 @@ from uds.core.util.decorators import denyBrowsers
logger = logging.getLogger(__name__)
CSRF_FIELD = 'csrfmiddlewaretoken'
@denyBrowsers(browsers=['ie<10'])
@webLoginRequired(admin=True)
def index(request):
return render(request, 'uds/admin/index.html')
# Gets csrf token
csrf_token = csrf.get_token(request)
if csrf_token is not None:
csrf_token = str(csrf_token)
return render(
request,
'uds/admin/index.html',
{'csrf_field': CSRF_FIELD, 'csrf_token': csrf_token},
)
@denyBrowsers(browsers=['ie<10'])

View File

@@ -99,13 +99,20 @@ class InternalDBAuth(auths.Authenticator):
if self.reverseDns.isTrue():
try:
return str(
dns.resolver.query(dns.reversename.from_address(ip), 'PTR')[0]
dns.resolver.query(dns.reversename.from_address(ip).to_text(), 'PTR')[0]
)
except Exception:
pass
return ip
def mfaIdentifier(self, username: str) -> str:
try:
return self.dbAuthenticator().users.get(name=username.lower(), state=State.ACTIVE).mfa_data
except Exception: # User not found
return ''
def transformUsername(self, username: str) -> str:
username = username.lower()
if self.differentForEachHost.isTrue():
newUsername = self.getIp() + '-' + username
# Duplicate basic user into username.
@@ -131,6 +138,7 @@ class InternalDBAuth(auths.Authenticator):
def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
) -> bool:
username = username.lower()
logger.debug('Username: %s, Password: %s', username, credentials)
dbAuth = self.dbAuthenticator()
try:
@@ -152,7 +160,7 @@ class InternalDBAuth(auths.Authenticator):
def getGroups(self, username: str, groupsManager: 'auths.GroupsManager'):
dbAuth = self.dbAuthenticator()
try:
user: 'models.User' = dbAuth.users.get(name=username, state=State.ACTIVE)
user: 'models.User' = dbAuth.users.get(name=username.lower(), state=State.ACTIVE)
except Exception:
return
@@ -161,7 +169,7 @@ class InternalDBAuth(auths.Authenticator):
def getRealName(self, username: str) -> str:
# Return the real name of the user, if it is set
try:
user = self.dbAuthenticator().users.get(name=username, state=State.ACTIVE)
user = self.dbAuthenticator().users.get(name=username.lower(), state=State.ACTIVE)
return user.real_name or username
except Exception:
return super().getRealName(username)

View File

@@ -112,6 +112,15 @@ class RadiusAuth(auths.Authenticator):
tooltip=_('If set, this value will be added as group for all radius users'),
tab=gui.ADVANCED_TAB,
)
mfaAttr = gui.TextField(
length=2048,
multiline=2,
label=_('MFA attribute'),
order=13,
tooltip=_('Attribute from where to extract the MFA code'),
required=False,
tab=gui.MFA_TAB,
)
def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None:
pass
@@ -126,12 +135,25 @@ class RadiusAuth(auths.Authenticator):
appClassPrefix=self.appClassPrefix.value,
)
def mfaStorageKey(self, username: str) -> str:
return 'mfa_' + str(self.dbAuthenticator().uuid) + username
def mfaIdentifier(self, username: str) -> str:
return self.storage.getPickle(self.mfaStorageKey(username)) or ''
def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
) -> bool:
try:
connection = self.radiusClient()
groups = connection.authenticate(username=username, password=credentials)
groups, mfaCode = connection.authenticate(username=username, password=credentials, mfaField=self.mfaAttr.value.strip())
# store the user mfa attribute if it is set
if mfaCode:
self.storage.putPickle(
self.mfaStorageKey(username),
mfaCode,
)
except Exception:
authLogLogin(getRequest(), self.dbAuthenticator(), username, 'Access denied by Raiuds')
return False
@@ -178,7 +200,7 @@ class RadiusAuth(auths.Authenticator):
try:
connection = self.radiusClient()
# Reply is not important...
connection.authenticate(cryptoManager().randomString(10), cryptoManager().randomString(10))
connection.authenticate(cryptoManager().randomString(10), cryptoManager().randomString(10), mfaField=self.mfaAttr.value.strip())
except client.RadiusAuthenticationError as e:
pass
except Exception:

View File

@@ -1,16 +1,12 @@
import io
import logging
import enum
import typing
from pyrad.client import Client
from pyrad.dictionary import Dictionary
import pyrad.packet
__all__ = ['RadiusClient', 'RadiusAuthenticationError', 'RADDICT']
class RadiusAuthenticationError(Exception):
pass
logger = logging.getLogger(__name__)
RADDICT = """ATTRIBUTE User-Name 1 string
@@ -51,6 +47,36 @@ ATTRIBUTE Framed-AppleTalk-Link 37 integer
ATTRIBUTE Framed-AppleTalk-Network 38 integer
ATTRIBUTE Framed-AppleTalk-Zone 39 string"""
# For AccessChallenge return values
NOT_CHECKED, INCORRECT, CORRECT = -1, 0, 1 # for pwd and otp
NOT_NEEDED, NEEDED = INCORRECT, CORRECT # for otp_needed
class RadiusAuthenticationError(Exception):
pass
class RadiusStates(enum.IntEnum):
NOT_CHECKED = -1
INCORRECT = 0
CORRECT = 1
# Aliases
NOT_NEEDED = INCORRECT
NEEDED = CORRECT
class RadiusResult(typing.NamedTuple):
"""
Result of an AccessChallenge request.
"""
pwd: RadiusStates = RadiusStates.INCORRECT
replyMessage: typing.Optional[bytes] = None
state: typing.Optional[bytes] = None
otp: RadiusStates = RadiusStates.NOT_CHECKED
otp_needed: RadiusStates = RadiusStates.NOT_CHECKED
class RadiusClient:
radiusServer: Client
@@ -68,12 +94,27 @@ class RadiusClient:
dictionary: str = RADDICT,
) -> None:
self.radiusServer = Client(
server=server, authport=authPort, secret=secret, dict=Dictionary(io.StringIO(dictionary))
server=server,
authport=authPort,
secret=secret,
dict=Dictionary(io.StringIO(dictionary)),
)
self.nasIdentifier = nasIdentifier
self.appClassPrefix = appClassPrefix
def authenticate(self, username: str, password: str) -> typing.List[str]:
def extractAccessChallenge(self, reply: pyrad.packet.AuthPacket) -> RadiusResult:
return RadiusResult(
pwd=RadiusStates.CORRECT,
replyMessage=typing.cast(
typing.List[bytes], reply.get('Reply-Message') or ['']
)[0],
state=typing.cast(typing.List[bytes], reply.get('State') or [b''])[0],
otp_needed=RadiusStates.NEEDED,
)
def sendAccessRequest(
self, username: str, password: str, **kwargs
) -> pyrad.packet.AuthPacket:
req: pyrad.packet.AuthPacket = self.radiusServer.CreateAuthPacket(
code=pyrad.packet.AccessRequest,
User_Name=username,
@@ -82,9 +123,19 @@ class RadiusClient:
req["User-Password"] = req.PwCrypt(password)
reply = typing.cast(pyrad.packet.AuthPacket, self.radiusServer.SendPacket(req))
# Fill in extra fields
for k, v in kwargs.items():
req[k] = v
if reply.code != pyrad.packet.AccessAccept:
return typing.cast(pyrad.packet.AuthPacket, self.radiusServer.SendPacket(req))
# Second element of return value is the mfa code from field
def authenticate(
self, username: str, password: str, mfaField: str
) -> typing.Tuple[typing.List[str], str]:
reply = self.sendAccessRequest(username, password)
if reply.code not in (pyrad.packet.AccessAccept, pyrad.packet.AccessChallenge):
raise RadiusAuthenticationError('Access denied')
# User accepted, extract groups...
@@ -92,10 +143,111 @@ class RadiusClient:
groupClassPrefix = (self.appClassPrefix + 'group=').encode()
groupClassPrefixLen = len(groupClassPrefix)
if 'Class' in reply:
groups = [i[groupClassPrefixLen:].decode() for i in typing.cast(typing.Iterable[bytes], reply['Class']) if i.startswith(groupClassPrefix)]
groups = [
i[groupClassPrefixLen:].decode()
for i in typing.cast(typing.Iterable[bytes], reply['Class'])
if i.startswith(groupClassPrefix)
]
else:
logger.info('No "Class (25)" attribute found')
return []
return ([], '')
return groups
# ...and mfa code
mfaCode = ''
if mfaField and mfaField in reply:
mfaCode = ''.join(
i[groupClassPrefixLen:].decode()
for i in typing.cast(typing.Iterable[bytes], reply['Class'])
if i.startswith(groupClassPrefix)
)
return (groups, mfaCode)
def authenticate_only(self, username: str, password: str) -> RadiusResult:
reply = self.sendAccessRequest(username, password)
if reply.code == pyrad.packet.AccessChallenge:
return self.extractAccessChallenge(reply)
# user/pwd accepted: this user does not have challenge data
if reply.code == pyrad.packet.AccessAccept:
return RadiusResult(
pwd=RadiusStates.CORRECT,
otp_needed=RadiusStates.NOT_CHECKED,
)
# user/pwd rejected
return RadiusResult(
pwd=RadiusStates.INCORRECT,
)
def challenge_only(
self, username: str, otp: str, state: bytes = b'0000000000000000'
) -> RadiusResult:
# clean otp code
otp = ''.join([x for x in otp if x in '0123456789'])
reply = self.sendAccessRequest(username, otp, State=state)
# correct OTP challenge
if reply.code == pyrad.packet.AccessAccept:
return RadiusResult(
otp=RadiusStates.CORRECT,
)
# incorrect OTP challenge
return RadiusResult(
otp=RadiusStates.INCORRECT,
)
def authenticate_and_challenge(
self, username: str, password: str, otp: str
) -> RadiusResult:
reply = self.sendAccessRequest(username, password)
if reply.code == pyrad.packet.AccessChallenge:
state = typing.cast(typing.List[bytes], reply.get('State') or [b''])[0]
replyMessage = typing.cast(
typing.List[bytes], reply.get('Reply-Message') or ['']
)[0]
return self.challenge_only(username, otp, state=state)
# user/pwd accepted: but this user does not have challenge data
# we should not be here...
if reply.code == pyrad.packet.AccessAccept:
logger.warning(
"Radius OTP error: cheking for OTP for not needed user [%s]", username
)
return RadiusResult(
pwd=RadiusStates.CORRECT,
otp_needed=RadiusStates.NOT_NEEDED,
)
# TODO: accept more AccessChallenge authentications (as RFC says)
# incorrect user/pwd
return RadiusResult()
def authenticate_challenge(
self, username: str, password: str = '', otp: str = '', state: bytes = b''
) -> RadiusResult:
'''
wrapper for above 3 functions: authenticate_only, challenge_only, authenticate_and_challenge
calls wrapped functions based on passed input values: (pwd/otp/state)
'''
# clean input data
otp = ''.join([x for x in otp if x in '0123456789'])
username = username.strip()
password = password.strip()
state = state.strip()
if not username or (not password and not otp):
return RadiusResult() # no user/pwd provided
if not otp:
return self.authenticate_only(username, password)
if otp and not password:
# check only otp with static/invented state. allow this ?
return self.challenge_only(username, otp, state=b'0000000000000000')
# otp and password
return self.authenticate_and_challenge(username, password, otp)

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# Copyright (c) 2012-2022 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -12,7 +12,7 @@
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
@@ -106,16 +106,34 @@ class RegexLdap(auths.Authenticator):
length=3,
label=_('Timeout'),
defvalue='10',
order=6,
order=10,
tooltip=_('Timeout in seconds of connection to LDAP'),
required=True,
minValue=1,
tab=gui.ADVANCED_TAB,
)
verifySsl = gui.CheckBoxField(
label=_('Verify SSL'),
defvalue=True,
order=11,
tooltip=_(
'If checked, SSL verification will be enforced. If not, SSL verification will be disabled'
),
tab=gui.ADVANCED_TAB,
)
certificate = gui.TextField(
length=8192,
multiline=4,
label=_('Certificate'),
order=12,
tooltip=_('Certificate to use for SSL verification'),
required=False,
tab=gui.ADVANCED_TAB,
)
ldapBase = gui.TextField(
length=64,
label=_('Base'),
order=7,
order=20,
tooltip=_('Common search base (used for "users" and "groups")'),
required=True,
tab=_('Ldap info'),
@@ -124,7 +142,7 @@ class RegexLdap(auths.Authenticator):
length=64,
label=_('User class'),
defvalue='posixAccount',
order=8,
order=21,
tooltip=_('Class for LDAP users (normally posixAccount)'),
required=True,
tab=_('Ldap info'),
@@ -133,7 +151,7 @@ class RegexLdap(auths.Authenticator):
length=64,
label=_('User Id Attr'),
defvalue='uid',
order=9,
order=22,
tooltip=_('Attribute that contains the user id'),
required=True,
tab=_('Ldap info'),
@@ -143,7 +161,7 @@ class RegexLdap(auths.Authenticator):
label=_('User Name Attr'),
multiline=2,
defvalue='uid',
order=10,
order=23,
tooltip=_(
'Attributes that contains the user name attributes or attribute patterns (one for each line)'
),
@@ -155,7 +173,7 @@ class RegexLdap(auths.Authenticator):
label=_('Group Name Attr'),
multiline=2,
defvalue='cn',
order=11,
order=24,
tooltip=_(
'Attribute that contains the group name attributes or attribute patterns (one for each line)'
),
@@ -168,7 +186,7 @@ class RegexLdap(auths.Authenticator):
length=64,
label=_('Alt. class'),
defvalue='',
order=20,
order=25,
tooltip=_(
'Class for LDAP objects that will be also checked for groups retrieval (normally empty)'
),
@@ -176,6 +194,16 @@ class RegexLdap(auths.Authenticator):
tab=_('Advanced'),
)
mfaAttr = gui.TextField(
length=2048,
multiline=2,
label=_('MFA attribute'),
order=30,
tooltip=_('Attribute from where to extract the MFA code'),
required=False,
tab=gui.MFA_TAB,
)
typeName = _('Regex LDAP Authenticator')
typeType = 'RegexLdapAuthenticator'
typeDescription = _('Regular Expressions LDAP authenticator')
@@ -205,14 +233,11 @@ class RegexLdap(auths.Authenticator):
_groupNameAttr: str = ''
_userNameAttr: str = ''
_altClass: str = ''
_mfaAttr: str = ''
_verifySsl: bool = True
_certificate: str = ''
def __init__(
self,
dbAuth: 'models.Authenticator',
environment: 'Environment',
values: typing.Optional[typing.Dict[str, str]],
):
super().__init__(dbAuth, environment, values)
def initialize(self, values: typing.Optional[typing.Dict[str, str]]) -> None:
if values:
self.__validateField(values['userNameAttr'], str(self.userNameAttr.label))
self.__validateField(values['userIdAttr'], str(self.userIdAttr.label))
@@ -231,6 +256,9 @@ class RegexLdap(auths.Authenticator):
# self._regex = values['regex']
self._userNameAttr = values['userNameAttr']
self._altClass = values['altClass']
self._mfaAttr = values['mfaAttr']
self._verifySsl = gui.strToBool(values['verifySsl'])
self._certificate = values['certificate']
def __validateField(self, field: str, fieldLabel: str) -> None:
"""
@@ -256,14 +284,46 @@ class RegexLdap(auths.Authenticator):
attr = line[:equalPos]
else:
attr = line
res.append(attr)
# If + is present, we must split it
if '+' in attr:
for a in attr.split('+'):
if a not in res:
res.append(a)
elif ':' in attr:
res.append(attr.split(':')[0])
else:
if attr not in res:
res.append(attr)
return res
def __processField(
self, field: str, attributes: typing.MutableMapping[str, typing.Any]
) -> typing.List[str]:
res: typing.List[str] = []
logger.debug('Attributes: %s', attributes)
def getAttr(attrName: str) -> typing.List[str]:
def asList(val: typing.Any) -> typing.List[str]:
if isinstance(val, list):
return val
return [val]
if '+' in attrName:
attrList = attrName.split('+')
# Check all attributes are present, and has only one value
if not all([len(attributes.get(a, [])) <= 1 for a in attrList]):
logger.warning('Attribute %s do not has exactly one value, skipping %s', attrName, line)
return []
val = [''.join([asList(attributes.get(a, ['']))[0] for a in attrList])]
elif ':' in attrName:
# Prepend the value after : to value before :
attr, prependable = attrName.split(':')
val = [prependable + a for a in asList(attributes.get(attr, []))]
else:
val = asList(attributes.get(attrName, []))
return val
logger.debug('******** Attributes: %s', attributes)
for line in field.splitlines():
equalPos = line.find('=')
if (
@@ -275,15 +335,13 @@ class RegexLdap(auths.Authenticator):
# if pattern do not have groups, define one with complete pattern (i.e. id=.* --> id=(.*))
if pattern.find('(') == -1:
pattern = '(' + pattern + ')'
val = attributes.get(attr, [])
if not isinstance(val, list): # May we have a single value
val = [val]
val = getAttr(attr)
logger.debug('Pattern: %s', pattern)
for v in val:
try:
logger.debug('Pattern: %s on value %s', pattern, v)
searchResult = re.search(
pattern, v, re.IGNORECASE
) # @UndefinedVariable
@@ -291,11 +349,17 @@ class RegexLdap(auths.Authenticator):
continue
logger.debug("Found against %s: %s ", v, searchResult.groups())
res.append(''.join(searchResult.groups()))
except Exception:
except Exception: # nosec: If not a valid regex, just ignore it
pass # Ignore exceptions here
logger.debug('Res: %s', res)
return res
def mfaStorageKey(self, username: str) -> str:
return 'mfa_' + self.dbAuthenticator().uuid + username # type: ignore
def mfaIdentifier(self, username: str) -> str:
return self.storage.getPickle(self.mfaStorageKey(username)) or ''
def valuesDict(self) -> gui.ValuesDictType:
return {
'host': self._host,
@@ -310,12 +374,15 @@ class RegexLdap(auths.Authenticator):
'groupNameAttr': self._groupNameAttr,
'userNameAttr': self._userNameAttr,
'altClass': self._altClass,
'mfaAttr': self._mfaAttr,
'verifySsl': gui.boolToStr(self._verifySsl),
'certificate': self._certificate,
}
def marshal(self) -> bytes:
return '\t'.join(
[
'v3',
'v5',
self._host,
self._port,
gui.boolToStr(self._ssl),
@@ -328,63 +395,67 @@ class RegexLdap(auths.Authenticator):
self._groupNameAttr,
self._userNameAttr,
self._altClass,
self._mfaAttr,
gui.boolToStr(self._verifySsl),
self._certificate.strip(),
]
).encode('utf8')
def unmarshal(self, data: bytes) -> None:
vals = data.decode('utf8').split('\t')
self._verifySsl = False # Backward compatibility
self._mfaAttr = '' # Backward compatibility
self._certificate = '' # Backward compatibility
# Common
logger.debug('Common: %s', vals[1:11])
(
self._host,
self._port,
ssl,
self._username,
self._password,
self._timeout,
self._ldapBase,
self._userClass,
self._userIdAttr,
self._groupNameAttr,
) = vals[1:11]
self._ssl = gui.strToBool(ssl)
if vals[0] == 'v1':
logger.debug("Data: %s", vals[1:])
(
self._host,
self._port,
ssl,
self._username,
self._password,
self._timeout,
self._ldapBase,
self._userClass,
self._userIdAttr,
self._groupNameAttr,
_regex,
self._userNameAttr,
) = vals[1:]
self._ssl = gui.strToBool(ssl)
logger.debug("Data: %s", vals[11:])
_regex, self._userNameAttr = vals[11:]
self._groupNameAttr = self._groupNameAttr + '=' + _regex
self._userNameAttr = '\n'.join(self._userNameAttr.split(','))
elif vals[0] == 'v2':
logger.debug("Data v2: %s", vals[1:])
(
self._host,
self._port,
ssl,
self._username,
self._password,
self._timeout,
self._ldapBase,
self._userClass,
self._userIdAttr,
self._groupNameAttr,
self._userNameAttr,
) = vals[1:]
self._userNameAttr = vals[11]
self._ssl = gui.strToBool(ssl)
elif vals[0] == 'v3':
logger.debug("Data v3: %s", vals[1:])
(
self._host,
self._port,
ssl,
self._username,
self._password,
self._timeout,
self._ldapBase,
self._userClass,
self._userIdAttr,
self._groupNameAttr,
self._userNameAttr,
self._altClass,
) = vals[1:]
self._ssl = gui.strToBool(ssl)
) = vals[11:]
elif vals[0] == 'v4':
logger.debug("Data v4: %s", vals[1:])
(
self._userNameAttr,
self._altClass,
self._mfaAttr,
) = vals[11:]
elif vals[0] == 'v5':
logger.debug("Data v5: %s", vals[1:])
(
self._userNameAttr,
self._altClass,
self._mfaAttr,
verifySsl,
self._certificate,
) = vals[11:]
self._verifySsl = gui.strToBool(verifySsl)
def __connection(self) -> typing.Any:
"""
@@ -392,7 +463,7 @@ class RegexLdap(auths.Authenticator):
@return: Connection established
@raise exception: If connection could not be established
"""
if self._connection is None: # We want this method also to check credentials
if self._connection is None: # If connection is not established, try to connect
self._connection = ldaputil.connection(
self._username,
self._password,
@@ -428,6 +499,9 @@ class RegexLdap(auths.Authenticator):
+ self.__getAttrsFromField(self._userNameAttr)
+ self.__getAttrsFromField(self._groupNameAttr)
)
if self._mfaAttr:
attributes = attributes + self.__getAttrsFromField(self._mfaAttr)
user = ldaputil.getFirst(
con=self.__connection(),
base=self._ldapBase,
@@ -517,6 +591,13 @@ class RegexLdap(auths.Authenticator):
)
return False
# store the user mfa attribute if it is set
if self._mfaAttr:
self.storage.putPickle(
self.mfaStorageKey(username),
usr[self._mfaAttr][0],
)
groupsManager.validate(self.__getGroups(usr))
return True

View File

@@ -97,15 +97,35 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=3,
label=_('Timeout'),
defvalue='10',
order=6,
order=10,
tooltip=_('Timeout in seconds of connection to LDAP'),
required=True,
minValue=1,
tab=gui.ADVANCED_TAB,
)
verifySsl = gui.CheckBoxField(
label=_('Verify SSL'),
defvalue=True,
order=11,
tooltip=_(
'If checked, SSL verification will be enforced. If not, SSL verification will be disabled'
),
tab=gui.ADVANCED_TAB,
)
certificate = gui.TextField(
length=8192,
multiline=4,
label=_('Certificate'),
order=12,
tooltip=_('Certificate to use for SSL verification'),
required=False,
tab=gui.ADVANCED_TAB,
)
ldapBase = gui.TextField(
length=64,
label=_('Base'),
order=7,
order=30,
tooltip=_('Common search base (used for "users" and "groups")'),
required=True,
tab=_('Ldap info'),
@@ -114,7 +134,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('User class'),
defvalue='posixAccount',
order=8,
order=31,
tooltip=_('Class for LDAP users (normally posixAccount)'),
required=True,
tab=_('Ldap info'),
@@ -123,7 +143,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('User Id Attr'),
defvalue='uid',
order=9,
order=32,
tooltip=_('Attribute that contains the user id'),
required=True,
tab=_('Ldap info'),
@@ -132,7 +152,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('User Name Attr'),
defvalue='uid',
order=10,
order=33,
tooltip=_(
'Attributes that contains the user name (list of comma separated values)'
),
@@ -143,7 +163,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('Group class'),
defvalue='posixGroup',
order=11,
order=34,
tooltip=_('Class for LDAP groups (normally poxisGroup)'),
required=True,
tab=_('Ldap info'),
@@ -152,7 +172,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('Group Id Attr'),
defvalue='cn',
order=12,
order=35,
tooltip=_('Attribute that contains the group id'),
required=True,
tab=_('Ldap info'),
@@ -161,15 +181,25 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('Group membership attr'),
defvalue='memberUid',
order=13,
order=36,
tooltip=_('Attribute of the group that contains the users belonging to it'),
required=True,
tab=_('Ldap info'),
)
mfaAttr = gui.TextField(
length=2048,
multiline=2,
label=_('MFA attribute'),
order=13,
tooltip=_('Attribute from where to extract the MFA code'),
required=False,
tab=gui.MFA_TAB,
)
typeName = _('SimpleLDAP (DEPRECATED)')
typeName = _('SimpleLDAP')
typeType = 'SimpleLdapAuthenticator'
typeDescription = _('Simple LDAP authenticator (DEPRECATED)')
typeDescription = _('Simple LDAP authenticator')
iconFile = 'auth.png'
# If it has and external source where to get "new" users (groups must be declared inside UDS)
@@ -197,6 +227,10 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
_groupIdAttr: str = ''
_memberAttr: str = ''
_userNameAttr: str = ''
_mfaAttr: str = ''
_verifySsl: bool = True
_certificate: str = ''
def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None:
if values:
@@ -215,6 +249,9 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
self._userNameAttr = values['userNameAttr'].replace(
' ', ''
) # Removes white spaces
self._mfaAttr = values['mfaAttr']
self._verifySsl = gui.strToBool(values['verifySsl'])
self._certificate = values['certificate']
def valuesDict(self) -> gui.ValuesDictType:
return {
@@ -231,12 +268,15 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
'groupIdAttr': self._groupIdAttr,
'memberAttr': self._memberAttr,
'userNameAttr': self._userNameAttr,
'mfaAttr': self._mfaAttr,
'verifySsl': gui.boolToStr(self._verifySsl),
'certificate': self._certificate,
}
def marshal(self) -> bytes:
return '\t'.join(
[
'v1',
'v2',
self._host,
self._port,
gui.boolToStr(self._ssl),
@@ -250,41 +290,59 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
self._groupIdAttr,
self._memberAttr,
self._userNameAttr,
self._mfaAttr,
gui.boolToStr(self._verifySsl),
self._certificate.strip(),
]
).encode('utf8')
def unmarshal(self, data: bytes):
vals = data.decode('utf8').split('\t')
if vals[0] == 'v1':
logger.debug("Data: %s", vals[1:])
self._verifySsl = False # Backward compatibility
self._mfaAttr = '' # Backward compatibility
self._certificate = '' # Backward compatibility
logger.debug("Data: %s", vals[1:])
(
self._host,
self._port,
ssl,
self._username,
self._password,
self._timeout,
self._ldapBase,
self._userClass,
self._groupClass,
self._userIdAttr,
self._groupIdAttr,
self._memberAttr,
self._userNameAttr,
) = vals[1:14]
self._ssl = gui.strToBool(ssl)
if vals[0] == 'v2':
(
self._host,
self._port,
ssl,
self._username,
self._password,
self._timeout,
self._ldapBase,
self._userClass,
self._groupClass,
self._userIdAttr,
self._groupIdAttr,
self._memberAttr,
self._userNameAttr,
) = vals[1:]
self._ssl = gui.strToBool(ssl)
self._mfaAttr,
verifySsl,
self._certificate
) = vals[14:17]
self._verifySsl = gui.strToBool(verifySsl)
def mfaStorageKey(self, username: str) -> str:
return 'mfa_' + str(self.dbAuthenticator().uuid) + username
def mfaIdentifier(self, username: str) -> str:
return self.storage.getPickle(self.mfaStorageKey(username)) or ''
def __connection(
self,
username: typing.Optional[str] = None,
password: typing.Optional[str] = None,
self
):
"""
Tries to connect to ldap. If username is None, it tries to connect using user provided credentials.
@return: Connection established
@raise exception: If connection could not be established
"""
if self._connection is None: # We want this method also to check credentials
if self._connection is None: # We are not connected
self._connection = ldaputil.connection(
self._username,
self._password,
@@ -293,6 +351,8 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
ssl=self._ssl,
timeout=int(self._timeout),
debug=False,
verify_ssl=self._verifySsl,
certificate=self._certificate,
)
return self._connection
@@ -306,6 +366,8 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
ssl=self._ssl,
timeout=int(self._timeout),
debug=False,
verify_ssl=self._verifySsl,
certificate=self._certificate,
)
def __getUser(self, username: str) -> typing.Optional[ldaputil.LDAPResultType]:
@@ -315,13 +377,17 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
@return: None if username is not found, an dictionary of LDAP entry attributes if found.
@note: Active directory users contains the groups it belongs to in "memberOf" attribute
"""
attributes = [i for i in self._userNameAttr.split(',') + [self._userIdAttr]]
if self._mfaAttr:
attributes = attributes + [self._mfaAttr]
return ldaputil.getFirst(
con=self.__connection(),
base=self._ldapBase,
objectClass=self._userClass,
field=self._userIdAttr,
value=username,
attributes=[i for i in self._userNameAttr.split(',') + [self._userIdAttr]],
attributes=attributes,
sizeLimit=LDAP_RESULT_LIMIT,
)
@@ -417,6 +483,13 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
)
return False
# store the user mfa attribute if it is set
if self._mfaAttr:
self.storage.putPickle(
self.mfaStorageKey(username),
user[self._mfaAttr][0],
)
groupsManager.validate(self.__getGroups(user))
return True

View File

@@ -53,7 +53,7 @@ def __init__():
from uds.core import auths
# Dinamycally import children of this package. The __init__.py files must declare authenticators as subclasses of auths.Authenticator
pkgpath = os.path.dirname(sys.modules[__name__].__file__)
pkgpath = os.path.dirname(sys.modules[__name__].__file__) # type: ignore
for _, name, _ in pkgutil.iter_modules([pkgpath]):
# __import__(name, globals(), locals(), [], 1)
importlib.import_module('.' + name, __name__) # import module

View File

@@ -69,6 +69,7 @@ authLogger = logging.getLogger('authLog')
USER_KEY = 'uk'
PASS_KEY = 'pk'
EXPIRY_KEY = 'ek'
AUTHORIZED_KEY = 'ak'
ROOT_ID = -20091204 # Any negative number will do the trick
UDS_COOKIE_LENGTH = 48
@@ -86,7 +87,7 @@ def getUDSCookie(
if 'uds' not in request.COOKIES:
cookie = cryptoManager().randomString(UDS_COOKIE_LENGTH)
if response is not None:
response.set_cookie('uds', cookie, samesite='Lax')
response.set_cookie('uds', cookie, samesite='Lax', httponly=GlobalConfig.ENHANCED_SECURITY.getBool())
request.COOKIES['uds'] = cookie
else:
cookie = request.COOKIES['uds'][:UDS_COOKIE_LENGTH]
@@ -122,32 +123,45 @@ def getRootUser() -> User:
# Decorator to make easier protect pages that needs to be logged in
def webLoginRequired(
admin: typing.Union[bool, str] = False
) -> typing.Callable[[typing.Callable[..., RT]], typing.Callable[..., RT]]:
"""
Decorator to set protection to access page
admin: typing.Union[bool, typing.Literal['admin']] = False
) -> typing.Callable[
[typing.Callable[..., HttpResponse]], typing.Callable[..., HttpResponse]
]:
"""Decorator to set protection to access page
Look for samples at uds.core.web.views
if admin == True, needs admin or staff
if admin == 'admin', needs admin
Args:
admin (bool, optional): If True, needs admin or staff. Is it's "admin" literal, needs admin . Defaults to False (any user).
Returns:
typing.Callable[[typing.Callable[..., HttpResponse]], typing.Callable[..., HttpResponse]]: Decorator
Note:
This decorator is used to protect pages that needs to be logged in.
To protect against ajax calls, use `denyNonAuthenticated` instead
"""
def decorator(view_func: typing.Callable[..., RT]) -> typing.Callable[..., RT]:
def _wrapped_view(request: 'ExtendedHttpRequest', *args, **kwargs) -> RT:
def decorator(
view_func: typing.Callable[..., HttpResponse]
) -> typing.Callable[..., HttpResponse]:
@wraps(view_func)
def _wrapped_view(
request: 'ExtendedHttpRequest', *args, **kwargs
) -> HttpResponse:
"""
Wrapped function for decorator
"""
if not request.user:
# url = request.build_absolute_uri(GlobalConfig.LOGIN_URL.get())
# if GlobalConfig.REDIRECT_TO_HTTPS.getBool() is True:
# url = url.replace('http://', 'https://')
# logger.debug('No user found, redirecting to %s', url)
return HttpResponseRedirect(reverse('page.login')) # type: ignore
# If no user or user authorization is not completed...
if not request.user or not request.authorized:
return HttpResponseRedirect(reverse('page.login'))
if admin is True or admin == 'admin': # bool or string "admin"
if admin in (True, 'admin'):
if request.user.isStaff() is False or (
admin == 'admin' and not request.user.is_admin
):
return HttpResponseForbidden(_('Forbidden')) # type: ignore
return HttpResponseForbidden(_('Forbidden'))
return view_func(request, *args, **kwargs)
@@ -167,7 +181,6 @@ def trustedSourceRequired(
) -> typing.Callable[..., RT]:
"""
Decorator to set protection to access page
look for sample at uds.dispatchers.pam
"""
@wraps(view_func)
@@ -189,12 +202,14 @@ def trustedSourceRequired(
# decorator to deny non authenticated requests
# The difference with webLoginRequired is that this one does not redirect to login page
# it's designed to be used in ajax calls mainly
def denyNonAuthenticated(
view_func: typing.Callable[..., RT]
) -> typing.Callable[..., RT]:
@wraps(view_func)
def _wrapped_view(request: 'ExtendedHttpRequest', *args, **kwargs) -> RT:
if not request.user:
if not request.user or not request.authorized:
return HttpResponseForbidden() # type: ignore
return view_func(request, *args, **kwargs)
@@ -277,7 +292,7 @@ def authenticate(
return None
if isinstance(res, str):
return res # type: ignore # note: temporal fix on 3.5 for possible redirect on failed login
return res # type: ignore # note: temporal fix on >= 3.5 for possible redirect on failed login
logger.debug('Groups manager: %s', gm)
@@ -372,7 +387,13 @@ def webLogin(
cookie = getUDSCookie(request, response)
user.updateLastAccess()
request.session.clear()
request.authorized = (
False # For now, we don't know if the user is authorized until MFA is checked
)
# If Enabled zero trust, do not cache credentials
if GlobalConfig.ENFORCE_ZERO_TRUST.getBool(False):
password = ''
request.session[USER_KEY] = user.id
request.session[PASS_KEY] = cryptoManager().symCrypt(
password, cookie
@@ -415,31 +436,30 @@ def webLogout(
Helper function to clear user related data from session. If this method is not used, the session we be cleaned anyway
by django in regular basis.
"""
if exit_url is None:
exit_url = request.build_absolute_uri(reverse('page.logout'))
# exit_url = GlobalConfig.LOGIN_URL.get()
# if GlobalConfig.REDIRECT_TO_HTTPS.getBool() is True:
# exit_url = exit_url.replace('http://', 'https://')
exit_url = exit_url or reverse('page.login')
try:
if request.user:
authenticator = request.user.manager.getInstance()
username = request.user.name
exit_url = authenticator.logout(username) or exit_url
if request.user.id != ROOT_ID:
# Log the event if not root user
events.addEvent(
request.user.manager,
events.ET_LOGOUT,
username=request.user.name,
srcip=request.ip,
)
else: # No user, redirect to /
return HttpResponseRedirect(reverse('page.login'))
except Exception:
raise
finally:
# Try to delete session
request.session.flush()
request.authorized = False
if request.user:
authenticator = request.user.manager.getInstance()
username = request.user.name
exit_url = authenticator.logout(username) or exit_url
if request.user.id != ROOT_ID:
# Try yo invoke logout of auth
events.addEvent(
request.user.manager,
events.ET_LOGOUT,
username=request.user.name,
srcip=request.ip,
)
else: # No user, redirect to /
return HttpResponseRedirect(reverse('page.login'))
# Try to delete session
request.session.flush()
response = HttpResponseRedirect(request.build_absolute_uri(exit_url))
response = HttpResponseRedirect(exit_url)
if authenticator:
authenticator.webLogoutHook(username, request, response)
return response
@@ -484,7 +504,9 @@ def authLogLogin(
log.doLog(
user,
level,
'{} from {} where OS is {}'.format(logStr, request.ip, request.os['OS'].value[0]),
'{} from {} where OS is {}'.format(
logStr, request.ip, request.os['OS'].value[0]
),
log.WEB,
)
except Exception:

View File

@@ -290,6 +290,25 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
"""
return []
def mfaIdentifier(self, username: str) -> str:
"""
If this method is provided by an authenticator, the user will be allowed to enter a MFA code
You must return the value used by a MFA provider to identify the user (i.e. email, phone number, etc)
If not provided, or the return value is '', the user will be allowed to access UDS without MFA
Note: Field capture will be responsible of provider. Put it on MFA tab of user form.
Take into consideration that mfaIdentifier will never be invoked if the user has not been
previously authenticated. (that is, authenticate method has already been called)
"""
return ''
@classmethod
def providesMfa(cls) -> bool:
"""
Returns if this authenticator provides a MFA identifier
"""
return cls.mfaIdentifier is not Authenticator.mfaIdentifier
def authenticate(
self, username: str, credentials: str, groupsManager: 'GroupsManager'
) -> bool:

View File

@@ -37,27 +37,45 @@ class AuthenticatorException(Exception):
Generic authentication exception
"""
pass
class InvalidUserException(AuthenticatorException):
"""
Invalid user specified. The user cant access the requested service
"""
pass
class InvalidAuthenticatorException(AuthenticatorException):
"""
Invalida authenticator has been specified
"""
pass
class Redirect(Exception):
class Redirect(AuthenticatorException):
"""
This exception indicates that a redirect is required.
Used in authUrlCallback to indicate that redirect is needed
"""
pass
class Logout(Exception):
class Logout(AuthenticatorException):
"""
This exceptions redirects logouts an user and redirects to an url
"""
pass
class MFAError(AuthenticatorException):
"""
This exceptions indicates than an MFA error has ocurred
"""
pass

View File

@@ -30,6 +30,7 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
import logging
from uds.core.environment import Environmentable, Environment
@@ -43,11 +44,11 @@ class DelayedTask(Environmentable):
This is an object that represents an execution to be done "later"
"""
def __init__(self):
def __init__(self, environment: typing.Optional[Environment] = None) -> None:
"""
Remember to invoke parent init in derived clases using super(myClass,self).__init__() to let this initialize its own variables
"""
super().__init__(Environment('DelayedTask'))
super().__init__(environment or Environment.getEnvForType(self.__class__))
def execute(self) -> None:
"""

View File

@@ -139,6 +139,7 @@ class DelayedTaskRunner:
if taskInstance:
logger.debug('Executing delayedTask:>%s<', task)
# Re-create environment data
taskInstance.env = Environment.getEnvForType(taskInstance.__class__)
DelayedTaskThread(taskInstance).start()
@@ -146,7 +147,13 @@ class DelayedTaskRunner:
now = getSqlDatetime()
exec_time = now + timedelta(seconds=delay)
cls = instance.__class__
# Save "env" from delayed task, set it to None and restore it after save
env = instance.env
instance.env = None # type: ignore # clean env before saving pickle, save space (the env will be created again when executing)
instanceDump = codecs.encode(pickle.dumps(instance), 'base64').decode()
instance.env = env
typeName = str(cls.__module__ + '.' + cls.__name__)
logger.debug(

View File

@@ -33,11 +33,14 @@ import hashlib
import array
import uuid
import codecs
import datetime
import struct
import random
import re
import string
import logging
import typing
import secrets
import time
from cryptography import x509
@@ -61,6 +64,10 @@ if typing.TYPE_CHECKING:
class CryptoManager(metaclass=singleton.Singleton):
_rsa: 'RSAPrivateKey'
_namespace: uuid.UUID
_counter: int
def __init__(self):
self._rsa = serialization.load_pem_private_key(
settings.RSA_KEY.encode(), password=None, backend=default_backend()
@@ -91,7 +98,7 @@ class CryptoManager(metaclass=singleton.Singleton):
def encrypt(self, value: str) -> str:
return codecs.encode(
self._rsa.public_key().encrypt( # type: ignore
self._rsa.public_key().encrypt(
value.encode(),
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
@@ -107,7 +114,7 @@ class CryptoManager(metaclass=singleton.Singleton):
try:
# First, try new "cryptografy" decrpypting
decrypted: bytes = self._rsa.decrypt( # type: ignore
decrypted: bytes = self._rsa.decrypt(
data,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
@@ -128,9 +135,7 @@ class CryptoManager(metaclass=singleton.Singleton):
modes.CBC(b'udsinitvectoruds'),
backend=default_backend(),
)
rndStr = self.randomString(
16
).encode() # Same as block size of CBC (that is 16 here)
rndStr = secrets.token_bytes(16) # Same as block size of CBC (that is 16 here)
paddedLength = ((len(text) + 4 + 15) // 16) * 16
toEncode = (
struct.pack('>i', len(text)) + text + rndStr[: paddedLength - len(text) - 4]
@@ -139,7 +144,7 @@ class CryptoManager(metaclass=singleton.Singleton):
encoded = encryptor.update(toEncode) + encryptor.finalize()
if base64:
return codecs.encode(encoded, 'base64') # Return as binary
encoded = codecs.encode(encoded, 'base64').strip() # Return as bytes
return encoded
@@ -157,19 +162,25 @@ class CryptoManager(metaclass=singleton.Singleton):
toDecode = decryptor.update(text) + decryptor.finalize()
return toDecode[4 : 4 + struct.unpack('>i', toDecode[:4])[0]]
def xor(self, s1: typing.Union[str, bytes], s2: typing.Union[str, bytes]) -> bytes:
if not s2:
def xor(
self, value: typing.Union[str, bytes], key: typing.Union[str, bytes]
) -> bytes:
if not key:
return b'' # Protect against division by cero
if isinstance(s1, str):
s1 = s1.encode('utf-8')
if isinstance(s2, str):
s2 = s2.encode('utf-8')
mult = len(s1) // len(s2) + 1
s1a = array.array('B', s1)
s2a = array.array('B', s2 * mult)
# We must return bynary in xor, because result is in fact binary
return array.array('B', (s1a[i] ^ s2a[i] for i in range(len(s1a)))).tobytes()
if isinstance(value, str):
value = value.encode('utf-8')
if isinstance(key, str):
key = key.encode('utf-8')
mult = len(value) // len(key) + 1
value_array = array.array('B', value)
key_array = array.array(
'B', key * mult
) # Ensure key array is at least as long as value_array
# We must return binary in xor, because result is in fact binary
return array.array(
'B', (value_array[i] ^ key_array[i] for i in range(len(value_array)))
).tobytes()
def symCrypt(
self, text: typing.Union[str, bytes], key: typing.Union[str, bytes]
@@ -223,20 +234,30 @@ class CryptoManager(metaclass=singleton.Singleton):
raise Exception('Invalid certificate')
def certificateString(self, certificate: str) -> str:
return (
certificate.replace('-----BEGIN CERTIFICATE-----', '')
.replace('-----END CERTIFICATE-----', '')
.replace('\n', '')
)
# Remove -----.*-----\n strings using regex
return re.sub(r'(-----.*-----\n)', '', certificate)
def secret(self, length: int = 16) -> str:
"""
Get a random secret string from config.SECRET_KEY
"""
from django.conf import settings
return settings.SECRET_KEY[:length]
def salt(self, length: int = 16) -> str:
"""
Get a random salt random string
"""
return secrets.token_hex(length)
def hash(self, value: typing.Union[str, bytes]) -> str:
if isinstance(value, str):
value = value.encode()
if not value:
return ''
salt = self.salt(8) # 8 bytes = 16 chars
value = salt.encode() + value
return '{SHA256}' + str(hashlib.sha3_256(value).hexdigest())
return '{SHA256SALT}' + salt + str(hashlib.sha3_256(value).hexdigest())
def checkHash(self, value: typing.Union[str, bytes], hash: str) -> bool:
if isinstance(value, str):
@@ -246,9 +267,14 @@ class CryptoManager(metaclass=singleton.Singleton):
return not hash
if hash[:8] == '{SHA256}':
return str(hashlib.sha3_256(value).hexdigest()) == hash[8:]
return secrets.compare_digest(hashlib.sha3_256(value).hexdigest(), hash[8:])
elif hash[:12] == '{SHA256SALT}':
# Extract 16 chars salt and hash
salt = hash[12:28].encode()
value = salt + value
return secrets.compare_digest(hashlib.sha3_256(value).hexdigest(), hash[28:])
else: # Old sha1
return hash == str(hashlib.sha1(value).hexdigest())
return secrets.compare_digest(hash, str(hashlib.sha1(value).hexdigest())) # nosec: Old compatibility SHA1, not used anymore but need to be supported
def uuid(self, obj: typing.Any = None) -> str:
"""
@@ -269,4 +295,12 @@ class CryptoManager(metaclass=singleton.Singleton):
def randomString(self, length: int = 40, digits: bool = True) -> str:
base = string.ascii_letters + (string.digits if digits else '')
return ''.join(random.SystemRandom().choices(base, k=length))
return ''.join(secrets.choice(base) for _ in range(length))
def unique(self) -> str:
return hashlib.sha3_256(
(
self.randomString(24, True)
+ datetime.datetime.now().strftime('%H%M%S%f')
).encode()
).hexdigest()

View File

@@ -36,10 +36,13 @@ import typing
from uds.core.util.config import GlobalConfig
from uds.core.util import singleton
from uds.models import StatsCounters
from uds.models import StatsCounters, StatsCountersAccum
from uds.models import getSqlDatetime, getSqlDatetimeAsUnix
from uds.models import StatsEvents
if typing.TYPE_CHECKING:
from django.db import models
logger = logging.getLogger(__name__)
FLDS_EQUIV: typing.Mapping[str, typing.Iterable[str]] = {
@@ -70,7 +73,7 @@ class StatsManager(metaclass=singleton.Singleton):
def manager() -> 'StatsManager':
return StatsManager() # Singleton pattern will return always the same instance
def __doCleanup(self, model):
def __doCleanup(self, model: typing.Type[typing.Union['StatsCounters', 'StatsEvents']]) -> None:
minTime = time.mktime(
(
getSqlDatetime()
@@ -246,7 +249,7 @@ class StatsManager(metaclass=singleton.Singleton):
ownerType: typing.Union[int, typing.Iterable[int]],
eventType: typing.Union[int, typing.Iterable[int]],
**kwargs
):
) -> 'models.QuerySet[StatsEvents]':
"""
Retrieves counters from item
@@ -263,9 +266,25 @@ class StatsManager(metaclass=singleton.Singleton):
"""
return StatsEvents.get_stats(ownerType, eventType, **kwargs)
def tailEvents(
self,
*,
fromId: typing.Optional[str] = None,
number: typing.Optional[int] = None
) -> 'models.QuerySet[StatsEvents]':
# If number is not specified, we return five last events
number = number or 5
if fromId:
return StatsEvents.objects.filter(id__gt=fromId).order_by('-id')[:number] # type: ignore # Slicing is not supported by pylance right now
return StatsEvents.objects.order_by('-id')[:number] # type: ignore # Slicing is not supported by pylance right now
def cleanupEvents(self):
"""
Removes all events previous to configured max keep time for stat information from database.
"""
self.__doCleanup(StatsEvents)
def acummulate(self, max_days: int = 7):
for interval in StatsCountersAccum.IntervalType:
StatsCountersAccum.acummulate(interval, max_days)

View File

@@ -66,6 +66,9 @@ from uds.web.util.errors import MAX_SERVICES_REACHED
from .userservice import comms
from .userservice.opchecker import UserServiceOpChecker
if typing.TYPE_CHECKING:
from uds import models
logger = logging.getLogger(__name__)
traceLogger = logging.getLogger('traceLog')
@@ -80,14 +83,12 @@ class UserServiceManager(metaclass=singleton.Singleton):
UserServiceManager()
) # Singleton pattern will return always the same instance
@staticmethod
def getCacheStateFilter(servicePool: ServicePool, level: int) -> Q:
return Q(cache_level=level) & UserServiceManager.getStateFilter(servicePool)
def getCacheStateFilter(self, servicePool: ServicePool, level: int) -> Q:
return Q(cache_level=level) & self.getStateFilter(servicePool.service)
@staticmethod
def getStateFilter(servicePool: ServicePool) -> Q:
def getStateFilter(self, service: 'models.Service') -> Q:
if (
servicePool.service.getInstance().maxDeployed == services.Service.UNLIMITED
service.getInstance().maxDeployed == services.Service.UNLIMITED
and GlobalConfig.MAX_SERVICES_COUNT_NEW.getBool() is False
):
states = [State.PREPARING, State.USABLE]
@@ -95,23 +96,38 @@ class UserServiceManager(metaclass=singleton.Singleton):
states = [State.PREPARING, State.USABLE, State.REMOVING, State.REMOVABLE]
return Q(state__in=states)
def getExistingUserServices(self, service: 'models.Service') -> int:
"""
Returns the number of running user services for this service
"""
return UserService.objects.filter(
self.getStateFilter(service) & Q(deployed_service__service=service)
).count()
def maximumUserServicesDeployed(self, service: 'models.Service') -> bool:
"""
Checks if the maximum number of user services for this service has been reached
"""
serviceInstance = service.getInstance()
# Early return, so no database count is needed
if serviceInstance.maxDeployed == services.Service.UNLIMITED:
return False
if self.getExistingUserServices(service) >= serviceInstance.maxDeployed:
return True
return False
def __checkMaxDeployedReached(self, servicePool: ServicePool) -> None:
"""
Checks if maxDeployed for the service has been reached, and, if so,
raises an exception that no more services of this kind can be reached
"""
serviceInstance = servicePool.service.getInstance()
# Early return, so no database count is needed
if serviceInstance.maxDeployed == services.Service.UNLIMITED:
return
numberOfServices = servicePool.userServices.filter(
state__in=[State.PREPARING, State.USABLE]
).count()
if serviceInstance.maxDeployed <= numberOfServices:
if self.maximumUserServicesDeployed(servicePool.service):
raise MaxServicesReachedError(
'Max number of allowed deployments for service reached'
_('Maximum number of user services reached for this {}').format(
servicePool
)
)
def __createCacheAtDb(
@@ -183,7 +199,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
"""
Creates a new cache for the deployed service publication at level indicated
"""
logger.debug(
logger.info(
'Creating a new cache element at level %s for publication %s',
cacheLevel,
publication,
@@ -209,7 +225,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
if servicePool.service.getType().publicationType is not None:
publication = servicePool.activePublication()
logger.debug(
logger.info(
'Creating a new assigned element for user %s por publication %s',
user,
publication,
@@ -223,7 +239,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
)
)
else:
logger.debug('Creating a new assigned element for user %s', user)
logger.info('Creating a new assigned element for user %s', user)
assigned = self.__createAssignedAtDbForNoPublication(servicePool, user)
assignedInstance = assigned.getInstance()
@@ -307,7 +323,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
@return: the Uservice canceling
"""
userService.refresh_from_db()
logger.debug('Canceling userService %s creation', userService)
logger.info('Canceling userService %s creation', userService)
if userService.isPreparing() is False:
logger.info(
@@ -340,7 +356,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
"""
with transaction.atomic():
userService = UserService.objects.select_for_update().get(id=userService.id)
logger.debug('Removing userService %a', userService)
logger.info('Removing userService %a', userService)
if (
userService.isUsable() is False
and State.isRemovable(userService.state) is False
@@ -528,7 +544,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
if serviceType.usesCache:
inAssigned = (
servicePool.assignedUserServices()
.filter(UserServiceManager.getStateFilter(servicePool))
.filter(self.getStateFilter(servicePool.service))
.count()
)
if (
@@ -546,12 +562,14 @@ class UserServiceManager(metaclass=singleton.Singleton):
events.addEvent(servicePool, events.ET_CACHE_MISS, fld1=0)
return self.createAssignedFor(servicePool, user)
def getServicesInStateForProvider(self, provider_id: int, state: str) -> int:
def getUserServicesInStatesForProvider(
self, provider: 'models.Provider', states: typing.List[str]
) -> int:
"""
Returns the number of services of a service provider in the state indicated
"""
return UserService.objects.filter(
deployed_service__service__provider__id=provider_id, state=state
deployed_service__service__provider=provider, state__in=states
).count()
def canRemoveServiceFromDeployedService(self, servicePool: ServicePool) -> bool:
@@ -559,8 +577,8 @@ class UserServiceManager(metaclass=singleton.Singleton):
checks if we can do a "remove" from a deployed service
serviceIsntance is just a helper, so if we already have unserialized deployedService
"""
removing = self.getServicesInStateForProvider(
servicePool.service.provider.id, State.REMOVING
removing = self.getUserServicesInStatesForProvider(
servicePool.service.provider, [State.REMOVING]
)
serviceInstance = servicePool.service.getInstance()
if (
@@ -574,12 +592,12 @@ class UserServiceManager(metaclass=singleton.Singleton):
"""
Checks if we can start a new service
"""
preparing = self.getServicesInStateForProvider(
servicePool.service.provider.id, State.PREPARING
preparingForProvider = self.getUserServicesInStatesForProvider(
servicePool.service.provider, [State.PREPARING]
)
serviceInstance = servicePool.service.getInstance()
if (
preparing >= serviceInstance.parent().getMaxPreparingServices()
if self.maximumUserServicesDeployed(servicePool.service) or (
preparingForProvider >= serviceInstance.parent().getMaxPreparingServices()
and serviceInstance.parent().getIgnoreLimits() is False
):
return False
@@ -918,7 +936,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
return metaId[0] == 'M'
def locateMetaService(
self, user: User, idService: str, create: bool = False
self, user: User, idService: str
) -> typing.Optional[UserService]:
kind, uuidMetapool = idService[0], idService[1:]
if kind != 'M':
@@ -1002,12 +1020,12 @@ class UserServiceManager(metaclass=singleton.Singleton):
found = None
t: Transport
if idTransport == 'meta': # Autoselected:
q = pool.transports.all().order_by('priority')
q = pool.transports.all()
elif idTransport[:6] == 'LABEL:':
q = pool.transports.filter(label=idTransport[6:])
else:
q = pool.transports.filter(uuid=idTransport)
for t in q:
for t in q.order_by('priority'):
typeTrans = t.getType()
if (
typeTrans

View File

@@ -35,7 +35,7 @@ import tempfile
import logging
import typing
import requests
from uds.core.util.security import secureRequestsSession
if typing.TYPE_CHECKING:
from uds.models import UserService
@@ -90,8 +90,8 @@ def _requestActor(
r = proxy.doProxyRequest(url=url, data=data, timeout=TIMEOUT)
else:
verify: typing.Union[bool, str]
cert = userService.getProperty('cert')
# cert = '' # Untils more tests, keep as previous.... TODO: Fix this when fully tested
cert = userService.getProperty('cert') or ''
# cert = '' # Uncomment to test without cert
if cert:
# Generate temp file, and delete it after
verify = tempfile.mktemp('udscrt')
@@ -99,10 +99,11 @@ def _requestActor(
f.write(cert.encode()) # Save cert
else:
verify = False
session = secureRequestsSession(verify=cert)
if data is None:
r = requests.get(url, verify=verify, timeout=TIMEOUT)
r = session.get(url, verify=verify, timeout=TIMEOUT)
else:
r = requests.post(
r = session.post(
url,
data=json.dumps(data),
headers={'content-type': 'application/json'},
@@ -136,11 +137,18 @@ def notifyPreconnect(userService: 'UserService', userName: str, protocol: str) -
Notifies a preconnect to an user service
"""
ip, hostname = userService.getConnectionSource()
try:
_requestActor(
userService,
'preConnect',
{'user': userName, 'protocol': protocol, 'ip': ip, 'hostname': hostname},
{
'user': userName,
'protocol': protocol,
'ip': ip,
'hostname': hostname,
'udsuser': userService.user.name + '@' + userService.user.manager.name if userService.user else '',
},
)
except NoActorComms:
pass # If no preconnect, warning will appear on UDS log

Some files were not shown because too many files have changed in this diff Show More