Compare commits

..

103 Commits
server ... v3.6

Author SHA1 Message Date
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
6692e5ce6d Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-09-02 16:45:25 +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
4f0ea76666 Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-08-17 15:06:49 +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
11041ff44f Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-08-14 21:52:40 +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
8a150439ae Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-08-06 19:29:43 +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
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
d1329849f3 Merge remote-tracking branch 'origin/v3.5' into v3.6 2022-07-14 12:49:29 +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
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
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
171 changed files with 24149 additions and 13893 deletions

View File

@@ -1,43 +0,0 @@
<IfModule ssl_module>
#Listen 443
<VirtualHost *:443>
SSLEngine On
SSLCertificateFile /etc/httpd2/ssl.crt/openuds-server.crt
SSLCertificateKeyFile /etc/httpd2/ssl.key/openuds-server.key
ServerName openuds.example.com
ServerAdmin webmaster@openuds.example.com
DocumentRoot /usr/share/openuds
Alias /favicon.ico /usr/share/openuds/uds/static/modern/img/favicon.ico
Alias /static/ /usr/share/openuds/uds/static/
Alias /uds/res/ /usr/share/openuds/uds/static/
LogLevel warn
ErrorLog /var/log/openuds/error.log
# CustomLog /var/log/openuds/access.log combined
WSGIScriptReloading On
WSGIDaemonProcess openuds processes=2 threads=10 python-path=/usr/share/openuds user=openuds group=openuds display-name=%{GROUP}
WSGIProcessGroup openuds
WSGIApplicationGroup openuds
WSGIPassAuthorization On
WSGIScriptAlias / /usr/share/openuds/server/wsgi.py
<Directory /usr/share/openuds/uds>
Require all granted
</Directory>
<Directory /usr/share/openuds/server>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
</VirtualHost>
</IfModule>

View File

@@ -1,34 +0,0 @@
#Listen 443
<VirtualHost *:80>
DocumentRoot /usr/share/openuds
Alias /favicon.ico /usr/share/openuds/uds/static/modern/img/favicon.ico
Alias /static/ /usr/share/openuds/uds/static/
Alias /uds/res/ /usr/share/openuds/uds/static/
LogLevel warn
ErrorLog /var/log/openuds/error.log
# CustomLog /var/log/openuds/access.log combined
WSGIScriptReloading On
WSGIDaemonProcess openuds processes=2 threads=10 python-path=/usr/share/openuds user=openuds group=openuds display-name=%{GROUP}
WSGIProcessGroup openuds
WSGIApplicationGroup openuds
WSGIPassAuthorization On
WSGIScriptAlias / /usr/share/openuds/server/wsgi.py
<Directory /usr/share/openuds/uds>
Require all granted
</Directory>
<Directory /usr/share/openuds/server>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
</VirtualHost>

View File

@@ -1,79 +0,0 @@
upstream uds_server {
server unix:/run/openuds/socket fail_timeout=10s;
}
map $http_x_forwarded_proto $thescheme {
default $scheme;
https https;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
# SSL configuration
#
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
#resolver $DNS-IP-1 $DNS-IP-2 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
ssl_certificate /var/lib/ssl/certs/nginx-openuds.cert;
ssl_certificate_key /var/lib/ssl/private/nginx-openuds.key;
root /usr/share/openuds/;
# Add index.php to the list if you are using PHP
index index.html;
server_name _;
# Activate GZIP
# In our app, saves around 80% or the traffic.
#
gzip on;
gzip_proxied any;
# text/html is always included
gzip_types
text/css
text/javascript
text/xml
text/plain
application/javascript
application/x-javascript
application/json;
location /favicon.ico {
alias /usr/share/openuds/uds/static/modern/img/favicon.ico;
}
location /uds/res/ {
autoindex off;
alias /usr/share/openuds/uds/static/;
}
location / {
# First attempt to server /maintenance (to allow easy backend maintenance) if exists
# if not, fallback to UDS
try_files /maintenance.html @proxy_to_uds;
}
location @proxy_to_uds {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $thescheme;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://uds_server;
}
}

View File

@@ -1,218 +0,0 @@
%add_python3_lib_path %_datadir/openuds
%allow_python3_import_path %_datadir/openuds
%add_findreq_skiplist %_datadir/openuds/uds/transports/*/scripts/windows/* %_datadir/openuds/uds/transports/*/scripts/macosx/*
%add_python3_req_skip uds.forward
%add_python3_req_skip uds.tunnel
%filter_from_provides /^python3(manage)/d
%filter_from_provides /^python3(server)/d
%filter_from_provides /^python3(server\.settings)/d
%filter_from_provides /^python3(server\.urls)/d
%filter_from_provides /^python3(server\.wsgi)/d
Name: openuds-server
Version: 3.5.0
Release: alt2
Summary: Universal Desktop Services (UDS) Broker
License: BSD-3-Clause and MIT and Apache-2.0
Group: Networking/Remote access
URL: https://github.com/dkmstr/openuds
AutoReqProv: yes, nopython
Source0: %name-%version.tar
Source10: openuds-httpd.conf
Source11: openuds-httpd-ssl.conf
Source12: openuds.logrotate
Source13: openuds-nginx-sites.conf
Source15: openuds-taskmanager.service
Source16: openuds-web.service
Source17: openuds-web.socket
#Patch: %name-%version.patch
BuildRequires(pre): rpm-macros-systemd
Requires: python3-module-django >= 2.2
Requires: python3-module-django-dbbackend-mysql >= 2.2
Requires: python3-module-django-dbbackend-sqlite3 >= 2.2
Requires: openssl
Requires: logrotate
Requires: openuds-installers
Conflicts: openuds-tunnel openuds-guacamole-tunnel
BuildArch: noarch
BuildRequires(pre): rpm-build-python3
BuildRequires(pre): webserver-common rpm-build-webserver-common rpm-macros-apache2
BuildRequires: python3-module-django
%description
OpenUDS (Universal Desktop Services) is a multiplatform connection broker for:
- VDI: Windows and Linux virtual desktops administration and deployment
- App virtualization
- Desktop services consolidation
This package provides the required components
to allow this machine to work as UDS Broker.
%package apache2
Group: Networking/WWW
BuildArch: noarch
Summary: apache2 configs for %name
Requires: %name = %version-%release
Requires: apache2-httpd-prefork-like
Requires: apache2-base
Requires: apache2-mod_wsgi-py3
%description apache2
%summary
%package nginx
Group: Networking/WWW
BuildArch: noarch
Summary: nginx configs for %name
Requires: %name = %version-%release
Requires: nginx
Requires: python3-module-gunicorn
Requires: cert-sh-functions
%description nginx
%summary
%prep
%setup
#%patch -p1
sed -i 's|#!/usr/bin/env python3|#!/usr/bin/python3|' \
$(find . -name '*.py')
%build
# Compile localization files
django-admin compilemessages
#find src/uds/locale -name \*.po -delete
%install
mkdir -p %buildroot{%_datadir,%_logdir,%_sysconfdir,%_sharedstatedir}/openuds
cp -r src/* %buildroot%_datadir/openuds/
mkdir -p %buildroot%_datadir/openuds/uds/static/clients
mkdir -p %buildroot%_datadir/openuds/uds/osmanagers/WindowsOsManager/files
mv %buildroot%_datadir/openuds/server/settings.py.sample %buildroot%_sysconfdir/openuds/settings.py
ln -r -s %buildroot%_logdir/openuds %buildroot%_datadir/openuds/log
ln -r -s %buildroot%_sysconfdir/openuds/settings.py %buildroot%_datadir/openuds/server/settings.py
# drop httpd-conf snippet
install -p -D -m 644 %SOURCE10 %buildroot%apache2_sites_available/openuds.conf
install -p -D -m 644 %SOURCE11 %buildroot%apache2_sites_available/openuds-ssl.conf
mkdir -p %buildroot%apache2_sites_enabled
touch %buildroot%apache2_sites_enabled/openuds.conf
install -p -D -m 644 %SOURCE12 %buildroot%_logrotatedir/openuds-server
install -p -D -m 644 %SOURCE13 %buildroot%_sysconfdir/nginx/sites-available.d/openuds.conf
mkdir -p %buildroot%_sysconfdir/nginx/sites-enabled.d
touch %buildroot%_sysconfdir/nginx/sites-enabled.d/openuds.conf
install -p -D -m 644 %SOURCE15 %buildroot%_unitdir/openuds-taskmanager.service
install -p -D -m 644 %SOURCE16 %buildroot%_unitdir/openuds-web.service
install -p -D -m 644 %SOURCE17 %buildroot%_unitdir/openuds-web.socket
%pre
%_sbindir/groupadd -r -f openuds >/dev/null 2>&1 ||:
%_sbindir/useradd -M -r -g openuds -G _webserver -c 'OpenUDS Brocker Daemon' \
-s /bin/false -d %_sharedstatedir/openuds openuds >/dev/null 2>&1 ||:
%post
if [ $1 -eq 1 ]; then
# ugly hack to set a unique SECRET_KEY
sed -i "/^SECRET_KEY.*$/{N;s/^.*$/SECRET_KEY='`openssl rand -hex 10`'/}" %_sysconfdir/openuds/settings.py
fi
%post_systemd_postponed openuds-taskmanager.service
%preun
%preun_systemd openuds-taskmanager.service
%post nginx
%post_systemd_postponed openuds-web.socket openuds-web.service
# Create SSL certificate for HTTPS server
cert-sh generate nginx-openuds ||:
%preun nginx
%preun_systemd openuds-web.service openuds-web.socket
%files
%_datadir/openuds
%dir %attr(0750, root, openuds) %_sysconfdir/openuds
%config(noreplace) %attr(0640, root, openuds) %_sysconfdir/openuds/settings.py
%dir %attr(0770, root, openuds) %_sharedstatedir/openuds
%dir %attr(0770, root, openuds) %_logdir/openuds
%config(noreplace) %_logrotatedir/openuds-server
%_unitdir/openuds-taskmanager.service
%files apache2
%config(noreplace) %apache2_sites_available/*.conf
%ghost %apache2_sites_enabled/*.conf
%files nginx
%config(noreplace) %_sysconfdir/nginx/sites-available.d/openuds.conf
%ghost %_sysconfdir/nginx/sites-enabled.d/openuds.conf
%_unitdir/openuds-web.service
%_unitdir/openuds-web.socket
%changelog
* Tue Oct 04 2022 Alexey Shabalin <shaba@altlinux.org> 3.5.0-alt2
- Build release-v3.5
* Mon Aug 22 2022 Alexey Shabalin <shaba@altlinux.org> 3.5.0-alt1
- v3.5 snapshot 83394f0d34daf18722923be8d57b35627b330121
* Mon Nov 29 2021 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt13
- Add link for download python 3.9 client.
* Thu Oct 28 2021 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt12
- Switch to use macros from rpm-build-systemd for post scripts.
* Wed Oct 27 2021 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt11
- Add requires openuds-installers (client and actor windows installers).
- Revert "Remove download pages".
- Fix client and actor file name on download page.
* Mon Sep 06 2021 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt10
- Updated RSA key to 4096 bit in config.
* Wed Aug 18 2021 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt9
- v3.0 snapshot 51b0cec5365698dffdb9a3a468d52bbba4656ba4
* Fri Jul 09 2021 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt8
- Fix Russian translation
- Update SECRET_KEY config for install only in %%post
* Wed Jun 23 2021 Andrey Cherepanov <cas@altlinux.org> 3.0.0-alt7.2
- Compile l10n messages using django-admin
- Add Russian language to server config file
* Sat Jun 05 2021 Andrey Cherepanov <cas@altlinux.org> 3.0.0-alt7.1
- NMU: package compiled localization files (ALT #40161)
* Fri Apr 23 2021 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt7
- Fix create home dir for user openuds
* Thu Apr 22 2021 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt6
- Switch to local memory from memcached by default in settings.py.
- Fix openuds-web.service for execute gunicorn.py3 for use python3.
- Add conflicts with openuds-tunnel,openuds-guacamole-tunnel.
* Wed Apr 21 2021 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt5
- Fix typo in nginx config (ALT #39968)
* Wed Apr 14 2021 Mikhail Gordeev <obirvalger@altlinux.org> 3.0.0-alt4
- Remove pages and buttons with downloading clients and actors
* Mon Dec 07 2020 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt3
- merge with upstream v3.0 branch (b1c43850908c5c207afa5812edc6c1ce46d8ca78)
- update nginx config
* Thu Dec 03 2020 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt2
- move apache config to apache2 package
- add package with nginx config and service for start django app over gunicorn
* Thu Nov 05 2020 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt1
- 3.0.0 Release
* Tue Apr 14 2020 Alexey Shabalin <shaba@altlinux.org> 3.0.0-alt0.1.git.d7e30d14
- Initial build for ALT

View File

@@ -1,16 +0,0 @@
[Unit]
Description=OpenUDS Broker task manager
After=network.target
[Service]
User=openuds
Group=openuds
RuntimeDirectory=openuds
WorkingDirectory=/usr/share/openuds
ExecStart=/usr/bin/python3 /usr/share/openuds/manage.py taskManager --start --foreground
PrivateTmp=true
Restart=always
RestartSec=16
[Install]
WantedBy=multi-user.target

View File

@@ -1,21 +0,0 @@
[Unit]
Description=OpenUDS Broker Web server daemon
Requires=openuds-web.socket
After=network.target
[Service]
PIDFile=/run/openuds/pid
User=openuds
Group=openuds
RuntimeDirectory=openuds
WorkingDirectory=/usr/share/openuds
ExecStart=/usr/bin/gunicorn.py3 --pid /run/openuds/pid \
--bind unix:/run/openuds/socket server.wsgi \
--workers 5 --threads 8
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Also=openuds-web.socket

View File

@@ -1,10 +0,0 @@
[Unit]
Description=OpenUDS Broker Web server socket
[Socket]
ListenStream=/run/openuds/socket
SocketUser=openuds
SocketGroup=_webserver
[Install]
WantedBy=sockets.target

View File

@@ -1,8 +0,0 @@
/var/log/openuds/*.log {
weekly
rotate 4
missingok
compress
minsize 100k
}

View File

@@ -1,106 +0,0 @@
#!/usr/bin/python3
# Copyright (C) 2022
# Alexander Burmatov
# 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 the Alexander Burmatov 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
# OWNER 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: Alexander Burmatov, thatman at altlinux dot org
'''
import argparse
import socket
import os
import sys
import secrets
import MySQLdb
import datetime
sys.path.append('/etc/openuds/')
from settings import DATABASES
ip_addr = socket.gethostbyname(socket.gethostname())
creation_datetime = datetime.datetime.today()
parser = argparse.ArgumentParser(description='Register tunnel token in MySQL DB')
parser.add_argument(
'-H',
'--host',
type=str,
default='',
help='Input tunnel server IP Address'
)
parser.add_argument(
'-n',
'--name',
type=str,
default='',
help='Input tunnel server name'
)
parser.add_argument(
'-t',
'--token',
type=str,
default='',
help='Input tunnel server token (default: "")'
)
parser.add_argument(
'-N',
'--generate_new_token',
type=bool,
default=False,
help='Input True if you want to generate a new token (default: False)'
)
args = parser.parse_args()
empty_name = args.name == ''
empty_ip = args.host == ''
only_token = args.token != '' and not args.generate_new_token
only_gen_new_token = args.token == '' and args.generate_new_token
if empty_ip:
print('Empty tunnel server IP Address')
elif empty_name:
print('Empty tunnel server name')
elif args.token == '' and not args.generate_new_token:
print('Choose to generate a new token or enter a token')
elif only_token != only_gen_new_token:
if only_gen_new_token:
token = secrets.token_urlsafe(36)
else:
token = args.token
db=MySQLdb.connect(host=DATABASES['default']['HOST'], user=DATABASES['default']['USER'],
passwd=DATABASES['default']['PASSWORD'], db=DATABASES['default']['NAME'])
c=db.cursor()
c.execute("""INSERT INTO uds_tunneltoken(username, ip_from, ip, hostname, token, stamp) VALUES (%s,%s,%s,%s,%s,%s);""",
(os.getlogin(), ip_addr, args.host, args.name, token, creation_datetime,))
db.commit()
c.close()
print(f'Tunnel token register success. (With token: {token})')
else:
print('Choose to generate a new token only or only enter the token')

View File

@@ -1,7 +0,0 @@
tar: server name=@name@-@version@ base=@name@-@version@
spec: .gear/openuds-server.spec
copy?: .gear/*.logrotate
copy?: .gear/*.conf
copy?: .gear/*.service
copy?: .gear/*.socket
copy?: .gear/*.patch

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

@@ -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

@@ -214,10 +214,10 @@
<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>
@@ -268,10 +268,10 @@
<item row="3" column="1">
<widget class="QLineEdit" name="restrictNet">
<property name="toolTip">
<string>UDS user with administration rights (Will not be stored on template)</string>
<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;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;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>

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

@@ -71,7 +71,7 @@ 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

View File

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

View File

@@ -146,7 +146,7 @@ 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.setToolTip(_translate("UdsActorSetupDialog", "UDS Service Token"))
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.label_loglevel.setText(_translate("UdsActorSetupDialog", "Log Level"))
self.label_restrictNet.setText(_translate("UdsActorSetupDialog", "Restrict Net"))

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,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

@@ -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

@@ -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,

View File

@@ -59,9 +59,8 @@ LANGUAGE_CODE = 'en'
ugettext = lambda s: s
LANGUAGES = (
('ru', ugettext('Russian')),
('en', ugettext('English')),
('es', ugettext('Spanish')),
('en', ugettext('English')),
('fr', ugettext('French')),
('de', ugettext('German')),
('pt', ugettext('Portuguese')),
@@ -131,13 +130,13 @@ CACHES = {
'CULL_FREQUENCY': 3, # 0 = Entire cache will be erased once MAX_ENTRIES is reached, this is faster on DB. if other value, will remove 1/this number items fromm cache
},
},
'memory': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
#'memory': {
# 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
# 'LOCATION': '127.0.0.1:11211',
#},
# 'memory': {
# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
# }
'memory': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
},
}
# Related to file uploading
@@ -148,58 +147,7 @@ FILE_UPLOAD_MAX_MEMORY_SIZE = 512 * 1024 # 512 Kb
# Make this unique, and don't share it with anybody.
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-----
MIIJKAIBAAKCAgEAu6Zr65Q6Y81W1j/tD0GrpqOmwiLhifbeY/xYbUgq/4555wrR
nb2Z2oynF6aaSbWaaCsCJSoziGAItYUaY58vlx1RPnrr3Kb/c9zsCnWINy++LkrZ
NzwrVzo1XyzKjMlCwXRYOCrXdyzuRhhw32gzARzHbBWtHvMQ8vOxWDM1yjkSFZ+i
CDEXruKQ8HrcZvGvKoqMWZeihi+g+RlZvaWimM9wkbfzXsTljIwYwb8YhNLfzad6
VEymnBeX6COlig2hOup+7HHcuuYAgURm0cvkwPKwBt9tbZ0C3hB04HH9bIJ92DLI
eI0ljICHI2BNyafQO74MngAJYc2O0Kak4MSIeqCVf/i5W77rSsDF3rzsNzCnD2lw
S8hIl0pp8n8kNLalyAIidnxivLFnCiz47F+We1HAVx2WcQ7x6BUQjoUPV2Ao9Nk2
pwdXGjyHAq5cbDKQNd+Z09ohYnRnLQ7bPQqsIPUuAr/xsp1LugOQxXRrsGNKIhb9
gIY6Xuk19VJN2HDCPb3E79fE0MRjFrPbD57tWxjZvaJ589f4HybSAFr5EDWcoq+K
jCCEp3rl1iRNmBt6ga7MMpuWOleOIFgzas4Pqcir5Ir9JuCnTBl4WS9536p1rSRR
KgwgWJuCmereRqznV79PXBmjWP7PzZHQKEgqQTLGx8yzSdlfr+Xn5/QY6CsCAwEA
AQKCAgBajzpN+r8jIxnhVACH+F1qklgMIOFPv8Ab6NCUDNwTcSMLiYEX60Sw4GhT
VafoIqhd1UO3w+IS0qXhVFcj9NL2zsNCr/fcCQwHUnrnoUkXuQbDOIQT0AnqksDn
Kaqmvwpkak2Y7bQGY7yqP2lZp+PViZoEY4GlChEH7uuLcUtoSJqdrOh5o5eWYukn
5WMwmzq6ARsysadRsvKwxLc2exU5QgNFj8z303YkmgfomNywXUR873Jt9IADnK9G
7M0bDvDcigau/VRVLmLX/7bPUOMY6wZgirs4xyl3GeIN33U6RM+O4xM0eonAgNa3
D+b6hu5LprBYIBRnCtj9gS7Y+oASONDUA2/dA81SWGxUGwtRhSMXlypM4qILMm+I
UwpJdbnbPFo31N10B9Y/VDk2cfdnuDbkQif8DkyHxGEmyEOVWaI4Uibwg5TNX2+N
fQOZveEXbLlsiTLMa8vxc07w/nUJ7TbZ2vxGzO5XVZmZbwoB9XE5VSTVaV660P6q
B5T6m/KRMCcAX5mWstyWdjjbGB/IS3O7jxjl2EzbNp8SES8WY/UjEc82rwq9cjon
3sibMwwJf1j5HSvqOBDfU/uWWleAkSgENOeC0oym38TxeWY3YD19f88KDeKNa/qJ
ko/2/tHrworxYHk6X0/XHBF9Ylwm6FHRCcrjhGRpoOpq99WjYQKCAQEA+YDUUU1h
3aD1jvkFAhVoIL/fi2yqiUx8O0jP2f6k5RV8AaiTqZkZQs9LUy3xfZkbXf/TeUar
ONYd0iPiPM5cyx1tZW6rO32HY3P9AZSRpB1KlEHrFqVq5N9zuyUbiQU7iLUQ1qVG
+5eafPfifCi4pZQ14+LuxvLCiCOaGBa/XidjR82yAjZKESwJsTyS1AmLwlHQuAXr
3TxBh8/21+RXPnV4sqCuHFiYBQYyZIODRxxoo0Maz7dXP4F/7LaJ4rNAr5VZu4xe
UgFH5Y1lhI8orUlMN5QKUcbAdQb2+/DEz8uiiRaxdGA2fZiP366VQf2Hjg/syM9S
ePfZEAF0EFZqtwKCAQEAwIlIlIPVdZZuqR4s+X32p50i5f7vVI5eCryNTe+eY0uW
IDm/N4AKJtxTTw5E1klyFgR36cHI1DGQCpUJczK4XNfXMTiqBomg7gOrFMwSns70
YJuZGLRJ8AJCJ9aaGN2Tp7cEnCkl6mEbpB3tz4lg1tdgmxyOVG/BjKuy4mskM+DX
hmELFFGTQimamgyGLHs7F8DBBd9RoECc32LWQhebgrs6o09EHPX85qTLd6alQ2y6
QIQBkZ4Cf8pjtQ5DEaWv5ll8TFnUroLYGk8+Z94tylXe/OPlUD9oT1efElXHawGo
B+a4/jYg+otmZMd1h8wakxA0DaszLntYhklMeCsKLQKCAQEAkE2CfsNSpuxBGb/M
tbfL0aUnlWvz9hTWhTNHg71TgWs7nXnybVSu14Us+57G3O2Ado1PsgS9YtHzzWYT
ozd3U8JhUxj+0Bl6BHtBCXj0Awct6zF01lQ1zsmuFb8Qfd8Y36hZZMuSklDMeRql
U0n6AUoHIRZPI6GkATkWFniRldMSCKVfHwhnLidaM7fk893Rh0HqhYRnNj1zaSlO
iu71xpoKhMWJ7bsPsVg0LQ4jDy3PFx3ok9pmC8TKSA4LEaK69tJaY657ntI+0vVE
kbZ6wD50ZnCox0M8bHLqxiwqqEQObVtCpxw+Z8Wk8Kn4iYIotHFhcYL3IB+42xj8
F7bjYQKCAQAtFXhrXltzh2AuNaSuXzKMmRz84i9Ei3m15eTopP1fnulublc6Nb8q
zliroFm6G8SdJzq0/+140oo6ECAAW5YUF26KVgxqL3wBf9ZlrkuF6EwM+yJkSIMv
sjevgG7g97GFijOIJZJ9SXPhgCiZej+0zwYODCe/2dNmjyX6IsB9bV7KVprzjQ6A
ZaGQBPK+I7T4oOgR6fMBJWAWZtxo6YI+oHzglMUMSwWHNHt4bwsvuJv8U2zSnj9T
kR673LUTwspnk/ipIDfPDVBxCLFCPSJXyLfIW8zsd4yDV68l6fQiOGcSQpJ65E3g
nRfC7Xm17LMkUJz/vmDjt3pJJ4zCbsGZAoIBABfQCRUEqniropccsF09FiXDvKF8
e5jN1HybXltnjlGlas4IJdVaGMPdJC3zZGpdnbGK8y8YsXwWlEgTQuUqBgjRdmt3
toRDTVSKc+4g2P7Vv2hG2Q2ofayXiyez1h9wwcdUyn+oeSvOTJuxL4P4Gh0psfsS
jyoGbNL4P5ONJaknVxsv/zcbHW3BrawbpBo4Pao7hWCnyJd5f33uOW/9eAfa5gXq
fsHnIw6EsZLMJmZgQcx6be4ZkMX00FR4+uxqouVbgdWIhjDxTdFXD4mPoa1XExTd
7NIOhKdva4tz0Re3b5qWwNnsHLitx+YJor1JjlZ8wwlZ7LC2d/wcxQtQIpI=
-----END RSA PRIVATE KEY-----'''
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-----'
TEMPLATES = [
{

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

@@ -85,7 +85,7 @@ def checkBlockedIp(ip: str) -> None:
def incFailedIp(ip: str) -> None:
cache = Cache('actorv3')
fails = (cache.get(ip) or 0) + 1
fails = cache.get(ip, 0) + 1
cache.put(ip, fails, GlobalConfig.LOGIN_BLOCK.getInt())
@@ -114,6 +114,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]:
@@ -121,13 +122,13 @@ class ActorV3Action(Handler):
def post(self) -> typing.MutableMapping[str, typing.Any]:
try:
checkBlockedIp(self._request.ip) # pylint: disable=protected-access
checkBlockedIp(self._request.ip)
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.ip)
except Exception as e:
logger.exception('Posting %s: %s', self.__class__, e)
@@ -182,6 +183,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,
@@ -454,8 +456,10 @@ class LoginLogout(ActorV3Action):
else:
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()

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,
@@ -34,12 +34,13 @@ 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 +59,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 +71,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 +89,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 +109,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)
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 +153,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 +210,24 @@ 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
return
except MFA.DoesNotExist:
pass # will set field to null
fields['mfa_id'] = None
def deleteItem(self, item: Authenticator):
# For every user, remove assigned services (mark them for removal)

View File

@@ -46,6 +46,8 @@ 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__)
@@ -53,6 +55,7 @@ CLIENT_VERSION = UDS_VERSION
REQUIRED_CLIENT_VERSION = '3.5.0'
# Enclosed methods under /client path
class Client(Handler):
"""
@@ -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,
@@ -180,9 +184,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 +219,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 +229,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,27 +39,6 @@ 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
@@ -67,30 +46,8 @@ class Config(Handler):
def get(self):
cfg: CfgConfig.Value
res: typing.Dict[str, typing.Dict[str, typing.Any]] = {}
addCrypt = self.is_admin()
return CfgConfig.getConfigValues(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(),
}
logger.debug('Configuration: %s', res)
return res
def put(self):
for section, secDict in self._params.items():

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

@@ -80,21 +80,21 @@ class Users(DetailHandler):
custom_methods = ['servicesPools', 'userServices']
@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
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': {
@@ -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))
@@ -414,7 +419,7 @@ class Groups(DetailHandler):
except Exception:
raise self.invalidRequestException()
def saveItem(self, parent: Authenticator, item) -> None:
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'
@@ -429,7 +434,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
@@ -482,7 +487,9 @@ class Groups(DetailHandler):
except Exception:
raise self.invalidItemException()
def servicesPools(self, parent: Authenticator, item: str) -> typing.List[typing.Mapping[str, typing.Any]]:
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: typing.List[typing.Mapping[str, typing.Any]] = []
@@ -503,7 +510,9 @@ class Groups(DetailHandler):
return res
def users(self, parent: Authenticator, item: str) -> typing.List[typing.Mapping[str, typing.Any]]:
def users(
self, parent: Authenticator, item: str
) -> typing.List[typing.Mapping[str, typing.Any]]:
uuid = processUuid(item)
group = parent.groups.get(uuid=processUuid(uuid))

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

@@ -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

View File

@@ -99,12 +99,18 @@ 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, state=State.ACTIVE).mfa_data
finally:
return ''
def transformUsername(self, username: str) -> str:
if self.differentForEachHost.isTrue():
newUsername = self.getIp() + '-' + 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.
#
@@ -176,6 +176,16 @@ class RegexLdap(auths.Authenticator):
tab=_('Advanced'),
)
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 = _('Regex LDAP Authenticator')
typeType = 'RegexLdapAuthenticator'
typeDescription = _('Regular Expressions LDAP authenticator')
@@ -205,6 +215,7 @@ class RegexLdap(auths.Authenticator):
_groupNameAttr: str = ''
_userNameAttr: str = ''
_altClass: str = ''
_mfaAttr: str = ''
def __init__(
self,
@@ -231,6 +242,7 @@ class RegexLdap(auths.Authenticator):
# self._regex = values['regex']
self._userNameAttr = values['userNameAttr']
self._altClass = values['altClass']
self._mfaAttr = values['mfaAttr']
def __validateField(self, field: str, fieldLabel: str) -> None:
"""
@@ -296,6 +308,12 @@ class RegexLdap(auths.Authenticator):
logger.debug('Res: %s', res)
return res
def mfaStorageKey(self, username: str) -> str:
return 'mfa_' + self.dbAuthenticator().uuid + username
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 +328,13 @@ class RegexLdap(auths.Authenticator):
'groupNameAttr': self._groupNameAttr,
'userNameAttr': self._userNameAttr,
'altClass': self._altClass,
'mfaAttr': self._mfaAttr,
}
def marshal(self) -> bytes:
return '\t'.join(
[
'v3',
'v4',
self._host,
self._port,
gui.boolToStr(self._ssl),
@@ -328,6 +347,7 @@ class RegexLdap(auths.Authenticator):
self._groupNameAttr,
self._userNameAttr,
self._altClass,
self._mfaAttr,
]
).encode('utf8')
@@ -385,6 +405,24 @@ class RegexLdap(auths.Authenticator):
self._altClass,
) = vals[1:]
self._ssl = gui.strToBool(ssl)
elif vals[0] == 'v4':
logger.debug("Data v4: %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,
self._mfaAttr,
) = vals[1:]
self._ssl = gui.strToBool(ssl)
def __connection(self) -> typing.Any:
"""
@@ -428,6 +466,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 +558,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

@@ -40,5 +40,5 @@ from .serializable import Serializable
from .module import Module
VERSION = '3.5.0'
VERSION_STAMP = '{}'.format(time.strftime("%Y%m%d"))
VERSION = '3.x.x-DEVEL'
VERSION_STAMP = '{}-DEVEL'.format(time.strftime("%Y%m%d"))

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
@@ -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)
@@ -372,6 +387,13 @@ def webLogin(
cookie = getUDSCookie(request, response)
user.updateLastAccess()
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
@@ -414,12 +436,7 @@ 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.login'))
# 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()
@@ -440,8 +457,9 @@ def webLogout(
finally:
# Try to delete session
request.session.flush()
request.authorized = False
response = HttpResponseRedirect(request.build_absolute_uri(exit_url))
response = HttpResponseRedirect(exit_url)
if authenticator:
authenticator.webLogoutHook(username, request, response)
return response
@@ -486,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

@@ -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(
@@ -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':

View File

@@ -0,0 +1,44 @@
# -*- 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. 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.
"""
UDS os managers related interfaces and classes
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from .mfa import MFA
def factory():
"""
Returns factory for register/access to authenticators
"""
from .mfafactory import MFAsFactory
return MFAsFactory.factory()

BIN
server/src/uds/core/mfas/mfa.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,215 @@
# -*- 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 datetime
import random
import enum
import logging
import typing
from django.utils.translation import ugettext_noop as _
from uds.models import getSqlDatetime
from uds.core import Module
from uds.core.auths import exceptions
if typing.TYPE_CHECKING:
from uds.core.environment import Environment
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
class MFA(Module):
"""
this class provides an abstraction of a Multi Factor Authentication
"""
# informational related data
# : Name of type, used at administration interface to identify this
# : notifier type (e.g. "Email", "SMS", etc.)
# : This string will be translated when provided to admin interface
# : using gettext, so you can mark it as "_" at derived classes (using gettext_noop)
# : if you want so it can be translated.
typeName: typing.ClassVar[str] = _('Base MFA')
# : Name of type used by Managers to identify this type of service
# : We could have used here the Class name, but we decided that the
# : module implementator will be the one that will provide a name that
# : will relation the class (type) and that name.
typeType: typing.ClassVar[str] = 'baseMFA'
# : Description shown at administration level for this authenticator.
# : This string will be translated when provided to admin interface
# : using gettext, so you can mark it as "_" at derived classes (using gettext_noop)
# : if you want so it can be translated.
typeDescription: typing.ClassVar[str] = _('Base MFA')
# : Icon file, used to represent this authenticator at administration interface
# : This file should be at same folder as this class is, except if you provide
# : your own :py:meth:uds.core.module.BaseModule.icon method.
iconFile: typing.ClassVar[str] = 'mfa.png'
# : Cache time for the generated MFA code
# : this means that the code will be valid for this time, and will not
# : be resent to the user until the time expires.
# : This value is in minutes
# : Note: This value is used by default "process" methos, but you can
# : override it in your own implementation.
cacheTime: typing.ClassVar[int] = 5
class RESULT(enum.IntEnum):
"""
This enum is used to know if the MFA code was sent or not.
"""
OK = 1
ALLOWED = 2
def __init__(self, environment: 'Environment', values: Module.ValuesType):
super().__init__(environment, values)
self.initialize(values)
def initialize(self, values: Module.ValuesType) -> None:
"""
This method will be invoked from __init__ constructor.
This is provided so you don't have to provide your own __init__ method,
and invoke base methods.
This will get invoked when all initialization stuff is done
Args:
values: If values is not none, this object is being initialized
from administration interface, and not unmarshal will be done.
If it's None, this is initialized internally, and unmarshal will
be called after this.
Default implementation does nothing
"""
def label(self) -> str:
"""
This method will be invoked from the MFA form, to know the human name of the field
that will be used to enter the MFA code.
"""
return 'MFA Code'
def html(self, request: 'ExtendedHttpRequest') -> str:
"""
This method will be invoked from the MFA form, to know the HTML that will be presented
to the user below the MFA code form.
"""
return ''
def validity(self) -> int:
"""
This method will be invoked from the MFA form, to know the validity in secods
of the MFA code.
If value is 0 or less, means the code is always valid.
"""
return self.cacheTime
def emptyIndentifierAllowedToLogin(self, request: 'ExtendedHttpRequest') -> bool:
"""
If this method returns True, an user that has no "identifier" is allowed to login without MFA
"""
return True
def sendCode(self, request: 'ExtendedHttpRequest', userId: str, username: str, identifier: str, code: str) -> 'MFA.RESULT':
"""
This method will be invoked from "process" method, to send the MFA code to the user.
If returns MFA.RESULT.OK, the MFA code was sent.
If returns MFA.RESULT.ALLOW, the MFA code was not sent, the user does not need to enter the MFA code.
If raises an error, the MFA code was not sent, and the user needs to enter the MFA code.
"""
raise NotImplementedError('sendCode method not implemented')
def process(self, request: 'ExtendedHttpRequest', userId: str, username: str, identifier: str, validity: typing.Optional[int] = None) -> 'MFA.RESULT':
"""
This method will be invoked from the MFA form, to send the MFA code to the user.
The identifier where to send the code, will be obtained from "mfaIdentifier" method.
Default implementation generates a random code and sends invokes "sendCode" method.
If returns MFA.RESULT.OK, the MFA code was sent.
If returns MFA.RESULT.ALLOW, the MFA code was not sent, the user does not need to enter the MFA code.
If raises an error, the MFA code was not sent, and the user needs to enter the MFA code.
"""
# try to get the stored code
storageKey = request.ip + userId
data: typing.Any = self.storage.getPickle(storageKey)
validity = validity if validity is not None else self.validity() * 60
try:
if data and validity:
# if we have a stored code, check if it's still valid
if data[0] + datetime.timedelta(seconds=validity) > getSqlDatetime():
# if it's still valid, just return without sending a new one
return MFA.RESULT.OK
except Exception:
# if we have a problem, just remove the stored code
self.storage.remove(storageKey)
# Generate a 6 digit code (0-9)
code = ''.join(random.SystemRandom().choices('0123456789', k=6))
logger.debug('Generated OTP is %s', code)
# Store the code in the database, own storage space
self.storage.putPickle(storageKey, (getSqlDatetime(), code))
# Send the code to the user
return self.sendCode(request, userId, username, identifier, code)
def validate(self, request: 'ExtendedHttpRequest', userId: str, username: str, identifier: str, code: str, validity: typing.Optional[int] = None) -> None:
"""
If this method is provided by an authenticator, the user will be allowed to enter a MFA code
You must raise an "exceptions.MFAError" if the code is not valid.
"""
# Validate the code
try:
err = _('Invalid MFA code')
storageKey = request.ip + userId
data = self.storage.getPickle(storageKey)
if data and len(data) == 2:
validity = validity if validity is not None else self.validity() * 60
if validity > 0 and data[0] + datetime.timedelta(seconds=validity) < getSqlDatetime():
# if it is no more valid, raise an error
# Remove stored code and raise error
self.storage.remove(storageKey)
raise exceptions.MFAError('MFA Code expired')
# Check if the code is valid
if data[1] == code:
# Code is valid, remove it from storage
self.storage.remove(storageKey)
return
except Exception as e:
# Any error means invalid code
err = str(e)
raise exceptions.MFAError(err)

View File

@@ -0,0 +1,61 @@
# -*- 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 logging
import typing
from uds.core.util import singleton
if typing.TYPE_CHECKING:
from .mfa import MFA
logger = logging.getLogger(__name__)
class MFAsFactory(metaclass=singleton.Singleton):
_factory: typing.Optional['MFAsFactory'] = None
_mfas: typing.MutableMapping[str, typing.Type['MFA']] = {}
@staticmethod
def factory() -> 'MFAsFactory':
return MFAsFactory()
def providers(self) -> typing.Mapping[str, typing.Type['MFA']]:
return self._mfas
def insert(self, type_: typing.Type['MFA']) -> None:
logger.debug('Adding Multi Factor Auth %s as %s', type_.type(), type_)
typeName = type_.type().lower()
self._mfas[typeName] = type_
def lookup(self, typeName: str) -> typing.Optional[typing.Type['MFA']]:
return self._mfas.get(typeName.lower(), None)

View File

@@ -181,7 +181,7 @@ class Module(UserInterface, Environmentable, Serializable):
'iconFile' class attribute
"""
file_ = open(
os.path.dirname(sys.modules[cls.__module__].__file__) + '/' + cls.iconFile,
os.path.dirname(typing.cast(str, sys.modules[cls.__module__].__file__)) + '/' + cls.iconFile,
'rb',
)
data = file_.read()

View File

@@ -243,15 +243,33 @@ class Service(Module):
"""
return self._provider
def unmarshal(self, data: bytes) -> None:
# In fact, we will not unmarshall anything here, but setup maxDeployed
# if maxServices exists and it is a gui.NumericField
# Invoke base unmarshall, so "gui fields" gets loaded from data
super().unmarshal(data)
if hasattr(self, 'maxServices'):
# Fix Own "maxDeployed" value after loading fields
try:
self.maxDeployed = getattr(self, 'maxServices').num()
except Exception:
self.maxDeployed = Service.UNLIMITED
if self.maxDeployed < 1:
self.maxDeployed = Service.UNLIMITED
# Keep untouched if maxServices is not present
def requestServicesForAssignation(
self, **kwargs
) -> typing.Iterable[UserDeployment]:
"""
override this if mustAssignManualy is True
@params kwargs: Named arguments
@return an array with the services that we can assign (they must be of type deployedType)
We will access the returned array in "name" basis. This means that the service will be assigned by "name", so be care that every single service
returned are not repeated... :-)
@return an iterable with the services that we can assign manually (they must be of type UserDeployment)
We will access the returned iterable in "name" basis. This means that the service will be assigned by "name", so be care that every single service
name returned is unique :-)
"""
raise Exception(
'The class {0} has been marked as manually asignable but no requestServicesForAssignetion provided!!!'.format(

View File

@@ -160,7 +160,7 @@ class UserDeployment(
self._uuid = kwargs.get('uuid', '')
# If it has dbService, got uuid from it
if self._dbService:
self._uuid = self._dbService.uuid
self._uuid = self._dbService.uuid or ''
self.initialize()

View File

@@ -30,6 +30,8 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import os
import sys
import codecs
import logging
import typing
@@ -267,6 +269,33 @@ class Transport(Module):
params,
)
def getRelativeScript(
self, scriptName: str, params: typing.Mapping[str, typing.Any]
) -> typing.Tuple[str, str, typing.Mapping[str, typing.Any]]:
"""Returns a script that will be executed on client, but will be downloaded from server
Args:
scriptName: Name of the script to be downloaded, relative path (i.e. 'scripts/direct/transport.py')
params: Parameters for the return tuple
"""
# Reads script and signature
basePath = os.path.dirname(sys.modules[self.__module__].__file__ or 'not_found') # Will raise if not found
script = open(os.path.join(basePath, scriptName), 'r').read()
signature = open(os.path.join(basePath, scriptName + '.signature'), 'r').read()
return script, signature, params
def getScript(
self, osName: str, type: typing.Literal['tunnel', 'direct'], params: typing.Mapping[str, typing.Any]
) -> typing.Tuple[str, str, typing.Mapping[str, typing.Any]]:
"""
Returns a script for the given os and type
"""
return self.getRelativeScript(
'scripts/{os}/{type}.py'.format(os=osName, type=type), params
)
def getLink(
self,
userService: 'models.UserService',

View File

@@ -37,15 +37,17 @@ import pickle
import copy
import typing
import logging
from collections import abc
from django.utils.translation import get_language, ugettext as _, ugettext_noop
from django.conf import settings
from uds.core.managers import cryptoManager
logger = logging.getLogger(__name__)
UDSB = b'udsprotect'
UDSB = b'udsprotect' # UDS base key, old
UDSK = settings.SECRET_KEY[8:24].encode() # UDS key, new
class gui:
"""
@@ -101,6 +103,7 @@ class gui:
CREDENTIALS_TAB: typing.ClassVar[str] = ugettext_noop('Credentials')
TUNNEL_TAB: typing.ClassVar[str] = ugettext_noop('Tunnel')
DISPLAY_TAB: typing.ClassVar[str] = ugettext_noop('Display')
MFA_TAB: typing.ClassVar[str] = ugettext_noop('MFA')
# : Static Callbacks simple registry
callbacks: typing.Dict[
@@ -111,17 +114,30 @@ class gui:
# Helpers
@staticmethod
def convertToChoices(
vals: typing.Union[typing.List[str], typing.MutableMapping[str, str]]
vals: typing.Union[typing.Iterable[typing.Union[str, typing.Dict[str, str]]], typing.Dict[str, str]]
) -> typing.List[typing.Dict[str, str]]:
"""
Helper to convert from array of strings to the same dict used in choice,
Helper to convert from array of strings (or dictionaries) to the same dict used in choice,
multichoice, ..
"""
if isinstance(vals, (list, tuple)):
return [{'id': v, 'text': v} for v in vals]
if not vals:
return []
# Helper to convert an item to a dict
def choiceFromValue(val: typing.Union[str, typing.Dict[str, str]]) -> typing.Dict[str, str]:
if isinstance(val, str):
return {'id': val, 'text': val}
return copy.deepcopy(val)
# Dictionary
return [{'id': k, 'text': v} for k, v in vals.items()]
# If is a dict
if isinstance(vals, abc.Mapping):
return [{'id': str(k), 'text': v} for k, v in vals.items()]
# If is an iterator
if isinstance(vals, abc.Iterable):
return [choiceFromValue(v) for v in vals]
raise ValueError('Invalid type for convertToChoices: {}'.format(type(vals)))
@staticmethod
def convertToList(vals: typing.Iterable[str]) -> typing.List[str]:
@@ -252,7 +268,7 @@ class gui:
def __init__(self, **options) -> None:
# Added defaultValue as alias for defvalue
defvalue = options.get('defvalue', options.get('defaultValue', ''))
defvalue = options.get('defvalue', options.get('defaultValue', options.get('defValue', '')))
if callable(defvalue):
defvalue = defvalue()
self._data = {
@@ -533,7 +549,7 @@ class gui:
except Exception:
return datetime.date.min if min else datetime.date.max
def datetime(self, min: bool) -> datetime.datetime:
def datetime(self, min: bool = True) -> datetime.datetime:
"""
Returns the date this object represents
@@ -767,7 +783,8 @@ class gui:
def __init__(self, **options):
super().__init__(**options)
if options.get('values') and isinstance(options.get('values'), dict):
vals = options.get('values')
if vals and isinstance(vals, (dict, list, tuple)):
options['values'] = gui.convertToChoices(options['values'])
self._data['values'] = options.get('values', [])
if 'fills' in options:
@@ -779,7 +796,7 @@ class gui:
gui.callbacks[fills['callbackName']] = fnc
self._type(gui.InputField.CHOICE_TYPE)
def setValues(self, values: typing.List[typing.Any]):
def setValues(self, values: typing.List[typing.Dict[str, typing.Any]]):
"""
Set the values for this choice field
"""
@@ -989,6 +1006,12 @@ class UserInterface(metaclass=UserInterfaceType):
of this posibility in a near version...
"""
@classmethod
def initClassGui(cls) -> None:
"""
This method is used to initialize the gui fields of the class.
"""
def valuesDict(self) -> gui.ValuesDictType:
"""
Returns own data needed for user interaction as a dict of key-names ->
@@ -1058,8 +1081,9 @@ class UserInterface(metaclass=UserInterfaceType):
# logger.debug('Serializing value {0}'.format(v.value))
val = b'\001' + pickle.dumps(v.value, protocol=0)
elif v.isType(gui.InfoField.PASSWORD_TYPE):
val = b'\004' + cryptoManager().AESCrypt(
v.value.encode('utf8'), UDSB, True
# Old \004 field type is not used anymore, is for old "udsprotect" encryption
val = b'\005' + cryptoManager().AESCrypt(
v.value.encode('utf8'), UDSK, True
)
elif v.isType(gui.InputField.NUMERIC_TYPE):
val = str(int(v.num())).encode('utf8')
@@ -1110,6 +1134,8 @@ class UserInterface(metaclass=UserInterfaceType):
val = pickle.loads(v[1:])
elif v and v[0] == 4:
val = cryptoManager().AESDecrypt(v[1:], UDSB, True).decode()
elif v and v[0] == 5:
val = cryptoManager().AESDecrypt(v[1:], UDSK, True).decode()
else:
val = v
# Ensure "legacy bytes" values are loaded correctly as unicode
@@ -1143,6 +1169,8 @@ class UserInterface(metaclass=UserInterfaceType):
if obj:
obj.initGui() # We give the "oportunity" to fill necesary theGui data before providing it to client
theGui = obj
else:
cls.initClassGui() # We give the "oportunity" to fill necesary theGui data before providing it to client
res: typing.List[typing.MutableMapping[str, typing.Any]] = []

View File

@@ -31,6 +31,7 @@
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
import datetime
import hashlib
import time
import typing
import logging
@@ -53,21 +54,22 @@ ONE_DAY = 3600 * 24
class CalendarChecker:
__slots__ = ('calendar',)
calendar: Calendar
# For performance checking
updates: int = 0
cache_hit: int = 0
hits: int = 0
updates: typing.ClassVar[int] = 0
cache_hit: typing.ClassVar[int] = 0
hits: typing.ClassVar[int] = 0
cache = Cache('calChecker')
cache: typing.ClassVar[Cache] = Cache('calChecker')
def __init__(self, calendar: Calendar) -> None:
self.calendar = calendar
def _updateData(self, dtime: datetime.datetime) -> bitarray.bitarray:
logger.debug('Updating %s', dtime)
# Else, update the array
CalendarChecker.updates += 1
data = bitarray.bitarray(60 * 24) # Granurality is minute
@@ -123,7 +125,9 @@ class CalendarChecker:
return data
def _updateEvents(self, checkFrom, startEvent=True):
def _updateEvents(
self, checkFrom: datetime.datetime, startEvent: bool = True
) -> typing.Optional[datetime.datetime]:
next_event = None
for rule in self.calendar.rules.all():
# logger.debug('RULE: start = {}, checkFrom = {}, end'.format(rule.start.date(), checkFrom.date()))
@@ -140,7 +144,7 @@ class CalendarChecker:
return next_event
def check(self, dtime=None) -> bool:
def check(self, dtime: typing.Optional[datetime.datetime] = None) -> bool:
"""
Checks if the given time is a valid event on calendar
@param dtime: Datetime object to check
@@ -152,10 +156,10 @@ class CalendarChecker:
memCache = caches['memory']
# First, try to get data from cache if it is valid
cacheKey = (
str(self.calendar.modified.toordinal())
+ str(dtime.date().toordinal())
+ self.calendar.uuid
cacheKey = CalendarChecker._cacheKey(
str(self.calendar.modified)
+ str(dtime.date())
+ (self.calendar.uuid or '')
+ 'checker'
)
# First, check "local memory cache", and if not found, from DB cache
@@ -180,33 +184,38 @@ class CalendarChecker:
return bool(data[dtime.hour * 60 + dtime.minute])
def nextEvent(
self, checkFrom=None, startEvent=True, offset=None
self,
checkFrom: typing.Optional[datetime.datetime] = None,
startEvent: bool = True,
offset: typing.Optional[datetime.timedelta] = None,
) -> typing.Optional[datetime.datetime]:
"""
Returns next event for this interval
"""
logger.debug('Obtaining nextEvent')
if checkFrom is None:
if not checkFrom:
checkFrom = getSqlDatetime()
if offset is None:
if not offset:
offset = datetime.timedelta(minutes=0)
cacheKey = (
str(hash(self.calendar.modified))
+ self.calendar.uuid
cacheKey = CalendarChecker._cacheKey(
str(self.calendar.modified)
+ (self.calendar.uuid or '')
+ str(offset.seconds)
+ str(int(time.mktime(checkFrom.timetuple())))
+ str(checkFrom)
+ 'event'
+ ('x' if startEvent else '_')
)
next_event = CalendarChecker.cache.get(cacheKey, None)
if next_event is None:
next_event: typing.Optional[datetime.datetime] = CalendarChecker.cache.get(
cacheKey, None
)
if not next_event:
logger.debug('Regenerating cached nextEvent')
next_event = self._updateEvents(
checkFrom + offset, startEvent
) # We substract on checkin, so we can take into account for next execution the "offset" on start & end (just the inverse of current, so we substract it)
if next_event is not None:
if next_event:
next_event += offset
CalendarChecker.cache.put(cacheKey, next_event, 3600)
else:
@@ -217,3 +226,12 @@ class CalendarChecker:
def debug(self) -> str:
return "Calendar checker for {}".format(self.calendar)
@staticmethod
def _cacheKey(key: str) -> str:
# Returns a valid cache key for all caching backends (memcached, redis, or whatever)
# Simple, fastest algorihm is to use md5
h = hashlib.md5()
h.update(key.encode('utf-8'))
return h.hexdigest()

View File

@@ -52,6 +52,27 @@ _getLater = []
_configParams = {}
# Pair of section/value removed from current UDS version
REMOVED_CONFIG_ELEMENTS = {
'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'),
}
class Config:
# Fields types, so inputs get more "beautiful"
TEXT_FIELD: int = 0
@@ -119,7 +140,11 @@ class Config:
self.set(self._default)
self._data = self._default
except Exception as e:
logger.info('Error accessing db config {0}.{1}'.format(self._section.name(), self._key))
logger.info(
'Error accessing db config {0}.{1}'.format(
self._section.name(), self._key
)
)
logger.exception(e)
self._data = self._default
@@ -276,6 +301,36 @@ class Config:
except Exception:
return False
@staticmethod
def getConfigValues(
addCrypt: bool = False,
) -> typing.Mapping[str, typing.Mapping[str, typing.Mapping[str, typing.Any]]]:
"""
Returns a dictionary with all config values
"""
res: typing.Dict[str, typing.Dict[str, typing.Any]] = {}
for cfg in Config.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_CONFIG_ELEMENTS.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(),
}
logger.debug('Configuration: %s', res)
return res
class GlobalConfig:
"""
@@ -345,6 +400,10 @@ class GlobalConfig:
ENHANCED_SECURITY: Config.Value = Config.section(SECURITY_SECTION).value(
'Enable Enhanced Security', '1', type=Config.BOOLEAN_FIELD
)
# Paranoid security
ENFORCE_ZERO_TRUST: Config.Value = Config.section(SECURITY_SECTION).value(
'Enforze Zero-Trust Mode', '0', type=Config.BOOLEAN_FIELD
)
# Time an admi session can be idle before being "logged out"
# ADMIN_IDLE_TIME: Config.Value = Config.section(SECURITY_SECTION).value('adminIdleTime', '14400', type=Config.NUMERIC_FIELD) # Defaults to 4 hous
# Time betwen checks of unused services by os managers
@@ -419,6 +478,11 @@ class GlobalConfig:
'New Max restriction', '0', type=Config.BOOLEAN_FIELD
)
# Maximum security logs duration in days
MAX_AUDIT_LOGS_DURATION: Config.Value = Config.section(SECURITY_SECTION).value(
'Max Audit Logs duration', '365', type=Config.NUMERIC_FIELD
)
# Allowed "trusted sources" for request
TRUSTED_SOURCES: Config.Value = Config.section(SECURITY_SECTION).value(
'Trusted Hosts', '*', type=Config.TEXT_FIELD

View File

@@ -34,6 +34,7 @@ from functools import wraps
import logging
import inspect
import typing
import threading
from uds.core.util.html import checkBrowser
from uds.web.util import errors
@@ -188,3 +189,14 @@ def allowCache(
return wrapper
return allowCacheDecorator
# Decorator to execute method in a thread
def threaded(func: typing.Callable[..., None]) -> typing.Callable[..., None]:
"""Decorator to execute method in a thread"""
@wraps(func)
def wrapper(*args, **kwargs) -> None:
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
thread.start()
return wrapper

View File

@@ -43,12 +43,15 @@ logger = logging.getLogger(__name__)
useLogger = logging.getLogger('useLog')
# Logging levels
OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (
OTHER, DEBUG, INFO, WARNING, ERROR, CRITICAL = (
10000 * (x + 1) for x in range(6)
) # @UndefinedVariable
WARN = WARNING
FATAL = CRITICAL
# Logging sources
INTERNAL, ACTOR, TRANSPORT, OSMANAGER, UNKNOWN, WEB, ADMIN, SERVICE = (
INTERNAL, ACTOR, TRANSPORT, OSMANAGER, UNKNOWN, WEB, ADMIN, SERVICE, REST = (
'internal',
'actor',
'transport',
@@ -57,6 +60,7 @@ INTERNAL, ACTOR, TRANSPORT, OSMANAGER, UNKNOWN, WEB, ADMIN, SERVICE = (
'web',
'admin',
'service',
'rest',
)
OTHERSTR, DEBUGSTR, INFOSTR, WARNSTR, ERRORSTR, FATALSTR = (
@@ -81,6 +85,10 @@ __nameLevels = {
# Reverse dict of names
__valueLevels = {v: k for k, v in __nameLevels.items()}
# Global log owner types:
OWNER_TYPE_GLOBAL = -1
OWNER_TYPE_AUDIT = -2
def logLevelFromStr(level: str) -> int:
"""

View File

@@ -37,7 +37,7 @@ from django.utils import timezone
from uds.core.util import os_detector as OsDetector
from uds.core.util.config import GlobalConfig
from uds.core.auths.auth import EXPIRY_KEY, ROOT_ID, USER_KEY, getRootUser, webLogout
from uds.core.auths.auth import AUTHORIZED_KEY, EXPIRY_KEY, ROOT_ID, USER_KEY, getRootUser, webLogout
from uds.core.util.request import (
setRequest,
delCurrentRequest,
@@ -70,6 +70,7 @@ class GlobalRequestMiddleware:
def __call__(self, request: ExtendedHttpRequest):
# Add IP to request
GlobalRequestMiddleware.fillIps(request)
request.authorized = request.session.get(AUTHORIZED_KEY, False)
# Store request on cache
setRequest(request=request)
@@ -103,6 +104,10 @@ class GlobalRequestMiddleware:
response = self._get_response(request)
# Update authorized on session
if hasattr(request, 'session'):
request.session[AUTHORIZED_KEY] = request.authorized
return self._process_response(request, response)
@staticmethod

View File

@@ -49,7 +49,9 @@ class ExtendedHttpRequest(HttpRequest):
ip: str
ip_proxy: str
os: DictAsObj
user: typing.Optional[User] # type: ignore
user: typing.Optional[User]
authorized: bool
class ExtendedHttpRequestWithUser(ExtendedHttpRequest):

View File

@@ -178,6 +178,9 @@ class StorageAsDict(MutableMapping):
def get(self, key: str, default: typing.Any = None) -> typing.Any:
return self[key] or default
def delete(self, key: str) -> None:
self.__delitem__(key)
# Custom utility methods
@property
def group(self) -> str:

View File

@@ -130,7 +130,7 @@ def packageRelativeFile(moduleName: str, fileName: str) -> str:
This allows to keep images alongside report
"""
mod = sys.modules[moduleName]
if mod and mod.__file__:
if mod and hasattr(mod, '__file__') and mod.__file__:
pkgpath = os.path.dirname(mod.__file__)
return os.path.join(pkgpath, fileName)
# Not found, return fileName
@@ -189,6 +189,14 @@ def checkValidBasename(baseName: str, length: int = -1) -> None:
ugettext('The machine name can\'t be only numbers')
)
def removeControlCharacters(s: str) -> str:
"""
Removes control characters from an unicode string
Arguments:
s {str} -- string to remove control characters from
Returns:
str -- string without control characters
"""
return ''.join(ch for ch in s if unicodedata.category(ch)[0] != "C")

View File

@@ -116,6 +116,22 @@ def validatePort(portStr: str) -> int:
return validateNumeric(portStr, minValue=0, maxValue=65535, fieldName='Port')
def validateHostPortPair(hostPortPair: str) -> typing.Tuple[str, int]:
"""
Validates that a host:port pair is valid
:param hostPortPair: host:port pair to validate
:param returnAsInteger: if True, returns value as integer, if not, as string
:return: Raises Module.Validation exception if is invalid, else return the value "fixed"
"""
try:
host, port = hostPortPair.split(':')
return validateHostname(host, 255, False), validatePort(port)
except Exception:
raise Module.ValidationException(
_('{} is not a valid host:port pair').format(hostPortPair)
)
def validateTimeout(timeOutStr: str) -> int:
"""
Validates that a timeout value is valid
@@ -154,3 +170,21 @@ def validateMacRange(macRange: str) -> str:
)
return macRange
def validateEmail(email: str) -> str:
"""
Validates that an email is valid
:param email: email to validate
:return: Raises Module.Validation exception if is invalid, else return the value "fixed"
"""
if len(email) > 254:
raise Module.ValidationException(
_('Email address is too long')
)
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
raise Module.ValidationException(
_('Email address is not valid')
)
return email

View File

@@ -47,7 +47,7 @@ class HangedCleaner(Job):
frecuency_cfg = GlobalConfig.MAX_INITIALIZING_TIME
friendly_name = 'Hanged services checker'
def run(self):
def run(self) -> None:
now = getSqlDatetime()
since_state = now - timedelta(
seconds=GlobalConfig.MAX_INITIALIZING_TIME.getInt()
@@ -56,7 +56,7 @@ class HangedCleaner(Job):
# Filter for locating machine not ready
flt = Q(state_date__lt=since_state, state=State.PREPARING) | Q(
state_date__lt=since_state, state=State.USABLE, os_state=State.PREPARING
)
) | Q(state_date__lt=since_removing, state__in=[State.REMOVING, State.CANCELING])
withHangedServices = (
ServicePool.objects.annotate(
@@ -74,7 +74,7 @@ class HangedCleaner(Job):
)
| Q(
userServices__state_date__lt=since_removing,
userServices__state=State.REMOVING,
userServices__state__in=[State.REMOVING, State.CANCELING],
),
)
)
@@ -96,7 +96,7 @@ class HangedCleaner(Job):
continue
logger.debug('Found hanged service %s', us)
if (
us.state == State.REMOVING
us.state in [State.REMOVING, State.CANCELING]
): # Removing too long, remark it as removable
log.doLog(
us,

View File

@@ -146,7 +146,7 @@ class ServiceCacheUpdater(Job):
)
inAssigned: int = (
servicePool.assignedUserServices()
.filter(userServiceManager().getStateFilter(servicePool))
.filter(userServiceManager().getStateFilter(servicePool.service))
.count()
)
# if we bypasses max cache, we will reduce it in first place. This is so because this will free resources on service provider

View File

@@ -32,12 +32,14 @@
"""
from importlib import import_module
import logging
import datetime
import typing
from django.conf import settings
from uds.core.util.cache import Cache
from uds.core.jobs import Job
from uds.models import TicketStore
from uds.models import TicketStore, Log, getSqlDatetime
from uds.core.util import config, log
logger = logging.getLogger(__name__)
@@ -65,7 +67,6 @@ class TicketStoreCleaner(Job):
class SessionsCleaner(Job):
frecuency = 3600 * 24 * 7 # Once a week will be enough
friendly_name = 'User Sessions cleaner'
@@ -83,3 +84,20 @@ class SessionsCleaner(Job):
pass # No problem if no cleanup
logger.debug('Done session cleanup')
class AuditLogCleanup(Job):
frecuency = 60 * 60 * 24 # Once a day
friendly_name = 'Audit Log Cleanup'
def run(self) -> None:
"""
Cleans logs older than days
"""
Log.objects.filter(
date__lt=getSqlDatetime()
- datetime.timedelta(
days=config.GlobalConfig.MAX_AUDIT_LOGS_DURATION.getInt()
),
owner_type=log.OWNER_TYPE_AUDIT,
).delete()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
import logging
from django.core.management.base import BaseCommand
@@ -42,10 +43,10 @@ class Command(BaseCommand):
args = "<mod.name=value mod.name=value mod.name=value...>"
help = "Updates configuration values. If mod is omitted, UDS will be used. Omit whitespaces betwen name, =, and value (they must be a single param)"
def add_arguments(self, parser):
def add_arguments(self, parser) -> None:
parser.add_argument('name_value', nargs='+', type=str)
def handle(self, *args, **options):
def handle(self, *args, **options) -> None:
logger.debug("Handling settings")
GlobalConfig.initialize()
try:
@@ -62,5 +63,5 @@ class Command(BaseCommand):
): # If not exists, try to store value without any special parameters
Config.section(mod).value(name, value).get()
except Exception as e:
print('The command could not be processed: {}'.format(e))
self.stderr.write('The command could not be processed: {}'.format(e))
logger.exception('Exception processing %s', args)

View File

@@ -0,0 +1,568 @@
# -*- 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
"""
from functools import reduce
import logging
import operator
import typing
import yaml
from django.core.management.base import BaseCommand
from django.db.models import Q
from uds import models
if typing.TYPE_CHECKING:
import argparse
from django.db.models import Model
from uds.models.uuid_model import UUIDModel
logger = logging.getLogger(__name__)
ModelType = typing.TypeVar('ModelType', bound='UUIDModel')
T = typing.TypeVar('T')
def uuid_object_exporter(obj: 'UUIDModel') -> typing.Dict[str, typing.Any]:
"""
Exports a uuid model to a dict
"""
return {
'uuid': obj.uuid,
}
def managed_object_exporter(
obj: models.ManagedObjectModel,
) -> typing.Dict[str, typing.Any]:
"""
Exports a managed object to a dict
"""
# Get uuid model
m = uuid_object_exporter(obj)
# Extend with managed object fields
m.update(
{
'name': obj.name,
'comments': obj.comments,
'data': obj.data,
'data_type': obj.data_type,
}
)
return m
def provider_exporter(provider: models.Provider) -> typing.Dict[str, typing.Any]:
"""
Exports a provider to a dict
"""
p = managed_object_exporter(provider)
p['maintenance_mode'] = provider.maintenance_mode
return p
def service_exporter(service: models.Service) -> typing.Dict[str, typing.Any]:
"""
Exports a service to a dict
"""
s = managed_object_exporter(service)
s['provider'] = service.provider.uuid
s['token'] = service.token
return s
def authenticator_exporter(
authenticator: models.Authenticator,
) -> typing.Dict[str, typing.Any]:
"""
Exports an authenticator to a dict
"""
a = managed_object_exporter(authenticator)
a['priority'] = authenticator.priority
a['provider'] = authenticator.small_name
a['visible'] = authenticator.visible
return a
def user_exporter(user: models.User) -> typing.Dict[str, typing.Any]:
"""
Exports a user to a dict
"""
u = uuid_object_exporter(user)
u.update(
{
'manager': user.manager.uuid,
'name': user.name,
'comments': user.comments,
'real_name': user.real_name,
'state': user.state,
'password': user.password,
'mfa_data': user.mfa_data,
'staff_member': user.staff_member,
'is_admin': user.is_admin,
'last_access': user.last_access,
'parent': user.parent,
'created': user.created,
'groups': [g.uuid for g in user.groups.all()],
}
)
return u
def group_export(group: models.Group) -> typing.Dict[str, typing.Any]:
"""
Exports a group to a dict
"""
g = uuid_object_exporter(group)
g.update(
{
'manager': group.manager.uuid,
'name': group.name,
'comments': group.comments,
'state': group.state,
'is_meta': group.is_meta,
'meta_if_any': group.meta_if_any,
'created': group.created,
}
)
return g
def transport_exporter(transport: models.Transport) -> typing.Dict[str, typing.Any]:
"""
Exports a transport to a dict
"""
t = managed_object_exporter(transport)
t.update(
{
'priority': transport.priority,
'nets_positive': transport.nets_positive,
'allowed_oss': transport.allowed_oss,
'label': transport.label,
'networks': [n.uuid for n in transport.networks.all()],
}
)
return t
def network_exporter(network: models.Network) -> typing.Dict[str, typing.Any]:
"""
Exports a network to a dict
"""
n = uuid_object_exporter(network)
n.update(
{
'name': network.name,
'net_start': network.net_start,
'net_end': network.net_end,
'net_string': network.net_string,
}
)
return n
def osmanager_exporter(osmanager: models.OSManager) -> typing.Dict[str, typing.Any]:
"""
Exports an osmanager to a dict
"""
o = managed_object_exporter(osmanager)
return o
def calendar_exporter(calendar: models.Calendar) -> typing.Dict[str, typing.Any]:
"""
Exports a calendar to a dict
"""
c = uuid_object_exporter(calendar)
c.update(
{
'name': calendar.name,
'comments': calendar.comments,
'modified': calendar.modified,
}
)
return c
def calendar_rule_exporter(calendar_rule: models.CalendarRule) -> typing.Dict[str, typing.Any]:
"""
Exports a calendar rule to a dict
"""
c = uuid_object_exporter(calendar_rule)
c.update(
{
'calendar': calendar_rule.calendar.uuid,
'name': calendar_rule.name,
'comments': calendar_rule.comments,
'start': calendar_rule.start,
'end': calendar_rule.end,
'frequency': calendar_rule.frequency,
'interval': calendar_rule.interval,
'duration': calendar_rule.duration,
'duration_unit': calendar_rule.duration_unit,
}
)
return c
class Command(BaseCommand):
help = 'Export entities from UDS to be imported in another UDS instance'
VALID_ENTITIES: typing.Mapping[str, typing.Callable[[], str]]
verbose: bool = True
filter_args: typing.List[typing.Tuple[str, str]] = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.VALID_ENTITIES = {
'providers': self.export_providers,
'services': self.export_services,
'authenticators': self.export_authenticators,
'users': self.export_users,
'groups': self.export_groups,
'networks': self.export_networks,
'transports': self.export_transports,
'osmanagers': self.export_osmanagers,
}
def add_arguments(self, parser: 'argparse.ArgumentParser') -> None:
# Accepts a list of valid entities to export
parser.add_argument(
'entities',
nargs='+',
choices=self.VALID_ENTITIES.keys(),
default=self.VALID_ENTITIES.keys(),
help='Entities to export',
)
# Output file name (will be appended .csv or .yaml)
parser.add_argument(
'--output',
action='store',
dest='output',
default='/tmp/export.yaml',
help='Output file name. Defaults to /tmp/export.yaml',
)
# Filter ALL entities by name, multiple names can be specified
parser.add_argument(
'--filter-name',
action='append',
dest='filter_name',
default=[],
help='Filter ALL entities by name',
)
# Filter ALL entities by uuid, multiple uuids can be specified
parser.add_argument(
'--filter-uuid',
action='append',
dest='filter_uuid',
default=[],
help='Filter ALL entities by uuid',
)
# quiet mode
parser.add_argument(
'--quiet',
action='store_false',
dest='verbose',
default=True,
help='Quiet mode',
)
def handle(self, *args, **options) -> None:
self.verbose = options['verbose']
if self.verbose:
self.stderr.write(f'Exporting entities: {",".join(options["entities"])}')
# Compose filter name for kwargs
for i in options['filter_name']:
self.filter_args.append(('name__icontains', i))
for i in options['filter_uuid']:
self.filter_args.append(('uuid', i))
# some entities are redundant, so remove them from the list
entities = self.remove_reduntant_entities(options['entities'])
# For each entity, export it as yaml to output file
with open(options['output'], 'w') as f:
for entity in entities:
self.stderr.write(f'Exporting {entity}')
f.write(self.VALID_ENTITIES[entity]())
f.write('')
if self.verbose:
self.stderr.write(f'Exported to {options["output"]}')
def apply_filter(
self, model: typing.Type[ModelType]
) -> typing.Iterable[ModelType]:
"""
Applies a filter to a model
"""
if self.verbose:
# Filter is a filter name, and an array of values
values = [f'{k.split("__")[0]}={v}' for k, v in self.filter_args]
self.stderr.write(f'Filtering {model.__name__}: \n ', ending='')
self.stderr.write("\n ".join(values))
# Generate "OR" filter with all kwargs
if self.filter_args:
return model.objects.filter(reduce(operator.or_, (Q(**{k: v}) for k, v in self.filter_args)))
return model.objects.all()
def output_count(
self, message: str, iterable: typing.Iterable[T]
) -> typing.Iterable[T]:
"""
Outputs the count of an iterable
"""
count = 0
for v in iterable:
count += 1
if self.verbose:
self.stderr.write(f'{message} {count}', ending='\r')
yield v
if self.verbose:
self.stderr.write('\n') # New line after count
def export_providers(self) -> str:
"""
Exports all providers to a list of dicts
"""
return '# Providers\n' + yaml.safe_dump(
[provider_exporter(p) for p in self.apply_filter(models.Provider)]
)
def export_services(self) -> str:
# First, locate providers for services with the filter
services_list = list(
self.output_count(
'Filtering services', self.apply_filter(models.Service)
)
)
providers_list = set(
[
s.provider
for s in self.output_count('Filtering providers', services_list)
]
)
# Now, export those providers
providers = [
provider_exporter(p)
for p in self.output_count('Saving providers', providers_list)
]
# Then, export services with the filter
services = [
service_exporter(s)
for s in self.output_count('Saving services', services_list)
]
return (
'# Providers\n'
+ yaml.safe_dump(providers)
+ '# Services\n'
+ yaml.safe_dump(services)
)
def export_authenticators(self) -> str:
"""
Exports all authenticators to a list of dicts
"""
return '# Authenticators\n' + yaml.safe_dump(
[
authenticator_exporter(a)
for a in self.output_count(
'Saving authenticators',
self.apply_filter(models.Authenticator),
)
]
)
def export_users(self) -> str:
"""
Exports all users to a list of dicts
"""
# first, locate authenticators for users with the filter
users_list = list(
self.output_count(
'Filtering users', self.apply_filter(models.User)
)
)
authenticators_list = set(
[
u.manager
for u in self.output_count('Filtering authenticators', users_list)
]
)
# Now, groups that contains those users
groups_list = set()
for u in self.output_count('Filtering groups', users_list):
groups_list.update(u.groups.all())
# now, export those authenticators
authenticators = [
authenticator_exporter(a)
for a in self.output_count('Saving authenticators', authenticators_list)
]
# then, export those groups
groups = [
group_export(g) for g in self.output_count('Saving groups', groups_list)
]
# finally, export users with the filter
users = [
user_exporter(u) for u in self.output_count('Saving users', users_list)
]
return (
'# Authenticators\n'
+ yaml.safe_dump(authenticators)
+ '# Groups\n'
+ yaml.safe_dump(groups)
+ '# Users\n'
+ yaml.safe_dump(users)
)
def export_groups(self) -> str:
"""
Exports all groups to a list of dicts
"""
# First export authenticators for groups with the filter
groups_list = list(
self.output_count(
'Filtering groups', self.apply_filter(models.Group)
)
)
authenticators_list = set(
[
g.manager
for g in self.output_count('Filtering authenticators', groups_list)
]
)
authenticators = [
authenticator_exporter(a)
for a in self.output_count('Saving authenticators', authenticators_list)
]
# then, export groups with the filter
groups = [
group_export(g) for g in self.output_count('Saving groups', groups_list)
]
return (
'# Authenticators\n'
+ yaml.safe_dump(authenticators)
+ '# Groups\n'
+ yaml.safe_dump(groups)
)
def export_networks(self) -> str:
"""
Exports all networks to a list of dicts
"""
return '# Networks\n' + yaml.safe_dump(
[
network_exporter(n)
for n in self.output_count(
'Saving networks', self.apply_filter(models.Network)
)
]
)
def export_transports(self) -> str:
"""
Exports all transports to a list of dicts
"""
# First, export networks for transports with the filter
transports_list = list(
self.output_count(
'Filtering transports', self.apply_filter(models.Transport)
)
)
networks_list = set()
for t in self.output_count('Filtering networks', transports_list):
networks_list.update(t.networks.all())
networks = [
network_exporter(n)
for n in self.output_count('Saving networks', networks_list)
]
# then, export transports with the filter
transports = [
transport_exporter(t)
for t in self.output_count('Saving transports', transports_list)
]
return (
'# Networks\n'
+ yaml.safe_dump(networks)
+ '# Transports\n'
+ yaml.safe_dump(transports)
)
def export_osmanagers(self) -> str:
"""
Exports all osmanagers to a list of dicts
"""
return '# OSManagers\n' + yaml.safe_dump(
[
osmanager_exporter(o)
for o in self.output_count(
'Saving osmanagers', self.apply_filter(models.OSManager)
)
]
)
def remove_reduntant_entities(self, entities: typing.List[str]) -> typing.List[str]:
"""
Removes redundant entities from the list
"""
REPLACES: typing.Mapping[str, typing.List[str]] = {
'users': ['authenticators', 'groups'],
'groups': ['authenticators'],
'authenticators': [],
'transports': ['networks'],
'networks': [],
'osmanagers': [],
'services': ['providers'],
'providers': [],
}
entities = list(set(entities)) # remove duplicates
# Remove entities that are replaced by other entities
for entity in entities:
for replace in REPLACES.get(entity, []):
if replace in entities:
entities.remove(replace)
return entities

View File

@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# 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. 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 logging
import typing
import csv
import yaml
from django.core.management.base import BaseCommand
from uds.core.util import config
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Show current PUBLIC configuration of UDS broker (passwords are not shown)"
def add_arguments(self, parser):
parser.add_argument(
'--csv',
action='store_true',
dest='csv',
default=False,
help='Shows configuration in CVS format',
)
parser.add_argument(
'--yaml',
action='store_true',
dest='yaml',
default=False,
help='Shows configuration in YAML format',
)
def handle(self, *args, **options):
logger.debug("Show settings")
config.GlobalConfig.initialize()
try:
writer: typing.Any = None
if options['csv']:
# Print header
writer = csv.writer(self.stdout, delimiter=';', quotechar='"', quoting=csv.QUOTE_MINIMAL)
writer.writerow(['Section', 'Name', 'Value'])
elif options['yaml']:
writer = {} # Create a dict to store data, and write at the end
# Get sections, key, value as a list of tuples
for section, data in config.Config.getConfigValues().items():
for key, value in data.items():
# value is a dict, get 'value' key
if options['csv']:
writer.writerow([section, key, value['value']])
elif options['yaml']:
if section not in writer:
writer[section] = {}
writer[section][key] = value['value']
else:
v = value['value'].replace('\n', '\\n')
self.stdout.write(f'{section}.{key}="{v}"')
if options['yaml']:
self.stdout.write(yaml.safe_dump(writer, default_flow_style=False))
except Exception as e:
print('The command could not be processed: {}'.format(e))
logger.exception('Exception processing %s', args)

View File

@@ -161,8 +161,8 @@ class Command(BaseCommand):
if not start and not stop:
if pid:
sys.stdout.write(
self.stdout.write(
"Task manager found running (pid file exists: {0})\n".format(pid)
)
else:
sys.stdout.write("Task manager not foud (pid file do not exits)\n")
self.stdout.write("Task manager not foud (pid file do not exits)\n")

View File

@@ -0,0 +1,379 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# 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. 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 logging
import typing
import yaml
import collections
from django.core.management.base import BaseCommand
from uds.core.util import log
from uds import models
from uds.core.util.state import State
logger = logging.getLogger(__name__)
if typing.TYPE_CHECKING:
from uds.core.module import Module
from django.db import models as dbmodels
def getSerializedFromManagedObject(
mod: 'models.ManagedObjectModel',
removableFields: typing.Optional[typing.List[str]] = None,
) -> typing.Mapping[str, typing.Any]:
try:
obj = mod.getInstance()
gui = {i['name']: i['gui']['type'] for i in obj.guiDescription()}
values = obj.valuesDict()
# Remove password fields
for k, v in gui.items():
if v == 'password':
values[k] = '********'
# Some names are know "secret data"
for i in ('serverCertificate', 'privateKey'):
if i in values:
values[i] = '********'
# remove removable fields
for i in removableFields or []:
if i in values:
del values[i]
# Append typeName to list
values['typeName'] = str(obj.typeName)
values['comments'] = mod.comments
return values
except Exception:
return {}
def getSerializedFromModel(
mod: 'dbmodels.Model',
removableFields: typing.Optional[typing.List[str]] = None,
passwordFields: typing.Optional[typing.List[str]] = None,
) -> typing.Mapping[str, typing.Any]:
removableFields = removableFields or []
passwordFields = passwordFields or []
try:
values = mod._meta.managers[0].filter(pk=mod.pk).values()[0]
for i in ['uuid', 'id'] + removableFields:
if i in values:
del values[i]
for i in passwordFields:
if i in values:
values[i] = '********'
return values
except Exception:
return {}
class Command(BaseCommand):
help = "Outputs all UDS Trees of elements in YAML format"
def add_arguments(self, parser):
parser.add_argument(
'--all-userservices',
action='store_true',
dest='alluserservices',
default=False,
help='Shows ALL user services, not just the ones with errors',
)
# Maximum items allowed for groups and user services
parser.add_argument(
'--max-items',
action='store',
dest='maxitems',
default=100,
help='Maximum elements exported for groups and user services',
)
def handle(self, *args, **options):
logger.debug("Show Tree")
# firt, genertate Provider-service-servicepool tree
cntr = 0
def counter(s: str) -> str:
nonlocal cntr
cntr += 1
return f'{cntr:02d}.-{s}'
max_items = int(options['maxitems'])
tree = {}
try:
providers = {}
for provider in models.Provider.objects.all():
services = {}
totalServices = 0
totalServicePools = 0
totalUserServices = 0
for service in provider.services.all():
servicePools = {}
numberOfServicePools = 0
numberOfUserServices = 0
for servicePool in service.deployedServices.all():
# get assigned user services with ERROR status
userServices = {}
fltr = servicePool.userServices.all()
if not options['alluserservices']:
fltr = fltr.filter(state=State.ERROR)
for item in fltr[:max_items]: # at most max_items items
logs = [
'{}: {} [{}] - {}'.format(
l['date'],
log.logStrFromLevel(l['level']),
l['source'],
l['message'],
)
for l in log.getLogs(item)
]
userServices[item.friendly_name] = {
'_': {
'id': item.uuid,
'unique_id': item.unique_id,
'friendly_name': item.friendly_name,
'state': State.toString(item.state),
'os_state': State.toString(item.os_state),
'state_date': item.state_date,
'creation_date': item.creation_date,
'revision': item.publication
and item.publication.revision
or '',
'is_cache': item.cache_level != 0,
'ip': item.getProperty('ip', 'unknown'),
'actor_version': item.getProperty(
'actor_version', 'unknown'
),
},
'logs': logs,
}
numberOfUserServices = len(userServices)
totalUserServices += numberOfUserServices
# get publications
publications = {}
for publication in servicePool.publications.all():
# Get all changelogs for this publication
try:
changelogs = models.ServicePoolPublicationChangelog.objects.filter(
publication=publication
).values(
'stamp', 'revision', 'log'
)
changelogs = list(changelogs)
except Exception:
changelogs = []
publications[publication.revision] = getSerializedFromModel(
publication, ['data']
)
publications[publication.revision][
'changelogs'
] = changelogs
# get assigned groups
groups = []
for group in servicePool.assignedGroups.all():
groups.append(group.pretty_name)
# get calendar actions
calendarActions = {}
for calendarAction in models.CalendarAction.objects.filter(
service_pool=servicePool
):
calendarActions[calendarAction.calendar.name] = {
'action': calendarAction.action,
'params': calendarAction.prettyParams,
'at_start': calendarAction.at_start,
'events_offset': calendarAction.events_offset,
'last_execution': calendarAction.last_execution,
'next_execution': calendarAction.next_execution,
}
# get calendar access
calendarAccess = {}
for ca in models.CalendarAccess.objects.filter(
service_pool=servicePool
):
calendarAccess[ca.calendar.name] = ca.access
servicePools[f'{servicePool.name} ({numberOfUserServices})'] = {
'_': getSerializedFromModel(servicePool),
'userServices': userServices,
'calendarAccess': calendarAccess,
'calendarActions': calendarActions,
'groups': groups,
'publications': publications,
}
numberOfServicePools = len(servicePools)
totalServicePools += numberOfServicePools
services[f'{service.name} ({numberOfServicePools}, {numberOfUserServices})'] = {
'_': getSerializedFromManagedObject(service),
'servicePools': servicePools,
}
totalServices += len(services)
providers[
f'{provider.name} ({totalServices}, {totalServicePools}, {totalUserServices})'
] = {
'_': getSerializedFromManagedObject(provider),
'services': services,
}
tree[counter('PROVIDERS')] = providers
# authenticators
authenticators = {}
for authenticator in models.Authenticator.objects.all():
# Groups
groups = {}
for group in authenticator.groups.all()[:max_items]: # at most max_items items
groups[group.name] = getSerializedFromModel(group, ['manager_id', 'name'])
authenticators[authenticator.name] = {
'_': getSerializedFromManagedObject(authenticator),
'groups': groups,
}
tree[counter('AUTHENTICATORS')] = authenticators
# transports
transports = {}
for transport in models.Transport.objects.all():
transports[transport.name] = getSerializedFromManagedObject(transport)
tree[counter('TRANSPORTS')] = transports
# Networks
networks = {}
for network in models.Network.objects.all():
networks[network.name] = {
'networks': network.net_string,
'transports': [t.name for t in network.transports.all()],
}
tree[counter('NETWORKS')] = networks
# os managers
osManagers = {}
for osManager in models.OSManager.objects.all():
osManagers[osManager.name] = getSerializedFromManagedObject(osManager)
tree[counter('OSMANAGERS')] = osManagers
# calendars
calendars = {}
for calendar in models.Calendar.objects.all():
# calendar rules
rules = {}
for rule in models.CalendarRule.objects.filter(calendar=calendar):
rules[rule.name] = getSerializedFromModel(
rule, ['calendar_id', 'name']
)
calendars[calendar.name] = {
'_': getSerializedFromModel(calendar),
'rules': rules,
}
tree[counter('CALENDARS')] = calendars
# Metapools
metapools = {}
for metapool in models.MetaPool.objects.all():
metapools[metapool.name] = getSerializedFromModel(metapool)
tree[counter('METAPOOLS')] = metapools
# accounts
accounts = {}
for account in models.Account.objects.all():
accounts[account.name] = {
'_': getSerializedFromModel(account),
'usages': list(
account.usages.all().values(
'user_name', 'pool_name', 'start', 'end'
)
),
}
tree[counter('ACCOUNTS')] = accounts
# Service pool groups
servicePoolGroups = {}
for servicePoolGroup in models.ServicePoolGroup.objects.all():
servicePoolGroups[servicePoolGroup.name] = {
'comments': servicePoolGroup.comments,
'servicePools': [sp.name for sp in servicePoolGroup.servicesPools.all()], # type: ignore
}
tree[counter('SERVICEPOOLGROUPS')] = servicePoolGroups
# Gallery
gallery = {}
for galleryItem in models.Image.objects.all():
gallery[galleryItem.name] = {
'size': f'{galleryItem.width}x{galleryItem.height}',
'stamp': galleryItem.stamp,
'length': len(galleryItem.data),
}
tree[counter('GALLERY')] = gallery
# Actor tokens
actorTokens = {}
for actorToken in models.ActorToken.objects.all():
actorTokens[actorToken.hostname] = getSerializedFromModel(
actorToken, passwordFields=['token']
)
tree[counter('ACTORTOKENS')] = actorTokens
# Tunnel tokens
tunnelTokens = {}
for tunnelToken in models.TunnelToken.objects.all():
tunnelTokens[tunnelToken.hostname] = getSerializedFromModel(
tunnelToken, passwordFields=['token']
)
tree[counter('TUNNELTOKENS')] = tunnelTokens
self.stdout.write(yaml.safe_dump(tree, default_flow_style=False))
except Exception as e:
print('The command could not be processed: {}'.format(e))
logger.exception('Exception processing %s', args)

View File

@@ -0,0 +1,29 @@
# -*- 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. 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.
from . import mfa

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,280 @@
# -*- 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. 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
"""
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from re import T
import smtplib
import ssl
import typing
import logging
from django.utils.translation import gettext_noop as _, gettext
from uds import models
from uds.core import mfas
from uds.core.ui import gui
from uds.core.util import validators, decorators
if typing.TYPE_CHECKING:
from uds.core.module import Module
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
class EmailMFA(mfas.MFA):
typeName = _('Email Multi Factor')
typeType = 'emailMFA'
typeDescription = _('Email Multi Factor Authenticator')
iconFile = 'mail.png'
hostname = gui.TextField(
length=128,
label=_('SMTP Host'),
order=1,
tooltip=_(
'SMTP Server hostname or IP address. If you are using a '
'non-standard port, add it after a colon, for example: '
'smtp.gmail.com:587'
),
required=True,
tab=_('SMTP Server'),
)
security = gui.ChoiceField(
label=_('Security'),
tooltip=_('Security protocol to use'),
values={
'tls': _('TLS'),
'ssl': _('SSL'),
'none': _('None'),
},
order=2,
required=True,
tab=_('SMTP Server'),
)
username = gui.TextField(
length=128,
label=_('Username'),
order=9,
tooltip=_('User with access to SMTP server'),
required=False,
defvalue='',
tab=_('SMTP Server'),
)
password = gui.PasswordField(
lenth=128,
label=_('Password'),
order=10,
tooltip=_('Password of the user with access to SMTP server'),
required=False,
defvalue='',
tab=_('SMTP Server'),
)
emailSubject = gui.TextField(
length=128,
defvalue='Verification Code',
label=_('Subject'),
order=3,
tooltip=_('Subject of the email'),
required=True,
tab=_('Config'),
)
fromEmail = gui.TextField(
length=128,
label=_('From Email'),
order=11,
tooltip=_('Email address that will be used as sender'),
required=True,
tab=_('Config'),
)
enableHTML = gui.CheckBoxField(
label=_('Enable HTML'),
order=13,
tooltip=_('Enable HTML in emails'),
defvalue=True,
tab=_('Config'),
)
allowLoginWithoutMFA = gui.ChoiceField(
label=_('Policy for users without MFA support'),
order=31,
defaultValue='0',
tooltip=_('Action for SMS response error'),
required=True,
values={
'0': _('Allow user login'),
'1': _('Deny user login'),
'2': _('Allow user to login if it IP is in the networks list'),
'3': _('Deny user to login if it IP is in the networks list'),
},
tab=_('Config'),
)
networks = gui.MultiChoiceField(
label=_('SMS networks'),
rdonly=False,
rows=5,
order=32,
tooltip=_('Networks for SMS authentication'),
required=True,
tab=_('Config'),
)
def initialize(self, values: 'Module.ValuesType' = None):
"""
We will use the "autosave" feature for form fields
"""
if not values:
return
# check hostname for stmp server si valid and is in the right format
# that is a hostname or ip address with optional port
# if hostname is not valid, we will raise an exception
hostname = self.hostname.cleanStr()
if not hostname:
raise EmailMFA.ValidationException(_('Invalid SMTP hostname'))
# Now check is valid format
if ':' in hostname:
host, port = validators.validateHostPortPair(hostname)
self.hostname.value = '{}:{}'.format(host, port)
else:
host = self.hostname.cleanStr()
self.hostname.value = validators.validateHostname(
host, 128, asPattern=False
)
# now check from email and to email
self.fromEmail.value = validators.validateEmail(self.fromEmail.value)
def html(self, request: 'ExtendedHttpRequest') -> str:
return gettext('Check your mail. You will receive an email with the verification code')
@classmethod
def initClassGui(cls) -> None:
# Populate the networks list
cls.networks.setValues([
gui.choiceItem(v.uuid, v.name)
for v in models.Network.objects.all().order_by('name') if v.uuid
])
def checkAction(self, action: str, request: 'ExtendedHttpRequest') -> bool:
def checkIp() -> bool:
return any(i.ipInNetwork(request.ip) for i in models.Network.objects.filter(uuid__in = self.networks.value))
if action == '0':
return True
elif action == '1':
return False
elif action == '2':
return checkIp()
elif action == '3':
return not checkIp()
else:
return False
def emptyIndentifierAllowedToLogin(self, request: 'ExtendedHttpRequest') -> bool:
return self.checkAction(self.allowLoginWithoutMFA.value, request)
def label(self) -> str:
return 'OTP received via email'
@decorators.threaded
def doSendCode(self, request: 'ExtendedHttpRequest', identifier: str, code: str) -> None:
# Send and email with the notification
with self.login() as smtp:
try:
# Create message container
msg = MIMEMultipart('alternative')
msg['Subject'] = self.emailSubject.cleanStr()
msg['From'] = self.fromEmail.cleanStr()
msg['To'] = identifier
msg.attach(MIMEText(f'A login attemt has been made from {request.ip}.\nTo continue, provide the verification code {code}', 'plain'))
if self.enableHTML.value:
msg.attach(MIMEText(f'<p>A login attemt has been made from <b>{request.ip}</b>.</p><p>To continue, provide the verification code <b>{code}</b></p>', 'html'))
smtp.sendmail(self.fromEmail.value, identifier, msg.as_string())
except smtplib.SMTPException as e:
logger.error('Error sending email: {}'.format(e))
raise
def sendCode(self, request: 'ExtendedHttpRequest', userId: str, username: str, identifier: str, code: str) -> mfas.MFA.RESULT:
self.doSendCode(request, identifier, code,)
return mfas.MFA.RESULT.OK
def login(self) -> smtplib.SMTP:
"""
Login to SMTP server
"""
host = self.hostname.cleanStr()
if ':' in host:
host, ports = host.split(':')
port = int(ports)
else:
port = None
if self.security.value in ('tls', 'ssl'):
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
if self.security.value == 'tls':
if port:
smtp = smtplib.SMTP(
host,
port,
)
else:
smtp = smtplib.SMTP(host)
smtp.starttls(context=context)
else:
if port:
smtp = smtplib.SMTP_SSL(host, port, context=context)
else:
smtp = smtplib.SMTP_SSL(host, context=context)
else:
if port:
smtp = smtplib.SMTP(host, port)
else:
smtp = smtplib.SMTP(host)
if self.username.value and self.password.value:
smtp.login(self.username.value, self.password.value)
return smtp

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