From f596af90ba4cc25f77c4f7bcc88e8881d22bec04 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Mon, 11 Oct 2021 11:10:56 -0400 Subject: [PATCH 001/106] wip fixing downloadurl --- scripts_wip/Win_Dell_Command_Install.ps1 | 39 +++++++++++++----------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/scripts_wip/Win_Dell_Command_Install.ps1 b/scripts_wip/Win_Dell_Command_Install.ps1 index 89f81caefa..2ef13a57e9 100644 --- a/scripts_wip/Win_Dell_Command_Install.ps1 +++ b/scripts_wip/Win_Dell_Command_Install.ps1 @@ -1,31 +1,34 @@ -$Source = "https://davidthegeek.com/utils/dell/DCU_4.3.0.EXE" +$Source = "$downloadurl" $SourceDownloadLocation = "C:\temp\Dell_Command_Update_4.3" $SourceInstallFile = "$SourceDownloadLocation\DCU_Setup_4_3_0.exe" $ProgressPreference = 'SilentlyContinue' If (Test-Path -Path $SourceInstallFile -PathType Leaf) { - $proc = Start-Process "$SourceInstallFile" -ArgumentList "/s" -PassThru - Wait-Process -InputObject $proc - if ($proc.ExitCode -ne 0) { - Write-Warning "Exited with error code: $($proc.ExitCode)" - }else{ - Write-Output "Successful install with exit code: $($proc.ExitCode)" - } + $proc = Start-Process "$SourceInstallFile" -ArgumentList "/s" -PassThru + Wait-Process -InputObject $proc + if ($proc.ExitCode -ne 0) { + Write-Warning "Exited with error code: $($proc.ExitCode)" + } + else { + Write-Output "Successful install with exit code: $($proc.ExitCode)" + } -}else{ +} +else { - New-Item -Path $SourceDownloadLocation -ItemType directory - Invoke-WebRequest $Source -OutFile $SourceInstallFile + New-Item -Path $SourceDownloadLocation -ItemType directory + Invoke-WebRequest $Source -OutFile $SourceInstallFile - $proc = Start-Process "$SourceInstallFile" -ArgumentList "/s" -PassThru - Wait-Process -InputObject $proc - if ($proc.ExitCode -ne 0) { - Write-Warning "Exited with error code: $($proc.ExitCode)" - }else{ - Write-Output "Successful install with exit code: $($proc.ExitCode)" - } + $proc = Start-Process "$SourceInstallFile" -ArgumentList "/s" -PassThru + Wait-Process -InputObject $proc + if ($proc.ExitCode -ne 0) { + Write-Warning "Exited with error code: $($proc.ExitCode)" + } + else { + Write-Output "Successful install with exit code: $($proc.ExitCode)" + } } \ No newline at end of file From ce00481f47ac42117ec3dd0abf34efb120cef5be Mon Sep 17 00:00:00 2001 From: silversword411 Date: Tue, 12 Oct 2021 02:02:42 -0400 Subject: [PATCH 002/106] docs fix typo and domain name reference consistency --- .../contributing_using_a_remote_server.md | 6 ++-- docs/docs/functions/django_admin.md | 2 +- docs/docs/functions/email_alert.md | 6 ++-- docs/docs/securing_nginx.md | 4 +-- docs/docs/tipsntricks.md | 2 +- docs/docs/troubleshooting.md | 2 +- docs/docs/unsupported_scripts.md | 20 +++++++------ .../unsupported_synology_docker_install.md | 8 ++--- docs/migration-0.3.0.md | 30 +++++++++---------- 9 files changed, 41 insertions(+), 39 deletions(-) diff --git a/docs/docs/contributing_using_a_remote_server.md b/docs/docs/contributing_using_a_remote_server.md index b49f20b3a4..8bf473c2e3 100644 --- a/docs/docs/contributing_using_a_remote_server.md +++ b/docs/docs/contributing_using_a_remote_server.md @@ -38,8 +38,8 @@ sudo systemctl disable --now rmm.service && sudo systemctl disable --now daphne. Open /rmm/web/.env and make it look like the following ```bash -DEV_URL = "http://api.domain.com:8000" -APP_URL = "http://rmm.domain.com:8080" +DEV_URL = "http://api.EXAMPLE.COM:8000" +APP_URL = "http://rmm.EXAMPLE.COM:8080" ``` Open /rmm/api/tacticalrmm/tacticalrmm/local_settings.py @@ -58,7 +58,7 @@ CORS_ORIGIN_ALLOW_ALL = True Add the following to the ALLOWED HOSTS ```bash -rmm.doamin.com +rmm.EXAMPLE.COM ``` cd /rmm/api/tacticalrmm/ diff --git a/docs/docs/functions/django_admin.md b/docs/docs/functions/django_admin.md index b841f03d6b..8cf813023c 100644 --- a/docs/docs/functions/django_admin.md +++ b/docs/docs/functions/django_admin.md @@ -11,7 +11,7 @@ To enable it, edit `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` and chan Login to the django admin using the same credentials as your normal web ui login. -If you did not save the django admin url (which was printed out at the end of the install script), check the `local_settings.py` file referenced above for the `ADMIN_URL` variable. Then simply append the value of this variable to your api domain (`https://api.yourdomain.com/`) to get the full url. +If you did not save the django admin url (which was printed out at the end of the install script), check the `local_settings.py` file referenced above for the `ADMIN_URL` variable. Then simply append the value of this variable to your api domain (`https://api.EXAMPLE.COM/`) to get the full url. Example of a full django admin url: ``` diff --git a/docs/docs/functions/email_alert.md b/docs/docs/functions/email_alert.md index e312513930..65f358f845 100644 --- a/docs/docs/functions/email_alert.md +++ b/docs/docs/functions/email_alert.md @@ -10,8 +10,8 @@ MS 365 in this example 2. Go to Settings 3. Go to Global Settings 4. Click on Alerts -5. Enter the email address (or addresses) you want to receive alerts to eg info@mydomain.com -6. Enter the from email address (this will need to be part of your domain on 365, however it doesn’t need a license) eg rmm@mydomain.com +5. Enter the email address (or addresses) you want to receive alerts to eg info@EXAMPLE.COM +6. Enter the from email address (this will need to be part of your domain on 365, however it doesn’t need a license) eg rmm@EXAMPLE.COM 7. Go to MXToolbox.com and enter your domain name in, copy the hostname from there and paste into Host 8. Change the port to 25 9. Click Save @@ -37,7 +37,7 @@ Gmail in this example 2. Go to Settings 3. Go to Global Settings 4. Click on Alerts -5. Enter the email address (or addresses) you want to receive alerts to eg info@mydomain.com +5. Enter the email address (or addresses) you want to receive alerts to eg info@EXAMPLE.COM 6. Enter the from email address myrmm@gmail.com 7. Tick the box “My server requires Authentication” 8. Enter your username e.g. myrmm@gmail.com diff --git a/docs/docs/securing_nginx.md b/docs/docs/securing_nginx.md index c6c38504a1..3d27baa0b4 100644 --- a/docs/docs/securing_nginx.md +++ b/docs/docs/securing_nginx.md @@ -362,7 +362,7 @@ Create a .conf file under “/etc/nginx/modsec/coreruleset/rules” named “RMM ```conf #ADMIN UI/FRONTEND ACCESS - DENY BY DEFAULT, ALLOW BY EXCEPTION -SecRule SERVER_NAME "rmm.yourdomain.com" "id:1001,phase:1,nolog,msg:'Remote IP Not allowed',deny,chain" +SecRule SERVER_NAME "rmm.EXAMPLE.COM" "id:1001,phase:1,nolog,msg:'Remote IP Not allowed',deny,chain" ### ALLOWED PUBLIC IP 1 ######### SecRule REMOTE_ADDR "!@eq IP1" chain ### ALLOWED PUBLIC IP 2 ######### @@ -383,7 +383,7 @@ SecRule REQUEST_URI "@beginsWith /api/v3/winupdates" "chain,id:'1007',phase:1,t: SecRule REQUEST_METHOD "POST" ##REQUIRED FOR MANAGEMENT ACTIONS FROM ADMIN/FRONT-END UI. WHITELIST BY REFERRER's URL -SecRule REQUEST_HEADERS:REFERER "https://rmm.yourdomain.com/" "id:1008,phase:1,nolog,ctl:ruleRemoveById=920170,allow" +SecRule REQUEST_HEADERS:REFERER "https://rmm.EXAMPLE.COM/" "id:1008,phase:1,nolog,ctl:ruleRemoveById=920170,allow" #REQUIRED FOR NEW CLIENTS TO CONNECT TO MESH SERVICE WHILE INSTALLING THE AGENT SecRule REQUEST_URI "@beginsWith /api/v3/meshexe" "id:1009,phase:1,nolog,ctl:ruleRemoveById=920170,allow" diff --git a/docs/docs/tipsntricks.md b/docs/docs/tipsntricks.md index f88e01c384..9f8513dfea 100644 --- a/docs/docs/tipsntricks.md +++ b/docs/docs/tipsntricks.md @@ -27,7 +27,7 @@ Right-click the connect button in *Take Control* for connect options !!!note These settings are independant of Tactical RMM. Enable features (like auto remove inactive devices) with caution -1. Remote background a machine then go to mesh.yourdomain.com +1. Remote background a machine then go to mesh.EXAMPLE.COM 2. Click on My Account 3. Click on the device group you want to enable notifications or accept connection etc on (probably TacticalRMM) 4. Next to User Consent click edit (the wee pencil) diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index d75c55a930..42d511320e 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -156,7 +156,7 @@ Are you trying to use a proxy to share your single public IP with multiple servi ## Mesh Agent x86 x64 integration with TRMM 1. Log into Mesh (you can right-click any agent, choose remote control or Remote Background) -2. Goto your mesh interface (eg `https://mesh.domain.com`) +2. Goto your mesh interface (eg `https://mesh.EXAMPLE.COM`) 3. Find your TacticalRMM group 4. Click the add link 5. Download both agents diff --git a/docs/docs/unsupported_scripts.md b/docs/docs/unsupported_scripts.md index 01be065f79..73d055897f 100644 --- a/docs/docs/unsupported_scripts.md +++ b/docs/docs/unsupported_scripts.md @@ -3,6 +3,8 @@ !!!note These are not supported scripts/configurations by Tactical RMM, but it's provided here for your reference. + Although these aren't officially supported configurations, we generally will help point you in the right direction. Please use the Discord [#unsupported channel](https://discord.com/channels/736478043522072608/888474319750066177) to discuss issues replated to these complex installations + ## General Notes on Proxies and Tactical RMM ### Port 443 @@ -201,19 +203,19 @@ You need to add the certificate private key and public keys to the following fil 2. Now move your certs into that folder. -3. Open the api file and add the api certificate or if its a wildcard the directory should be `/certs/yourdomain.com/` +3. Open the api file and add the api certificate or if its a wildcard the directory should be `/certs/EXAMPLE.COM/` sudo nano /etc/nginx/sites-available/rmm.conf replace - ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/EXAMPLE.COM/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/EXAMPLE.COM/privkey.pem; with - ssl_certificate /certs/api.yourdomain.com/fullchain.pem; - ssl_certificate_key /certs/api.yourdomain.com/privkey.pem; + ssl_certificate /certs/api.EXAMPLE.COM/fullchain.pem; + ssl_certificate_key /certs/api.EXAMPLE.COM/privkey.pem; 4. Repeat the process for @@ -228,8 +230,8 @@ You need to add the certificate private key and public keys to the following fil add - CERT_FILE = "/certs/api.yourdomain.com/fullchain.pem" - KEY_FILE = "/certs/api.yourdomain.com/privkey.pem" + CERT_FILE = "/certs/api.EXAMPLE.COM/fullchain.pem" + KEY_FILE = "/certs/api.EXAMPLE.COM/privkey.pem" 6. Regenerate Nats Conf @@ -244,7 +246,7 @@ You need to add the certificate private key and public keys to the following fil ## Use certbot to do acme challenge over http -The standard SSL cert process in Tactical uses a [DNS challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) that requires dns txt files to be updated with every run. +The standard SSL cert process in Tactical uses a [DNS challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) that requires dns txt files to be updated in your public DNS with every cert renewal. The below script uses [http challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) on the 3 separate ssl certs, one for each subdomain: rmm, api, mesh. They still have the same 3 month expiry. Restart the Tactical RMM server about every 2.5 months (80 days) for auto-renewed certs to become active. @@ -546,7 +548,7 @@ Let's Encrypt is the only officially supported method of obtaining wildcard cert If you are providing your own publicly signed certificates, ensure you download the **full chain** (combined CA/Root + Intermediary) certificate in pem format. If certificates are not provided, a self-signed certificate will be generated and most agent functions won't work. -## Restricting Access to rmm.yourdomain.com +## Restricting Access to rmm.EXAMPLE.COM ### Using DNS diff --git a/docs/docs/unsupported_synology_docker_install.md b/docs/docs/unsupported_synology_docker_install.md index 28c62d776f..9eded7c810 100644 --- a/docs/docs/unsupported_synology_docker_install.md +++ b/docs/docs/unsupported_synology_docker_install.md @@ -28,11 +28,11 @@ For the entries related to the mesh, add some custom headers and adjust the prox In regards to the certificate, I followed this [tutorial](https://www.nas-forum.com/forum/topic/68046-tuto-certificat-lets-encrypt-avec-acmesh-api-ovh-en-docker-dsm67-update-180621) (in french but still clear after translation) to automatically update it and manually updating it on the NAS and in TRMM ```bash -docker exec Acme sh -c "acme.sh --issue --keylength 4096 -d '*.mydomain.com' --dns dns_provider" +docker exec Acme sh -c "acme.sh --issue --keylength 4096 -d '*.EXAMPLE.COM' --dns dns_provider" sed -i '/CERT_PUB_KEY/d' /path/to/tactical/.env sed -i '/CERT_PRIV_KEY/d' /path/to/tactical/.env -echo "CERT_PUB_KEY=$(sudo base64 -w 0 /volume1/docker/acme/\*.mydomain.com/fullchain.cer)" >> /path/to/tactical/.env -echo "CERT_PRIV_KEY=$(sudo base64 -w 0 /volume1/docker/acme/\*.mydomain.com/*.whitesnew.com.key)" >> /path/to/tactical/.env -docker exec Acme sh -c "acme.sh --deploy -d '*.mydomain.com' --deploy-hook synology_provider" +echo "CERT_PUB_KEY=$(sudo base64 -w 0 /volume1/docker/acme/\*.EXAMPLE.COM/fullchain.cer)" >> /path/to/tactical/.env +echo "CERT_PRIV_KEY=$(sudo base64 -w 0 /volume1/docker/acme/\*.EXAMPLE.COM/*.whitesnew.com.key)" >> /path/to/tactical/.env +docker exec Acme sh -c "acme.sh --deploy -d '*.EXAMPLE.COM' --deploy-hook synology_provider" docker-compose -f /path/to/tactical/docker-compose.yml restart ``` diff --git a/docs/migration-0.3.0.md b/docs/migration-0.3.0.md index 17e791d9fc..22bea7c50e 100644 --- a/docs/migration-0.3.0.md +++ b/docs/migration-0.3.0.md @@ -52,18 +52,18 @@ map $http_user_agent $ignore_ua { server { listen 80; - server_name api.EXAMPLE.com; + server_name api.EXAMPLE.COM; return 301 https://$server_name$request_uri; } server { listen 443 ssl; - server_name api.yourdomain.com; + server_name api.EXAMPLE.COM; client_max_body_size 300M; access_log /rmm/api/tacticalrmm/tacticalrmm/private/log/access.log combined if=$ignore_ua; error_log /rmm/api/tacticalrmm/tacticalrmm/private/log/error.log; - ssl_certificate /etc/letsencrypt/live/EXAMPLE.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/EXAMPLE.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/EXAMPLE.COM/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/EXAMPLE.COM/privkey.pem; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; location /static/ { @@ -72,19 +72,19 @@ server { location /private/ { internal; - add_header "Access-Control-Allow-Origin" "https://rmm.EXAMPLE.com"; + add_header "Access-Control-Allow-Origin" "https://rmm.EXAMPLE.COM"; alias /rmm/api/tacticalrmm/tacticalrmm/private/; } location /saltscripts/ { internal; - add_header "Access-Control-Allow-Origin" "https://rmm.EXAMPLE.com"; + add_header "Access-Control-Allow-Origin" "https://rmm.EXAMPLE.COM"; alias /srv/salt/scripts/userdefined/; } location /builtin/ { internal; - add_header "Access-Control-Allow-Origin" "https://rmm.EXAMPLE.com"; + add_header "Access-Control-Allow-Origin" "https://rmm.EXAMPLE.COM"; alias /srv/salt/scripts/; } @@ -110,7 +110,7 @@ server { ```bash server { listen 80; - server_name mesh.EXAMPLE.com; + server_name mesh.EXAMPLE.COM; return 301 https://$server_name$request_uri; } @@ -118,9 +118,9 @@ server { listen 443 ssl; proxy_send_timeout 330s; proxy_read_timeout 330s; - server_name mesh.example.com; - ssl_certificate /etc/letsencrypt/live/EXAMPLE.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/EXAMPLE.com/privkey.pem; + server_name mesh.EXAMPLE.COM; + ssl_certificate /etc/letsencrypt/live/EXAMPLE.COM/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/EXAMPLE.COM/privkey.pem; ssl_session_cache shared:WEBSSL:10m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; @@ -139,11 +139,11 @@ server { } ``` -4. Edit `/meshcentral/meshcentral-data/config.json` and change to match the example below. Replace `mesh.example.com` with your mesh domain. After editing, use a json linter like `https://jsonlint.com/` to verify no syntax errors, otherwise meshcentral will fail to start. +4. Edit `/meshcentral/meshcentral-data/config.json` and change to match the example below. Replace `mesh.EXAMPLE.COM` with your mesh domain. After editing, use a json linter like `https://jsonlint.com/` to verify no syntax errors, otherwise meshcentral will fail to start. ``` { "settings": { - "Cert": "mesh.example.com", + "Cert": "mesh.EXAMPLE.COM", "MongoDb": "mongodb://127.0.0.1:27017", "MongoDbName": "meshcentral", "WANonly": true, @@ -168,7 +168,7 @@ server { "Title": "Tactical RMM", "Title2": "Tactical RMM", "NewAccounts": false, - "CertUrl": "https://mesh.example.com:443/", + "CertUrl": "https://mesh.EXAMPLE.COM:443/", "GeoLocation": true, "CookieIpCheck": false, "mstsc": true @@ -241,7 +241,7 @@ sudo nginx -t 10. Edit `/etc/hosts` and make sure the line starting with 127.0.1.1 or 127.0.0.1 has your 3 subdomains in it like this: ```bash 127.0.0.1 localhost -127.0.1.1 yourservername api.example.com rmm.example.com mesh.example.com +127.0.1.1 yourservername api.EXAMPLE.COM rmm.EXAMPLE.COM mesh.EXAMPLE.COM ``` 11. Start services From 4f0eb1d5660e129e76026e1e6ad005430390b915 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Tue, 12 Oct 2021 02:17:47 -0400 Subject: [PATCH 003/106] docs fix formatting --- docs/docs/unsupported_scripts.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/docs/unsupported_scripts.md b/docs/docs/unsupported_scripts.md index 73d055897f..8366e5c2b4 100644 --- a/docs/docs/unsupported_scripts.md +++ b/docs/docs/unsupported_scripts.md @@ -550,6 +550,8 @@ If you are providing your own publicly signed certificates, ensure you download ## Restricting Access to rmm.EXAMPLE.COM +Limit access to Tactical RMM's administration panel in nginx to specific locations + ### Using DNS 1. Create a file allowed-domain.list which contains the DNS names you want to grant access to your rmm: @@ -637,6 +639,7 @@ If you are providing your own publicly signed certificates, ensure you download 1. Create a file containg the fixed IP address (where xxx.xxx.xxx.xxx must be replaced by your real IP address) Edit `/etc/nginx//allowed-ips.conf` + # Private IP address allow 192.168.0.0/16; allow 172.16.0.0/12; From 9e7459b2047d7811da79ac4492162cba6d64ea29 Mon Sep 17 00:00:00 2001 From: Jaune Date: Tue, 12 Oct 2021 17:08:38 -0400 Subject: [PATCH 004/106] Add Traefikv2 reverse proxy Should give a good way to use traefikv2 with 1 minor modification to the install script (changing the port) with 2fa support on the mesh side --- docs/docs/unsupported_scripts.md | 164 +++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/docs/docs/unsupported_scripts.md b/docs/docs/unsupported_scripts.md index 01be065f79..f23c4fe00d 100644 --- a/docs/docs/unsupported_scripts.md +++ b/docs/docs/unsupported_scripts.md @@ -17,6 +17,170 @@ For `mesh` see the Section 10. TLS Offloading of the [MeshCentral 2 User Guide]( Is NATS (). You'll need a TCP forwarder as NATS only talks TCP not HTTP. +## Traefikv2 + +This section will assume that by default Traefik will reverse proxy everything on port 443. + +Here is a basic Traefik config with docker-composer note the file.directory and file.watch are important. +```bash +version: "3.7" +services: + traefik: + container_name: traefik24 + image: traefik:v2.4 + restart: unless-stopped + command: + - --entryPoints.http.address=:80 + - --entryPoints.https.address=:443 + - --providers.docker=true + - --providers.docker.endpoint=unix:///var/run/docker.sock + - --providers.docker.defaultrule=HostHeader(`{{ index .Labels "com.docker.compose.service" }}.$DOMAINNAME`) + ## This is important, to load the config for RMM and Mesh + - --providers.file.directory=rules # Load dynamic configuration from one or more .toml or .yml files in a directory. + - --providers.file.watch=true # Only works on top level files in the rules folder + #### + - --certificatesresolvers.dns-cloudflare.acme.dnschallenge=true + - --certificatesResolvers.dns-cloudflare.acme.email=$CLOUDFLARE_EMAIL + - --certificatesResolvers.dns-cloudflare.acme.storage=/acme.json + - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.provider=cloudflare + - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.resolvers=1.1.1.1:53,1.0.0.1:53 + ports: + - target: 80 + published: 80 + protocol: tcp + mode: host + - target: 443 + published: 443 + protocol: tcp + mode: host + volumes: + ##The rules that we will load## + - $USERDIR/docker/traefik2/rules:/rules + ## + - /var/run/docker.sock:/var/run/docker.sock:ro + - $USERDIR/docker/traefik2/acme/acme.json:/acme.json + - $USERDIR/docker/traefik2/traefik.log:/traefik.log + environment: + - CF_API_EMAIL=$CLOUDFLARE_EMAIL + - CF_API_KEY=$CLOUDFLARE_API_KEY + labels: + - "traefik.enable=true" + # HTTP-to-HTTPS Redirect + - "traefik.http.routers.http-catchall.entrypoints=http" + - "traefik.http.routers.http-catchall.rule=HostRegexp(`{host:.+}`)" + - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + # HTTP Routers + - "traefik.http.routers.traefik-rtr.entrypoints=https" + - "traefik.http.routers.traefik-rtr.rule=HostHeader(`traefik.$DOMAINNAME`)" + - "traefik.http.routers.traefik-rtr.tls=true" + - "traefik.http.routers.traefik-rtr.tls.domains[0].main=$DOMAINNAME" + - "traefik.http.routers.traefik-rtr.tls.domains[0].sans=*.$DOMAINNAME" +``` + +Before proceding, we need to change the port 443 to 4430 and 80 to 800 because the port 443 and 80 are alredy used by Traefik. + +Here is a snippet of the only thing you should modify into docker-compose file of the installation. + +```bash + # container for tactical reverse proxy + tactical-nginx: + container_name: trmm-nginx + image: ${IMAGE_REPO}tactical-nginx:${VERSION} + restart: always + environment: + APP_HOST: ${APP_HOST} + API_HOST: ${API_HOST} + MESH_HOST: ${MESH_HOST} + CERT_PUB_KEY: ${CERT_PUB_KEY} + CERT_PRIV_KEY: ${CERT_PRIV_KEY} + networks: + proxy: + ipv4_address: 172.20.0.20 + ports: + - "800:80" ## port 800 instead of 80 + - "4430:443" ## port 4430 instead of 443 +``` +Once save, make sure you run the docker-compose or installation script at least once, so all the directory structure are created. +Once you have your certificate (acme.json) generated by traefikv2 we will be able to extract it for rmm. + +Copy the acme.json create by traefik into the root of your rmm directory (In my case its $USERDIR/docker/rmm) which you should have already define. +After that we can run this docker to extract the certificates for us. +```bash +version: "3.7" +services: +##Copy the acme.json of Traefik2 at volumes: (userdir/docker/rmm in this case) + traefik-certs-dumper: + image: ldez/traefik-certs-dumper:v2.7.4 + entrypoint: sh -c ' + apk add jq + ; while ! [ -e /data/acme.json ] + || ! [ `jq ".[] | .Certificates | length" /data/acme.json` != 0 ]; do + sleep 1 + ; done + && traefik-certs-dumper file --version v2 --watch + --source /data/acme.json --dest data/certs' + volumes: + - $USERDIR/docker/rmm:/data +``` +Once completed, you should have 1 new folder into you rmm directory $USERDIR/docker/rmm/**certs** in this example. +As the installation instruction, we will pass those to the .env + +```bash +echo "CERT_PUB_KEY=$(sudo base64 -w 0 $USERDIR/docker/rmm/certs/certs/**yourdomaine.com.crt**)" >> .env +echo "CERT_PRIV_KEY=$(sudo base64 -w 0 $USERDIR/docker/rmm/certs/private/**yourdomaine.com.key**)" >> .env +``` + +Next we can create 2 rules to tell traefik to correctly route the https and agent +For that we will create 2 rules into traefik directory as per it configuration. folder/traefik/rules + +create +```bash +nano app-mesh.toml +``` +and inside it we add +```bash +[http.routers] + [http.routers.mesh-rtr] + entryPoints = ["https"] + rule = "Host(`mesh.**yourdomaine.com**`)" + service = "mesh-svc" +##middleware with 2fa +[http.services] + [http.services.mesh-svc] + [http.services.mesh-svc.loadBalancer] + passHostHeader = true + [[http.services.mesh-svc.loadBalancer.servers]] + url = "https://**xxx.xxx.xxx.xxx**:4430" # or whatever your external host's IP is +``` + + +create +```bash +nano app-meshagent.toml +``` +and inside it we add + +```bash +[http.routers] + [http.routers.mesh-rtr1] + entryPoints = ["https"] + rule = """Host(`mesh.**yourdomaine.com**`) && + PathPrefix( `/agent.ashx`, `/meshrelay.ashx`, ) && + Headers(`X-Forwarded-Proto`, `wss`) """ + ##Don't add middle where, the agent wont work. +[http.services] + [http.services.mesh-svc1] + [http.services.mesh-svc.loadBalancer] + passHostHeader = true + [[http.services.mesh-svc1.loadBalancer.servers]] + url = "https://**xxx.xxx.xxx.xxx**:4430" # or whatever your external host's IP is + +``` + +That it, you can now restart Tactical rmm and mesh.yourdomain.com should work, same for the agent. +Please note that if you have a middleware with 2FA you can still use it with the inside mesh.toml but do not add it with the agent. + ## HAProxy Check/Change the mesh central config.json, some of the values may be set already, CertUrl must be changed to point to the HAProxy server. From 23fcf3b0458982dd137749342cdd0a63e73f2886 Mon Sep 17 00:00:00 2001 From: Jaune Date: Tue, 12 Oct 2021 20:31:11 -0400 Subject: [PATCH 005/106] add RMM.toml forgot to add i file --- docs/docs/unsupported_scripts.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/docs/unsupported_scripts.md b/docs/docs/unsupported_scripts.md index f23c4fe00d..d45ee5c72a 100644 --- a/docs/docs/unsupported_scripts.md +++ b/docs/docs/unsupported_scripts.md @@ -131,7 +131,7 @@ echo "CERT_PUB_KEY=$(sudo base64 -w 0 $USERDIR/docker/rmm/certs/certs/**yourdoma echo "CERT_PRIV_KEY=$(sudo base64 -w 0 $USERDIR/docker/rmm/certs/private/**yourdomaine.com.key**)" >> .env ``` -Next we can create 2 rules to tell traefik to correctly route the https and agent +Next we can create 3 rules to tell traefik to correctly route the https and agent For that we will create 2 rules into traefik directory as per it configuration. folder/traefik/rules create @@ -143,7 +143,7 @@ and inside it we add [http.routers] [http.routers.mesh-rtr] entryPoints = ["https"] - rule = "Host(`mesh.**yourdomaine.com**`)" + rule = "Host(`mesh.**yourdomain.com**`)" service = "mesh-svc" ##middleware with 2fa [http.services] @@ -165,7 +165,7 @@ and inside it we add [http.routers] [http.routers.mesh-rtr1] entryPoints = ["https"] - rule = """Host(`mesh.**yourdomaine.com**`) && + rule = """Host(`mesh.**yourdomain.com**`) && PathPrefix( `/agent.ashx`, `/meshrelay.ashx`, ) && Headers(`X-Forwarded-Proto`, `wss`) """ ##Don't add middle where, the agent wont work. @@ -177,6 +177,28 @@ and inside it we add url = "https://**xxx.xxx.xxx.xxx**:4430" # or whatever your external host's IP is ``` +create +```bash +nano app-rmm.toml +``` +and inside it we add + +```bash +[http.routers] + [http.routers.rmm-rtr] + entryPoints = ["https"] + rule = "Host(`rmm.**yourdomain.com**`)" + service = "rmm-svc" + + ##middleware with 2fa + +[http.services] + [http.services.rmm-svc] + [http.services.rmm-svc.loadBalancer] + passHostHeader = true + [[http.services.rmm-svc.loadBalancer.servers]] + url = "https://xxx.xxx.xxx.xxx:4430" # or whatever your external host's IP:port is +``` That it, you can now restart Tactical rmm and mesh.yourdomain.com should work, same for the agent. Please note that if you have a middleware with 2FA you can still use it with the inside mesh.toml but do not add it with the agent. From be12505d2fda3e39753fa1e18cd0846e15d8bc09 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 14 Oct 2021 11:01:00 -0400 Subject: [PATCH 006/106] docs fix tipsntricks pic order --- docs/docs/tipsntricks.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docs/tipsntricks.md b/docs/docs/tipsntricks.md index 9f8513dfea..5218fd29f1 100644 --- a/docs/docs/tipsntricks.md +++ b/docs/docs/tipsntricks.md @@ -30,10 +30,10 @@ Right-click the connect button in *Take Control* for connect options 1. Remote background a machine then go to mesh.EXAMPLE.COM 2. Click on My Account 3. Click on the device group you want to enable notifications or accept connection etc on (probably TacticalRMM) -4. Next to User Consent click edit (the wee pencil) -5. Tick whatever boxes you want in there (Features: Sync server device name to hostname, Automatically remove inactive devices, Notify/Prompt for Consent/Connection Toolbar settings) -6. Click ok - +4. Next to User Consent click edit (the wee pencil)
+![Features](images/mesh_userconsent.png) +5. Then check Tick whatever boxes you want in there (Features: Sync server device name to hostname, Automatically remove inactive devices, Notify/Prompt for Consent/Connection Toolbar settings)
![Features](images/mesh_features.png) -![Features](images/mesh_userconsent.png) +6. Ok your way out + From 88651916b00f037ed0e4b5996b2fa43b2cd63ab5 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 14 Oct 2021 11:07:05 -0400 Subject: [PATCH 007/106] docs fix tipsntricks pic order --- docs/docs/tipsntricks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/tipsntricks.md b/docs/docs/tipsntricks.md index 5218fd29f1..a84b2cdc45 100644 --- a/docs/docs/tipsntricks.md +++ b/docs/docs/tipsntricks.md @@ -32,7 +32,7 @@ Right-click the connect button in *Take Control* for connect options 3. Click on the device group you want to enable notifications or accept connection etc on (probably TacticalRMM) 4. Next to User Consent click edit (the wee pencil)
![Features](images/mesh_userconsent.png) -5. Then check Tick whatever boxes you want in there (Features: Sync server device name to hostname, Automatically remove inactive devices, Notify/Prompt for Consent/Connection Toolbar settings)
+5. You can also change features by ticking whatever boxes you want in there (Features: Sync server device name to hostname, Automatically remove inactive devices, Notify/Prompt for Consent/Connection Toolbar settings)
![Features](images/mesh_features.png) 6. Ok your way out From 32a202aff4103e7b43b6c2e8077abb58aa0ff52c Mon Sep 17 00:00:00 2001 From: silversword411 Date: Mon, 18 Oct 2021 16:01:29 -0400 Subject: [PATCH 008/106] Inverting exit codes --- scripts/Win_Bitlocker_Drive_Check_Status.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/Win_Bitlocker_Drive_Check_Status.ps1 b/scripts/Win_Bitlocker_Drive_Check_Status.ps1 index b58db7d3ca..2d7e8f56d4 100644 --- a/scripts/Win_Bitlocker_Drive_Check_Status.ps1 +++ b/scripts/Win_Bitlocker_Drive_Check_Status.ps1 @@ -23,9 +23,9 @@ if ((Get-BitLockerVolume -MountPoint $Drive).ProtectionStatus -eq 'On') { Start-Sleep -Seconds 5 } until ($EncryptionPercentage -match 100) Write-Output "Bitlocker is enabled and Encryption completed" - Exit 1 + Exit 0 } else { Write-Output "BitLocker is not turned on for this volume!" - Exit 0 -} \ No newline at end of file + Exit 1 +} From 170687226d90f16dd208025eed476579cc41acc4 Mon Sep 17 00:00:00 2001 From: cocorocho Date: Mon, 25 Oct 2021 22:41:03 +0300 Subject: [PATCH 009/106] formatting --- api/tacticalrmm/accounts/permissions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/tacticalrmm/accounts/permissions.py b/api/tacticalrmm/accounts/permissions.py index 55df65df2d..3b02ab6639 100644 --- a/api/tacticalrmm/accounts/permissions.py +++ b/api/tacticalrmm/accounts/permissions.py @@ -36,5 +36,4 @@ def has_permission(self, r, view): class APIKeyPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_manage_api_keys") From f226206703ed3b41ec484e9efc9af1f65c86d0ed Mon Sep 17 00:00:00 2001 From: cocorocho Date: Mon, 25 Oct 2021 23:03:53 +0300 Subject: [PATCH 010/106] change API key creation function --- api/tacticalrmm/accounts/views.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index b6ae6bf53e..154193827f 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -272,15 +272,9 @@ def get(self, request): def post(self, request): # generate a random API Key - # https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits/23728630#23728630 - import random - import string - - request.data["key"] = "".join( - random.SystemRandom().choice(string.ascii_uppercase + string.digits) - for _ in range(32) - ) + from django.utils.crypto import get_random_string + request.data["key"] = get_random_string(length=32).upper() serializer = APIKeySerializer(data=request.data) serializer.is_valid(raise_exception=True) obj = serializer.save() From 5560bbeecbd2e0be0a1c824ba8b5d5720f3430d4 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 29 Oct 2021 06:48:41 -0400 Subject: [PATCH 011/106] scripts - Defender status adding full details --- scripts/Win_Defender_Status_Report.ps1 | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/scripts/Win_Defender_Status_Report.ps1 b/scripts/Win_Defender_Status_Report.ps1 index efe5c5d6ea..f1caf96b52 100644 --- a/scripts/Win_Defender_Status_Report.ps1 +++ b/scripts/Win_Defender_Status_Report.ps1 @@ -1,11 +1,14 @@ <# .Synopsis - Defender - Status Report + Defender - Status Report .DESCRIPTION - This will check Event Log for Windows Defender Malware and Antispyware reports, otherwise will report as Healthy. By default if no command parameter is provided it will check the last 1 day (good for a scheduled daily task). - If a number is provided as a command parameter it will search back that number of days back provided (good for collecting all AV alerts on the computer). + This will check Event Log for Windows Defender Malware and Antispyware reports, otherwise will report as Healthy. By default if no command parameter is provided it will check the last 1 day (good for a scheduled daily task). + If a number is provided as a command parameter it will search back that number of days back provided (good for collecting all AV alerts on the computer). .EXAMPLE - Win_Defender_Status_reports.ps1 365 + Win_Defender_Status_reports.ps1 365 +.NOTES + v1 dinger initial release 2021 + v1.1 bdrayer Adding full message output if items found #> $param1 = $args[0] @@ -20,7 +23,7 @@ else { if (Get-WinEvent -FilterHashtable @{LogName = 'Microsoft-Windows-Windows Defender/Operational'; ID = '1116', '1118', '1015', '1006', '5010', '5012', '5001', '1123'; StartTime = $TimeSpan }) { Write-Output "Virus Found or Issue with Defender" - Get-WinEvent -FilterHashtable @{LogName = 'Microsoft-Windows-Windows Defender/Operational'; ID = '1116', '1118', '1015', '1006', '5010', '5012', '5001', '1123'; StartTime = $TimeSpan } + Get-WinEvent -FilterHashtable @{LogName = 'Microsoft-Windows-Windows Defender/Operational'; ID = '1116', '1118', '1015', '1006', '5010', '5012', '5001', '1123'; StartTime = $TimeSpan } | Format-List TimeCreated, Id, LevelDisplayName, Message exit 1 } @@ -32,4 +35,4 @@ else { } -Exit $LASTEXITCODE +Exit $LASTEXITCODE \ No newline at end of file From a9aedea2bd6cf6c6a1834f16421bd8f30b384dd4 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Tue, 2 Nov 2021 11:28:24 -0400 Subject: [PATCH 012/106] docs Linking to Traefik howto --- docs/docs/unsupported_scripts.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/docs/unsupported_scripts.md b/docs/docs/unsupported_scripts.md index dd5678ed4a..17920a0022 100644 --- a/docs/docs/unsupported_scripts.md +++ b/docs/docs/unsupported_scripts.md @@ -21,6 +21,8 @@ Is NATS (). You'll need a TCP forwarder as NATS only talks TCP ## Traefikv2 +Offsite Resource: + This section will assume that by default Traefik will reverse proxy everything on port 443. Here is a basic Traefik config with docker-composer note the file.directory and file.watch are important. From 322d492540a6a3f0f43c1daa545a352f966fa8f1 Mon Sep 17 00:00:00 2001 From: sadnub Date: Mon, 4 Oct 2021 13:04:29 -0400 Subject: [PATCH 013/106] change dashboard hostname default in env.example to be consistent with docs. Log nginx container access/error logs to stdout/stderr --- docker/.env.example | 2 +- docker/containers/tactical-nginx/entrypoint.sh | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index f6be551a30..a24cb86b59 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -6,7 +6,7 @@ TRMM_USER=tactical TRMM_PASS=tactical # dns settings -APP_HOST=app.example.com +APP_HOST=rmm.example.com API_HOST=api.example.com MESH_HOST=mesh.example.com diff --git a/docker/containers/tactical-nginx/entrypoint.sh b/docker/containers/tactical-nginx/entrypoint.sh index 00e6b8f0cb..f630337227 100644 --- a/docker/containers/tactical-nginx/entrypoint.sh +++ b/docker/containers/tactical-nginx/entrypoint.sh @@ -27,6 +27,7 @@ else fi fi +# increase default nginx worker connections /bin/bash -c "sed -i 's/worker_connections.*/worker_connections ${WORKER_CONNECTIONS};/g' /etc/nginx/nginx.conf" @@ -93,9 +94,6 @@ server { proxy_set_header X-Forwarded-Host \$server_name; } - error_log /var/log/nginx/api-error.log; - access_log /var/log/nginx/api-access.log; - client_max_body_size 300M; listen 443 ssl; @@ -135,9 +133,6 @@ server { proxy_set_header X-Forwarded-Port \$server_port; } - error_log /var/log/nginx/app-error.log; - access_log /var/log/nginx/app-access.log; - listen 443 ssl; ssl_certificate ${CERT_PUB_PATH}; ssl_certificate_key ${CERT_PRIV_PATH}; From 1f77390366df364ee12024693ca6e618eb2650a7 Mon Sep 17 00:00:00 2001 From: sadnub Date: Sat, 9 Oct 2021 22:52:56 -0400 Subject: [PATCH 014/106] agent per site/client permissions initial, uri updates, comp api rework --- .../migrations/0028_auto_20211010_0249.py | 150 ++ api/tacticalrmm/accounts/models.py | 67 +- api/tacticalrmm/accounts/serializers.py | 5 + api/tacticalrmm/accounts/tests.py | 6 +- api/tacticalrmm/accounts/urls.py | 3 +- api/tacticalrmm/accounts/views.py | 19 +- api/tacticalrmm/agents/baker_recipes.py | 3 +- .../migrations/0040_auto_20211010_0249.py | 28 + api/tacticalrmm/agents/models.py | 15 +- api/tacticalrmm/agents/permissions.py | 85 +- api/tacticalrmm/agents/serializers.py | 33 +- api/tacticalrmm/agents/tests.py | 729 ++++++--- api/tacticalrmm/agents/urls.py | 59 +- api/tacticalrmm/agents/views.py | 556 ++++--- .../migrations/0010_auto_20210917_1954.py | 23 + .../migrations/0009_auto_20210917_1954.py | 23 + api/tacticalrmm/automation/permissions.py | 6 +- api/tacticalrmm/automation/serializers.py | 18 +- api/tacticalrmm/automation/tests.py | 18 - api/tacticalrmm/automation/urls.py | 3 +- api/tacticalrmm/automation/views.py | 5 - .../migrations/0023_auto_20210917_1954.py | 23 + api/tacticalrmm/autotasks/models.py | 4 + api/tacticalrmm/autotasks/permissions.py | 15 +- api/tacticalrmm/autotasks/serializers.py | 15 +- api/tacticalrmm/autotasks/urls.py | 6 +- api/tacticalrmm/autotasks/views.py | 128 +- .../migrations/0025_auto_20210917_1954.py | 23 + api/tacticalrmm/checks/models.py | 24 +- api/tacticalrmm/checks/permissions.py | 21 +- api/tacticalrmm/checks/serializers.py | 14 +- api/tacticalrmm/checks/tests.py | 763 +++++----- api/tacticalrmm/checks/urls.py | 11 +- api/tacticalrmm/checks/views.py | 140 +- .../migrations/0018_auto_20211010_0249.py | 33 + api/tacticalrmm/clients/models.py | 7 + api/tacticalrmm/clients/permissions.py | 24 + api/tacticalrmm/clients/views.py | 26 +- .../migrations/0028_auto_20210917_1954.py | 53 + .../0019_alter_auditlog_username.py | 18 + api/tacticalrmm/logs/models.py | 6 +- .../migrations/0012_auto_20210917_1954.py | 23 + api/tacticalrmm/scripts/tests.py | 16 +- api/tacticalrmm/scripts/urls.py | 4 +- api/tacticalrmm/scripts/views.py | 4 +- .../services/default_services.json | 1347 ----------------- api/tacticalrmm/services/permissions.py | 11 +- api/tacticalrmm/services/serializers.py | 13 - api/tacticalrmm/services/tests.py | 93 +- api/tacticalrmm/services/urls.py | 7 +- api/tacticalrmm/services/views.py | 141 +- api/tacticalrmm/software/models.py | 4 + api/tacticalrmm/software/permissions.py | 16 +- api/tacticalrmm/software/tests.py | 129 +- api/tacticalrmm/software/urls.py | 5 +- api/tacticalrmm/software/views.py | 131 +- api/tacticalrmm/tacticalrmm/models.py | 62 + api/tacticalrmm/tacticalrmm/permissions.py | 26 +- api/tacticalrmm/tacticalrmm/test.py | 58 +- api/tacticalrmm/tacticalrmm/urls.py | 15 +- .../migrations/0011_auto_20210917_1954.py | 23 + api/tacticalrmm/winupdate/permissions.py | 11 +- api/tacticalrmm/winupdate/serializers.py | 29 +- api/tacticalrmm/winupdate/tests.py | 212 ++- api/tacticalrmm/winupdate/urls.py | 8 +- api/tacticalrmm/winupdate/views.py | 104 +- web/src/App.vue | 13 +- web/src/api/accounts.js | 26 +- web/src/api/agents.js | 107 +- web/src/api/checks.js | 32 + web/src/api/clients.js | 4 +- web/src/api/core.js | 7 +- web/src/api/scripts.js | 6 +- web/src/api/services.js | 31 + web/src/api/software.js | 35 + web/src/api/tasks.js | 32 + web/src/api/winupdates.js | 22 + web/src/boot/axios.js | 21 +- web/src/components/AgentTable.vue | 86 +- web/src/components/AssetsTab.vue | 138 -- web/src/components/AutomatedTasksTab.vue | 423 ------ web/src/components/ChecksTab.vue | 529 ------- web/src/components/EventLog.vue | 134 -- web/src/components/FileBar.vue | 4 +- web/src/components/NotesTab.vue | 220 --- web/src/components/PermissionsManager.vue | 127 -- web/src/components/ProcessManager.vue | 220 --- web/src/components/Services.vue | 329 ---- web/src/components/SoftwareTab.vue | 133 -- web/src/components/SubTableTabs.vue | 146 +- web/src/components/WindowsUpdates.vue | 205 --- web/src/components/WmiDetail.vue | 32 - .../accounts/PermissionsManager.vue | 160 ++ web/src/components/accounts/RolesForm.vue | 330 ++++ web/src/components/agents/AssetsTab.vue | 123 ++ .../components/agents/AutomatedTasksTab.vue | 435 ++++++ web/src/components/agents/ChecksTab.vue | 495 ++++++ web/src/components/agents/HistoryTab.vue | 15 +- web/src/components/agents/NotesTab.vue | 225 +++ web/src/components/agents/SoftwareTab.vue | 167 ++ .../components/{ => agents}/SummaryTab.vue | 115 +- web/src/components/agents/WinUpdateTab.vue | 276 ++++ web/src/components/agents/WmiDetail.vue | 36 + .../agents/remotebg/EventLogManager.vue | 140 ++ .../agents/remotebg/ProcessManager.vue | 231 +++ .../agents/remotebg/ServicesManager.vue | 373 +++++ .../automation/PolicyAutomatedTasksTab.vue | 4 +- .../components/automation/PolicyChecksTab.vue | 18 +- .../automation/modals/PolicyAdd.vue | 5 +- .../automation/modals/PolicyStatus.vue | 4 +- web/src/components/checks/CpuLoadCheck.vue | 110 ++ web/src/components/checks/DiskSpaceCheck.vue | 116 ++ web/src/components/checks/EventLogCheck.vue | 232 +++ .../components/checks/EventLogCheckOutput.vue | 87 ++ web/src/components/checks/MemCheck.vue | 110 ++ web/src/components/checks/PingCheck.vue | 143 ++ web/src/components/checks/ScriptCheck.vue | 186 +++ .../{modals => }/checks/ScriptOutput.vue | 29 +- web/src/components/checks/WinSvcCheck.vue | 204 +++ web/src/components/graphs/CheckGraph.vue | 2 +- web/src/components/modals/admin/RolesForm.vue | 266 ---- .../modals/agents/AgentRecovery.vue | 5 +- .../components/modals/agents/EditAgent.vue | 6 +- .../components/modals/agents/InstallAgent.vue | 6 +- .../components/modals/agents/RebootLater.vue | 6 +- .../components/modals/agents/SendCommand.vue | 5 +- .../modals/alerts/AlertsOverview.vue | 2 +- .../components/modals/checks/CpuLoadCheck.vue | 134 -- .../modals/checks/DiskSpaceCheck.vue | 165 -- .../modals/checks/EventLogCheck.vue | 270 ---- .../modals/checks/EventLogCheckOutput.vue | 52 - web/src/components/modals/checks/MemCheck.vue | 134 -- .../components/modals/checks/PingCheck.vue | 134 -- .../components/modals/checks/ScriptCheck.vue | 213 --- .../components/modals/checks/WinSvcCheck.vue | 247 --- .../modals/software/InstallSoftware.vue | 112 -- .../components/scripts/TestScriptModal.vue | 3 +- .../components/software/InstallSoftware.vue | 136 ++ .../{modals => }/tasks/AddAutomatedTask.vue | 8 +- .../{modals => }/tasks/EditAutomatedTask.vue | 0 web/src/composables/checks.js | 1174 ++++++++++++++ web/src/composables/clients.js | 10 +- web/src/composables/scripts.js | 2 +- web/src/mixins/mixins.js | 2 +- web/src/router/routes.js | 4 +- web/src/store/index.js | 133 +- web/src/utils/format.js | 33 +- web/src/utils/validation.js | 37 + web/src/views/Dashboard.vue | 40 +- web/src/views/RemoteBackground.vue | 103 +- web/src/views/TakeControl.vue | 198 ++- 151 files changed, 8986 insertions(+), 7787 deletions(-) create mode 100644 api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py create mode 100644 api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py create mode 100644 api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py create mode 100644 api/tacticalrmm/automation/migrations/0009_auto_20210917_1954.py create mode 100644 api/tacticalrmm/autotasks/migrations/0023_auto_20210917_1954.py create mode 100644 api/tacticalrmm/checks/migrations/0025_auto_20210917_1954.py create mode 100644 api/tacticalrmm/clients/migrations/0018_auto_20211010_0249.py create mode 100644 api/tacticalrmm/core/migrations/0028_auto_20210917_1954.py create mode 100644 api/tacticalrmm/logs/migrations/0019_alter_auditlog_username.py create mode 100644 api/tacticalrmm/scripts/migrations/0012_auto_20210917_1954.py delete mode 100644 api/tacticalrmm/services/default_services.json delete mode 100644 api/tacticalrmm/services/serializers.py create mode 100644 api/tacticalrmm/tacticalrmm/models.py create mode 100644 api/tacticalrmm/winupdate/migrations/0011_auto_20210917_1954.py create mode 100644 web/src/api/checks.js create mode 100644 web/src/api/services.js create mode 100644 web/src/api/software.js create mode 100644 web/src/api/tasks.js create mode 100644 web/src/api/winupdates.js delete mode 100644 web/src/components/AssetsTab.vue delete mode 100644 web/src/components/AutomatedTasksTab.vue delete mode 100644 web/src/components/ChecksTab.vue delete mode 100644 web/src/components/EventLog.vue delete mode 100644 web/src/components/NotesTab.vue delete mode 100644 web/src/components/PermissionsManager.vue delete mode 100644 web/src/components/ProcessManager.vue delete mode 100644 web/src/components/Services.vue delete mode 100644 web/src/components/SoftwareTab.vue delete mode 100644 web/src/components/WindowsUpdates.vue delete mode 100644 web/src/components/WmiDetail.vue create mode 100644 web/src/components/accounts/PermissionsManager.vue create mode 100644 web/src/components/accounts/RolesForm.vue create mode 100644 web/src/components/agents/AssetsTab.vue create mode 100644 web/src/components/agents/AutomatedTasksTab.vue create mode 100644 web/src/components/agents/ChecksTab.vue create mode 100644 web/src/components/agents/NotesTab.vue create mode 100644 web/src/components/agents/SoftwareTab.vue rename web/src/components/{ => agents}/SummaryTab.vue (67%) create mode 100644 web/src/components/agents/WinUpdateTab.vue create mode 100644 web/src/components/agents/WmiDetail.vue create mode 100644 web/src/components/agents/remotebg/EventLogManager.vue create mode 100644 web/src/components/agents/remotebg/ProcessManager.vue create mode 100644 web/src/components/agents/remotebg/ServicesManager.vue create mode 100644 web/src/components/checks/CpuLoadCheck.vue create mode 100644 web/src/components/checks/DiskSpaceCheck.vue create mode 100644 web/src/components/checks/EventLogCheck.vue create mode 100644 web/src/components/checks/EventLogCheckOutput.vue create mode 100644 web/src/components/checks/MemCheck.vue create mode 100644 web/src/components/checks/PingCheck.vue create mode 100644 web/src/components/checks/ScriptCheck.vue rename web/src/components/{modals => }/checks/ScriptOutput.vue (72%) create mode 100644 web/src/components/checks/WinSvcCheck.vue delete mode 100644 web/src/components/modals/admin/RolesForm.vue delete mode 100644 web/src/components/modals/checks/CpuLoadCheck.vue delete mode 100644 web/src/components/modals/checks/DiskSpaceCheck.vue delete mode 100644 web/src/components/modals/checks/EventLogCheck.vue delete mode 100644 web/src/components/modals/checks/EventLogCheckOutput.vue delete mode 100644 web/src/components/modals/checks/MemCheck.vue delete mode 100644 web/src/components/modals/checks/PingCheck.vue delete mode 100644 web/src/components/modals/checks/ScriptCheck.vue delete mode 100644 web/src/components/modals/checks/WinSvcCheck.vue delete mode 100644 web/src/components/modals/software/InstallSoftware.vue create mode 100644 web/src/components/software/InstallSoftware.vue rename web/src/components/{modals => }/tasks/AddAutomatedTask.vue (96%) rename web/src/components/{modals => }/tasks/EditAutomatedTask.vue (100%) create mode 100644 web/src/composables/checks.js create mode 100644 web/src/utils/validation.js diff --git a/api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py b/api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py new file mode 100644 index 0000000000..0e5ad0fa7c --- /dev/null +++ b/api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py @@ -0,0 +1,150 @@ +# Generated by Django 3.2.6 on 2021-10-10 02:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('clients', '0018_auto_20211010_0249'), + ('accounts', '0027_auto_20210903_0054'), + ] + + operations = [ + migrations.AddField( + model_name='role', + name='can_list_accounts', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_agent_history', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_agents', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_alerts', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_api_keys', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_automation_policies', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_autotasks', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_checks', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_clients', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_deployments', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_notes', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_pendingactions', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_roles', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_scripts', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_sites', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_list_software', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_ping_agents', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_recover_agents', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='role', + name='can_view_clients', + field=models.ManyToManyField(blank=True, related_name='role_clients', to='clients.Client'), + ), + migrations.AddField( + model_name='role', + name='can_view_sites', + field=models.ManyToManyField(blank=True, related_name='role_sites', to='clients.Site'), + ), + migrations.AlterField( + model_name='apikey', + name='created_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='apikey', + name='modified_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='role', + name='created_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='role', + name='modified_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='user', + name='created_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='user', + name='modified_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='user', + name='role', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='accounts.role'), + ), + ] diff --git a/api/tacticalrmm/accounts/models.py b/api/tacticalrmm/accounts/models.py index 8256c3c62b..3154d8ac87 100644 --- a/api/tacticalrmm/accounts/models.py +++ b/api/tacticalrmm/accounts/models.py @@ -64,7 +64,7 @@ class User(AbstractUser, BaseAuditModel): "accounts.Role", null=True, blank=True, - related_name="roles", + related_name="users", on_delete=models.SET_NULL, ) @@ -81,6 +81,8 @@ class Role(BaseAuditModel): is_superuser = models.BooleanField(default=False) # agents + can_list_agents = models.BooleanField(default=False) + can_ping_agents = models.BooleanField(default=False) can_use_mesh = models.BooleanField(default=False) can_uninstall_agents = models.BooleanField(default=False) can_update_agents = models.BooleanField(default=False) @@ -92,8 +94,11 @@ class Role(BaseAuditModel): can_install_agents = models.BooleanField(default=False) can_run_scripts = models.BooleanField(default=False) can_run_bulk = models.BooleanField(default=False) + can_recover_agents = models.BooleanField(default=False) + can_list_agent_history = models.BooleanField(default=False) # core + can_list_notes = models.BooleanField(default=False) can_manage_notes = models.BooleanField(default=False) can_view_core_settings = models.BooleanField(default=False) can_edit_core_settings = models.BooleanField(default=False) @@ -101,46 +106,65 @@ class Role(BaseAuditModel): can_code_sign = models.BooleanField(default=False) # checks + can_list_checks = models.BooleanField(default=False) can_manage_checks = models.BooleanField(default=False) can_run_checks = models.BooleanField(default=False) # clients + can_list_clients = models.BooleanField(default=False) can_manage_clients = models.BooleanField(default=False) + can_list_sites = models.BooleanField(default=False) can_manage_sites = models.BooleanField(default=False) + can_list_deployments = models.BooleanField(default=False) can_manage_deployments = models.BooleanField(default=False) + can_view_clients = models.ManyToManyField( + "clients.Client", related_name="role_clients", blank=True + ) + can_view_sites = models.ManyToManyField( + "clients.Site", related_name="role_sites", blank=True + ) # automation + can_list_automation_policies = models.BooleanField(default=False) can_manage_automation_policies = models.BooleanField(default=False) # automated tasks + can_list_autotasks = models.BooleanField(default=False) can_manage_autotasks = models.BooleanField(default=False) can_run_autotasks = models.BooleanField(default=False) # logs can_view_auditlogs = models.BooleanField(default=False) + can_list_pendingactions = models.BooleanField(default=False) can_manage_pendingactions = models.BooleanField(default=False) can_view_debuglogs = models.BooleanField(default=False) # scripts + can_list_scripts = models.BooleanField(default=False) can_manage_scripts = models.BooleanField(default=False) # alerts + can_list_alerts = models.BooleanField(default=False) can_manage_alerts = models.BooleanField(default=False) # win services can_manage_winsvcs = models.BooleanField(default=False) # software + can_list_software = models.BooleanField(default=False) can_manage_software = models.BooleanField(default=False) # windows updates can_manage_winupdates = models.BooleanField(default=False) # accounts + can_list_accounts = models.BooleanField(default=False) can_manage_accounts = models.BooleanField(default=False) + can_list_roles = models.BooleanField(default=False) can_manage_roles = models.BooleanField(default=False) # authentication + can_list_api_keys = models.BooleanField(default=False) can_manage_api_keys = models.BooleanField(default=False) def __str__(self): @@ -153,47 +177,6 @@ def serialize(role): return RoleAuditSerializer(role).data - @staticmethod - def perms(): - return [ - "is_superuser", - "can_use_mesh", - "can_uninstall_agents", - "can_update_agents", - "can_edit_agent", - "can_manage_procs", - "can_view_eventlogs", - "can_send_cmd", - "can_reboot_agents", - "can_install_agents", - "can_run_scripts", - "can_run_bulk", - "can_manage_notes", - "can_view_core_settings", - "can_edit_core_settings", - "can_do_server_maint", - "can_code_sign", - "can_manage_checks", - "can_run_checks", - "can_manage_clients", - "can_manage_sites", - "can_manage_deployments", - "can_manage_automation_policies", - "can_manage_autotasks", - "can_run_autotasks", - "can_view_auditlogs", - "can_manage_pendingactions", - "can_view_debuglogs", - "can_manage_scripts", - "can_manage_alerts", - "can_manage_winsvcs", - "can_manage_software", - "can_manage_winupdates", - "can_manage_accounts", - "can_manage_roles", - "can_manage_api_keys", - ] - class APIKey(BaseAuditModel): name = CharField(unique=True, max_length=25) diff --git a/api/tacticalrmm/accounts/serializers.py b/api/tacticalrmm/accounts/serializers.py index b52a39fe40..8c9ffca156 100644 --- a/api/tacticalrmm/accounts/serializers.py +++ b/api/tacticalrmm/accounts/serializers.py @@ -61,10 +61,15 @@ def get_qr_url(self, obj): class RoleSerializer(ModelSerializer): + user_count = SerializerMethodField() + class Meta: model = Role fields = "__all__" + def get_user_count(self, obj): + return obj.users.count() + class RoleAuditSerializer(ModelSerializer): class Meta: diff --git a/api/tacticalrmm/accounts/tests.py b/api/tacticalrmm/accounts/tests.py index 9efd337fc1..c3daff7902 100644 --- a/api/tacticalrmm/accounts/tests.py +++ b/api/tacticalrmm/accounts/tests.py @@ -27,12 +27,12 @@ def test_check_creds(self): data = {"username": "bob", "password": "a3asdsa2314"} r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 400) - self.assertEqual(r.data, "bad credentials") + self.assertEqual(r.data, "Bad credentials") data = {"username": "billy", "password": "hunter2"} r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 400) - self.assertEqual(r.data, "bad credentials") + self.assertEqual(r.data, "Bad credentials") self.bob.totp_key = "AB5RI6YPFTZAS52G" self.bob.save() @@ -61,7 +61,7 @@ def test_login_view(self, mock_verify): mock_verify.return_value = False r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 400) - self.assertEqual(r.data, "bad credentials") + self.assertEqual(r.data, "Bad credentials") mock_verify.return_value = True data = {"username": "bob", "password": "asd234234asd", "twofactor": "123456"} diff --git a/api/tacticalrmm/accounts/urls.py b/api/tacticalrmm/accounts/urls.py index a8356869d2..97769dc991 100644 --- a/api/tacticalrmm/accounts/urls.py +++ b/api/tacticalrmm/accounts/urls.py @@ -9,9 +9,8 @@ path("users/reset_totp/", views.UserActions.as_view()), path("users/setup_totp/", views.TOTPSetup.as_view()), path("users/ui/", views.UserUI.as_view()), - path("permslist/", views.PermsList.as_view()), path("roles/", views.GetAddRoles.as_view()), - path("/role/", views.GetUpdateDeleteRole.as_view()), + path("roles//", views.GetUpdateDeleteRole.as_view()), path("apikeys/", views.GetAddAPIKeys.as_view()), path("apikeys//", views.GetUpdateDeleteAPIKey.as_view()), ] diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index b6ae6bf53e..56f4b6f1f5 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -44,12 +44,12 @@ def post(self, request, format=None): AuditLog.audit_user_failed_login( request.data["username"], debug_info={"ip": request._client_ip} ) - return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) + return notify_error("Bad credentials") user = serializer.validated_data["user"] if user.block_dashboard_login: - return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) + return notify_error("Bad credentials") # if totp token not set modify response to notify frontend if not user.totp_key: @@ -73,7 +73,7 @@ def post(self, request, format=None): user = serializer.validated_data["user"] if user.block_dashboard_login: - return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) + return notify_error("Bad credentials") token = request.data["twofactor"] totp = pyotp.TOTP(user.totp_key) @@ -99,7 +99,7 @@ def post(self, request, format=None): AuditLog.audit_user_failed_twofactor( request.data["username"], debug_info={"ip": request._client_ip} ) - return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) + return notify_error("Bad credentials") class GetAddUsers(APIView): @@ -224,11 +224,6 @@ def patch(self, request): return Response("ok") -class PermsList(APIView): - def get(self, request): - return Response(Role.perms()) - - class GetAddRoles(APIView): permission_classes = [IsAuthenticated, RolesPerms] @@ -240,7 +235,7 @@ def post(self, request): serializer = RoleSerializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - return Response("ok") + return Response("Role was added") class GetUpdateDeleteRole(APIView): @@ -255,12 +250,12 @@ def put(self, request, pk): serializer = RoleSerializer(instance=role, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - return Response("ok") + return Response("Role was edited") def delete(self, request, pk): role = get_object_or_404(Role, pk=pk) role.delete() - return Response("ok") + return Response("Role was removed") class GetAddAPIKeys(APIView): diff --git a/api/tacticalrmm/agents/baker_recipes.py b/api/tacticalrmm/agents/baker_recipes.py index 1eb30d2254..cacb2657f3 100644 --- a/api/tacticalrmm/agents/baker_recipes.py +++ b/api/tacticalrmm/agents/baker_recipes.py @@ -30,7 +30,8 @@ def get_wmi_data(): hostname="DESKTOP-TEST123", version="1.3.0", monitoring_type=cycle(["workstation", "server"]), - agent_id=seq("asdkj3h4234-1234hg3h4g34-234jjh34|DESKTOP-TEST123"), + agent_id=seq(generate_agent_id("DESKTOP-TEST123")), + last_seen=djangotime.now() - djangotime.timedelta(days=5), ) server_agent = agent.extend( diff --git a/api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py b/api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py new file mode 100644 index 0000000000..8c235e2b4c --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.6 on 2021-10-10 02:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0039_auto_20210714_0738'), + ] + + operations = [ + migrations.AlterField( + model_name='agent', + name='agent_id', + field=models.CharField(max_length=200, unique=True), + ), + migrations.AlterField( + model_name='agent', + name='created_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='agent', + name='modified_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index d4996fc3b8..94aa96ea77 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -22,9 +22,13 @@ from core.models import TZ_CHOICES, CoreSettings from logs.models import BaseAuditModel, DebugLog +from tacticalrmm.models import PermissionManager class Agent(BaseAuditModel): + objects = models.Manager() + permissions = PermissionManager() + version = models.CharField(default="0.1.0", max_length=255) salt_ver = models.CharField(default="1.0.3", max_length=255) operating_system = models.CharField(null=True, blank=True, max_length=255) @@ -33,7 +37,7 @@ class Agent(BaseAuditModel): hostname = models.CharField(max_length=255) salt_id = models.CharField(null=True, blank=True, max_length=255) local_ip = models.TextField(null=True, blank=True) # deprecated - agent_id = models.CharField(max_length=200) + agent_id = models.CharField(max_length=200, unique=True) last_seen = models.DateTimeField(null=True, blank=True) services = models.JSONField(null=True, blank=True) public_ip = models.CharField(null=True, max_length=255) @@ -872,6 +876,9 @@ def send_recovery_sms(self): class RecoveryAction(models.Model): + objects = models.Manager() + permissions = PermissionManager() + agent = models.ForeignKey( Agent, related_name="recoveryactions", @@ -886,6 +893,9 @@ def __str__(self): class Note(models.Model): + objects = models.Manager() + permissions = PermissionManager() + agent = models.ForeignKey( Agent, related_name="notes", @@ -966,6 +976,9 @@ def save_to_field(self, value): class AgentHistory(models.Model): + objects = models.Manager() + permissions = PermissionManager() + agent = models.ForeignKey( Agent, related_name="history", diff --git a/api/tacticalrmm/agents/permissions.py b/api/tacticalrmm/agents/permissions.py index 6d6c7add60..fc58cbca5e 100644 --- a/api/tacticalrmm/agents/permissions.py +++ b/api/tacticalrmm/agents/permissions.py @@ -1,16 +1,39 @@ from rest_framework import permissions -from tacticalrmm.permissions import _has_perm +from tacticalrmm.permissions import _has_perm, _has_perm_on_agent -class MeshPerms(permissions.BasePermission): +class AgentPerms(permissions.BasePermission): + def has_permission(self, r, view): + if r.method == "GET": + if "agent_id" in view.kwargs.keys(): + return _has_perm(r, "can_list_agents") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) + else: + return _has_perm(r, "can_list_agents") + elif r.method == "DELETE": + return _has_perm(r, "can_uninstall_agents") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) + else: + return _has_perm(r, "can_edit_agent") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) + + +class RecoverAgentPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_use_mesh") + return _has_perm(r, "can_recover_agents") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) -class UninstallPerms(permissions.BasePermission): +class MeshPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_uninstall_agents") + return _has_perm(r, "can_use_mesh") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) class UpdateAgentPerms(permissions.BasePermission): @@ -18,29 +41,39 @@ def has_permission(self, r, view): return _has_perm(r, "can_update_agents") -class EditAgentPerms(permissions.BasePermission): +class PingAgentPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_edit_agent") + return _has_perm(r, "can_ping_agents") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) class ManageProcPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_manage_procs") + return _has_perm(r, "can_manage_procs") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) class EvtLogPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_view_eventlogs") + return _has_perm(r, "can_view_eventlogs") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) class SendCMDPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_send_cmd") + return _has_perm(r, "can_send_cmd") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) class RebootAgentPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_reboot_agents") + return _has_perm(r, "can_reboot_agents") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) class InstallAgentPerms(permissions.BasePermission): @@ -50,14 +83,38 @@ def has_permission(self, r, view): class RunScriptPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_run_scripts") + return _has_perm(r, "can_run_scripts") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) -class ManageNotesPerms(permissions.BasePermission): +class AgentNotesPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_manage_notes") + + # permissions for GET /agents/notes/ endpoint + if r.method == "GET": + + # permissions for /agents//notes endpoint + if "agent_id" in view.kwargs.keys(): + return _has_perm(r, "can_list_notes") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) + else: + return _has_perm(r, "can_list_notes") + else: + return _has_perm(r, "can_manage_notes") class RunBulkPerms(permissions.BasePermission): def has_permission(self, r, view): return _has_perm(r, "can_run_bulk") + + +class AgentHistoryPerms(permissions.BasePermission): + def has_permission(self, r, view): + if "agent_id" in view.kwargs.keys(): + return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) + else: + return _has_perm(r, "can_list_agent_history") diff --git a/api/tacticalrmm/agents/serializers.py b/api/tacticalrmm/agents/serializers.py index 73740b2c5e..efa188f16d 100644 --- a/api/tacticalrmm/agents/serializers.py +++ b/api/tacticalrmm/agents/serializers.py @@ -1,7 +1,6 @@ import pytz from clients.serializers import ClientSerializer from rest_framework import serializers -from tacticalrmm.utils import get_default_timezone from winupdate.serializers import WinUpdatePolicySerializer from .models import Agent, AgentCustomField, Note, AgentHistory @@ -32,17 +31,6 @@ class Meta: ] -class AgentOverdueActionSerializer(serializers.ModelSerializer): - class Meta: - model = Agent - fields = [ - "pk", - "overdue_email_alert", - "overdue_text_alert", - "overdue_dashboard_alert", - ] - - class AgentTableSerializer(serializers.ModelSerializer): status = serializers.ReadOnlyField() checks = serializers.ReadOnlyField() @@ -88,10 +76,9 @@ def get_italic(self, obj) -> bool: class Meta: model = Agent fields = [ - "id", + "agent_id", "alert_template", "hostname", - "agent_id", "site_name", "client_name", "monitoring_type", @@ -146,7 +133,7 @@ def get_all_timezones(self, obj): class Meta: model = Agent fields = [ - "id", + "agent_id", "hostname", "block_policy_inheritance", "client", @@ -182,26 +169,20 @@ class Meta: model = Agent fields = ( "hostname", - "pk", + "agent_id", "client", "site", ) -class NoteSerializer(serializers.ModelSerializer): +class AgentNoteSerializer(serializers.ModelSerializer): username = serializers.ReadOnlyField(source="user.username") + agent_id = serializers.ReadOnlyField(source="agent.agent_id") class Meta: model = Note - fields = "__all__" - - -class NotesSerializer(serializers.ModelSerializer): - notes = NoteSerializer(many=True, read_only=True) - - class Meta: - model = Agent - fields = ["hostname", "pk", "notes"] + fields = ("pk", "entry_time", "agent", "user", "note", "username", "agent_id") + extra_kwargs = {"agent": {"write_only": True}, "user": {"write_only": True}} class AgentHistorySerializer(serializers.ModelSerializer): diff --git a/api/tacticalrmm/agents/tests.py b/api/tacticalrmm/agents/tests.py index 3a1b74805a..422d6b2d12 100644 --- a/api/tacticalrmm/agents/tests.py +++ b/api/tacticalrmm/agents/tests.py @@ -3,6 +3,7 @@ import pytz from django.utils import timezone as djangotime from unittest.mock import patch +from itertools import cycle from django.conf import settings from logs.models import PendingAction @@ -12,18 +13,26 @@ from winupdate.models import WinUpdatePolicy from winupdate.serializers import WinUpdatePolicySerializer -from .models import Agent, AgentCustomField, AgentHistory -from .serializers import AgentHistorySerializer, AgentSerializer +from .models import Agent, AgentCustomField, AgentHistory, Note +from .serializers import ( + AgentHistorySerializer, + AgentSerializer, + AgentHostnameSerializer, + AgentNoteSerializer, +) from .tasks import auto_self_agent_update_task +base_url = "/agents" + + class TestAgentsList(TacticalTestCase): def setUp(self): self.authenticate() self.setup_coresettings() - def test_agents_list(self): - url = "/agents/listagents/" + def test_get_agents(self): + url = f"{base_url}/" # 36 total agents company1 = baker.make("clients.Client") @@ -55,23 +64,31 @@ def test_agents_list(self): ) # test all agents - r = self.client.patch(url, format="json") + r = self.client.get(url, format="json") self.assertEqual(r.status_code, 200) self.assertEqual(len(r.data), 36) # type: ignore # test client1 - data = {"clientPK": company1.pk} # type: ignore - r = self.client.patch(url, data, format="json") + r = self.client.get(f"{url}?client={company1.pk}", format="json") self.assertEqual(r.status_code, 200) self.assertEqual(len(r.data), 25) # type: ignore # test site3 - data = {"sitePK": site3.pk} # type: ignore - r = self.client.patch(url, data, format="json") + r = self.client.get(f"{url}?site={site3.pk}", format="json") self.assertEqual(r.status_code, 200) self.assertEqual(len(r.data), 11) # type: ignore - self.check_not_authenticated("patch", url) + # test with no details + r = self.client.get(f"{url}?site={site3.pk}&detail=false", format="json") + self.assertEqual(r.status_code, 200) + self.assertEqual(len(r.data), 11) # type: ignore + + # make sure data is returned with the AgentHostnameSerializer + agents = Agent.objects.filter(site=site3) + serializer = AgentHostnameSerializer(agents, many=True) + self.assertEqual(r.data, serializer.data) + + self.check_not_authenticated("get", url) class TestAgentViews(TacticalTestCase): @@ -86,6 +103,100 @@ def setUp(self): ) baker.make_recipe("winupdate.winupdate_policy", agent=self.agent) + def test_get_agent(self): + url = f"{base_url}/{self.agent.agent_id}/" + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + + self.check_not_authenticated("get", url) + + def test_edit_agent(self): + # setup data + site = baker.make("clients.Site", name="Ny Office") + + url = f"{base_url}/{self.agent.agent_id}/" + + data = { + "site": site.id, # type: ignore + "monitoring_type": "workstation", + "description": "asjdk234andasd", + "offline_time": 4, + "overdue_time": 300, + "check_interval": 60, + "overdue_email_alert": True, + "overdue_text_alert": False, + "winupdatepolicy": [ + { + "critical": "approve", + "important": "approve", + "moderate": "manual", + "low": "ignore", + "other": "ignore", + "run_time_hour": 5, + "run_time_days": [2, 3, 6], + "reboot_after_install": "required", + "reprocess_failed": True, + "reprocess_failed_times": 13, + "email_if_fail": True, + "agent": self.agent.pk, + } + ], + } + + r = self.client.put(url, data, format="json") + self.assertEqual(r.status_code, 200) + + agent = Agent.objects.get(pk=self.agent.pk) + data = AgentSerializer(agent).data + self.assertEqual(data["site"], site.id) # type: ignore + + policy = WinUpdatePolicy.objects.get(agent=self.agent) + data = WinUpdatePolicySerializer(policy).data + self.assertEqual(data["run_time_days"], [2, 3, 6]) + + # test adding custom fields + field = baker.make("core.CustomField", model="agent", type="number") + data = { + "site": site.id, # type: ignore + "description": "asjdk234andasd", + "custom_fields": [{"field": field.id, "string_value": "123"}], # type: ignore + } + + r = self.client.put(url, data, format="json") + self.assertEqual(r.status_code, 200) + self.assertTrue( + AgentCustomField.objects.filter(agent=self.agent, field=field).exists() + ) + + # test edit custom field + data = { + "site": site.id, # type: ignore + "description": "asjdk234andasd", + "custom_fields": [{"field": field.id, "string_value": "456"}], # type: ignore + } + + r = self.client.put(url, data, format="json") + self.assertEqual(r.status_code, 200) + self.assertEqual( + AgentCustomField.objects.get(agent=agent, field=field).value, + "456", + ) + self.check_not_authenticated("put", url) + + @patch("agents.models.Agent.nats_cmd") + @patch("agents.views.reload_nats") + def test_agent_uninstall(self, reload_nats, nats_cmd): + url = f"{base_url}/{self.agent.agent_id}/" + + r = self.client.delete(url, format="json") + self.assertEqual(r.status_code, 200) + + nats_cmd.assert_called_with({"func": "uninstall"}, wait=False) + reload_nats.assert_called_once() + + self.check_not_authenticated("delete", url) + def test_get_patch_policy(self): # make sure get_patch_policy doesn't error out when agent has policy with # an empty patch policy @@ -111,7 +222,7 @@ def test_get_patch_policy(self): _ = self.agent.get_patch_policy() def test_get_agent_versions(self): - url = "/agents/getagentversions/" + url = "/agents/versions/" r = self.client.get(url) self.assertEqual(r.status_code, 200) assert any(i["hostname"] == self.agent.hostname for i in r.json()["agents"]) @@ -120,7 +231,7 @@ def test_get_agent_versions(self): @patch("agents.tasks.send_agent_update_task.delay") def test_update_agents(self, mock_task): - url = "/agents/updateagents/" + url = f"{base_url}/update/" baker.make_recipe( "agents.agent", operating_system="Windows 10 Pro, 64 bit (build 19041.450)", @@ -152,10 +263,10 @@ def test_update_agents(self, mock_task): self.check_not_authenticated("post", url) - @patch("time.sleep") + @patch("time.sleep", return_value=None) @patch("agents.models.Agent.nats_cmd") - def test_ping(self, nats_cmd, mock_sleep): - url = f"/agents/{self.agent.pk}/ping/" + def test_agent_ping(self, nats_cmd, mock_sleep): + url = f"{base_url}/{self.agent.agent_id}/ping/" nats_cmd.return_value = "timeout" r = self.client.get(url) @@ -183,24 +294,10 @@ def test_ping(self, nats_cmd, mock_sleep): self.check_not_authenticated("get", url) - @patch("agents.models.Agent.nats_cmd") - @patch("agents.views.reload_nats") - def test_uninstall(self, reload_nats, nats_cmd): - url = "/agents/uninstall/" - data = {"pk": self.agent.pk} - - r = self.client.delete(url, data, format="json") - self.assertEqual(r.status_code, 200) - - nats_cmd.assert_called_with({"func": "uninstall"}, wait=False) - reload_nats.assert_called_once() - - self.check_not_authenticated("delete", url) - @patch("agents.models.Agent.nats_cmd") def test_get_processes(self, mock_ret): agent = baker.make_recipe("agents.online_agent", version="1.2.0") - url = f"/agents/{agent.pk}/getprocs/" + url = f"{base_url}/{agent.agent_id}/processes/" with open( os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/procs.json") @@ -219,26 +316,26 @@ def test_get_processes(self, mock_ret): self.check_not_authenticated("get", url) @patch("agents.models.Agent.nats_cmd") - def test_kill_proc(self, nats_cmd): - url = f"/agents/{self.agent.pk}/8234/killproc/" + def test_kill_process(self, nats_cmd): + url = f"{base_url}/{self.agent.agent_id}/processes/123/" nats_cmd.return_value = "ok" - r = self.client.get(url) + r = self.client.delete(url) self.assertEqual(r.status_code, 200) nats_cmd.return_value = "timeout" - r = self.client.get(url) + r = self.client.delete(url) self.assertEqual(r.status_code, 400) nats_cmd.return_value = "process doesn't exist" - r = self.client.get(url) + r = self.client.delete(url) self.assertEqual(r.status_code, 400) - self.check_not_authenticated("get", url) + self.check_not_authenticated("delete", url) @patch("agents.models.Agent.nats_cmd") def test_get_event_log(self, nats_cmd): - url = f"/agents/{self.agent.pk}/geteventlog/Application/22/" + url = f"/agents/{self.agent.agent_id}/eventlog/Application/22/" with open( os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/appeventlog.json") @@ -259,7 +356,7 @@ def test_get_event_log(self, nats_cmd): timeout=32, ) - url = f"/agents/{self.agent.pk}/geteventlog/Security/6/" + url = f"{base_url}/{self.agent.agent_id}/eventlog/Security/6/" r = self.client.get(url) self.assertEqual(r.status_code, 200) nats_cmd.assert_called_with( @@ -282,26 +379,24 @@ def test_get_event_log(self, nats_cmd): @patch("agents.models.Agent.nats_cmd") def test_reboot_now(self, nats_cmd): - url = f"/agents/reboot/" + url = f"{base_url}/{self.agent.agent_id}/reboot/" - data = {"pk": self.agent.pk} nats_cmd.return_value = "ok" - r = self.client.post(url, data, format="json") + r = self.client.post(url, format="json") self.assertEqual(r.status_code, 200) nats_cmd.assert_called_with({"func": "rebootnow"}, timeout=10) nats_cmd.return_value = "timeout" - r = self.client.post(url, data, format="json") + r = self.client.post(url, format="json") self.assertEqual(r.status_code, 400) self.check_not_authenticated("post", url) @patch("agents.models.Agent.nats_cmd") def test_send_raw_cmd(self, mock_ret): - url = f"/agents/sendrawcmd/" + url = f"{base_url}/{self.agent.agent_id}/cmd/" data = { - "pk": self.agent.pk, "cmd": "ipconfig", "shell": "cmd", "timeout": 30, @@ -319,10 +414,9 @@ def test_send_raw_cmd(self, mock_ret): @patch("agents.models.Agent.nats_cmd") def test_reboot_later(self, nats_cmd): - url = f"/agents/reboot/" + url = f"{base_url}/{self.agent.agent_id}/reboot/" data = { - "pk": self.agent.pk, "datetime": "2025-08-29 18:41", } @@ -353,7 +447,6 @@ def test_reboot_later(self, nats_cmd): self.assertEqual(r.status_code, 400) data_invalid = { - "pk": self.agent.pk, "datetime": "rm -rf /", } r = self.client.patch(url, data_invalid, format="json") @@ -365,7 +458,7 @@ def test_reboot_later(self, nats_cmd): @patch("os.path.exists") def test_install_agent(self, mock_file_exists): - url = "/agents/installagent/" + url = f"{base_url}/installer/" site = baker.make("clients.Site") data = { @@ -384,7 +477,7 @@ def test_install_agent(self, mock_file_exists): mock_file_exists.return_value = False r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 406) + self.assertEqual(r.status_code, 400) mock_file_exists.return_value = True r = self.client.post(url, data, format="json") @@ -393,7 +486,7 @@ def test_install_agent(self, mock_file_exists): data["arch"] = "32" mock_file_exists.return_value = False r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 415) + self.assertEqual(r.status_code, 400) data["arch"] = "64" mock_file_exists.return_value = True @@ -416,11 +509,11 @@ def test_recover(self, nats_cmd): from agents.models import RecoveryAction RecoveryAction.objects.all().delete() - url = "/agents/recover/" agent = baker.make_recipe("agents.online_agent") + url = f"{base_url}/{agent.agent_id}/recover/" # test mesh realtime - data = {"pk": agent.pk, "cmd": None, "mode": "mesh"} + data = {"cmd": None, "mode": "mesh"} nats_cmd.return_value = "ok" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) @@ -431,7 +524,7 @@ def test_recover(self, nats_cmd): nats_cmd.reset_mock() # test mesh with agent rpc not working - data = {"pk": agent.pk, "cmd": None, "mode": "mesh"} + data = {"cmd": None, "mode": "mesh"} nats_cmd.return_value = "timeout" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) @@ -442,7 +535,7 @@ def test_recover(self, nats_cmd): RecoveryAction.objects.all().delete() # test tacagent realtime - data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"} + data = {"cmd": None, "mode": "tacagent"} nats_cmd.return_value = "ok" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) @@ -453,7 +546,7 @@ def test_recover(self, nats_cmd): nats_cmd.reset_mock() # test tacagent with rpc not working - data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"} + data = {"cmd": None, "mode": "tacagent"} nats_cmd.return_value = "timeout" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 400) @@ -461,13 +554,13 @@ def test_recover(self, nats_cmd): nats_cmd.reset_mock() # test shell cmd without command - data = {"pk": agent.pk, "cmd": None, "mode": "command"} + data = {"cmd": None, "mode": "command"} r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 400) self.assertEqual(RecoveryAction.objects.count(), 0) # test shell cmd - data = {"pk": agent.pk, "cmd": "shutdown /r /t 10 /f", "mode": "command"} + data = {"cmd": "shutdown /r /t 10 /f", "mode": "command"} r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) self.assertEqual(RecoveryAction.objects.count(), 1) @@ -475,93 +568,9 @@ def test_recover(self, nats_cmd): self.assertEqual(cmd_recovery.mode, "command") # type: ignore self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f") # type: ignore - def test_agents_agent_detail(self): - url = f"/agents/{self.agent.pk}/agentdetail/" - - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - - self.check_not_authenticated("get", url) - - def test_edit_agent(self): - # setup data - site = baker.make("clients.Site", name="Ny Office") - - url = "/agents/editagent/" - - edit = { - "id": self.agent.pk, - "site": site.id, # type: ignore - "monitoring_type": "workstation", - "description": "asjdk234andasd", - "offline_time": 4, - "overdue_time": 300, - "check_interval": 60, - "overdue_email_alert": True, - "overdue_text_alert": False, - "winupdatepolicy": [ - { - "critical": "approve", - "important": "approve", - "moderate": "manual", - "low": "ignore", - "other": "ignore", - "run_time_hour": 5, - "run_time_days": [2, 3, 6], - "reboot_after_install": "required", - "reprocess_failed": True, - "reprocess_failed_times": 13, - "email_if_fail": True, - "agent": self.agent.pk, - } - ], - } - - r = self.client.patch(url, edit, format="json") - self.assertEqual(r.status_code, 200) - - agent = Agent.objects.get(pk=self.agent.pk) - data = AgentSerializer(agent).data - self.assertEqual(data["site"], site.id) # type: ignore - - policy = WinUpdatePolicy.objects.get(agent=self.agent) - data = WinUpdatePolicySerializer(policy).data - self.assertEqual(data["run_time_days"], [2, 3, 6]) - - # test adding custom fields - field = baker.make("core.CustomField", model="agent", type="number") - edit = { - "id": self.agent.pk, - "site": site.id, # type: ignore - "description": "asjdk234andasd", - "custom_fields": [{"field": field.id, "string_value": "123"}], # type: ignore - } - - r = self.client.patch(url, edit, format="json") - self.assertEqual(r.status_code, 200) - self.assertTrue( - AgentCustomField.objects.filter(agent=self.agent, field=field).exists() - ) - - # test edit custom field - edit = { - "id": self.agent.pk, - "site": site.id, # type: ignore - "description": "asjdk234andasd", - "custom_fields": [{"field": field.id, "string_value": "456"}], # type: ignore - } - - r = self.client.patch(url, edit, format="json") - self.assertEqual(r.status_code, 200) - self.assertEqual( - AgentCustomField.objects.get(agent=agent, field=field).value, - "456", - ) - self.check_not_authenticated("patch", url) - @patch("agents.models.Agent.get_login_token") def test_meshcentral_tabs(self, mock_token): - url = f"/agents/{self.agent.pk}/meshcentral/" + url = f"{base_url}/{self.agent.agent_id}/meshcentral/" mock_token.return_value = "askjh1k238uasdhk487234jadhsajksdhasd" r = self.client.get(url) self.assertEqual(r.status_code, 200) @@ -593,43 +602,6 @@ def test_meshcentral_tabs(self, mock_token): self.check_not_authenticated("get", url) - def test_overdue_action(self): - url = "/agents/overdueaction/" - - payload = {"pk": self.agent.pk, "overdue_email_alert": True} - r = self.client.post(url, payload, format="json") - self.assertEqual(r.status_code, 200) - agent = Agent.objects.get(pk=self.agent.pk) - self.assertTrue(agent.overdue_email_alert) - self.assertEqual(self.agent.hostname, r.data) # type: ignore - - payload = {"pk": self.agent.pk, "overdue_text_alert": False} - r = self.client.post(url, payload, format="json") - self.assertEqual(r.status_code, 200) - agent = Agent.objects.get(pk=self.agent.pk) - self.assertFalse(agent.overdue_text_alert) - self.assertEqual(self.agent.hostname, r.data) # type: ignore - - self.check_not_authenticated("post", url) - - def test_list_agents_no_detail(self): - url = "/agents/listagentsnodetail/" - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - - self.check_not_authenticated("get", url) - - def test_agent_edit_details(self): - url = f"/agents/{self.agent.pk}/agenteditdetails/" - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - - url = "/agents/48234982/agenteditdetails/" - r = self.client.get(url) - self.assertEqual(r.status_code, 404) - - self.check_not_authenticated("get", url) - """ @patch("winupdate.tasks.bulk_check_for_updates_task.delay") @patch("scripts.tasks.handle_bulk_script_task.delay") @patch("scripts.tasks.handle_bulk_command_task.delay") @@ -748,9 +720,9 @@ def test_bulk_cmd_script( @patch("agents.models.Agent.nats_cmd") def test_recover_mesh(self, nats_cmd): - url = f"/agents/{self.agent.pk}/recovermesh/" + url = f"{base_url}/{self.agent.agent_id}/meshcentral/recover/" nats_cmd.return_value = "ok" - r = self.client.get(url) + r = self.client.post(url) self.assertEqual(r.status_code, 200) self.assertIn(self.agent.hostname, r.data) # type: ignore nats_cmd.assert_called_with( @@ -758,14 +730,14 @@ def test_recover_mesh(self, nats_cmd): ) nats_cmd.return_value = "timeout" - r = self.client.get(url) + r = self.client.post(url) self.assertEqual(r.status_code, 400) - url = f"/agents/543656/recovermesh/" - r = self.client.get(url) + url = f"{base_url}/{self.agent.agent_id}123/meshcentral/recover/" + r = self.client.post(url) self.assertEqual(r.status_code, 404) - self.check_not_authenticated("get", url) + self.check_not_authenticated("post", url) @patch("agents.tasks.run_script_email_results_task.delay") @patch("agents.models.Agent.run_script") @@ -774,12 +746,11 @@ def test_run_script(self, run_script, email_task): from clients.models import ClientCustomField, SiteCustomField run_script.return_value = "ok" - url = "/agents/runscript/" + url = f"/agents/{self.agent.agent_id}/runscript/" script = baker.make_recipe("scripts.script") # test wait data = { - "pk": self.agent.pk, "script": script.pk, "output": "wait", "args": [], @@ -795,7 +766,6 @@ def test_run_script(self, run_script, email_task): # test email default data = { - "pk": self.agent.pk, "script": script.pk, "output": "email", "args": ["abc", "123"], @@ -828,7 +798,6 @@ def test_run_script(self, run_script, email_task): # test fire and forget data = { - "pk": self.agent.pk, "script": script.pk, "output": "forget", "args": ["hello", "world"], @@ -847,7 +816,6 @@ def test_run_script(self, run_script, email_task): # save to agent custom field custom_field = baker.make("core.CustomField", model="agent") data = { - "pk": self.agent.pk, "script": script.pk, "output": "collector", "args": ["hello", "world"], @@ -875,7 +843,6 @@ def test_run_script(self, run_script, email_task): # save to site custom field custom_field = baker.make("core.CustomField", model="site") data = { - "pk": self.agent.pk, "script": script.pk, "output": "collector", "args": ["hello", "world"], @@ -905,7 +872,6 @@ def test_run_script(self, run_script, email_task): # save to client custom field custom_field = baker.make("core.CustomField", model="client") data = { - "pk": self.agent.pk, "script": script.pk, "output": "collector", "args": ["hello", "world"], @@ -934,7 +900,6 @@ def test_run_script(self, run_script, email_task): # test save to note data = { - "pk": self.agent.pk, "script": script.pk, "output": "note", "args": ["hello", "world"], @@ -954,15 +919,101 @@ def test_run_script(self, run_script, email_task): self.assertEqual(Note.objects.get(agent=self.agent).note, "ok") + def test_get_notes(self): + url = f"{base_url}/notes/" + + # setup + agent = baker.make_recipe("agents.agent") + notes = baker.make("agents.Note", agent=agent, _quantity=4) + + r = self.client.get(url) + serializer = AgentNoteSerializer(notes, many=True) + self.assertEqual(r.status_code, 200) + self.assertEqual(len(r.data), 4) # type: ignore + self.assertEqual(r.data, serializer.data) # type: ignore + + # test with agent_id + url = f"{base_url}/{agent.agent_id}/notes/" + + r = self.client.get(url) + serializer = AgentNoteSerializer(notes, many=True) + self.assertEqual(r.status_code, 200) + self.assertEqual(len(r.data), 4) # type: ignore + self.assertEqual(r.data, serializer.data) # type: ignore + + self.check_not_authenticated("get", url) + + def test_add_note(self): + url = f"{base_url}/notes/" + agent = baker.make_recipe("agents.agent") + + data = {"note": "This is a note", "agent_id": agent.agent_id} + r = self.client.post(url, data) + self.assertEqual(r.status_code, 200) + self.assertTrue(Note.objects.filter(agent=agent).exists()) # type: ignore + + self.check_not_authenticated("post", url) + + def test_get_note(self): + # setup + agent = baker.make_recipe("agents.agent") + note = baker.make("agents.Note", agent=agent) + url = f"{base_url}/notes/{note.id}/" + + # test not found + r = self.client.get(f"{base_url}/notes/500/") + self.assertEqual(r.status_code, 404) + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + + self.check_not_authenticated("put", url) + + def test_update_note(self): + # setup + agent = baker.make_recipe("agents.agent") + note = baker.make("agents.Note", agent=agent) + url = f"{base_url}/notes/{note.id}/" + + # test not found + r = self.client.put(f"{base_url}/notes/500/") + self.assertEqual(r.status_code, 404) + + data = {"note": "New"} + r = self.client.put(url, data) + self.assertEqual(r.status_code, 200) + + new_note = Note.objects.get(pk=note.id) # type: ignore + self.assertEqual(new_note.note, data["note"]) + + self.check_not_authenticated("put", url) + + def test_delete_note(self): + # setup + agent = baker.make_recipe("agents.agent") + note = baker.make("agents.Note", agent=agent) + url = f"{base_url}/notes/{note.id}/" + + # test not found + r = self.client.delete(f"{base_url}/notes/500/") + self.assertEqual(r.status_code, 404) + + r = self.client.delete(url) + self.assertEqual(r.status_code, 200) + + self.assertFalse(Note.objects.filter(pk=note.id).exists()) # type: ignore + + self.check_not_authenticated("delete", url) + def test_get_agent_history(self): # setup data agent = baker.make_recipe("agents.agent") history = baker.make("agents.AgentHistory", agent=agent, _quantity=30) - url = f"/agents/history/{agent.id}/" + url = f"{base_url}/{agent.agent_id}/history/" # test agent not found - r = self.client.get("/agents/history/500/", format="json") + r = self.client.get(f"{base_url}/{agent.agent_id}123/history/", format="json") self.assertEqual(r.status_code, 404) # test pulling data @@ -1008,28 +1059,27 @@ def setUp(self): self.check_not_authenticated("post", url) """ def test_agent_maintenance_mode(self): - url = "/agents/maintenance/" + url = f"{base_url}/maintenance/" # setup data - site = baker.make("clients.Site") - agent = baker.make_recipe("agents.agent", site=site) + agent = baker.make_recipe("agents.agent") # Test client toggle maintenance mode - data = {"type": "Client", "id": site.client.id, "action": True} # type: ignore + data = {"type": "Client", "id": agent.site.client.id, "action": True} # type: ignore r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) self.assertTrue(Agent.objects.get(pk=agent.pk).maintenance_mode) # Test site toggle maintenance mode - data = {"type": "Site", "id": site.id, "action": False} # type: ignore + data = {"type": "Site", "id": agent.site.id, "action": False} # type: ignore r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) self.assertFalse(Agent.objects.get(pk=agent.pk).maintenance_mode) # Test agent toggle maintenance mode - data = {"type": "Agent", "id": agent.id, "action": True} + data = {"type": "Agent", "agent_id": agent.agent_id, "action": True} r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) @@ -1044,6 +1094,283 @@ def test_agent_maintenance_mode(self): self.check_not_authenticated("post", url) +class TestAgentPermissions(TacticalTestCase): + def setUp(self): + self.client_setup() + self.setup_coresettings() + + def test_list_agents_permissions(self): + # create user with empty role + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + url = f"{base_url}/" + + sites = baker.make("clients.Site", _quantity=5) + agents = baker.make_recipe("agents.agent", site=cycle(sites), _quantity=10) + + # test getting all agents + + # user with empty role should fail + self.check_not_authorized("get", url) + + # add can_list_agents roles and should succeed + user.role.can_list_agents = True + user.role.save() + + # all agents should be returned + response = self.check_authorized("get", url) + self.assertEqual(len(response.data), 10) + + # limit user to specific client. only 1 agent should be returned + user.role.can_view_clients.set([agents[4].client]) + response = self.check_authorized("get", url) + self.assertEqual(len(response.data), 2) + + # limit agent to specific site. 2 should be returned now + user.role.can_view_sites.set([agents[6].site]) + response = self.check_authorized("get", url) + self.assertEqual(len(response.data), 4) + + # make sure superusers work + self.check_authorized_superuser("get", url) + + @patch("agents.models.Agent.nats_cmd") + @patch("agents.views.reload_nats") + def test_get_edit_uninstall_permissions(self, reload_nats, nats_cmd): + # create user with empty role + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + agent = baker.make_recipe("agents.agent") + methods = ["get", "put", "delete"] + url = f"{base_url}/{agent.agent_id}/" + + # test user with no roles + for method in methods: + self.check_not_authorized(method, url) + + # add correct roles for view edit and delete + user.role.can_list_agents = True + user.role.can_edit_agent = True + user.role.can_uninstall_agents = True + user.role.save() + + for method in methods: + self.check_authorized(method, url) + + # test limiting users to clients and sites + sites = baker.make("clients.Site", _quantity=5) + agents = baker.make_recipe("agents.agent", site=cycle(sites), _quantity=10) + + # limit to client + user.role.can_view_clients.set([agents[5].client]) + + for method in methods: + self.check_not_authorized(method, f"{base_url}/{agents[6].agent_id}/") + self.check_authorized(method, f"{base_url}/{agents[5].agent_id}/") + + # limit to site + user.role.can_view_clients.clear() + user.role.can_view_sites.set([agents[1].site, agents[7].site]) + + for method in methods: + self.check_not_authorized(method, f"{base_url}/{agents[4].agent_id}/") + self.check_authorized(method, f"{base_url}/{agents[1].agent_id}/") + + # limit both client and site + user.role.can_view_clients.set([agents[0].client]) + + for method in methods: + self.check_not_authorized(method, f"{base_url}/{agents[9].agent_id}/") + self.check_authorized(method, f"{base_url}/{agents[0].agent_id}/") + + @patch("time.sleep") + @patch("agents.models.Agent.nats_cmd", return_value="ok") + def test_agent_actions_permissions(self, nats_cmd, sleep): + + agent = baker.make_recipe("agents.agent") + unauthorized_agent = baker.make_recipe("agents.agent") + + test_data = [ + {"method": "post", "action": "cmd", "role": "can_send_cmd"}, + {"method": "post", "action": "runscript", "role": "can_run_scripts"}, + {"method": "post", "action": "wmi", "role": "can_edit_agent"}, + {"method": "post", "action": "recover", "role": "can_recover_agents"}, + {"method": "post", "action": "reboot", "role": "can_reboot_agents"}, + {"method": "patch", "action": "reboot", "role": "can_reboot_agents"}, + {"method": "get", "action": "ping", "role": "can_ping_agents"}, + {"method": "get", "action": "meshcentral", "role": "can_use_mesh"}, + {"method": "post", "action": "meshcentral/recover", "role": "can_use_mesh"}, + {"method": "get", "action": "processes", "role": "can_manage_procs"}, + {"method": "delete", "action": "processes/1", "role": "can_manage_procs"}, + { + "method": "get", + "action": "eventlog/Application/30", + "role": "can_view_eventlogs", + }, + ] + + for test in test_data: + url = f"{base_url}/{agent.agent_id}/{test['action']}/" + + # test superuser access + self.check_authorized_superuser(test["method"], url) + + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + # test user without role + self.check_not_authorized(test["method"], url) + + # add user to role and test + setattr(user.role, test["role"], True) + user.role.save() + + self.check_authorized(test["method"], url) + self.check_authorized( + test["method"], + f"{base_url}/{unauthorized_agent.agent_id}/{test['action']}/", + ) + + # limit user to client + user.role.can_view_clients.set([agent.client]) + self.check_authorized(test["method"], url) + self.check_not_authorized( + test["method"], + f"{base_url}/{unauthorized_agent.agent_id}/{test['action']}/", + ) + + def test_agent_notes_permissions(self): + + agent = baker.make_recipe("agents.agent") + notes = baker.make("agents.Note", agent=agent, _quantity=5) + + unauthorized_agent = baker.make_recipe("agents.agent") + unauthorized_notes = baker.make( + "agents.Note", agent=unauthorized_agent, _quantity=7 + ) + + test_data = [ + {"url": f"{base_url}/notes/", "method": "get", "role": "can_list_notes"}, + {"url": f"{base_url}/notes/", "method": "post", "role": "can_manage_notes"}, + { + "url": f"{base_url}/notes/{notes[0].id}/", + "method": "get", + "role": "can_list_notes", + }, + { + "url": f"{base_url}/notes/{notes[0].id}/", + "method": "put", + "role": "can_manage_notes", + }, + { + "url": f"{base_url}/notes/{notes[0].id}/", + "method": "delete", + "role": "can_manage_notes", + }, + ] + + # check superuser access, user with no roles access, and with with roles access + for test in test_data: + self.check_authorized_superuser(test["method"], test["url"]) + + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + self.check_not_authorized(test["method"], test["url"]) + + setattr(user.role, test["role"], True) + user.role.save() + self.check_authorized(test["method"], test["url"]) + + # test limiting user to clients and sites + user = self.create_user_with_roles(["can_list_notes", "can_manage_notes"]) + user.role.can_view_sites.set([agent.site]) + user.role.save() + self.client.force_authenticate(user=user) + + authorized_data = {"note": "Test not here", "agent_id": agent.agent_id} + + unauthorized_data = { + "note": "Test note here", + "agent_id": unauthorized_agent.agent_id, + } + + # should only return the 4 allowed agent notes (one got deleted above in loop) + r = self.client.get(f"{base_url}/notes/") + self.assertEqual(len(r.data), 4) + + # test with agent_id in url + self.check_authorized("get", f"{base_url}/{agent.agent_id}/notes/") + self.check_not_authorized( + "get", f"{base_url}/{unauthorized_agent.agent_id}/notes/" + ) + + # test post get, put, and delete and make sure unauthorized is returned with unauthorized agent and works for authorized + self.check_authorized("post", f"{base_url}/notes/", authorized_data) + self.check_not_authorized("post", f"{base_url}/notes/", unauthorized_data) + self.check_authorized("get", f"{base_url}/notes/{notes[2].id}/") + self.check_not_authorized( + "get", f"{base_url}/notes/{unauthorized_notes[2].id}/" + ) + self.check_authorized( + "put", f"{base_url}/notes/{notes[3].id}/", authorized_data + ) + self.check_not_authorized( + "put", f"{base_url}/notes/{unauthorized_notes[3].id}/", unauthorized_data + ) + self.check_authorized("delete", f"{base_url}/notes/{notes[3].id}/") + self.check_not_authorized( + "delete", f"{base_url}/notes/{unauthorized_notes[3].id}/" + ) + + def test_get_agent_history_permissions(self): + # create user with empty role + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + sites = baker.make("clients.Site", _quantity=2) + agent = baker.make_recipe("agents.agent", site=sites[0]) + history = baker.make("agents.AgentHistory", agent=agent, _quantity=5) + unauthorized_agent = baker.make_recipe("agents.agent", site=sites[1]) + unauthorized_history = baker.make( + "agents.AgentHistory", agent=unauthorized_agent, _quantity=6 + ) + + url = f"{base_url}/history/" + authorized_url = f"{base_url}/{agent.agent_id}/history/" + unauthorized_url = f"{base_url}/{unauthorized_agent.agent_id}/history/" + + # test getting all agents + + # user with empty role should fail + self.check_not_authorized("get", url) + self.check_not_authorized("get", authorized_url) + self.check_not_authorized("get", unauthorized_url) + + # add can_list_agents roles and should succeed + user.role.can_list_agent_history = True + user.role.save() + + # all agents should be returned + r = self.check_authorized("get", url) + self.check_authorized("get", authorized_url) + self.check_authorized("get", unauthorized_url) + self.assertEqual(len(r.data), 11) + + # limit user to specific client. + user.role.can_view_clients.set([agent.client]) + self.check_authorized("get", authorized_url) + self.check_not_authorized("get", unauthorized_url) + r = self.check_authorized("get", url) + self.assertEqual(len(r.data), 5) + + # make sure superusers work + self.check_authorized_superuser("get", url) + self.check_authorized_superuser("get", authorized_url) + self.check_authorized_superuser("get", unauthorized_url) + + class TestAgentTasks(TacticalTestCase): def setUp(self): self.authenticate() diff --git a/api/tacticalrmm/agents/urls.py b/api/tacticalrmm/agents/urls.py index fb497519aa..5fd5cc29f1 100644 --- a/api/tacticalrmm/agents/urls.py +++ b/api/tacticalrmm/agents/urls.py @@ -1,33 +1,40 @@ from django.urls import path from . import views +from checks.views import GetAddChecks +from autotasks.views import GetAddAutoTasks urlpatterns = [ - path("listagents/", views.AgentsTableList.as_view()), - path("listagentsnodetail/", views.list_agents_no_detail), - path("/agenteditdetails/", views.agent_edit_details), - path("overdueaction/", views.overdue_action), - path("sendrawcmd/", views.send_raw_cmd), - path("/agentdetail/", views.agent_detail), - path("/meshcentral/", views.meshcentral), - path("/getmeshexe/", views.get_mesh_exe), - path("uninstall/", views.uninstall), - path("editagent/", views.edit_agent), - path("/geteventlog///", views.get_event_log), - path("getagentversions/", views.get_agent_versions), - path("updateagents/", views.update_agents), - path("/getprocs/", views.get_processes), - path("//killproc/", views.kill_proc), - path("reboot/", views.Reboot.as_view()), - path("installagent/", views.install_agent), - path("/ping/", views.ping), - path("recover/", views.recover), - path("runscript/", views.run_script), - path("/recovermesh/", views.recover_mesh), - path("/notes/", views.GetAddNotes.as_view()), - path("/note/", views.GetEditDeleteNote.as_view()), - path("bulk/", views.bulk), + # agent views + path("", views.GetAgents.as_view()), + path("/", views.GetUpdateDeleteAgent.as_view()), + path("/cmd/", views.send_raw_cmd), + path("/runscript/", views.run_script), + path("/wmi/", views.WMI.as_view()), + path("/recover/", views.recover), + path("/reboot/", views.Reboot.as_view()), + path("/ping/", views.ping), + # alias for checks get view + path("/checks/", GetAddChecks.as_view()), + # alias for autotasks get view + path("/tasks/", GetAddAutoTasks.as_view()), + # agent remote background + path("/meshcentral/", views.AgentMeshCentral.as_view()), + path("/meshcentral/recover/", views.AgentMeshCentral.as_view()), + path("/processes/", views.AgentProcesses.as_view()), + path("/processes//", views.AgentProcesses.as_view()), + path("/eventlog///", views.get_event_log), + # agent history + path("history/", views.AgentHistoryView.as_view()), + path("/history/", views.AgentHistoryView.as_view()), + # agent notes + path("notes/", views.GetAddNotes.as_view()), + path("notes//", views.GetEditDeleteNote.as_view()), + path("/notes/", views.GetAddNotes.as_view()), path("maintenance/", views.agent_maintenance), - path("/wmi/", views.WMI.as_view()), - path("history//", views.AgentHistoryView.as_view()), + path("versions/", views.get_agent_versions), + path("update/", views.update_agents), + path("installer/", views.install_agent), + path("bulk/", views.bulk), + path("/getmeshexe/", views.get_mesh_exe), ] diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index 1d29840d17..f02ada52fb 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -8,12 +8,14 @@ from django.conf import settings from django.http import HttpResponse from django.shortcuts import get_object_or_404 +from django.db.models import Q from packaging import version as pyver from rest_framework import status from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.exceptions import PermissionDenied from core.models import CoreSettings from logs.models import AuditLog, DebugLog, PendingAction @@ -22,20 +24,23 @@ from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats from winupdate.serializers import WinUpdatePolicySerializer from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task +from tacticalrmm.permissions import _has_perm_on_agent from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory from .permissions import ( - EditAgentPerms, + AgentHistoryPerms, + AgentPerms, EvtLogPerms, InstallAgentPerms, - ManageNotesPerms, + RecoverAgentPerms, + AgentNotesPerms, ManageProcPerms, MeshPerms, RebootAgentPerms, RunBulkPerms, RunScriptPerms, SendCMDPerms, - UninstallPerms, + PingAgentPerms, UpdateAgentPerms, ) from .serializers import ( @@ -43,16 +48,211 @@ AgentEditSerializer, AgentHistorySerializer, AgentHostnameSerializer, - AgentOverdueActionSerializer, AgentSerializer, AgentTableSerializer, - NoteSerializer, - NotesSerializer, + AgentNoteSerializer, ) from .tasks import run_script_email_results_task, send_agent_update_task -@api_view() +class GetAgents(APIView): + permission_classes = [IsAuthenticated, AgentPerms] + + def get(self, request): + if "site" in request.query_params.keys(): + filter = Q(site_id=request.query_params["site"]) + elif "client" in request.query_params.keys(): + filter = Q(site__client_id=request.query_params["client"]) + else: + filter = Q() + + # by default detail=true + if ( + "detail" not in request.query_params.keys() + or "detail" in request.query_params.keys() + and request.query_params["detail"] == "true" + ): + agents = ( + Agent.permissions.filter_by_role(request.user) + .select_related("site", "policy", "alert_template") + .prefetch_related("agentchecks") + .filter(filter) + .only( + "pk", + "hostname", + "agent_id", + "site", + "policy", + "alert_template", + "monitoring_type", + "description", + "needs_reboot", + "overdue_text_alert", + "overdue_email_alert", + "overdue_time", + "offline_time", + "last_seen", + "boot_time", + "logged_in_username", + "last_logged_in_user", + "time_zone", + "maintenance_mode", + "pending_actions_count", + "has_patches_pending", + ) + ) + ctx = {"default_tz": get_default_timezone()} + serializer = AgentTableSerializer(agents, many=True, context=ctx) + + # if detail=false + else: + agents = ( + Agent.permissions.filter_by_role(request.user) + .select_related("site") + .filter(filter) + .only("agent_id", "hostname", "site") + ) + serializer = AgentHostnameSerializer(agents, many=True) + + return Response(serializer.data) + + +class GetUpdateDeleteAgent(APIView): + permission_classes = [IsAuthenticated, AgentPerms] + + # get agent details + def get(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + return Response(AgentSerializer(agent).data) + + # edit agent + def put(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + + a_serializer = AgentEditSerializer( + instance=agent, data=request.data, partial=True + ) + a_serializer.is_valid(raise_exception=True) + a_serializer.save() + + if "winupdatepolicy" in request.data.keys(): + policy = agent.winupdatepolicy.get() # type: ignore + p_serializer = WinUpdatePolicySerializer( + instance=policy, data=request.data["winupdatepolicy"][0] + ) + p_serializer.is_valid(raise_exception=True) + p_serializer.save() + + if "custom_fields" in request.data.keys(): + + for field in request.data["custom_fields"]: + + custom_field = field + custom_field["agent"] = agent.id # type: ignore + + if AgentCustomField.objects.filter( + field=field["field"], agent=agent.id # type: ignore + ): + value = AgentCustomField.objects.get( + field=field["field"], agent=agent.id # type: ignore + ) + serializer = AgentCustomFieldSerializer( + instance=value, data=custom_field + ) + serializer.is_valid(raise_exception=True) + serializer.save() + else: + serializer = AgentCustomFieldSerializer(data=custom_field) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response("ok") + + # uninstall agent + def delete(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) + name = agent.hostname + agent.delete() + reload_nats() + return Response(f"{name} will now be uninstalled.") + + +class AgentProcesses(APIView): + permission_classes = [IsAuthenticated, ManageProcPerms] + + # list agent processes + def get(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5)) + if r == "timeout": + return notify_error("Unable to contact the agent") + return Response(r) + + # kill agent process + def delete(self, request, agent_id, pid): + agent = get_object_or_404(Agent, agent_id=agent_id) + r = asyncio.run( + agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15) + ) + + if r == "timeout": + return notify_error("Unable to contact the agent") + elif r != "ok": + return notify_error(r) + + return Response(f"Process with PID: {pid} was ended successfully") + + +class AgentMeshCentral(APIView): + permission_classes = [IsAuthenticated, MeshPerms] + + # get mesh urls + def get(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + core = CoreSettings.objects.first() + + token = agent.get_login_token( + key=core.mesh_token, + user=f"user//{core.mesh_username.lower()}", # type:ignore + ) + + if token == "err": + return notify_error("Invalid mesh token") + + control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" # type:ignore + terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" # type:ignore + file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" # type:ignore + + AuditLog.audit_mesh_session( + username=request.user.username, + agent=agent, + debug_info={"ip": request._client_ip}, + ) + + ret = { + "hostname": agent.hostname, + "control": control, + "terminal": terminal, + "file": file, + "status": agent.status, + "client": agent.client.name, + "site": agent.site.name, + } + return Response(ret) + + # start mesh recovery + def post(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + data = {"func": "recover", "payload": {"mode": "mesh"}} + r = asyncio.run(agent.nats_cmd(data, timeout=90)) + if r != "ok": + return notify_error("Unable to contact the agent") + + return Response(f"Repaired mesh agent on {agent.hostname}") + + +@api_view(["GET"]) def get_agent_versions(request): agents = Agent.objects.prefetch_related("site").only("pk", "hostname") return Response( @@ -76,10 +276,10 @@ def update_agents(request): return Response("ok") -@api_view() -@permission_classes([IsAuthenticated, UninstallPerms]) -def ping(request, pk): - agent = get_object_or_404(Agent, pk=pk) +@api_view(["GET"]) +@permission_classes([IsAuthenticated, PingAgentPerms]) +def ping(request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) status = "offline" attempts = 0 while 1: @@ -97,131 +297,12 @@ def ping(request, pk): return Response({"name": agent.hostname, "status": status}) -@api_view(["DELETE"]) -@permission_classes([IsAuthenticated, UninstallPerms]) -def uninstall(request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) - asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) - name = agent.hostname - agent.delete() - reload_nats() - return Response(f"{name} will now be uninstalled.") - - -@api_view(["PATCH", "PUT"]) -@permission_classes([IsAuthenticated, EditAgentPerms]) -def edit_agent(request): - agent = get_object_or_404(Agent, pk=request.data["id"]) - - a_serializer = AgentEditSerializer(instance=agent, data=request.data, partial=True) - a_serializer.is_valid(raise_exception=True) - a_serializer.save() - - if "winupdatepolicy" in request.data.keys(): - policy = agent.winupdatepolicy.get() # type: ignore - p_serializer = WinUpdatePolicySerializer( - instance=policy, data=request.data["winupdatepolicy"][0] - ) - p_serializer.is_valid(raise_exception=True) - p_serializer.save() - - if "custom_fields" in request.data.keys(): - - for field in request.data["custom_fields"]: - - custom_field = field - custom_field["agent"] = agent.id # type: ignore - - if AgentCustomField.objects.filter( - field=field["field"], agent=agent.id # type: ignore - ): - value = AgentCustomField.objects.get( - field=field["field"], agent=agent.id # type: ignore - ) - serializer = AgentCustomFieldSerializer( - instance=value, data=custom_field - ) - serializer.is_valid(raise_exception=True) - serializer.save() - else: - serializer = AgentCustomFieldSerializer(data=custom_field) - serializer.is_valid(raise_exception=True) - serializer.save() - - return Response("ok") - - -@api_view() -@permission_classes([IsAuthenticated, MeshPerms]) -def meshcentral(request, pk): - agent = get_object_or_404(Agent, pk=pk) - core = CoreSettings.objects.first() - - token = agent.get_login_token( - key=core.mesh_token, user=f"user//{core.mesh_username}" # type:ignore - ) - - if token == "err": - return notify_error("Invalid mesh token") - - control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" # type:ignore - terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" # type:ignore - file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" # type:ignore - - AuditLog.audit_mesh_session( - username=request.user.username, - agent=agent, - debug_info={"ip": request._client_ip}, - ) - - ret = { - "hostname": agent.hostname, - "control": control, - "terminal": terminal, - "file": file, - "status": agent.status, - "client": agent.client.name, - "site": agent.site.name, - } - return Response(ret) - - -@api_view() -def agent_detail(request, pk): - agent = get_object_or_404(Agent, pk=pk) - return Response(AgentSerializer(agent).data) - - -@api_view() -def get_processes(request, pk): - agent = get_object_or_404(Agent, pk=pk) - r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5)) - if r == "timeout": - return notify_error("Unable to contact the agent") - return Response(r) - - -@api_view() -@permission_classes([IsAuthenticated, ManageProcPerms]) -def kill_proc(request, pk, pid): - agent = get_object_or_404(Agent, pk=pk) - r = asyncio.run( - agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15) - ) - - if r == "timeout": - return notify_error("Unable to contact the agent") - elif r != "ok": - return notify_error(r) - - return Response("ok") - - -@api_view() +@api_view(["GET"]) @permission_classes([IsAuthenticated, EvtLogPerms]) -def get_event_log(request, pk, logtype, days): - agent = get_object_or_404(Agent, pk=pk) +def get_event_log(request, agent_id, logtype, days): + agent = get_object_or_404(Agent, agent_id=agent_id) timeout = 180 if logtype == "Security" else 30 + data = { "func": "eventlog", "timeout": timeout, @@ -239,8 +320,8 @@ def get_event_log(request, pk, logtype, days): @api_view(["POST"]) @permission_classes([IsAuthenticated, SendCMDPerms]) -def send_raw_cmd(request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) +def send_raw_cmd(request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) timeout = int(request.data["timeout"]) data = { "func": "rawcmd", @@ -276,81 +357,11 @@ def send_raw_cmd(request): return Response(r) -class AgentsTableList(APIView): - def patch(self, request): - if "sitePK" in request.data.keys(): - queryset = ( - Agent.objects.select_related("site", "policy", "alert_template") - .prefetch_related("agentchecks") - .filter(site_id=request.data["sitePK"]) - ) - elif "clientPK" in request.data.keys(): - queryset = ( - Agent.objects.select_related("site", "policy", "alert_template") - .prefetch_related("agentchecks") - .filter(site__client_id=request.data["clientPK"]) - ) - else: - queryset = Agent.objects.select_related( - "site", "policy", "alert_template" - ).prefetch_related("agentchecks") - - queryset = queryset.only( - "pk", - "hostname", - "agent_id", - "site", - "policy", - "alert_template", - "monitoring_type", - "description", - "needs_reboot", - "overdue_text_alert", - "overdue_email_alert", - "overdue_time", - "offline_time", - "last_seen", - "boot_time", - "logged_in_username", - "last_logged_in_user", - "time_zone", - "maintenance_mode", - "pending_actions_count", - "has_patches_pending", - ) - ctx = {"default_tz": get_default_timezone()} - serializer = AgentTableSerializer(queryset, many=True, context=ctx) - return Response(serializer.data) - - -@api_view() -def list_agents_no_detail(request): - agents = Agent.objects.select_related("site").only("pk", "hostname", "site") - return Response(AgentHostnameSerializer(agents, many=True).data) - - -@api_view() -def agent_edit_details(request, pk): - agent = get_object_or_404(Agent, pk=pk) - return Response(AgentEditSerializer(agent).data) - - -@api_view(["POST"]) -def overdue_action(request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) - serializer = AgentOverdueActionSerializer( - instance=agent, data=request.data, partial=True - ) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(agent.hostname) - - class Reboot(APIView): permission_classes = [IsAuthenticated, RebootAgentPerms] # reboot now - def post(self, request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) + def post(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10)) if r != "ok": return notify_error("Unable to contact the agent") @@ -358,8 +369,8 @@ def post(self, request): return Response("ok") # reboot later - def patch(self, request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) + def patch(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) try: obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M") @@ -417,12 +428,16 @@ def install_agent(request): if arch == "64" and not os.path.exists( os.path.join(settings.EXE_DIR, "meshagent.exe") ): - return Response(status=status.HTTP_406_NOT_ACCEPTABLE) + return notify_error( + "Missing 64 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral" + ) if arch == "32" and not os.path.exists( os.path.join(settings.EXE_DIR, "meshagent-x86.exe") ): - return Response(status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) + return notify_error( + "Missing 32 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral" + ) inno = ( f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe" @@ -539,8 +554,9 @@ def install_agent(request): @api_view(["POST"]) -def recover(request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) +@permission_classes([IsAuthenticated, RecoverAgentPerms]) +def recover(request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) mode = request.data["mode"] # attempt a realtime recovery, otherwise fall back to old recovery method @@ -577,8 +593,8 @@ def recover(request): @api_view(["POST"]) @permission_classes([IsAuthenticated, RunScriptPerms]) -def run_script(request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) +def run_script(request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) script = get_object_or_404(Script, pk=request.data["script"]) output = request.data["output"] args = request.data["args"] @@ -671,17 +687,6 @@ def run_script(request): return Response(f"{script.name} will now be run on {agent.hostname}") -@api_view() -def recover_mesh(request, pk): - agent = get_object_or_404(Agent, pk=pk) - data = {"func": "recover", "payload": {"mode": "mesh"}} - r = asyncio.run(agent.nats_cmd(data, timeout=90)) - if r != "ok": - return notify_error("Unable to contact the agent") - - return Response(f"Repaired mesh agent on {agent.hostname}") - - @api_view(["POST"]) def get_mesh_exe(request, arch): filename = "meshagent.exe" if arch == "64" else "meshagent-x86.exe" @@ -704,34 +709,62 @@ def get_mesh_exe(request, arch): class GetAddNotes(APIView): - def get(self, request, pk): - agent = get_object_or_404(Agent, pk=pk) - return Response(NotesSerializer(agent).data) + permission_classes = [IsAuthenticated, AgentNotesPerms] + + def get(self, request, agent_id=None): + if agent_id: + agent = get_object_or_404(Agent, agent_id=agent_id) + notes = Note.objects.filter(agent=agent) + else: + notes = Note.permissions.filter_by_role(request.user) + + return Response(AgentNoteSerializer(notes, many=True).data) - def post(self, request, pk): - agent = get_object_or_404(Agent, pk=pk) - serializer = NoteSerializer(data=request.data, partial=True) + def post(self, request): + agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) + if not _has_perm_on_agent(request.user, agent.agent_id): + raise PermissionDenied() + + data = { + "note": request.data["note"], + "agent": agent.pk, + "user": request.user.pk, + } + + serializer = AgentNoteSerializer(data=data) serializer.is_valid(raise_exception=True) - serializer.save(agent=agent, user=request.user) + serializer.save() return Response("Note added!") class GetEditDeleteNote(APIView): - permission_classes = [IsAuthenticated, ManageNotesPerms] + permission_classes = [IsAuthenticated, AgentNotesPerms] def get(self, request, pk): note = get_object_or_404(Note, pk=pk) - return Response(NoteSerializer(note).data) - def patch(self, request, pk): + if not _has_perm_on_agent(request.user, note.agent.agent_id): + raise PermissionDenied() + + return Response(AgentNoteSerializer(note).data) + + def put(self, request, pk): note = get_object_or_404(Note, pk=pk) - serializer = NoteSerializer(instance=note, data=request.data, partial=True) + + if not _has_perm_on_agent(request.user, note.agent.agent_id): + raise PermissionDenied() + + serializer = AgentNoteSerializer(instance=note, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() return Response("Note edited!") def delete(self, request, pk): note = get_object_or_404(Note, pk=pk) + + if not _has_perm_on_agent(request.user, note.agent.agent_id): + raise PermissionDenied() + note.delete() return Response("Note was deleted!") @@ -807,40 +840,63 @@ def bulk(request): @api_view(["POST"]) +@permission_classes([IsAuthenticated, RunBulkPerms]) def agent_maintenance(request): if request.data["type"] == "Client": - Agent.objects.filter(site__client_id=request.data["id"]).update( - maintenance_mode=request.data["action"] + count = ( + Agent.permissions.filter_by_role(request.user) + .filter(site__client_id=request.data["id"]) + .update(maintenance_mode=request.data["action"]) ) elif request.data["type"] == "Site": - Agent.objects.filter(site_id=request.data["id"]).update( - maintenance_mode=request.data["action"] + count = ( + Agent.permissions.filter_by_role(request.user) + .filter(site_id=request.data["id"]) + .update(maintenance_mode=request.data["action"]) ) elif request.data["type"] == "Agent": - agent = Agent.objects.get(pk=request.data["id"]) + agent = Agent.permissions.filter_by_role(request.user).get( + agent_id=request.data["agent_id"] + ) + if agent: + count = 1 + else: + count = 0 agent.maintenance_mode = request.data["action"] agent.save(update_fields=["maintenance_mode"]) else: return notify_error("Invalid data") - return Response("ok") + if count: + return Response(f"{count} agents have been put in maintenance mode.") + else: + return Response( + f"No agents have been put in maintenance mode. You might not have permissions to the resources." + ) class WMI(APIView): - def get(self, request, pk): - agent = get_object_or_404(Agent, pk=pk) + permission_classes = [IsAuthenticated, AgentPerms] + + def post(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20)) if r != "ok": return notify_error("Unable to contact the agent") - return Response("ok") + return Response("Agent WMI data refreshed successfully") class AgentHistoryView(APIView): - def get(self, request, pk): - agent = get_object_or_404(Agent, pk=pk) - history = AgentHistory.objects.filter(agent=agent) + permission_classes = [IsAuthenticated, AgentHistoryPerms] + + def get(self, request, agent_id=None): + if agent_id: + agent = get_object_or_404(Agent, agent_id=agent_id) + history = AgentHistory.objects.filter(agent=agent) + else: + history = AgentHistory.permissions.filter_by_role(request.user) ctx = {"default_tz": get_default_timezone()} return Response(AgentHistorySerializer(history, many=True, context=ctx).data) diff --git a/api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py b/api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py new file mode 100644 index 0000000000..48835f65a7 --- /dev/null +++ b/api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.6 on 2021-09-17 19:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("alerts", "0009_auto_20210721_1810"), + ] + + operations = [ + migrations.AlterField( + model_name="alerttemplate", + name="created_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="alerttemplate", + name="modified_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/automation/migrations/0009_auto_20210917_1954.py b/api/tacticalrmm/automation/migrations/0009_auto_20210917_1954.py new file mode 100644 index 0000000000..617e3a3de3 --- /dev/null +++ b/api/tacticalrmm/automation/migrations/0009_auto_20210917_1954.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.6 on 2021-09-17 19:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("automation", "0008_auto_20210302_0415"), + ] + + operations = [ + migrations.AlterField( + model_name="policy", + name="created_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="policy", + name="modified_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/automation/permissions.py b/api/tacticalrmm/automation/permissions.py index e4fce1cb31..bae7a55d3e 100644 --- a/api/tacticalrmm/automation/permissions.py +++ b/api/tacticalrmm/automation/permissions.py @@ -6,6 +6,6 @@ class AutomationPolicyPerms(permissions.BasePermission): def has_permission(self, r, view): if r.method == "GET": - return True - - return _has_perm(r, "can_manage_automation_policies") + return _has_perm(r, "can_list_automation_polcies") + else: + return _has_perm(r, "can_manage_automation_policies") diff --git a/api/tacticalrmm/automation/serializers.py b/api/tacticalrmm/automation/serializers.py index 85b9273a0e..c292d028e8 100644 --- a/api/tacticalrmm/automation/serializers.py +++ b/api/tacticalrmm/automation/serializers.py @@ -10,6 +10,7 @@ from clients.models import Client from clients.serializers import ClientSerializer, SiteSerializer from winupdate.serializers import WinUpdatePolicySerializer +from checks.serializers import CheckSerializer from .models import Policy @@ -65,23 +66,8 @@ class Meta: fields = "__all__" -class PolicyCheckSerializer(ModelSerializer): - class Meta: - model = Check - fields = ( - "id", - "check_type", - "readable_desc", - "assignedtask", - "text_alert", - "email_alert", - "dashboard_alert", - ) - depth = 1 - - class AutoTasksFieldSerializer(ModelSerializer): - assigned_check = PolicyCheckSerializer(read_only=True) + assigned_check = CheckSerializer(read_only=True) script = ReadOnlyField(source="script.id") custom_field = ReadOnlyField(source="custom_field.id") diff --git a/api/tacticalrmm/automation/tests.py b/api/tacticalrmm/automation/tests.py index c0b1001838..0d287f5bd4 100644 --- a/api/tacticalrmm/automation/tests.py +++ b/api/tacticalrmm/automation/tests.py @@ -9,7 +9,6 @@ from .serializers import ( AutoTasksFieldSerializer, - PolicyCheckSerializer, PolicyCheckStatusSerializer, PolicyOverviewSerializer, PolicySerializer, @@ -196,23 +195,6 @@ def test_get_all_policy_tasks(self): self.check_not_authenticated("get", url) - def test_get_all_policy_checks(self): - - # setup data - policy = baker.make("automation.Policy") - checks = self.create_checks(policy=policy) - - url = f"/automation/{policy.pk}/policychecks/" # type: ignore - - resp = self.client.get(url, format="json") - serializer = PolicyCheckSerializer(checks, many=True) - - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore - self.assertEqual(len(resp.data), 7) # type: ignore - - self.check_not_authenticated("get", url) - def test_get_policy_check_status(self): # setup data site = baker.make("clients.Site") diff --git a/api/tacticalrmm/automation/urls.py b/api/tacticalrmm/automation/urls.py index 6ae5aae9a9..41b8615aa6 100644 --- a/api/tacticalrmm/automation/urls.py +++ b/api/tacticalrmm/automation/urls.py @@ -1,6 +1,7 @@ from django.urls import path from . import views +from checks.views import GetAddChecks urlpatterns = [ path("policies/", views.GetAddPolicies.as_view()), @@ -8,7 +9,7 @@ path("policies/overview/", views.OverviewPolicy.as_view()), path("policies//", views.GetUpdateDeletePolicy.as_view()), path("sync/", views.PolicySync.as_view()), - path("/policychecks/", views.PolicyCheck.as_view()), + path("policies//checks/", GetAddChecks.as_view()), path("/policyautomatedtasks/", views.PolicyAutoTask.as_view()), path("policycheckstatus//check/", views.PolicyCheck.as_view()), path("policyautomatedtaskstatus//task/", views.PolicyAutoTask.as_view()), diff --git a/api/tacticalrmm/automation/views.py b/api/tacticalrmm/automation/views.py index e8efb679ea..037b1df36d 100644 --- a/api/tacticalrmm/automation/views.py +++ b/api/tacticalrmm/automation/views.py @@ -16,7 +16,6 @@ from .permissions import AutomationPolicyPerms from .serializers import ( AutoTasksFieldSerializer, - PolicyCheckSerializer, PolicyCheckStatusSerializer, PolicyOverviewSerializer, PolicySerializer, @@ -124,10 +123,6 @@ def put(self, request, task): class PolicyCheck(APIView): permission_classes = [IsAuthenticated, AutomationPolicyPerms] - def get(self, request, pk): - checks = Check.objects.filter(policy__pk=pk, agent=None) - return Response(PolicyCheckSerializer(checks, many=True).data) - def patch(self, request, check): checks = Check.objects.filter(parent_check=check) return Response(PolicyCheckStatusSerializer(checks, many=True).data) diff --git a/api/tacticalrmm/autotasks/migrations/0023_auto_20210917_1954.py b/api/tacticalrmm/autotasks/migrations/0023_auto_20210917_1954.py new file mode 100644 index 0000000000..5c1aa3318e --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0023_auto_20210917_1954.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.6 on 2021-09-17 19:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("autotasks", "0022_automatedtask_collector_all_output"), + ] + + operations = [ + migrations.AlterField( + model_name="automatedtask", + name="created_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="automatedtask", + name="modified_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/autotasks/models.py b/api/tacticalrmm/autotasks/models.py index 12f6bdea3d..7c48b5752a 100644 --- a/api/tacticalrmm/autotasks/models.py +++ b/api/tacticalrmm/autotasks/models.py @@ -12,6 +12,7 @@ from django.db.utils import DatabaseError from django.utils import timezone as djangotime from logs.models import BaseAuditModel, DebugLog +from tacticalrmm.models import PermissionManager from packaging import version as pyver from tacticalrmm.utils import bitdays_to_string @@ -47,6 +48,9 @@ class AutomatedTask(BaseAuditModel): + objects = models.Manager() + permissions = PermissionManager() + agent = models.ForeignKey( "agents.Agent", related_name="autotasks", diff --git a/api/tacticalrmm/autotasks/permissions.py b/api/tacticalrmm/autotasks/permissions.py index b49d9bf8d6..222fc58ddb 100644 --- a/api/tacticalrmm/autotasks/permissions.py +++ b/api/tacticalrmm/autotasks/permissions.py @@ -1,14 +1,19 @@ from rest_framework import permissions -from tacticalrmm.permissions import _has_perm +from tacticalrmm.permissions import _has_perm, _has_perm_on_agent -class ManageAutoTaskPerms(permissions.BasePermission): +class AutoTaskPerms(permissions.BasePermission): def has_permission(self, r, view): if r.method == "GET": - return True - - return _has_perm(r, "can_manage_autotasks") + if "agent_id" in view.kwargs.keys(): + return _has_perm(r, "can_list_autotasks") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) + else: + return _has_perm(r, "can_list_autotasks") + else: + return _has_perm(r, "can_manage_autotasks") class RunAutoTaskPerms(permissions.BasePermission): diff --git a/api/tacticalrmm/autotasks/serializers.py b/api/tacticalrmm/autotasks/serializers.py index b383f7987c..378ea9a792 100644 --- a/api/tacticalrmm/autotasks/serializers.py +++ b/api/tacticalrmm/autotasks/serializers.py @@ -10,7 +10,7 @@ class TaskSerializer(serializers.ModelSerializer): - assigned_check = CheckSerializer(read_only=True) + assigned_check = CheckSerializer() schedule = serializers.ReadOnlyField() last_run = serializers.ReadOnlyField(source="last_run_as_timezone") alert_template = serializers.SerializerMethodField() @@ -37,19 +37,6 @@ class Meta: fields = "__all__" -class AutoTaskSerializer(serializers.ModelSerializer): - - autotasks = TaskSerializer(many=True, read_only=True) - - class Meta: - model = Agent - fields = ( - "pk", - "hostname", - "autotasks", - ) - - # below is for the windows agent class TaskRunnerScriptField(serializers.ModelSerializer): class Meta: diff --git a/api/tacticalrmm/autotasks/urls.py b/api/tacticalrmm/autotasks/urls.py index f686d4f9a7..fd5d2c5bac 100644 --- a/api/tacticalrmm/autotasks/urls.py +++ b/api/tacticalrmm/autotasks/urls.py @@ -3,7 +3,7 @@ from . import views urlpatterns = [ - path("/automatedtasks/", views.AutoTask.as_view()), - path("automatedtasks/", views.AddAutoTask.as_view()), - path("runwintask//", views.run_task), + path("", views.GetAddAutoTasks.as_view()), + path("/", views.GetEditDeleteAutoTask.as_view()), + path("/run/", views.RunAutoTask.as_view()), ] diff --git a/api/tacticalrmm/autotasks/views.py b/api/tacticalrmm/autotasks/views.py index 0b0446d34d..13f02bc4c9 100644 --- a/api/tacticalrmm/autotasks/views.py +++ b/api/tacticalrmm/autotasks/views.py @@ -1,55 +1,59 @@ from django.shortcuts import get_object_or_404 -from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.exceptions import PermissionDenied from agents.models import Agent -from checks.models import Check -from scripts.models import Script -from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error +from automation.models import Policy +from tacticalrmm.utils import get_bit_days +from tacticalrmm.permissions import _has_perm_on_agent from .models import AutomatedTask -from .permissions import ManageAutoTaskPerms, RunAutoTaskPerms -from .serializers import AutoTaskSerializer, TaskSerializer +from .permissions import AutoTaskPerms, RunAutoTaskPerms +from .serializers import TaskSerializer -class AddAutoTask(APIView): - permission_classes = [IsAuthenticated, ManageAutoTaskPerms] +class GetAddAutoTasks(APIView): + permission_classes = [IsAuthenticated, AutoTaskPerms] + + def get(self, request, agent_id=None, policy=None): + + if agent_id: + agent = get_object_or_404(Agent, agent_id=agent_id) + tasks = AutomatedTask.objects.filter(agent=agent) + elif policy: + policy = get_object_or_404(Policy, id=policy) + tasks = AutomatedTask.objects.filter(policy=policy) + else: + tasks = AutomatedTask.permissions.filter_by_role(request.user) + return Response(TaskSerializer(tasks, many=True).data) def post(self, request): - from automation.models import Policy from automation.tasks import generate_agent_autotasks_task from autotasks.tasks import create_win_task_schedule - data = request.data - script = get_object_or_404(Script, pk=data["autotask"]["script"]) + data = request.data.copy() - # Determine if adding check to Policy or Agent - if "policy" in data: - policy = get_object_or_404(Policy, id=data["policy"]) - # Object used for filter and save - parent = {"policy": policy} - else: + # Determine if adding to an agent and replace agent_id with pk + if "agent" in data.keys(): agent = get_object_or_404(Agent, pk=data["agent"]) - parent = {"agent": agent} - check = None - if data["autotask"]["assigned_check"]: - check = get_object_or_404(Check, pk=data["autotask"]["assigned_check"]) + if _has_perm_on_agent(request.user, agent.agent_id): + raise PermissionDenied() + + data["agent"] = agent.pk bit_weekdays = None - if data["autotask"]["run_time_days"]: - bit_weekdays = get_bit_days(data["autotask"]["run_time_days"]) + if "run_time_days" in data.keys(): + if data["run_time_days"]: + bit_weekdays = get_bit_days(data["run_time_days"]) + data.pop("run_time_days") - del data["autotask"]["run_time_days"] - serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent) + serializer = TaskSerializer(data=data) serializer.is_valid(raise_exception=True) task = serializer.save( - **parent, - script=script, win_task_name=AutomatedTask.generate_task_name(), - assigned_check=check, run_time_bit_weekdays=bit_weekdays, ) @@ -59,26 +63,29 @@ def post(self, request): elif task.policy: generate_agent_autotasks_task.delay(policy=task.policy.pk) - return Response("Task will be created shortly!") + return Response("The task has been created. It will show up on the agent on next checkin") -class AutoTask(APIView): - permission_classes = [IsAuthenticated, ManageAutoTaskPerms] +class GetEditDeleteAutoTask(APIView): + permission_classes = [IsAuthenticated, AutoTaskPerms] def get(self, request, pk): - agent = get_object_or_404(Agent, pk=pk) - ctx = { - "default_tz": get_default_timezone(), - "agent_tz": agent.time_zone, - } - return Response(AutoTaskSerializer(agent, context=ctx).data) + task = get_object_or_404(AutomatedTask, pk=pk) + + if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id): + raise PermissionDenied() + + return Response(TaskSerializer(task).data) def put(self, request, pk): from automation.tasks import update_policy_autotasks_fields_task task = get_object_or_404(AutomatedTask, pk=pk) + if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id): + raise PermissionDenied() + serializer = TaskSerializer(instance=task, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() @@ -86,31 +93,7 @@ def put(self, request, pk): if task.policy: update_policy_autotasks_fields_task.delay(task=task.pk) - return Response("ok") - - def patch(self, request, pk): - from automation.tasks import update_policy_autotasks_fields_task - from autotasks.tasks import enable_or_disable_win_task - - task = get_object_or_404(AutomatedTask, pk=pk) - - if "enableordisable" in request.data: - action = request.data["enableordisable"] - task.enabled = action - task.save(update_fields=["enabled"]) - action = "enabled" if action else "disabled" - - if task.policy: - update_policy_autotasks_fields_task.delay( - task=task.pk, update_agent=True - ) - elif task.agent: - enable_or_disable_win_task.delay(pk=task.pk) - - return Response(f"Task will be {action} shortly") - - else: - return notify_error("The request was invalid") + return Response("The task was updated") def delete(self, request, pk): from automation.tasks import delete_policy_autotasks_task @@ -118,6 +101,9 @@ def delete(self, request, pk): task = get_object_or_404(AutomatedTask, pk=pk) + if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id): + raise PermissionDenied() + if task.agent: delete_win_task_schedule.delay(pk=task.pk) elif task.policy: @@ -126,12 +112,16 @@ def delete(self, request, pk): return Response(f"{task.name} will be deleted shortly") +class RunAutoTask(APIView): + permission_classes = [IsAuthenticated, RunAutoTaskPerms] + + def post(self, request, pk): + from autotasks.tasks import run_win_task + + task = get_object_or_404(AutomatedTask, pk=pk) -@api_view() -@permission_classes([IsAuthenticated, RunAutoTaskPerms]) -def run_task(request, pk): - from autotasks.tasks import run_win_task + if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id): + raise PermissionDenied() - task = get_object_or_404(AutomatedTask, pk=pk) - run_win_task.delay(pk=pk) - return Response(f"{task.name} will now be run on {task.agent.hostname}") + run_win_task.delay(pk=pk) + return Response(f"{task.name} will now be run on {task.agent.hostname}") diff --git a/api/tacticalrmm/checks/migrations/0025_auto_20210917_1954.py b/api/tacticalrmm/checks/migrations/0025_auto_20210917_1954.py new file mode 100644 index 0000000000..14d8d1b7ba --- /dev/null +++ b/api/tacticalrmm/checks/migrations/0025_auto_20210917_1954.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.6 on 2021-09-17 19:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("checks", "0024_auto_20210606_1632"), + ] + + operations = [ + migrations.AlterField( + model_name="check", + name="created_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="check", + name="modified_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/checks/models.py b/api/tacticalrmm/checks/models.py index 24d3f5ee5e..2e1cd27e12 100644 --- a/api/tacticalrmm/checks/models.py +++ b/api/tacticalrmm/checks/models.py @@ -12,6 +12,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from logs.models import BaseAuditModel +from tacticalrmm.models import PermissionManager CHECK_TYPE_CHOICES = [ ("diskspace", "Disk Space Check"), @@ -50,6 +51,8 @@ class Check(BaseAuditModel): + objects = models.Manager() + permissions = PermissionManager() # common fields @@ -230,16 +233,16 @@ def last_run_as_timezone(self): return self.last_run - @property - def non_editable_fields(self) -> list[str]: + @staticmethod + def non_editable_fields() -> list[str]: return [ "check_type", - "status", "more_info", "last_run", "fail_count", "outage_history", "extra_details", + "status", "stdout", "stderr", "retcode", @@ -475,21 +478,6 @@ def serialize(check): return CheckAuditSerializer(check).data - # for policy diskchecks - @staticmethod - def all_disks(): - return [f"{i}:" for i in string.ascii_uppercase] - - # for policy service checks - @staticmethod - def load_default_services(): - with open( - os.path.join(settings.BASE_DIR, "services/default_services.json") - ) as f: - default_services = json.load(f) - - return default_services - def create_policy_check(self, agent=None, policy=None): if (not agent and not policy) or (agent and policy): diff --git a/api/tacticalrmm/checks/permissions.py b/api/tacticalrmm/checks/permissions.py index be3b29593c..999a1a0696 100644 --- a/api/tacticalrmm/checks/permissions.py +++ b/api/tacticalrmm/checks/permissions.py @@ -1,16 +1,23 @@ from rest_framework import permissions -from tacticalrmm.permissions import _has_perm +from tacticalrmm.permissions import _has_perm, _has_perm_on_agent -class ManageChecksPerms(permissions.BasePermission): +class ChecksPerms(permissions.BasePermission): def has_permission(self, r, view): - if r.method == "GET": - return True - - return _has_perm(r, "can_manage_checks") + if r.method == "GET" or r.method == "PATCH": + if "agent_id" in view.kwargs.keys(): + return _has_perm(r, "can_list_checks") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) + else: + return _has_perm(r, "can_list_checks") + else: + return _has_perm(r, "can_manage_checks") class RunChecksPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_run_checks") + return _has_perm(r, "can_run_checks") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) diff --git a/api/tacticalrmm/checks/serializers.py b/api/tacticalrmm/checks/serializers.py index 2537712161..5bc7e10840 100644 --- a/api/tacticalrmm/checks/serializers.py +++ b/api/tacticalrmm/checks/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from autotasks.models import AutomatedTask -from scripts.serializers import ScriptCheckSerializer, ScriptSerializer +from scripts.serializers import ScriptCheckSerializer from .models import Check, CheckHistory from scripts.models import Script @@ -18,7 +18,6 @@ class Meta: class CheckSerializer(serializers.ModelSerializer): readable_desc = serializers.ReadOnlyField() - script = ScriptSerializer(read_only=True) assigned_task = serializers.SerializerMethodField() last_run = serializers.ReadOnlyField(source="last_run_as_timezone") history_info = serializers.ReadOnlyField() @@ -57,6 +56,11 @@ class Meta: def validate(self, val): try: check_type = val["check_type"] + filter = ( + {"agent": val["agent"]} + if "agent" in val.keys() + else {"policy": val["policy"]} + ) except KeyError: return val @@ -65,7 +69,7 @@ def validate(self, val): if check_type == "diskspace": if not self.instance: # only on create checks = ( - Check.objects.filter(**self.context) + Check.objects.filter(**filter) .filter(check_type="diskspace") .exclude(managed_by_policy=True) ) @@ -102,7 +106,7 @@ def validate(self, val): if check_type == "cpuload" and not self.instance: if ( - Check.objects.filter(**self.context, check_type="cpuload") + Check.objects.filter(**filter, check_type="cpuload") .exclude(managed_by_policy=True) .exists() ): @@ -126,7 +130,7 @@ def validate(self, val): if check_type == "memory" and not self.instance: if ( - Check.objects.filter(**self.context, check_type="memory") + Check.objects.filter(**filter, check_type="memory") .exclude(managed_by_policy=True) .exists() ): diff --git a/api/tacticalrmm/checks/tests.py b/api/tacticalrmm/checks/tests.py index 5fe9ef6750..51f1776510 100644 --- a/api/tacticalrmm/checks/tests.py +++ b/api/tacticalrmm/checks/tests.py @@ -8,21 +8,46 @@ from .serializers import CheckSerializer +base_url = "/checks" + class TestCheckViews(TacticalTestCase): def setUp(self): self.authenticate() self.setup_coresettings() + def test_get_checks(self): + url = f"{base_url}/" + agent = baker.make_recipe("agents.agent") + baker.make("checks.Check", agent=agent, _quantity=4) + baker.make("checks.Check", _quantity=4) + + resp = self.client.get(url, format="json") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.data), 8) # type: ignore + + # test checks agent url + url = f"/agents/{agent.agent_id}/checks/" + resp = self.client.get(url, format="json") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.data), 4) # type: ignore + + # test agent doesn't exist + url = f"/agents/jh3498uf8fkh4ro8hfd8df98/checks/" + resp = self.client.get(url, format="json") + self.assertEqual(resp.status_code, 404) + + self.check_not_authenticated("get", url) + def test_delete_agent_check(self): # setup data agent = baker.make_recipe("agents.agent") check = baker.make_recipe("checks.diskspace_check", agent=agent) - resp = self.client.delete("/checks/500/check/", format="json") + resp = self.client.delete(f"{base_url}/500/", format="json") self.assertEqual(resp.status_code, 404) - url = f"/checks/{check.pk}/check/" + url = f"{base_url}/{check.pk}/" resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) @@ -30,11 +55,11 @@ def test_delete_agent_check(self): self.check_not_authenticated("delete", url) - def test_get_disk_check(self): + def test_get_check(self): # setup data disk_check = baker.make_recipe("checks.diskspace_check") - url = f"/checks/{disk_check.pk}/check/" + url = f"{base_url}/{disk_check.pk}/" resp = self.client.get(url, format="json") serializer = CheckSerializer(disk_check) @@ -46,296 +71,161 @@ def test_get_disk_check(self): def test_add_disk_check(self): # setup data agent = baker.make_recipe("agents.agent") + policy = baker.make("automation.Policy") - url = "/checks/checks/" - - valid_payload = { - "pk": agent.pk, - "check": { - "check_type": "diskspace", - "disk": "C:", - "error_threshold": 55, - "warning_threshold": 0, - "fails_b4_alert": 3, - }, - } - - resp = self.client.post(url, valid_payload, format="json") - self.assertEqual(resp.status_code, 200) - - # this should fail because we already have a check for drive C: in setup - invalid_payload = { - "pk": agent.pk, - "check": { - "check_type": "diskspace", - "disk": "C:", - "error_threshold": 55, - "warning_threshold": 0, - "fails_b4_alert": 3, - }, - } + url = f"{base_url}/" - resp = self.client.post(url, invalid_payload, format="json") - self.assertEqual(resp.status_code, 400) - - # this should fail because both error and warning threshold are 0 - invalid_payload = { - "pk": agent.pk, - "check": { - "check_type": "diskspace", - "disk": "C:", - "error_threshold": 0, - "warning_threshold": 0, - "fails_b4_alert": 3, - }, + agent_payload = { + "agent": agent.agent_id, + "check_type": "diskspace", + "disk": "C:", + "error_threshold": 55, + "warning_threshold": 0, + "fails_b4_alert": 3, } - resp = self.client.post(url, invalid_payload, format="json") - self.assertEqual(resp.status_code, 400) - - # this should fail because both error is greater than warning threshold - invalid_payload = { - "pk": agent.pk, - "check": { - "check_type": "diskspace", - "disk": "C:", - "error_threshold": 50, - "warning_threshold": 30, - "fails_b4_alert": 3, - }, + policy_payload = { + "policy": policy.id, + "check_type": "diskspace", + "disk": "C:", + "error_threshold": 55, + "warning_threshold": 0, + "fails_b4_alert": 3, } - resp = self.client.post(url, invalid_payload, format="json") - self.assertEqual(resp.status_code, 400) + for payload in [agent_payload, policy_payload]: - self.check_not_authenticated("post", url) - - def test_add_cpuload_check(self): - url = "/checks/checks/" - agent = baker.make_recipe("agents.agent") - payload = { - "pk": agent.pk, - "check": { - "check_type": "cpuload", - "error_threshold": 66, - "warning_threshold": 0, - "fails_b4_alert": 9, - }, - } + # add valid check + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 200) - resp = self.client.post(url, payload, format="json") - self.assertEqual(resp.status_code, 200) + # this should fail since we just added it + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 400) - payload["error_threshold"] = 87 - resp = self.client.post(url, payload, format="json") - self.assertEqual(resp.status_code, 400) - self.assertEqual( - resp.json()["non_field_errors"][0], - "A cpuload check for this agent already exists", - ) + # this should fail because both error and warning threshold are 0 + payload["error_threshold"] = 0 + payload["warning_threshold"] = 0 - # should fail because both error and warning thresholds are 0 - invalid_payload = { - "pk": agent.pk, - "check": { - "check_type": "cpuload", - "error_threshold": 0, - "warning_threshold": 0, - "fails_b4_alert": 9, - }, - } + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 400) - resp = self.client.post(url, invalid_payload, format="json") - self.assertEqual(resp.status_code, 400) - - # should fail because error is less than warning - invalid_payload = { - "pk": agent.pk, - "check": { - "check_type": "cpuload", - "error_threshold": 10, - "warning_threshold": 50, - "fails_b4_alert": 9, - }, - } + # this should fail because error threshold is greater than warning threshold + payload["error_threshold"] = 50 + payload["warning_threshold"] = 30 - resp = self.client.post(url, invalid_payload, format="json") - self.assertEqual(resp.status_code, 400) + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 400) self.check_not_authenticated("post", url) - def test_add_memory_check(self): - url = "/checks/checks/" + def test_add_cpuload_check(self): + url = f"{base_url}/" agent = baker.make_recipe("agents.agent") - payload = { - "pk": agent.pk, - "check": { - "check_type": "memory", - "error_threshold": 78, - "warning_threshold": 0, - "fails_b4_alert": 1, - }, + policy = baker.make("automation.Policy") + + agent_payload = { + "agent": agent.agent_id, + "check_type": "cpuload", + "error_threshold": 66, + "warning_threshold": 0, + "fails_b4_alert": 9, } - resp = self.client.post(url, payload, format="json") - self.assertEqual(resp.status_code, 200) + policy_payload = { + "policy": policy.id, + "check_type": "cpuload", + "error_threshold": 66, + "warning_threshold": 0, + "fails_b4_alert": 9, + } - payload["error_threshold"] = 55 - resp = self.client.post(url, payload, format="json") - self.assertEqual(resp.status_code, 400) - self.assertEqual( - resp.json()["non_field_errors"][0], - "A memory check for this agent already exists", - ) + for payload in [agent_payload, policy_payload]: - # should fail because both error and warning thresholds are 0 - invalid_payload = { - "pk": agent.pk, - "check": { - "check_type": "memory", - "error_threshold": 0, - "warning_threshold": 0, - "fails_b4_alert": 9, - }, - } + # add cpu check + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 200) - resp = self.client.post(url, invalid_payload, format="json") - self.assertEqual(resp.status_code, 400) - - # should fail because error is less than warning - invalid_payload = { - "pk": agent.pk, - "check": { - "check_type": "memory", - "error_threshold": 10, - "warning_threshold": 50, - "fails_b4_alert": 9, - }, - } + # should fail since cpu check already exists + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 400) - resp = self.client.post(url, invalid_payload, format="json") - self.assertEqual(resp.status_code, 400) + # this should fail because both error and warning threshold are 0 + payload["error_threshold"] = 0 + payload["warning_threshold"] = 0 - def test_get_policy_disk_check(self): - # setup data - policy = baker.make("automation.Policy") - disk_check = baker.make_recipe("checks.diskspace_check", policy=policy) + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 400) - url = f"/checks/{disk_check.pk}/check/" + # this should fail because error threshold is less than warning threshold + payload["error_threshold"] = 20 + payload["warning_threshold"] = 30 - resp = self.client.get(url, format="json") - serializer = CheckSerializer(disk_check) + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 400) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore self.check_not_authenticated("post", url) - def test_add_policy_disk_check(self): - # setup data + def test_add_memory_check(self): + url = f"{base_url}/" + agent = baker.make_recipe("agents.agent") policy = baker.make("automation.Policy") - url = "/checks/checks/" - - valid_payload = { - "policy": policy.pk, # type: ignore - "check": { - "check_type": "diskspace", - "disk": "M:", - "error_threshold": 86, - "warning_threshold": 0, - "fails_b4_alert": 2, - }, - } - - # should fail because both error and warning thresholds are 0 - invalid_payload = { - "policy": policy.pk, # type: ignore - "check": { - "check_type": "diskspace", - "error_threshold": 0, - "warning_threshold": 0, - "fails_b4_alert": 9, - }, + agent_payload = { + "agent": agent.agent_id, + "check_type": "memory", + "error_threshold": 78, + "warning_threshold": 0, + "fails_b4_alert": 1, } - resp = self.client.post(url, invalid_payload, format="json") - self.assertEqual(resp.status_code, 400) - - # should fail because warning is less than error - invalid_payload = { - "policy": policy.pk, # type: ignore - "check": { - "check_type": "diskspace", - "error_threshold": 80, - "warning_threshold": 50, - "fails_b4_alert": 9, - }, - } - - resp = self.client.post(url, valid_payload, format="json") - self.assertEqual(resp.status_code, 200) - - # this should fail because we already have a check for drive M: in setup - invalid_payload = { - "policy": policy.pk, # type: ignore - "check": { - "check_type": "diskspace", - "disk": "M:", - "error_threshold": 34, - "warning_threshold": 0, - "fails_b4_alert": 9, - }, + policy_payload = { + "policy": policy.id, + "check_type": "memory", + "error_threshold": 78, + "warning_threshold": 0, + "fails_b4_alert": 1, } - resp = self.client.post(url, invalid_payload, format="json") - self.assertEqual(resp.status_code, 400) + for payload in [agent_payload, policy_payload]: - def test_get_disks_for_policies(self): - url = "/checks/getalldisks/" - r = self.client.get(url) - self.assertIsInstance(r.data, list) # type: ignore - self.assertEqual(26, len(r.data)) # type: ignore + # add memory check + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 200) - def test_edit_check_alert(self): - # setup data - policy = baker.make("automation.Policy") - agent = baker.make_recipe("agents.agent") + # should fail since cpu check already exists + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 400) - policy_disk_check = baker.make_recipe("checks.diskspace_check", policy=policy) - agent_disk_check = baker.make_recipe("checks.diskspace_check", agent=agent) - url_a = f"/checks/{agent_disk_check.pk}/check/" - url_p = f"/checks/{policy_disk_check.pk}/check/" + # this should fail because both error and warning threshold are 0 + payload["error_threshold"] = 0 + payload["warning_threshold"] = 0 - valid_payload = {"email_alert": False, "check_alert": True} - invalid_payload = {"email_alert": False} + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 400) - with self.assertRaises(KeyError) as err: - resp = self.client.patch(url_a, invalid_payload, format="json") + # this should fail because error threshold is less than warning threshold + payload["error_threshold"] = 20 + payload["warning_threshold"] = 30 - with self.assertRaises(KeyError) as err: - resp = self.client.patch(url_p, invalid_payload, format="json") + resp = self.client.post(url, payload, format="json") + self.assertEqual(resp.status_code, 400) - resp = self.client.patch(url_a, valid_payload, format="json") - self.assertEqual(resp.status_code, 200) - - resp = self.client.patch(url_p, valid_payload, format="json") - self.assertEqual(resp.status_code, 200) - - self.check_not_authenticated("patch", url_a) + self.check_not_authenticated("post", url) @patch("agents.models.Agent.nats_cmd") def test_run_checks(self, nats_cmd): agent = baker.make_recipe("agents.agent", version="1.4.1") agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0") - url = f"/checks/runchecks/{agent_b4_141.pk}/" + url = f"{base_url}/{agent_b4_141.agent_id}/run/" r = self.client.get(url) self.assertEqual(r.status_code, 200) nats_cmd.assert_called_with({"func": "runchecks"}, wait=False) nats_cmd.reset_mock() nats_cmd.return_value = "busy" - url = f"/checks/runchecks/{agent.pk}/" + url = f"{base_url}/{agent.agent_id}/run/" r = self.client.get(url) self.assertEqual(r.status_code, 400) nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) @@ -343,7 +233,7 @@ def test_run_checks(self, nats_cmd): nats_cmd.reset_mock() nats_cmd.return_value = "ok" - url = f"/checks/runchecks/{agent.pk}/" + url = f"{base_url}/{agent.agent_id}/run/" r = self.client.get(url) self.assertEqual(r.status_code, 200) nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) @@ -351,7 +241,7 @@ def test_run_checks(self, nats_cmd): nats_cmd.reset_mock() nats_cmd.return_value = "timeout" - url = f"/checks/runchecks/{agent.pk}/" + url = f"{base_url}/{agent.agent_id}/run/" r = self.client.get(url) self.assertEqual(r.status_code, 400) nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) @@ -379,7 +269,7 @@ def test_get_check_history(self): resp = self.client.patch("/checks/history/500/", format="json") self.assertEqual(resp.status_code, 404) - url = f"/checks/history/{check.id}/" + url = f"/checks/{check.id}/history/" # test with timeFilter last 30 days data = {"timeFilter": 30} @@ -873,74 +763,7 @@ def test_handle_winsvc_check(self, nats_cmd): self.assertEqual(new_check.status, "failing") self.assertEqual(new_check.alert_severity, "info") - """ # test failing and attempt start - winsvc.restart_if_stopped = True - winsvc.alert_severity = "warning" - winsvc.save() - - nats_cmd.return_value = "timeout" - - data = {"id": winsvc.id, "exists": True, "status": "not running"} - - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) - - new_check = Check.objects.get(pk=winsvc.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "warning") - nats_cmd.assert_called() - nats_cmd.reset_mock() - - # test failing and attempt start - winsvc.alert_severity = "error" - winsvc.save() - nats_cmd.return_value = {"success": False, "errormsg": "Some Error"} - - data = {"id": winsvc.id, "exists": True, "status": "not running"} - - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) - - new_check = Check.objects.get(pk=winsvc.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") - nats_cmd.assert_called() - nats_cmd.reset_mock() - - # test success and attempt start - nats_cmd.return_value = {"success": True} - - data = {"id": winsvc.id, "exists": True, "status": "not running"} - - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) - - new_check = Check.objects.get(pk=winsvc.id) - self.assertEqual(new_check.status, "passing") - nats_cmd.assert_called() - nats_cmd.reset_mock() - - # test failing and service not exist - data = {"id": winsvc.id, "exists": False, "status": ""} - - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) - - new_check = Check.objects.get(pk=winsvc.id) - self.assertEqual(new_check.status, "failing") - - # test success and service not exist - winsvc.pass_if_svc_not_exist = True - winsvc.save() - data = {"id": winsvc.id, "exists": False, "status": ""} - - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) - - new_check = Check.objects.get(pk=winsvc.id) - self.assertEqual(new_check.status, "passing") """ - - """ def test_handle_eventlog_check(self): + def test_handle_eventlog_check(self): from checks.models import Check url = "/api/v3/checkrunner/" @@ -984,6 +807,8 @@ def test_handle_winsvc_check(self, nats_cmd): ], } + no_logs_data = {"id": eventlog.id, "log": []} + # test failing when contains resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) @@ -993,11 +818,8 @@ def test_handle_winsvc_check(self, nats_cmd): self.assertEquals(new_check.alert_severity, "warning") self.assertEquals(new_check.status, "failing") - # test passing when not contains and message - eventlog.event_message = "doesnt exist" - eventlog.save() - - resp = self.client.patch(url, data, format="json") + # test passing when contains + resp = self.client.patch(url, no_logs_data, format="json") self.assertEqual(resp.status_code, 200) new_check = Check.objects.get(pk=eventlog.id) @@ -1007,11 +829,9 @@ def test_handle_winsvc_check(self, nats_cmd): # test failing when not contains and message and source eventlog.fail_when = "not_contains" eventlog.alert_severity = "error" - eventlog.event_message = "doesnt exist" - eventlog.event_source = "doesnt exist" eventlog.save() - resp = self.client.patch(url, data, format="json") + resp = self.client.patch(url, no_logs_data, format="json") self.assertEqual(resp.status_code, 200) new_check = Check.objects.get(pk=eventlog.id) @@ -1020,10 +840,6 @@ def test_handle_winsvc_check(self, nats_cmd): self.assertEquals(new_check.alert_severity, "error") # test passing when contains with source and message - eventlog.event_message = "test" - eventlog.event_source = "source" - eventlog.save() - resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) @@ -1031,115 +847,252 @@ def test_handle_winsvc_check(self, nats_cmd): self.assertEquals(new_check.status, "passing") - # test failing with wildcard not contains and source - eventlog.event_id_is_wildcard = True - eventlog.event_source = "doesn't exist" - eventlog.event_message = "" - eventlog.event_id = 0 - eventlog.save() - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) +class TestCheckPermissions(TacticalTestCase): + def setUp(self): + self.setup_coresettings() + self.client_setup() - new_check = Check.objects.get(pk=eventlog.id) + def test_get_checks_permissions(self): + agent = baker.make_recipe("agents.agent") + policy = baker.make("automation.Policy") + unauthorized_agent = baker.make_recipe("agents.agent") + check = baker.make("checks.Check", agent=agent, _quantity=5) + unauthorized_check = baker.make( + "checks.Check", agent=unauthorized_agent, _quantity=7 + ) - self.assertEquals(new_check.status, "failing") - self.assertEquals(new_check.alert_severity, "error") + policy_checks = baker.make("checks.Check", policy=policy, _quantity=2) - # test passing with wildcard contains - eventlog.event_source = "" - eventlog.event_message = "" - eventlog.save() + # test super user access + self.check_authorized_superuser("get", f"{base_url}/") + self.check_authorized_superuser("get", f"/agents/{agent.agent_id}/checks/") + self.check_authorized_superuser( + "get", f"/agents/{unauthorized_agent.agent_id}/checks/" + ) + self.check_authorized_superuser( + "get", f"/automation/policies/{policy.id}/checks/" + ) - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) - new_check = Check.objects.get(pk=eventlog.id) + self.check_not_authorized("get", f"{base_url}/") + self.check_not_authorized("get", f"/agents/{agent.agent_id}/checks/") + self.check_not_authorized( + "get", f"/agents/{unauthorized_agent.agent_id}/checks/" + ) + self.check_not_authorized("get", f"/automation/policies/{policy.id}/checks/") + + # add list software role to user + user.role.can_list_checks = True + user.role.save() + + r = self.check_authorized("get", f"{base_url}/") + self.assertEqual(len(r.data), 14) + r = self.check_authorized("get", f"/agents/{agent.agent_id}/checks/") + self.assertEqual(len(r.data), 5) + r = self.check_authorized( + "get", f"/agents/{unauthorized_agent.agent_id}/checks/" + ) + self.assertEqual(len(r.data), 7) + r = self.check_authorized("get", f"/automation/policies/{policy.id}/checks/") + self.assertEqual(len(r.data), 2) + + # test limiting to client + user.role.can_view_clients.set([agent.client]) + self.check_not_authorized( + "get", f"/agents/{unauthorized_agent.agent_id}/checks/" + ) + self.check_authorized("get", f"/agents/{agent.agent_id}/checks/") + self.check_authorized("get", f"/automation/policies/{policy.id}/checks/") - self.assertEquals(new_check.status, "passing") + # make sure queryset is limited too + r = self.client.get(f"{base_url}/") + self.assertEqual(len(r.data), 7) - # test failing with wildcard contains and message - eventlog.fail_when = "contains" - eventlog.event_type = "error" - eventlog.alert_severity = "info" - eventlog.event_message = "test" - eventlog.event_source = "" - eventlog.save() + def test_add_check_permissions(self): + agent = baker.make_recipe("agents.agent") + unauthorized_agent = baker.make_recipe("agents.agent") + policy = baker.make("automation.Policy") - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) + policy_data = { + "policy": policy.id, + "check_type": "diskspace", + "disk": "C:", + "error_threshold": 55, + "warning_threshold": 0, + "fails_b4_alert": 3, + } - new_check = Check.objects.get(pk=eventlog.id) + agent_data = { + "agent": agent.agent_id, + "check_type": "diskspace", + "disk": "C:", + "error_threshold": 55, + "warning_threshold": 0, + "fails_b4_alert": 3, + } - self.assertEquals(new_check.status, "failing") - self.assertEquals(new_check.alert_severity, "info") + unauthorized_agent_data = { + "agent": unauthorized_agent.agent_id, + "check_type": "diskspace", + "disk": "C:", + "error_threshold": 55, + "warning_threshold": 0, + "fails_b4_alert": 3, + } - # test passing with wildcard not contains message and source - eventlog.event_message = "doesnt exist" - eventlog.event_source = "doesnt exist" - eventlog.save() + url = f"{base_url}/" - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) + for data in [policy_data, agent_data]: + # test superuser access + self.check_authorized_superuser("post", url, data) - new_check = Check.objects.get(pk=eventlog.id) + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) - self.assertEquals(new_check.status, "passing") + # test user without role + self.check_not_authorized("post", url, data) - # test multiple events found and contains - # this should pass since only two events are found - eventlog.number_of_events_b4_alert = 3 - eventlog.event_id_is_wildcard = False - eventlog.event_source = None - eventlog.event_message = None - eventlog.event_id = 123 - eventlog.event_type = "error" - eventlog.fail_when = "contains" - eventlog.save() + # add user to role and test + setattr(user.role, "can_manage_checks", True) + user.role.save() - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) + self.check_authorized("post", url, data) - new_check = Check.objects.get(pk=eventlog.id) + # limit user to client + user.role.can_view_clients.set([agent.client]) + if "agent" in data.keys(): + self.check_authorized("post", url, data) + self.check_not_authorized("post", url, unauthorized_agent_data) + else: + self.check_authorized("post", url, data) - self.assertEquals(new_check.status, "passing") + # mock the check delete method so it actually isn't deleted + @patch("checks.models.Check.delete") + def test_check_get_edit_delete_permissions(self, delete_check): + agent = baker.make_recipe("agents.agent") + unauthorized_agent = baker.make_recipe("agents.agent") + policy = baker.make("automation.Policy") + check = baker.make("checks.Check", agent=agent) + unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent) + policy_check = baker.make("checks.Check", policy=policy) - # this should pass since there are two events returned - eventlog.number_of_events_b4_alert = 2 - eventlog.save() + for method in ["get", "put", "delete"]: - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) + url = f"{base_url}/{check.id}/" + unauthorized_url = f"{base_url}/{unauthorized_check.id}/" + policy_url = f"{base_url}/{policy_check.id}/" - new_check = Check.objects.get(pk=eventlog.id) + # test superuser access + self.check_authorized_superuser(method, url) + self.check_authorized_superuser(method, unauthorized_url) + self.check_authorized_superuser(method, policy_url) - self.assertEquals(new_check.status, "failing") + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) - # test not contains - # this should fail since only two events are found - eventlog.number_of_events_b4_alert = 3 - eventlog.event_id_is_wildcard = False - eventlog.event_source = None - eventlog.event_message = None - eventlog.event_id = 123 - eventlog.event_type = "error" - eventlog.fail_when = "not_contains" - eventlog.save() + # test user without role + self.check_not_authorized(method, url) + self.check_not_authorized(method, unauthorized_url) + self.check_not_authorized(method, policy_url) - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) + # add user to role and test + setattr( + user.role, + "can_list_checks" if method == "get" else "can_manage_checks", + True, + ) + user.role.save() - new_check = Check.objects.get(pk=eventlog.id) + self.check_authorized(method, url) + self.check_authorized(method, unauthorized_url) + self.check_authorized(method, policy_url) - self.assertEquals(new_check.status, "failing") + # limit user to client if agent check + user.role.can_view_clients.set([agent.client]) - # this should pass since there are two events returned - eventlog.number_of_events_b4_alert = 2 - eventlog.save() + self.check_authorized(method, url) + self.check_not_authorized(method, unauthorized_url) + self.check_authorized(method, policy_url) - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) + def test_check_action_permissions(self): - new_check = Check.objects.get(pk=eventlog.id) + agent = baker.make_recipe("agents.agent") + unauthorized_agent = baker.make_recipe("agents.agent") + check = baker.make("checks.Check", agent=agent) + unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent) + + for action in ["reset", "run"]: + if action == "reset": + url = f"{base_url}/{check.id}/{action}/" + unauthorized_url = f"{base_url}/{unauthorized_check.id}/{action}/" + else: + url = f"{base_url}/{agent.agent_id}/{action}/" + unauthorized_url = f"{base_url}/{unauthorized_agent.agent_id}/{action}/" + + # test superuser access + self.check_authorized_superuser("post", url) + self.check_authorized_superuser("post", unauthorized_url) + + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + # test user without role + self.check_not_authorized("post", url) + self.check_not_authorized("post", unauthorized_url) + + # add user to role and test + setattr( + user.role, + "can_manage_checks" if action == "reset" else "can_run_checks", + True, + ) + user.role.save() + + self.check_authorized("post", url) + self.check_authorized("post", unauthorized_url) + + # limit user to client if agent check + user.role.can_view_sites.set([agent.site]) + + self.check_authorized("post", url) + self.check_not_authorized("post", unauthorized_url) + + def test_check_history_permissions(self): + agent = baker.make_recipe("agents.agent") + unauthorized_agent = baker.make_recipe("agents.agent") + check = baker.make("checks.Check", agent=agent) + unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent) + + url = f"{base_url}/{check.id}/history/" + unauthorized_url = f"{base_url}/{unauthorized_check.id}/history/" + + # test superuser access + self.check_authorized_superuser("patch", url) + self.check_authorized_superuser("patch", unauthorized_url) + + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + # test user without role + self.check_not_authorized("patch", url) + self.check_not_authorized("patch", unauthorized_url) + + # add user to role and test + setattr( + user.role, + "can_list_checks", + True, + ) + user.role.save() + + self.check_authorized("patch", url) + self.check_authorized("patch", unauthorized_url) + + # limit user to client if agent check + user.role.can_view_sites.set([agent.site]) - self.assertEquals(new_check.status, "passing") """ + self.check_authorized("patch", url) + self.check_not_authorized("patch", unauthorized_url) diff --git a/api/tacticalrmm/checks/urls.py b/api/tacticalrmm/checks/urls.py index b1fddb1a4f..7765290632 100644 --- a/api/tacticalrmm/checks/urls.py +++ b/api/tacticalrmm/checks/urls.py @@ -3,10 +3,9 @@ from . import views urlpatterns = [ - path("checks/", views.AddCheck.as_view()), - path("/check/", views.GetUpdateDeleteCheck.as_view()), - path("/loadchecks/", views.load_checks), - path("getalldisks/", views.get_disks_for_policies), - path("runchecks//", views.run_checks), - path("history//", views.GetCheckHistory.as_view()), + path("", views.GetAddChecks.as_view()), + path("/", views.GetUpdateDeleteCheck.as_view()), + path("/reset/", views.ResetCheck.as_view()), + path("/run/", views.run_checks), + path("/history/", views.GetCheckHistory.as_view()), ] diff --git a/api/tacticalrmm/checks/views.py b/api/tacticalrmm/checks/views.py index da9c0ecf9f..79c4ee3ff5 100644 --- a/api/tacticalrmm/checks/views.py +++ b/api/tacticalrmm/checks/views.py @@ -9,57 +9,57 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.exceptions import PermissionDenied from agents.models import Agent from automation.models import Policy -from scripts.models import Script from tacticalrmm.utils import notify_error +from tacticalrmm.permissions import _has_perm_on_agent from .models import Check, CheckHistory -from .permissions import ManageChecksPerms, RunChecksPerms +from .permissions import ChecksPerms, RunChecksPerms from .serializers import CheckHistorySerializer, CheckSerializer -class AddCheck(APIView): - permission_classes = [IsAuthenticated, ManageChecksPerms] +class GetAddChecks(APIView): + permission_classes = [IsAuthenticated, ChecksPerms] + + def get(self, request, agent_id=None, policy=None): + if agent_id: + agent = get_object_or_404(Agent, agent_id=agent_id) + checks = Check.objects.filter(agent=agent) + elif policy: + policy = get_object_or_404(Policy, id=policy) + checks = Check.objects.filter(policy=policy) + else: + checks = Check.permissions.filter_by_role(request.user) + return Response(CheckSerializer(checks, many=True).data) def post(self, request): from automation.tasks import generate_agent_checks_task - policy = None - agent = None - - # Determine if adding check to Policy or Agent - if "policy" in request.data: - policy = get_object_or_404(Policy, id=request.data["policy"]) - # Object used for filter and save - parent = {"policy": policy} - else: - agent = get_object_or_404(Agent, pk=request.data["pk"]) - parent = {"agent": agent} + data = request.data.copy() + # Determine if adding check to Agent and replace agent_id with pk + if "agent" in data.keys(): + agent = get_object_or_404(Agent, agent_id=data["agent"]) + if not _has_perm_on_agent(request.user, agent.agent_id): + raise PermissionDenied() - script = None - if "script" in request.data["check"]: - script = get_object_or_404(Script, pk=request.data["check"]["script"]) + data["agent"] = agent.pk # set event id to 0 if wildcard because it needs to be an integer field for db # will be ignored anyway by the agent when doing wildcard check - if ( - request.data["check"]["check_type"] == "eventlog" - and request.data["check"]["event_id_is_wildcard"] - ): - request.data["check"]["event_id"] = 0 - - serializer = CheckSerializer( - data=request.data["check"], partial=True, context=parent - ) + if data["check_type"] == "eventlog" and data["event_id_is_wildcard"]: + data["event_id"] = 0 + + serializer = CheckSerializer(data=data, partial=True) serializer.is_valid(raise_exception=True) - new_check = serializer.save(**parent, script=script) + new_check = serializer.save() # Generate policy Checks - if policy: - generate_agent_checks_task.delay(policy=policy.pk) - elif agent: + if "policy" in data.keys(): + generate_agent_checks_task.delay(policy=data["policy"]) + elif "agent" in data.keys(): checks = agent.agentchecks.filter( # type: ignore check_type=new_check.check_type, managed_by_policy=True ) @@ -81,44 +81,43 @@ def post(self, request): class GetUpdateDeleteCheck(APIView): - permission_classes = [IsAuthenticated, ManageChecksPerms] + permission_classes = [IsAuthenticated, ChecksPerms] def get(self, request, pk): check = get_object_or_404(Check, pk=pk) + if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id): + raise PermissionDenied() + return Response(CheckSerializer(check).data) - def patch(self, request, pk): + def put(self, request, pk): from automation.tasks import update_policy_check_fields_task check = get_object_or_404(Check, pk=pk) + data = request.data.copy() + + if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id): + raise PermissionDenied() + # remove fields that should not be changed when editing a check from the frontend - if ( - "check_alert" not in request.data.keys() - and "check_reset" not in request.data.keys() - ): - [request.data.pop(i) for i in check.non_editable_fields] + [data.pop(i) for i in Check.non_editable_fields() if i in data.keys()] # set event id to 0 if wildcard because it needs to be an integer field for db # will be ignored anyway by the agent when doing wildcard check if check.check_type == "eventlog": try: - request.data["event_id_is_wildcard"] + data["event_id_is_wildcard"] except KeyError: pass else: - if request.data["event_id_is_wildcard"]: - request.data["event_id"] = 0 + if data["event_id_is_wildcard"]: + data["event_id"] = 0 - serializer = CheckSerializer(instance=check, data=request.data, partial=True) + serializer = CheckSerializer(instance=check, data=data, partial=True) serializer.is_valid(raise_exception=True) check = serializer.save() - # resolve any alerts that are open - if "check_reset" in request.data.keys(): - if check.alert.filter(resolved=False).exists(): - check.alert.get(resolved=False).resolve() - if check.policy: update_policy_check_fields_task.delay(check=check.pk) @@ -129,6 +128,9 @@ def delete(self, request, pk): check = get_object_or_404(Check, pk=pk) + if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id): + raise PermissionDenied() + check.delete() # Policy check deleted @@ -146,9 +148,32 @@ def delete(self, request, pk): return Response(f"{check.readable_desc} was deleted!") +class ResetCheck(APIView): + permission_classes = [IsAuthenticated, ChecksPerms] + + def post(self, request, pk): + check = get_object_or_404(Check, pk=pk) + + if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id): + raise PermissionDenied() + + check.status = "passing" + check.save() + + # resolve any alerts that are open + if check.alert.filter(resolved=False).exists(): + check.alert.get(resolved=False).resolve() + + return Response("The check status was reset") + + class GetCheckHistory(APIView): - def patch(self, request, checkpk): - check = get_object_or_404(Check, pk=checkpk) + permission_classes = [IsAuthenticated, ChecksPerms] + def patch(self, request, pk): + check = get_object_or_404(Check, pk=pk) + + if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id): + raise PermissionDenied() timeFilter = Q() @@ -160,7 +185,7 @@ def patch(self, request, checkpk): - djangotime.timedelta(days=request.data["timeFilter"]), ) - check_history = CheckHistory.objects.filter(check_id=checkpk).filter(timeFilter).order_by("-x") # type: ignore + check_history = CheckHistory.objects.filter(check_id=pk).filter(timeFilter).order_by("-x") # type: ignore return Response( CheckHistorySerializer( @@ -171,8 +196,8 @@ def patch(self, request, checkpk): @api_view() @permission_classes([IsAuthenticated, RunChecksPerms]) -def run_checks(request, pk): - agent = get_object_or_404(Agent, pk=pk) +def run_checks(request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) if pyver.parse(agent.version) >= pyver.parse("1.4.1"): r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15)) @@ -185,14 +210,3 @@ def run_checks(request, pk): else: asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False)) return Response(f"Checks will now be re-run on {agent.hostname}") - - -@api_view() -def load_checks(request, pk): - checks = Check.objects.filter(agent__pk=pk) - return Response(CheckSerializer(checks, many=True).data) - - -@api_view() -def get_disks_for_policies(request): - return Response(Check.all_disks()) diff --git a/api/tacticalrmm/clients/migrations/0018_auto_20211010_0249.py b/api/tacticalrmm/clients/migrations/0018_auto_20211010_0249.py new file mode 100644 index 0000000000..0505565873 --- /dev/null +++ b/api/tacticalrmm/clients/migrations/0018_auto_20211010_0249.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.6 on 2021-10-10 02:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('clients', '0017_auto_20210417_0125'), + ] + + operations = [ + migrations.AlterField( + model_name='client', + name='created_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='client', + name='modified_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='site', + name='created_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='site', + name='modified_by', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/clients/models.py b/api/tacticalrmm/clients/models.py index 231ba68130..b7e5aeea0f 100644 --- a/api/tacticalrmm/clients/models.py +++ b/api/tacticalrmm/clients/models.py @@ -5,9 +5,13 @@ from agents.models import Agent from logs.models import BaseAuditModel +from tacticalrmm.models import PermissionManager class Client(BaseAuditModel): + objects = models.Manager() + permissions = PermissionManager() + name = models.CharField(max_length=255, unique=True) block_policy_inheritance = models.BooleanField(default=False) workstation_policy = models.ForeignKey( @@ -130,6 +134,9 @@ def serialize(client): class Site(BaseAuditModel): + objects = models.Manager() + permissions = PermissionManager() + client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE) name = models.CharField(max_length=255) block_policy_inheritance = models.BooleanField(default=False) diff --git a/api/tacticalrmm/clients/permissions.py b/api/tacticalrmm/clients/permissions.py index 51861aae27..4824a8dacb 100644 --- a/api/tacticalrmm/clients/permissions.py +++ b/api/tacticalrmm/clients/permissions.py @@ -11,6 +11,14 @@ def has_permission(self, r, view): return _has_perm(r, "can_manage_clients") +class ListClientsPerms(permissions.BasePermission): + def has_permission(self, r, view): + if r.method != "GET": + return True + + return _has_perm(r, "can_list_clients") + + class ManageSitesPerms(permissions.BasePermission): def has_permission(self, r, view): if r.method == "GET": @@ -19,9 +27,25 @@ def has_permission(self, r, view): return _has_perm(r, "can_manage_sites") +class ListSitesPerms(permissions.BasePermission): + def has_permission(self, r, view): + if r.method != "GET": + return True + + return _has_perm(r, "can_list_sites") + + class ManageDeploymentPerms(permissions.BasePermission): def has_permission(self, r, view): if r.method == "GET": return True return _has_perm(r, "can_manage_deployments") + + +class ListDeploymentsPerms(permissions.BasePermission): + def has_permission(self, r, view): + if r.method != "GET": + return True + + return _has_perm(r, "can_list_deployments") diff --git a/api/tacticalrmm/clients/views.py b/api/tacticalrmm/clients/views.py index d88373abf4..1fd84bee4c 100644 --- a/api/tacticalrmm/clients/views.py +++ b/api/tacticalrmm/clients/views.py @@ -14,7 +14,14 @@ from tacticalrmm.utils import notify_error from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField -from .permissions import ManageClientsPerms, ManageDeploymentPerms, ManageSitesPerms +from .permissions import ( + ListClientsPerms, + ListDeploymentsPerms, + ListSitesPerms, + ManageClientsPerms, + ManageDeploymentPerms, + ManageSitesPerms, +) from .serializers import ( ClientCustomFieldSerializer, ClientSerializer, @@ -26,10 +33,10 @@ class GetAddClients(APIView): - permission_classes = [IsAuthenticated, ManageClientsPerms] + permission_classes = [IsAuthenticated, ManageClientsPerms, ListClientsPerms] def get(self, request): - clients = Client.objects.all() + clients = Client.permissions.filter_by_role(request.user) return Response(ClientSerializer(clients, many=True).data) def post(self, request): @@ -134,16 +141,21 @@ def delete(self, request, pk, sitepk): class GetClientTree(APIView): + permission_classes = [IsAuthenticated, ListClientsPerms] + def get(self, request): - clients = Client.objects.all() + clients = Client.permissions.filter_by_role(request.user) + return Response(ClientTreeSerializer(clients, many=True).data) class GetAddSites(APIView): - permission_classes = [IsAuthenticated, ManageSitesPerms] + permission_classes = [IsAuthenticated, ManageSitesPerms, ListSitesPerms] def get(self, request): - sites = Site.objects.all() + + sites = Site.permissions.filter_by_role(request.user) + return Response(SiteSerializer(sites, many=True).data) def post(self, request): @@ -239,7 +251,7 @@ def delete(self, request, pk, sitepk): class AgentDeployment(APIView): - permission_classes = [IsAuthenticated, ManageDeploymentPerms] + permission_classes = [IsAuthenticated, ManageDeploymentPerms, ListDeploymentsPerms] def get(self, request): deps = Deployment.objects.all() diff --git a/api/tacticalrmm/core/migrations/0028_auto_20210917_1954.py b/api/tacticalrmm/core/migrations/0028_auto_20210917_1954.py new file mode 100644 index 0000000000..1dc1695678 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0028_auto_20210917_1954.py @@ -0,0 +1,53 @@ +# Generated by Django 3.2.6 on 2021-09-17 19:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0027_auto_20210905_1606"), + ] + + operations = [ + migrations.AlterField( + model_name="coresettings", + name="created_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="coresettings", + name="modified_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="customfield", + name="created_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="customfield", + name="modified_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="globalkvstore", + name="created_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="globalkvstore", + name="modified_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="urlaction", + name="created_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="urlaction", + name="modified_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/logs/migrations/0019_alter_auditlog_username.py b/api/tacticalrmm/logs/migrations/0019_alter_auditlog_username.py new file mode 100644 index 0000000000..0a6d422130 --- /dev/null +++ b/api/tacticalrmm/logs/migrations/0019_alter_auditlog_username.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-09-17 19:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("logs", "0018_auto_20210905_1606"), + ] + + operations = [ + migrations.AlterField( + model_name="auditlog", + name="username", + field=models.CharField(max_length=255), + ), + ] diff --git a/api/tacticalrmm/logs/models.py b/api/tacticalrmm/logs/models.py index 244b2bed61..c791273aca 100644 --- a/api/tacticalrmm/logs/models.py +++ b/api/tacticalrmm/logs/models.py @@ -65,7 +65,7 @@ def get_debug_level(): class AuditLog(models.Model): - username = models.CharField(max_length=100) + username = models.CharField(max_length=255) agent = models.CharField(max_length=255, null=True, blank=True) agent_id = models.PositiveIntegerField(blank=True, null=True) entry_time = models.DateTimeField(auto_now_add=True) @@ -380,9 +380,9 @@ class Meta: abstract = True # create audit fields - created_by = models.CharField(max_length=100, null=True, blank=True) + created_by = models.CharField(max_length=255, null=True, blank=True) created_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) - modified_by = models.CharField(max_length=100, null=True, blank=True) + modified_by = models.CharField(max_length=255, null=True, blank=True) modified_time = models.DateTimeField(auto_now=True, null=True, blank=True) @abstractmethod diff --git a/api/tacticalrmm/scripts/migrations/0012_auto_20210917_1954.py b/api/tacticalrmm/scripts/migrations/0012_auto_20210917_1954.py new file mode 100644 index 0000000000..1b2d454926 --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0012_auto_20210917_1954.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.6 on 2021-09-17 19:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("scripts", "0011_auto_20210731_1707"), + ] + + operations = [ + migrations.AlterField( + model_name="script", + name="created_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="script", + name="modified_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/scripts/tests.py b/api/tacticalrmm/scripts/tests.py index 39281e979a..39305cab67 100644 --- a/api/tacticalrmm/scripts/tests.py +++ b/api/tacticalrmm/scripts/tests.py @@ -118,14 +118,12 @@ def test_get_script(self): self.check_not_authenticated("get", url) - @patch("agents.models.Agent.nats_cmd") + @patch("agents.models.Agent.nats_cmd", return_value="return value") def test_test_script(self, run_script): - url = "/scripts/testscript/" - - run_script.return_value = "return value" agent = baker.make_recipe("agents.agent") + url = f"/scripts/{agent.agent_id}/test/" + data = { - "agent": agent.pk, "code": "some_code", "timeout": 90, "args": [], @@ -161,7 +159,7 @@ def test_delete_script(self): def test_download_script(self): # test a call where script doesn't exist - resp = self.client.get("/scripts/download/500/", format="json") + resp = self.client.get("/scripts/500/download/", format="json") self.assertEqual(resp.status_code, 404) # return script code property should be "Test" @@ -170,7 +168,7 @@ def test_download_script(self): script = baker.make( "scripts.Script", code_base64="VGVzdA==", shell="powershell" ) - url = f"/scripts/download/{script.pk}/" # type: ignore + url = f"/scripts/{script.pk}/download/" # type: ignore resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) @@ -178,7 +176,7 @@ def test_download_script(self): # test batch file script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="cmd") - url = f"/scripts/download/{script.pk}/" # type: ignore + url = f"/scripts/{script.pk}/download/" # type: ignore resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) @@ -186,7 +184,7 @@ def test_download_script(self): # test python file script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="python") - url = f"/scripts/download/{script.pk}/" # type: ignore + url = f"/scripts/{script.pk}/download/" # type: ignore resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) diff --git a/api/tacticalrmm/scripts/urls.py b/api/tacticalrmm/scripts/urls.py index d418e3f980..110bff87e0 100644 --- a/api/tacticalrmm/scripts/urls.py +++ b/api/tacticalrmm/scripts/urls.py @@ -7,6 +7,6 @@ path("/", views.GetUpdateDeleteScript.as_view()), path("snippets/", views.GetAddScriptSnippets.as_view()), path("snippets//", views.GetUpdateDeleteScriptSnippet.as_view()), - path("testscript/", views.TestScript.as_view()), - path("download//", views.download), + path("/test/", views.TestScript.as_view()), + path("/download/", views.download), ] diff --git a/api/tacticalrmm/scripts/views.py b/api/tacticalrmm/scripts/views.py index 8ba0f7f237..246481ea49 100644 --- a/api/tacticalrmm/scripts/views.py +++ b/api/tacticalrmm/scripts/views.py @@ -121,11 +121,11 @@ def delete(self, request, pk): class TestScript(APIView): permission_classes = [IsAuthenticated, RunScriptPerms] - def post(self, request): + def post(self, request, agent_id): from .models import Script from agents.models import Agent - agent = get_object_or_404(Agent, pk=request.data["agent"]) + agent = get_object_or_404(Agent, agent_id=agent_id) parsed_args = Script.parse_script_args( agent, request.data["shell"], request.data["args"] diff --git a/api/tacticalrmm/services/default_services.json b/api/tacticalrmm/services/default_services.json deleted file mode 100644 index 8c74ac5af0..0000000000 --- a/api/tacticalrmm/services/default_services.json +++ /dev/null @@ -1,1347 +0,0 @@ -[ - { - "name": "AJRouter", - "description": "Routes AllJoyn messages for the local AllJoyn clients. If this service is stopped the AllJoyn clients that do not have their own bundled routers will be unable to run.", - "display_name": "AllJoyn Router Service" - }, - { - "name": "ALG", - "description": "Provides support for 3rd party protocol plug-ins for Internet Connection Sharing", - "display_name": "Application Layer Gateway Service" - }, - { - "name": "AppIDSvc", - "description": "Determines and verifies the identity of an application. Disabling this service will prevent AppLocker from being enforced.", - "display_name": "Application Identity" - }, - { - "name": "Appinfo", - "description": "Facilitates the running of interactive applications with additional administrative privileges. If this service is stopped, users will be unable to launch applications with the additional administrative privileges they may require to perform desired user tasks.", - "display_name": "Application Information" - }, - { - "name": "AppMgmt", - "description": "Processes installation, removal, and enumeration requests for software deployed through Group Policy. If the service is disabled, users will be unable to install, remove, or enumerate software deployed through Group Policy. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Application Management" - }, - { - "name": "AppReadiness", - "description": "Gets apps ready for use the first time a user signs in to this PC and when adding new apps.", - "display_name": "App Readiness" - }, - { - "name": "AppVClient", - "description": "Manages App-V users and virtual applications", - "display_name": "Microsoft App-V Client" - }, - { - "name": "AppXSvc", - "description": "Provides infrastructure support for deploying Store applications. This service is started on demand and if disabled Store applications will not be deployed to the system, and may not function properly.", - "display_name": "AppX Deployment Service (AppXSVC)" - }, - { - "name": "AssignedAccessManagerSvc", - "description": "AssignedAccessManager Service supports kiosk experience in Windows.", - "display_name": "AssignedAccessManager Service" - }, - { - "name": "atashost", - "description": "WebEx Support Center.", - "display_name": "WebEx Service Host for Support Center" - }, - { - "name": "AudioEndpointBuilder", - "description": "Manages audio devices for the Windows Audio service. If this service is stopped, audio devices and effects will not function properly. If this service is disabled, any services that explicitly depend on it will fail to start", - "display_name": "Windows Audio Endpoint Builder" - }, - { - "name": "Audiosrv", - "description": "Manages audio for Windows-based programs. If this service is stopped, audio devices and effects will not function properly. If this service is disabled, any services that explicitly depend on it will fail to start", - "display_name": "Windows Audio" - }, - { - "name": "autotimesvc", - "description": "This service sets time based on NITZ messages from a Mobile Network", - "display_name": "Cellular Time" - }, - { - "name": "AxInstSV", - "description": "Provides User Account Control validation for the installation of ActiveX controls from the Internet and enables management of ActiveX control installation based on Group Policy settings. This service is started on demand and if disabled the installation of ActiveX controls will behave according to default browser settings.", - "display_name": "ActiveX Installer (AxInstSV)" - }, - { - "name": "BDESVC", - "description": "BDESVC hosts the BitLocker Drive Encryption service. BitLocker Drive Encryption provides secure startup for the operating system, as well as full volume encryption for OS, fixed or removable volumes. This service allows BitLocker to prompt users for various actions related to their volumes when mounted, and unlocks volumes automatically without user interaction. Additionally, it stores recovery information to Active Directory, if available, and, if necessary, ensures the most recent recovery certificates are used. Stopping or disabling the service would prevent users from leveraging this functionality.", - "display_name": "BitLocker Drive Encryption Service" - }, - { - "name": "BFE", - "description": "The Base Filtering Engine (BFE) is a service that manages firewall and Internet Protocol security (IPsec) policies and implements user mode filtering. Stopping or disabling the BFE service will significantly reduce the security of the system. It will also result in unpredictable behavior in IPsec management and firewall applications.", - "display_name": "Base Filtering Engine" - }, - { - "name": "BITS", - "description": "Transfers files in the background using idle network bandwidth. If the service is disabled, then any applications that depend on BITS, such as Windows Update or MSN Explorer, will be unable to automatically download programs and other information.", - "display_name": "Background Intelligent Transfer Service" - }, - { - "name": "Bonjour Service", - "description": "Enables hardware devices and software services to automatically configure themselves on the network and advertise their presence.", - "display_name": "Bonjour Service" - }, - { - "name": "BrokerInfrastructure", - "description": "Windows infrastructure service that controls which background tasks can run on the system.", - "display_name": "Background Tasks Infrastructure Service" - }, - { - "name": "BTAGService", - "description": "Service supporting the audio gateway role of the Bluetooth Handsfree Profile.", - "display_name": "Bluetooth Audio Gateway Service" - }, - { - "name": "BthAvctpSvc", - "description": "This is Audio Video Control Transport Protocol service", - "display_name": "AVCTP service" - }, - { - "name": "bthserv", - "description": "The Bluetooth service supports discovery and association of remote Bluetooth devices. Stopping or disabling this service may cause already installed Bluetooth devices to fail to operate properly and prevent new devices from being discovered or associated.", - "display_name": "Bluetooth Support Service" - }, - { - "name": "camsvc", - "description": "Provides facilities for managing UWP apps access to app capabilities as well as checking an app's access to specific app capabilities", - "display_name": "Capability Access Manager Service" - }, - { - "name": "CDPSvc", - "description": "This service is used for Connected Devices Platform scenarios", - "display_name": "Connected Devices Platform Service" - }, - { - "name": "CertPropSvc", - "description": "Copies user certificates and root certificates from smart cards into the current user's certificate store, detects when a smart card is inserted into a smart card reader, and, if needed, installs the smart card Plug and Play minidriver.", - "display_name": "Certificate Propagation" - }, - { - "name": "ClipSVC", - "description": "Provides infrastructure support for the Microsoft Store. This service is started on demand and if disabled applications bought using Windows Store will not behave correctly.", - "display_name": "Client License Service (ClipSVC)" - }, - { - "name": "COMSysApp", - "description": "Manages the configuration and tracking of Component Object Model (COM)+-based components. If the service is stopped, most COM+-based components will not function properly. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "COM+ System Application" - }, - { - "name": "CoreMessagingRegistrar", - "description": "Manages communication between system components.", - "display_name": "CoreMessaging" - }, - { - "name": "cphs", - "description": "Intel(R) Content Protection HECI Service - enables communication with the Content Protection FW", - "display_name": "Intel(R) Content Protection HECI Service" - }, - { - "name": "cplspcon", - "description": "Intel(R) Content Protection HDCP Service - enables communication with Content Protection HDCP HW", - "display_name": "Intel(R) Content Protection HDCP Service" - }, - { - "name": "CryptSvc", - "description": "Provides three management services: Catalog Database Service, which confirms the signatures of Windows files and allows new programs to be installed; Protected Root Service, which adds and removes Trusted Root Certification Authority certificates from this computer; and Automatic Root Certificate Update Service, which retrieves root certificates from Windows Update and enable scenarios such as SSL. If this service is stopped, these management services will not function properly. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Cryptographic Services" - }, - { - "name": "CscService", - "description": "The Offline Files service performs maintenance activities on the Offline Files cache, responds to user logon and logoff events, implements the internals of the public API, and dispatches interesting events to those interested in Offline Files activities and changes in cache state.", - "display_name": "Offline Files" - }, - { - "name": "DcomLaunch", - "description": "The DCOMLAUNCH service launches COM and DCOM servers in response to object activation requests. If this service is stopped or disabled, programs using COM or DCOM will not function properly. It is strongly recommended that you have the DCOMLAUNCH service running.", - "display_name": "DCOM Server Process Launcher" - }, - { - "name": "defragsvc", - "description": "Helps the computer run more efficiently by optimizing files on storage drives.", - "display_name": "Optimize drives" - }, - { - "name": "DeviceAssociationService", - "description": "Enables pairing between the system and wired or wireless devices.", - "display_name": "Device Association Service" - }, - { - "name": "DeviceInstall", - "description": "Enables a computer to recognize and adapt to hardware changes with little or no user input. Stopping or disabling this service will result in system instability.", - "display_name": "Device Install Service" - }, - { - "name": "DevQueryBroker", - "description": "Enables apps to discover devices with a backgroud task", - "display_name": "DevQuery Background Discovery Broker" - }, - { - "name": "Dhcp", - "description": "Registers and updates IP addresses and DNS records for this computer. If this service is stopped, this computer will not receive dynamic IP addresses and DNS updates. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "DHCP Client" - }, - { - "name": "diagnosticshub.standardcollector.service", - "description": "Diagnostics Hub Standard Collector Service. When running, this service collects real time ETW events and processes them.", - "display_name": "Microsoft (R) Diagnostics Hub Standard Collector Service" - }, - { - "name": "diagsvc", - "description": "Executes diagnostic actions for troubleshooting support", - "display_name": "Diagnostic Execution Service" - }, - { - "name": "DiagTrack", - "description": "The Connected User Experiences and Telemetry service enables features that support in-application and connected user experiences. Additionally, this service manages the event driven collection and transmission of diagnostic and usage information (used to improve the experience and quality of the Windows Platform) when the diagnostics and usage privacy option settings are enabled under Feedback and Diagnostics.", - "display_name": "Connected User Experiences and Telemetry" - }, - { - "name": "DispBrokerDesktopSvc", - "description": "Manages the connection and configuration of local and remote displays", - "display_name": "Display Policy Service" - }, - { - "name": "DisplayEnhancementService", - "description": "A service for managing display enhancement such as brightness control.", - "display_name": "Display Enhancement Service" - }, - { - "name": "DmEnrollmentSvc", - "description": "Performs Device Enrollment Activities for Device Management", - "display_name": "Device Management Enrollment Service" - }, - { - "name": "dmwappushservice", - "description": "Routes Wireless Application Protocol (WAP) Push messages received by the device and synchronizes Device Management sessions", - "display_name": "Device Management Wireless Application Protocol (WAP) Push message Routing Service" - }, - { - "name": "Dnscache", - "description": "The DNS Client service (dnscache) caches Domain Name System (DNS) names and registers the full computer name for this computer. If the service is stopped, DNS names will continue to be resolved. However, the results of DNS name queries will not be cached and the computer's name will not be registered. If the service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "DNS Client" - }, - { - "name": "DoSvc", - "description": "Performs content delivery optimization tasks", - "display_name": "Delivery Optimization" - }, - { - "name": "dot3svc", - "description": "The Wired AutoConfig (DOT3SVC) service is responsible for performing IEEE 802.1X authentication on Ethernet interfaces. If your current wired network deployment enforces 802.1X authentication, the DOT3SVC service should be configured to run for establishing Layer 2 connectivity and/or providing access to network resources. Wired networks that do not enforce 802.1X authentication are unaffected by the DOT3SVC service.", - "display_name": "Wired AutoConfig" - }, - { - "name": "DPS", - "description": "The Diagnostic Policy Service enables problem detection, troubleshooting and resolution for Windows components. If this service is stopped, diagnostics will no longer function.", - "display_name": "Diagnostic Policy Service" - }, - { - "name": "DsmSvc", - "description": "Enables the detection, download and installation of device-related software. If this service is disabled, devices may be configured with outdated software, and may not work correctly.", - "display_name": "Device Setup Manager" - }, - { - "name": "DsSvc", - "description": "Provides data brokering between applications.", - "display_name": "Data Sharing Service" - }, - { - "name": "DusmSvc", - "description": "Network data usage, data limit, restrict background data, metered networks.", - "display_name": "Data Usage" - }, - { - "name": "Eaphost", - "description": "The Extensible Authentication Protocol (EAP) service provides network authentication in such scenarios as 802.1x wired and wireless, VPN, and Network Access Protection (NAP). EAP also provides application programming interfaces (APIs) that are used by network access clients, including wireless and VPN clients, during the authentication process. If you disable this service, this computer is prevented from accessing networks that require EAP authentication.", - "display_name": "Extensible Authentication Protocol" - }, - { - "name": "EFS", - "description": "Provides the core file encryption technology used to store encrypted files on NTFS file system volumes. If this service is stopped or disabled, applications will be unable to access encrypted files.", - "display_name": "Encrypting File System (EFS)" - }, - { - "name": "embeddedmode", - "description": "The Embedded Mode service enables scenarios related to Background Applications. Disabling this service will prevent Background Applications from being activated.", - "display_name": "Embedded Mode" - }, - { - "name": "EntAppSvc", - "description": "Enables enterprise application management.", - "display_name": "Enterprise App Management Service" - }, - { - "name": "EventLog", - "description": "This service manages events and event logs. It supports logging events, querying events, subscribing to events, archiving event logs, and managing event metadata. It can display events in both XML and plain text format. Stopping this service may compromise security and reliability of the system.", - "display_name": "Windows Event Log" - }, - { - "name": "EventSystem", - "description": "Supports System Event Notification Service (SENS), which provides automatic distribution of events to subscribing Component Object Model (COM) components. If the service is stopped, SENS will close and will not be able to provide logon and logoff notifications. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "COM+ Event System" - }, - { - "name": "Fax", - "description": "Enables you to send and receive faxes, utilizing fax resources available on this computer or on the network.", - "display_name": "Fax" - }, - { - "name": "fdPHost", - "description": "The FDPHOST service hosts the Function Discovery (FD) network discovery providers. These FD providers supply network discovery services for the Simple Services Discovery Protocol (SSDP) and Web Services \u2013 Discovery (WS-D) protocol. Stopping or disabling the FDPHOST service will disable network discovery for these protocols when using FD. When this service is unavailable, network services using FD and relying on these discovery protocols will be unable to find network devices or resources.", - "display_name": "Function Discovery Provider Host" - }, - { - "name": "FDResPub", - "description": "Publishes this computer and resources attached to this computer so they can be discovered over the network. If this service is stopped, network resources will no longer be published and they will not be discovered by other computers on the network.", - "display_name": "Function Discovery Resource Publication" - }, - { - "name": "fhsvc", - "description": "Protects user files from accidental loss by copying them to a backup location", - "display_name": "File History Service" - }, - { - "name": "FontCache", - "description": "Optimizes performance of applications by caching commonly used font data. Applications will start this service if it is not already running. It can be disabled, though doing so will degrade application performance.", - "display_name": "Windows Font Cache Service" - }, - { - "name": "FrameServer", - "description": "Enables multiple clients to access video frames from camera devices.", - "display_name": "Windows Camera Frame Server" - }, - { - "name": "GoogleChromeElevationService", - "description": "", - "display_name": "Google Chrome Elevation Service" - }, - { - "name": "gpsvc", - "description": "The service is responsible for applying settings configured by administrators for the computer and users through the Group Policy component. If the service is disabled, the settings will not be applied and applications and components will not be manageable through Group Policy. Any components or applications that depend on the Group Policy component might not be functional if the service is disabled.", - "display_name": "Group Policy Client" - }, - { - "name": "GraphicsPerfSvc", - "description": "Graphics performance monitor service", - "display_name": "GraphicsPerfSvc" - }, - { - "name": "hidserv", - "description": "Activates and maintains the use of hot buttons on keyboards, remote controls, and other multimedia devices. It is recommended that you keep this service running.", - "display_name": "Human Interface Device Service" - }, - { - "name": "hns", - "description": "Provides support for Windows Virtual Networks.", - "display_name": "Host Network Service" - }, - { - "name": "HvHost", - "description": "Provides an interface for the Hyper-V hypervisor to provide per-partition performance counters to the host operating system.", - "display_name": "HV Host Service" - }, - { - "name": "ibtsiva", - "description": "Intel(R) Wireless Bluetooth(R) iBtSiva Service", - "display_name": "Intel Bluetooth Service" - }, - { - "name": "icssvc", - "description": "Provides the ability to share a cellular data connection with another device.", - "display_name": "Windows Mobile Hotspot Service" - }, - { - "name": "IKEEXT", - "description": "The IKEEXT service hosts the Internet Key Exchange (IKE) and Authenticated Internet Protocol (AuthIP) keying modules. These keying modules are used for authentication and key exchange in Internet Protocol security (IPsec). Stopping or disabling the IKEEXT service will disable IKE and AuthIP key exchange with peer computers. IPsec is typically configured to use IKE or AuthIP; therefore, stopping or disabling the IKEEXT service might result in an IPsec failure and might compromise the security of the system. It is strongly recommended that you have the IKEEXT service running.", - "display_name": "IKE and AuthIP IPsec Keying Modules" - }, - { - "name": "InstallService", - "description": "Provides infrastructure support for the Microsoft Store. This service is started on demand and if disabled then installations will not function properly.", - "display_name": "Microsoft Store Install Service" - }, - { - "name": "iphlpsvc", - "description": "Provides tunnel connectivity using IPv6 transition technologies (6to4, ISATAP, Port Proxy, and Teredo), and IP-HTTPS. If this service is stopped, the computer will not have the enhanced connectivity benefits that these technologies offer.", - "display_name": "IP Helper" - }, - { - "name": "IpxlatCfgSvc", - "description": "Configures and enables translation from v4 to v6 and vice versa", - "display_name": "IP Translation Configuration Service" - }, - { - "name": "KeyIso", - "description": "The CNG key isolation service is hosted in the LSA process. The service provides key process isolation to private keys and associated cryptographic operations as required by the Common Criteria. The service stores and uses long-lived keys in a secure process complying with Common Criteria requirements.", - "display_name": "CNG Key Isolation" - }, - { - "name": "KtmRm", - "description": "Coordinates transactions between the Distributed Transaction Coordinator (MSDTC) and the Kernel Transaction Manager (KTM). If it is not needed, it is recommended that this service remain stopped. If it is needed, both MSDTC and KTM will start this service automatically. If this service is disabled, any MSDTC transaction interacting with a Kernel Resource Manager will fail and any services that explicitly depend on it will fail to start.", - "display_name": "KtmRm for Distributed Transaction Coordinator" - }, - { - "name": "LanmanServer", - "description": "Supports file, print, and named-pipe sharing over the network for this computer. If this service is stopped, these functions will be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Server" - }, - { - "name": "LanmanWorkstation", - "description": "Creates and maintains client network connections to remote servers using the SMB protocol. If this service is stopped, these connections will be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Workstation" - }, - { - "name": "lfsvc", - "description": "This service monitors the current location of the system and manages geofences (a geographical location with associated events). If you turn off this service, applications will be unable to use or receive notifications for geolocation or geofences.", - "display_name": "Geolocation Service" - }, - { - "name": "LicenseManager", - "description": "Provides infrastructure support for the Microsoft Store. This service is started on demand and if disabled then content acquired through the Microsoft Store will not function properly.", - "display_name": "Windows License Manager Service" - }, - { - "name": "lltdsvc", - "description": "Creates a Network Map, consisting of PC and device topology (connectivity) information, and metadata describing each PC and device. If this service is disabled, the Network Map will not function properly.", - "display_name": "Link-Layer Topology Discovery Mapper" - }, - { - "name": "lmhosts", - "description": "Provides support for the NetBIOS over TCP/IP (NetBT) service and NetBIOS name resolution for clients on the network, therefore enabling users to share files, print, and log on to the network. If this service is stopped, these functions might be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "TCP/IP NetBIOS Helper" - }, - { - "name": "LSM", - "description": "Core Windows Service that manages local user sessions. Stopping or disabling this service will result in system instability.", - "display_name": "Local Session Manager" - }, - { - "name": "LxpSvc", - "description": "Provides infrastructure support for deploying and configuring localized Windows resources. This service is started on demand and, if disabled, additional Windows languages will not be deployed to the system, and Windows may not function properly.", - "display_name": "Language Experience Service" - }, - { - "name": "LxssManager", - "description": "The LXSS Manager service supports running native ELF binaries. The service provides the infrastructure necessary for ELF binaries to run on Windows. If the service is stopped or disabled, those binaries will no longer run.", - "display_name": "LxssManager" - }, - { - "name": "MapsBroker", - "description": "Windows service for application access to downloaded maps. This service is started on-demand by application accessing downloaded maps. Disabling this service will prevent apps from accessing maps.", - "display_name": "Downloaded Maps Manager" - }, - { - "name": "mpssvc", - "description": "Windows Defender Firewall helps protect your computer by preventing unauthorized users from gaining access to your computer through the Internet or a network.", - "display_name": "Windows Defender Firewall" - }, - { - "name": "MSDTC", - "description": "Coordinates transactions that span multiple resource managers, such as databases, message queues, and file systems. If this service is stopped, these transactions will fail. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Distributed Transaction Coordinator" - }, - { - "name": "MSiSCSI", - "description": "Manages Internet SCSI (iSCSI) sessions from this computer to remote iSCSI target devices. If this service is stopped, this computer will not be able to login or access iSCSI targets. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Microsoft iSCSI Initiator Service" - }, - { - "name": "msiserver", - "description": "Adds, modifies, and removes applications provided as a Windows Installer (*.msi, *.msp) package. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Windows Installer" - }, - { - "name": "NaturalAuthentication", - "description": "Signal aggregator service, that evaluates signals based on time, network, geolocation, bluetooth and cdf factors. Supported features are Device Unlock, Dynamic Lock and Dynamo MDM policies", - "display_name": "Natural Authentication" - }, - { - "name": "NcaSvc", - "description": "Provides DirectAccess status notification for UI components", - "display_name": "Network Connectivity Assistant" - }, - { - "name": "NcbService", - "description": "Brokers connections that allow Windows Store Apps to receive notifications from the internet.", - "display_name": "Network Connection Broker" - }, - { - "name": "NcdAutoSetup", - "description": "Network Connected Devices Auto-Setup service monitors and installs qualified devices that connect to a qualified network. Stopping or disabling this service will prevent Windows from discovering and installing qualified network connected devices automatically. Users can still manually add network connected devices to a PC through the user interface.", - "display_name": "Network Connected Devices Auto-Setup" - }, - { - "name": "Net Driver HPZ12", - "description": "", - "display_name": "Net Driver HPZ12" - }, - { - "name": "Netlogon", - "description": "Maintains a secure channel between this computer and the domain controller for authenticating users and services. If this service is stopped, the computer may not authenticate users and services and the domain controller cannot register DNS records. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Netlogon" - }, - { - "name": "Netman", - "description": "Manages objects in the Network and Dial-Up Connections folder, in which you can view both local area network and remote connections.", - "display_name": "Network Connections" - }, - { - "name": "netprofm", - "description": "Identifies the networks to which the computer has connected, collects and stores properties for these networks, and notifies applications when these properties change.", - "display_name": "Network List Service" - }, - { - "name": "NetSetupSvc", - "description": "The Network Setup Service manages the installation of network drivers and permits the configuration of low-level network settings. If this service is stopped, any driver installations that are in-progress may be cancelled.", - "display_name": "Network Setup Service" - }, - { - "name": "NetTcpPortSharing", - "description": "Provides ability to share TCP ports over the net.tcp protocol.", - "display_name": "Net.Tcp Port Sharing Service" - }, - { - "name": "NgcCtnrSvc", - "description": "Manages local user identity keys used to authenticate user to identity providers as well as TPM virtual smart cards. If this service is disabled, local user identity keys and TPM virtual smart cards will not be accessible. It is recommended that you do not reconfigure this service.", - "display_name": "Microsoft Passport Container" - }, - { - "name": "NgcSvc", - "description": "Provides process isolation for cryptographic keys used to authenticate to a user\u2019s associated identity providers. If this service is disabled, all uses and management of these keys will not be available, which includes machine logon and single-sign on for apps and websites. This service starts and stops automatically. It is recommended that you do not reconfigure this service.", - "display_name": "Microsoft Passport" - }, - { - "name": "NlaSvc", - "description": "Collects and stores configuration information for the network and notifies programs when this information is modified. If this service is stopped, configuration information might be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Network Location Awareness" - }, - { - "name": "nsi", - "description": "This service delivers network notifications (e.g. interface addition/deleting etc) to user mode clients. Stopping this service will cause loss of network connectivity. If this service is disabled, any other services that explicitly depend on this service will fail to start.", - "display_name": "Network Store Interface Service" - }, - { - "name": "nvagent", - "description": "Provides network virtualization services.", - "display_name": "Network Virtualization Service" - }, - { - "name": "OpenVPNService", - "description": "", - "display_name": "OpenVPNService" - }, - { - "name": "OpenVPNServiceInteractive", - "description": "", - "display_name": "OpenVPN Interactive Service" - }, - { - "name": "OpenVPNServiceLegacy", - "description": "", - "display_name": "OpenVPN Legacy Service" - }, - { - "name": "ose64", - "description": "Saves installation files used for updates and repairs and is required for the downloading of Setup updates and Watson error reports.", - "display_name": "Office 64 Source Engine" - }, - { - "name": "p2pimsvc", - "description": "Provides identity services for the Peer Name Resolution Protocol (PNRP) and Peer-to-Peer Grouping services. If disabled, the Peer Name Resolution Protocol (PNRP) and Peer-to-Peer Grouping services may not function, and some applications, such as HomeGroup and Remote Assistance, may not function correctly.", - "display_name": "Peer Networking Identity Manager" - }, - { - "name": "p2psvc", - "description": "Enables multi-party communication using Peer-to-Peer Grouping. If disabled, some applications, such as HomeGroup, may not function.", - "display_name": "Peer Networking Grouping" - }, - { - "name": "PcaSvc", - "description": "This service provides support for the Program Compatibility Assistant (PCA). PCA monitors programs installed and run by the user and detects known compatibility problems. If this service is stopped, PCA will not function properly.", - "display_name": "Program Compatibility Assistant Service" - }, - { - "name": "PeerDistSvc", - "description": "This service caches network content from peers on the local subnet.", - "display_name": "BranchCache" - }, - { - "name": "perceptionsimulation", - "description": "Enables spatial perception simulation, virtual camera management and spatial input simulation.", - "display_name": "Windows Perception Simulation Service" - }, - { - "name": "PerfHost", - "description": "Enables remote users and 64-bit processes to query performance counters provided by 32-bit DLLs. If this service is stopped, only local users and 32-bit processes will be able to query performance counters provided by 32-bit DLLs.", - "display_name": "Performance Counter DLL Host" - }, - { - "name": "PhoneSvc", - "description": "Manages the telephony state on the device", - "display_name": "Phone Service" - }, - { - "name": "pla", - "description": "Performance Logs and Alerts Collects performance data from local or remote computers based on preconfigured schedule parameters, then writes the data to a log or triggers an alert. If this service is stopped, performance information will not be collected. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Performance Logs & Alerts" - }, - { - "name": "PlugPlay", - "description": "Enables a computer to recognize and adapt to hardware changes with little or no user input. Stopping or disabling this service will result in system instability.", - "display_name": "Plug and Play" - }, - { - "name": "Pml Driver HPZ12", - "description": "", - "display_name": "Pml Driver HPZ12" - }, - { - "name": "PNRPAutoReg", - "description": "This service publishes a machine name using the Peer Name Resolution Protocol. Configuration is managed via the netsh context 'p2p pnrp peer' ", - "display_name": "PNRP Machine Name Publication Service" - }, - { - "name": "PNRPsvc", - "description": "Enables serverless peer name resolution over the Internet using the Peer Name Resolution Protocol (PNRP). If disabled, some peer-to-peer and collaborative applications, such as Remote Assistance, may not function.", - "display_name": "Peer Name Resolution Protocol" - }, - { - "name": "PolicyAgent", - "description": "Internet Protocol security (IPsec) supports network-level peer authentication, data origin authentication, data integrity, data confidentiality (encryption), and replay protection. This service enforces IPsec policies created through the IP Security Policies snap-in or the command-line tool \"netsh ipsec\". If you stop this service, you may experience network connectivity issues if your policy requires that connections use IPsec. Also,remote management of Windows Defender Firewall is not available when this service is stopped.", - "display_name": "IPsec Policy Agent" - }, - { - "name": "Power", - "description": "Manages power policy and power policy notification delivery.", - "display_name": "Power" - }, - { - "name": "PrintNotify", - "description": "This service opens custom printer dialog boxes and handles notifications from a remote print server or a printer. If you turn off this service, you won\u2019t be able to see printer extensions or notifications.", - "display_name": "Printer Extensions and Notifications" - }, - { - "name": "ProfSvc", - "description": "This service is responsible for loading and unloading user profiles. If this service is stopped or disabled, users will no longer be able to successfully sign in or sign out, apps might have problems getting to users' data, and components registered to receive profile event notifications won't receive them.", - "display_name": "User Profile Service" - }, - { - "name": "PushToInstall", - "description": "Provides infrastructure support for the Microsoft Store. This service is started automatically and if disabled then remote installations will not function properly.", - "display_name": "Windows PushToInstall Service" - }, - { - "name": "QWAVE", - "description": "Quality Windows Audio Video Experience (qWave) is a networking platform for Audio Video (AV) streaming applications on IP home networks. qWave enhances AV streaming performance and reliability by ensuring network quality-of-service (QoS) for AV applications. It provides mechanisms for admission control, run time monitoring and enforcement, application feedback, and traffic prioritization.", - "display_name": "Quality Windows Audio Video Experience" - }, - { - "name": "RasAuto", - "description": "Creates a connection to a remote network whenever a program references a remote DNS or NetBIOS name or address.", - "display_name": "Remote Access Auto Connection Manager" - }, - { - "name": "RasMan", - "description": "Manages dial-up and virtual private network (VPN) connections from this computer to the Internet or other remote networks. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Remote Access Connection Manager" - }, - { - "name": "RemoteAccess", - "description": "Offers routing services to businesses in local area and wide area network environments.", - "display_name": "Routing and Remote Access" - }, - { - "name": "RemoteRegistry", - "description": "Enables remote users to modify registry settings on this computer. If this service is stopped, the registry can be modified only by users on this computer. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Remote Registry" - }, - { - "name": "RetailDemo", - "description": "The Retail Demo service controls device activity while the device is in retail demo mode.", - "display_name": "Retail Demo Service" - }, - { - "name": "RmSvc", - "description": "Radio Management and Airplane Mode Service", - "display_name": "Radio Management Service" - }, - { - "name": "RpcEptMapper", - "description": "Resolves RPC interfaces identifiers to transport endpoints. If this service is stopped or disabled, programs using Remote Procedure Call (RPC) services will not function properly.", - "display_name": "RPC Endpoint Mapper" - }, - { - "name": "RpcLocator", - "description": "In Windows 2003 and earlier versions of Windows, the Remote Procedure Call (RPC) Locator service manages the RPC name service database. In Windows Vista and later versions of Windows, this service does not provide any functionality and is present for application compatibility.", - "display_name": "Remote Procedure Call (RPC) Locator" - }, - { - "name": "RpcSs", - "description": "The RPCSS service is the Service Control Manager for COM and DCOM servers. It performs object activations requests, object exporter resolutions and distributed garbage collection for COM and DCOM servers. If this service is stopped or disabled, programs using COM or DCOM will not function properly. It is strongly recommended that you have the RPCSS service running.", - "display_name": "Remote Procedure Call (RPC)" - }, - { - "name": "SamSs", - "description": "The startup of this service signals other services that the Security Accounts Manager (SAM) is ready to accept requests. Disabling this service will prevent other services in the system from being notified when the SAM is ready, which may in turn cause those services to fail to start correctly. This service should not be disabled.", - "display_name": "Security Accounts Manager" - }, - { - "name": "SCardSvr", - "description": "Manages access to smart cards read by this computer. If this service is stopped, this computer will be unable to read smart cards. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Smart Card" - }, - { - "name": "ScDeviceEnum", - "description": "Creates software device nodes for all smart card readers accessible to a given session. If this service is disabled, WinRT APIs will not be able to enumerate smart card readers.", - "display_name": "Smart Card Device Enumeration Service" - }, - { - "name": "Schedule", - "description": "Enables a user to configure and schedule automated tasks on this computer. The service also hosts multiple Windows system-critical tasks. If this service is stopped or disabled, these tasks will not be run at their scheduled times. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Task Scheduler" - }, - { - "name": "SCPolicySvc", - "description": "Allows the system to be configured to lock the user desktop upon smart card removal.", - "display_name": "Smart Card Removal Policy" - }, - { - "name": "SDRSVC", - "description": "Provides Windows Backup and Restore capabilities.", - "display_name": "Windows Backup" - }, - { - "name": "seclogon", - "description": "Enables starting processes under alternate credentials. If this service is stopped, this type of logon access will be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Secondary Logon" - }, - { - "name": "SecurityHealthService", - "description": "Windows Security Service handles unified device protection and health information", - "display_name": "Windows Security Service" - }, - { - "name": "SEMgrSvc", - "description": "Manages payments and Near Field Communication (NFC) based secure elements.", - "display_name": "Payments and NFC/SE Manager" - }, - { - "name": "SENS", - "description": "Monitors system events and notifies subscribers to COM+ Event System of these events.", - "display_name": "System Event Notification Service" - }, - { - "name": "Sense", - "description": "Windows Defender Advanced Threat Protection service helps protect against advanced threats by monitoring and reporting security events that happen on the computer.", - "display_name": "Windows Defender Advanced Threat Protection Service" - }, - { - "name": "SensorDataService", - "description": "Delivers data from a variety of sensors", - "display_name": "Sensor Data Service" - }, - { - "name": "SensorService", - "description": "A service for sensors that manages different sensors' functionality. Manages Simple Device Orientation (SDO) and History for sensors. Loads the SDO sensor that reports device orientation changes. If this service is stopped or disabled, the SDO sensor will not be loaded and so auto-rotation will not occur. History collection from Sensors will also be stopped.", - "display_name": "Sensor Service" - }, - { - "name": "SensrSvc", - "description": "Monitors various sensors in order to expose data and adapt to system and user state. If this service is stopped or disabled, the display brightness will not adapt to lighting conditions. Stopping this service may affect other system functionality and features as well.", - "display_name": "Sensor Monitoring Service" - }, - { - "name": "SessionEnv", - "description": "Remote Desktop Configuration service (RDCS) is responsible for all Remote Desktop Services and Remote Desktop related configuration and session maintenance activities that require SYSTEM context. These include per-session temporary folders, RD themes, and RD certificates.", - "display_name": "Remote Desktop Configuration" - }, - { - "name": "SgrmBroker", - "description": "Monitors and attests to the integrity of the Windows platform.", - "display_name": "System Guard Runtime Monitor Broker" - }, - { - "name": "SharedAccess", - "description": "Provides network address translation, addressing, name resolution and/or intrusion prevention services for a home or small office network.", - "display_name": "Internet Connection Sharing (ICS)" - }, - { - "name": "SharedRealitySvc", - "description": "This service is used for Spatial Perception scenarios", - "display_name": "Spatial Data Service" - }, - { - "name": "ShellHWDetection", - "description": "Provides notifications for AutoPlay hardware events.", - "display_name": "Shell Hardware Detection" - }, - { - "name": "shpamsvc", - "description": "Manages profiles and accounts on a SharedPC configured device", - "display_name": "Shared PC Account Manager" - }, - { - "name": "smphost", - "description": "Host service for the Microsoft Storage Spaces management provider. If this service is stopped or disabled, Storage Spaces cannot be managed.", - "display_name": "Microsoft Storage Spaces SMP" - }, - { - "name": "SmsRouter", - "description": "Routes messages based on rules to appropriate clients.", - "display_name": "Microsoft Windows SMS Router Service." - }, - { - "name": "SNMPTRAP", - "description": "Receives trap messages generated by local or remote Simple Network Management Protocol (SNMP) agents and forwards the messages to SNMP management programs running on this computer. If this service is stopped, SNMP-based programs on this computer will not receive SNMP trap messages. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "SNMP Trap" - }, - { - "name": "spectrum", - "description": "Enables spatial perception, spatial input, and holographic rendering.", - "display_name": "Windows Perception Service" - }, - { - "name": "Spooler", - "description": "This service spools print jobs and handles interaction with the printer. If you turn off this service, you won\u2019t be able to print or see your printers.", - "display_name": "Print Spooler" - }, - { - "name": "sppsvc", - "description": "Enables the download, installation and enforcement of digital licenses for Windows and Windows applications. If the service is disabled, the operating system and licensed applications may run in a notification mode. It is strongly recommended that you not disable the Software Protection service.", - "display_name": "Software Protection" - }, - { - "name": "SSDPSRV", - "description": "Discovers networked devices and services that use the SSDP discovery protocol, such as UPnP devices. Also announces SSDP devices and services running on the local computer. If this service is stopped, SSDP-based devices will not be discovered. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "SSDP Discovery" - }, - { - "name": "ssh-agent", - "description": "Agent to hold private keys used for public key authentication.", - "display_name": "OpenSSH Authentication Agent" - }, - { - "name": "sshd", - "description": "SSH protocol based service to provide secure encrypted communications between two untrusted hosts over an insecure network.", - "display_name": "OpenSSH SSH Server" - }, - { - "name": "SstpSvc", - "description": "Provides support for the Secure Socket Tunneling Protocol (SSTP) to connect to remote computers using VPN. If this service is disabled, users will not be able to use SSTP to access remote servers.", - "display_name": "Secure Socket Tunneling Protocol Service" - }, - { - "name": "StateRepository", - "description": "Provides required infrastructure support for the application model.", - "display_name": "State Repository Service" - }, - { - "name": "stisvc", - "description": "Provides image acquisition services for scanners and cameras", - "display_name": "Windows Image Acquisition (WIA)" - }, - { - "name": "StorSvc", - "description": "Provides enabling services for storage settings and external storage expansion", - "display_name": "Storage Service" - }, - { - "name": "svsvc", - "description": "Verifies potential file system corruptions.", - "display_name": "Spot Verifier" - }, - { - "name": "swprv", - "description": "Manages software-based volume shadow copies taken by the Volume Shadow Copy service. If this service is stopped, software-based volume shadow copies cannot be managed. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Microsoft Software Shadow Copy Provider" - }, - { - "name": "SynTPEnhService", - "description": "", - "display_name": "SynTPEnh Caller Service" - }, - { - "name": "SysMain", - "description": "Maintains and improves system performance over time.", - "display_name": "SysMain" - }, - { - "name": "SystemEventsBroker", - "description": "Coordinates execution of background work for WinRT application. If this service is stopped or disabled, then background work might not be triggered.", - "display_name": "System Events Broker" - }, - { - "name": "TabletInputService", - "description": "Enables Touch Keyboard and Handwriting Panel pen and ink functionality", - "display_name": "Touch Keyboard and Handwriting Panel Service" - }, - { - "name": "TapiSrv", - "description": "Provides Telephony API (TAPI) support for programs that control telephony devices on the local computer and, through the LAN, on servers that are also running the service.", - "display_name": "Telephony" - }, - { - "name": "TermService", - "description": "Allows users to connect interactively to a remote computer. Remote Desktop and Remote Desktop Session Host Server depend on this service. To prevent remote use of this computer, clear the checkboxes on the Remote tab of the System properties control panel item.", - "display_name": "Remote Desktop Services" - }, - { - "name": "Themes", - "description": "Provides user experience theme management.", - "display_name": "Themes" - }, - { - "name": "TieringEngineService", - "description": "Optimizes the placement of data in storage tiers on all tiered storage spaces in the system.", - "display_name": "Storage Tiers Management" - }, - { - "name": "TimeBrokerSvc", - "description": "Coordinates execution of background work for WinRT application. If this service is stopped or disabled, then background work might not be triggered.", - "display_name": "Time Broker" - }, - { - "name": "TokenBroker", - "description": "This service is used by Web Account Manager to provide single-sign-on to apps and services.", - "display_name": "Web Account Manager" - }, - { - "name": "TrkWks", - "description": "Maintains links between NTFS files within a computer or across computers in a network.", - "display_name": "Distributed Link Tracking Client" - }, - { - "name": "TroubleshootingSvc", - "description": "Enables automatic mitigation for known problems by applying recommended troubleshooting. If stopped, your device will not get recommended troubleshooting for problems on your device.", - "display_name": "Recommended Troubleshooting Service" - }, - { - "name": "TrustedInstaller", - "description": "Enables installation, modification, and removal of Windows updates and optional components. If this service is disabled, install or uninstall of Windows updates might fail for this computer.", - "display_name": "Windows Modules Installer" - }, - { - "name": "tzautoupdate", - "description": "Automatically sets the system time zone.", - "display_name": "Auto Time Zone Updater" - }, - { - "name": "UevAgentService", - "description": "Provides support for application and OS settings roaming", - "display_name": "User Experience Virtualization Service" - }, - { - "name": "UmRdpService", - "description": "Allows the redirection of Printers/Drives/Ports for RDP connections", - "display_name": "Remote Desktop Services UserMode Port Redirector" - }, - { - "name": "upnphost", - "description": "Allows UPnP devices to be hosted on this computer. If this service is stopped, any hosted UPnP devices will stop functioning and no additional hosted devices can be added. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "UPnP Device Host" - }, - { - "name": "UserManager", - "description": "User Manager provides the runtime components required for multi-user interaction. If this service is stopped, some applications may not operate correctly.", - "display_name": "User Manager" - }, - { - "name": "UsoSvc", - "description": "Manages Windows Updates. If stopped, your devices will not be able to download and install the latest updates.", - "display_name": "Update Orchestrator Service" - }, - { - "name": "VacSvc", - "description": "Hosts spatial analysis for Mixed Reality audio simulation.", - "display_name": "Volumetric Audio Compositor Service" - }, - { - "name": "VaultSvc", - "description": "Provides secure storage and retrieval of credentials to users, applications and security service packages.", - "display_name": "Credential Manager" - }, - { - "name": "vds", - "description": "Provides management services for disks, volumes, file systems, and storage arrays.", - "display_name": "Virtual Disk" - }, - { - "name": "vmcompute", - "description": "Provides support for running Windows Containers and Virtual Machines.", - "display_name": "Hyper-V Host Compute Service" - }, - { - "name": "vmicguestinterface", - "description": "Provides an interface for the Hyper-V host to interact with specific services running inside the virtual machine.", - "display_name": "Hyper-V Guest Service Interface" - }, - { - "name": "vmicheartbeat", - "description": "Monitors the state of this virtual machine by reporting a heartbeat at regular intervals. This service helps you identify running virtual machines that have stopped responding.", - "display_name": "Hyper-V Heartbeat Service" - }, - { - "name": "vmickvpexchange", - "description": "Provides a mechanism to exchange data between the virtual machine and the operating system running on the physical computer.", - "display_name": "Hyper-V Data Exchange Service" - }, - { - "name": "vmicrdv", - "description": "Provides a platform for communication between the virtual machine and the operating system running on the physical computer.", - "display_name": "Hyper-V Remote Desktop Virtualization Service" - }, - { - "name": "vmicshutdown", - "description": "Provides a mechanism to shut down the operating system of this virtual machine from the management interfaces on the physical computer.", - "display_name": "Hyper-V Guest Shutdown Service" - }, - { - "name": "vmictimesync", - "description": "Synchronizes the system time of this virtual machine with the system time of the physical computer.", - "display_name": "Hyper-V Time Synchronization Service" - }, - { - "name": "vmicvmsession", - "description": "Provides a mechanism to manage virtual machine with PowerShell via VM session without a virtual network.", - "display_name": "Hyper-V PowerShell Direct Service" - }, - { - "name": "vmicvss", - "description": "Coordinates the communications that are required to use Volume Shadow Copy Service to back up applications and data on this virtual machine from the operating system on the physical computer.", - "display_name": "Hyper-V Volume Shadow Copy Requestor" - }, - { - "name": "VSS", - "description": "Manages and implements Volume Shadow Copies used for backup and other purposes. If this service is stopped, shadow copies will be unavailable for backup and the backup may fail. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Volume Shadow Copy" - }, - { - "name": "W32Time", - "description": "Maintains date and time synchronization on all clients and servers in the network. If this service is stopped, date and time synchronization will be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Windows Time" - }, - { - "name": "WaaSMedicSvc", - "description": "Enables remediation and protection of Windows Update components.", - "display_name": "Windows Update Medic Service" - }, - { - "name": "WalletService", - "description": "Hosts objects used by clients of the wallet", - "display_name": "WalletService" - }, - { - "name": "WarpJITSvc", - "description": "Provides a JIT out of process service for WARP when running with ACG enabled.", - "display_name": "WarpJITSvc" - }, - { - "name": "wbengine", - "description": "The WBENGINE service is used by Windows Backup to perform backup and recovery operations. If this service is stopped by a user, it may cause the currently running backup or recovery operation to fail. Disabling this service may disable backup and recovery operations using Windows Backup on this computer.", - "display_name": "Block Level Backup Engine Service" - }, - { - "name": "WbioSrvc", - "description": "The Windows biometric service gives client applications the ability to capture, compare, manipulate, and store biometric data without gaining direct access to any biometric hardware or samples. The service is hosted in a privileged SVCHOST process.", - "display_name": "Windows Biometric Service" - }, - { - "name": "Wcmsvc", - "description": "Makes automatic connect/disconnect decisions based on the network connectivity options currently available to the PC and enables management of network connectivity based on Group Policy settings.", - "display_name": "Windows Connection Manager" - }, - { - "name": "wcncsvc", - "description": "WCNCSVC hosts the Windows Connect Now Configuration which is Microsoft's Implementation of Wireless Protected Setup (WPS) protocol. This is used to configure Wireless LAN settings for an Access Point (AP) or a Wireless Device. The service is started programmatically as needed.", - "display_name": "Windows Connect Now - Config Registrar" - }, - { - "name": "WdiServiceHost", - "description": "The Diagnostic Service Host is used by the Diagnostic Policy Service to host diagnostics that need to run in a Local Service context. If this service is stopped, any diagnostics that depend on it will no longer function.", - "display_name": "Diagnostic Service Host" - }, - { - "name": "WdiSystemHost", - "description": "The Diagnostic System Host is used by the Diagnostic Policy Service to host diagnostics that need to run in a Local System context. If this service is stopped, any diagnostics that depend on it will no longer function.", - "display_name": "Diagnostic System Host" - }, - { - "name": "WdNisSvc", - "description": "Helps guard against intrusion attempts targeting known and newly discovered vulnerabilities in network protocols", - "display_name": "Windows Defender Antivirus Network Inspection Service" - }, - { - "name": "WebClient", - "description": "Enables Windows-based programs to create, access, and modify Internet-based files. If this service is stopped, these functions will not be available. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "WebClient" - }, - { - "name": "Wecsvc", - "description": "This service manages persistent subscriptions to events from remote sources that support WS-Management protocol. This includes Windows Vista event logs, hardware and IPMI-enabled event sources. The service stores forwarded events in a local Event Log. If this service is stopped or disabled event subscriptions cannot be created and forwarded events cannot be accepted.", - "display_name": "Windows Event Collector" - }, - { - "name": "WEPHOSTSVC", - "description": "Windows Encryption Provider Host Service brokers encryption related functionalities from 3rd Party Encryption Providers to processes that need to evaluate and apply EAS policies. Stopping this will compromise EAS compliancy checks that have been established by the connected Mail Accounts", - "display_name": "Windows Encryption Provider Host Service" - }, - { - "name": "wercplsupport", - "description": "This service provides support for viewing, sending and deletion of system-level problem reports for the Problem Reports and Solutions control panel.", - "display_name": "Problem Reports and Solutions Control Panel Support" - }, - { - "name": "WerSvc", - "description": "Allows errors to be reported when programs stop working or responding and allows existing solutions to be delivered. Also allows logs to be generated for diagnostic and repair services. If this service is stopped, error reporting might not work correctly and results of diagnostic services and repairs might not be displayed.", - "display_name": "Windows Error Reporting Service" - }, - { - "name": "WFDSConMgrSvc", - "description": "Manages connections to wireless services, including wireless display and docking.", - "display_name": "Wi-Fi Direct Services Connection Manager Service" - }, - { - "name": "WiaRpc", - "description": "Launches applications associated with still image acquisition events.", - "display_name": "Still Image Acquisition Events" - }, - { - "name": "WinDefend", - "description": "Helps protect users from malware and other potentially unwanted software", - "display_name": "Windows Defender Antivirus Service" - }, - { - "name": "WinHttpAutoProxySvc", - "description": "WinHTTP implements the client HTTP stack and provides developers with a Win32 API and COM Automation component for sending HTTP requests and receiving responses. In addition, WinHTTP provides support for auto-discovering a proxy configuration via its implementation of the Web Proxy Auto-Discovery (WPAD) protocol.", - "display_name": "WinHTTP Web Proxy Auto-Discovery Service" - }, - { - "name": "Winmgmt", - "description": "Provides a common interface and object model to access management information about operating system, devices, applications and services. If this service is stopped, most Windows-based software will not function properly. If this service is disabled, any services that explicitly depend on it will fail to start.", - "display_name": "Windows Management Instrumentation" - }, - { - "name": "WinRM", - "description": "Windows Remote Management (WinRM) service implements the WS-Management protocol for remote management. WS-Management is a standard web services protocol used for remote software and hardware management. The WinRM service listens on the network for WS-Management requests and processes them. The WinRM Service needs to be configured with a listener using winrm.cmd command line tool or through Group Policy in order for it to listen over the network. The WinRM service provides access to WMI data and enables event collection. Event collection and subscription to events require that the service is running. WinRM messages use HTTP and HTTPS as transports. The WinRM service does not depend on IIS but is preconfigured to share a port with IIS on the same machine. The WinRM service reserves the /wsman URL prefix. To prevent conflicts with IIS, administrators should ensure that any websites hosted on IIS do not use the /wsman URL prefix.", - "display_name": "Windows Remote Management (WS-Management)" - }, - { - "name": "wisvc", - "description": "Provides infrastructure support for the Windows Insider Program. This service must remain enabled for the Windows Insider Program to work.", - "display_name": "Windows Insider Service" - }, - { - "name": "WlanSvc", - "description": "The WLANSVC service provides the logic required to configure, discover, connect to, and disconnect from a wireless local area network (WLAN) as defined by IEEE 802.11 standards. It also contains the logic to turn your computer into a software access point so that other devices or computers can connect to your computer wirelessly using a WLAN adapter that can support this. Stopping or disabling the WLANSVC service will make all WLAN adapters on your computer inaccessible from the Windows networking UI. It is strongly recommended that you have the WLANSVC service running if your computer has a WLAN adapter.", - "display_name": "WLAN AutoConfig" - }, - { - "name": "wlidsvc", - "description": "Enables user sign-in through Microsoft account identity services. If this service is stopped, users will not be able to logon to the computer with their Microsoft account.", - "display_name": "Microsoft Account Sign-in Assistant" - }, - { - "name": "wlpasvc", - "description": "This service provides profile management for subscriber identity modules", - "display_name": "Local Profile Assistant Service" - }, - { - "name": "WManSvc", - "description": "Performs management including Provisioning and Enrollment activities", - "display_name": "Windows Management Service" - }, - { - "name": "wmiApSrv", - "description": "Provides performance library information from Windows Management Instrumentation (WMI) providers to clients on the network. This service only runs when Performance Data Helper is activated.", - "display_name": "WMI Performance Adapter" - }, - { - "name": "WMPNetworkSvc", - "description": "Shares Windows Media Player libraries to other networked players and media devices using Universal Plug and Play", - "display_name": "Windows Media Player Network Sharing Service" - }, - { - "name": "workfolderssvc", - "description": "This service syncs files with the Work Folders server, enabling you to use the files on any of the PCs and devices on which you've set up Work Folders.", - "display_name": "Work Folders" - }, - { - "name": "WpcMonSvc", - "description": "Enforces parental controls for child accounts in Windows. If this service is stopped or disabled, parental controls may not be enforced.", - "display_name": "Parental Controls" - }, - { - "name": "WPDBusEnum", - "description": "Enforces group policy for removable mass-storage devices. Enables applications such as Windows Media Player and Image Import Wizard to transfer and synchronize content using removable mass-storage devices.", - "display_name": "Portable Device Enumerator Service" - }, - { - "name": "WpnService", - "description": "This service runs in session 0 and hosts the notification platform and connection provider which handles the connection between the device and WNS server.", - "display_name": "Windows Push Notifications System Service" - }, - { - "name": "wscsvc", - "description": "The WSCSVC (Windows Security Center) service monitors and reports security health settings on the computer. The health settings include firewall (on/off), antivirus (on/off/out of date), antispyware (on/off/out of date), Windows Update (automatically/manually download and install updates), User Account Control (on/off), and Internet settings (recommended/not recommended). The service provides COM APIs for independent software vendors to register and record the state of their products to the Security Center service. The Security and Maintenance UI uses the service to provide systray alerts and a graphical view of the security health states in the Security and Maintenance control panel. Network Access Protection (NAP) uses the service to report the security health states of clients to the NAP Network Policy Server to make network quarantine decisions. The service also has a public API that allows external consumers to programmatically retrieve the aggregated security health state of the system.", - "display_name": "Security Center" - }, - { - "name": "WSearch", - "description": "Provides content indexing, property caching, and search results for files, e-mail, and other content.", - "display_name": "Windows Search" - }, - { - "name": "wuauserv", - "description": "Enables the detection, download, and installation of updates for Windows and other programs. If this service is disabled, users of this computer will not be able to use Windows Update or its automatic updating feature, and programs will not be able to use the Windows Update Agent (WUA) API.", - "display_name": "Windows Update" - }, - { - "name": "WwanSvc", - "description": "This service manages mobile broadband (GSM & CDMA) data card/embedded module adapters and connections by auto-configuring the networks. It is strongly recommended that this service be kept running for best user experience of mobile broadband devices.", - "display_name": "WWAN AutoConfig" - }, - { - "name": "XblAuthManager", - "description": "Provides authentication and authorization services for interacting with Xbox Live. If this service is stopped, some applications may not operate correctly.", - "display_name": "Xbox Live Auth Manager" - }, - { - "name": "XblGameSave", - "description": "This service syncs save data for Xbox Live save enabled games. If this service is stopped, game save data will not upload to or download from Xbox Live.", - "display_name": "Xbox Live Game Save" - }, - { - "name": "XboxGipSvc", - "description": "This service manages connected Xbox Accessories.", - "display_name": "Xbox Accessory Management Service" - }, - { - "name": "XboxNetApiSvc", - "description": "This service supports the Windows.Networking.XboxLive application programming interface.", - "display_name": "Xbox Live Networking Service" - }, - { - "name": "YMC", - "description": "", - "display_name": "YMC" - }, - { - "name": "AarSvc_35b28a", - "description": "Runtime for activating conversational agent applications", - "display_name": "Agent Activation Runtime_35b28a" - }, - { - "name": "BcastDVRUserService_35b28a", - "description": "This user service is used for Game Recordings and Live Broadcasts", - "display_name": "GameDVR and Broadcast User Service_35b28a" - }, - { - "name": "BluetoothUserService_35b28a", - "description": "The Bluetooth user service supports proper functionality of Bluetooth features relevant to each user session.", - "display_name": "Bluetooth User Support Service_35b28a" - }, - { - "name": "CaptureService_35b28a", - "description": "Enables optional screen capture functionality for applications that call the Windows.Graphics.Capture API.", - "display_name": "CaptureService_35b28a" - }, - { - "name": "cbdhsvc_35b28a", - "description": "This user service is used for Clipboard scenarios", - "display_name": "Clipboard User Service_35b28a" - }, - { - "name": "CDPUserSvc_35b28a", - "description": "This user service is used for Connected Devices Platform scenarios", - "display_name": "Connected Devices Platform User Service_35b28a" - }, - { - "name": "ConsentUxUserSvc_35b28a", - "description": "Allows ConnectUX and PC Settings to Connect and Pair with WiFi displays and Bluetooth devices.", - "display_name": "ConsentUX_35b28a" - }, - { - "name": "CredentialEnrollmentManagerUserSvc_35b28a", - "description": "Credential Enrollment Manager", - "display_name": "CredentialEnrollmentManagerUserSvc_35b28a" - }, - { - "name": "DeviceAssociationBrokerSvc_35b28a", - "description": "Enables apps to pair devices", - "display_name": "DeviceAssociationBroker_35b28a" - }, - { - "name": "DevicePickerUserSvc_35b28a", - "description": "This user service is used for managing the Miracast, DLNA, and DIAL UI", - "display_name": "DevicePicker_35b28a" - }, - { - "name": "DevicesFlowUserSvc_35b28a", - "description": "Allows ConnectUX and PC Settings to Connect and Pair with WiFi displays and Bluetooth devices.", - "display_name": "DevicesFlow_35b28a" - }, - { - "name": "LxssManagerUser_35b28a", - "description": "The LXSS Manager service supports running native ELF binaries. The service provides the infrastructure necessary for ELF binaries to run on Windows. If the service is stopped or disabled, those binaries will no longer run.", - "display_name": "LxssManagerUser_35b28a" - }, - { - "name": "MessagingService_35b28a", - "description": "Service supporting text messaging and related functionality.", - "display_name": "MessagingService_35b28a" - }, - { - "name": "OneSyncSvc_35b28a", - "description": "This service synchronizes mail, contacts, calendar and various other user data. Mail and other applications dependent on this functionality will not work properly when this service is not running.", - "display_name": "Sync Host_35b28a" - }, - { - "name": "PimIndexMaintenanceSvc_35b28a", - "description": "Indexes contact data for fast contact searching. If you stop or disable this service, contacts might be missing from your search results.", - "display_name": "Contact Data_35b28a" - }, - { - "name": "PrintWorkflowUserSvc_35b28a", - "description": "Print Workflow", - "display_name": "PrintWorkflow_35b28a" - }, - { - "name": "UnistoreSvc_35b28a", - "description": "Handles storage of structured user data, including contact info, calendars, messages, and other content. If you stop or disable this service, apps that use this data might not work correctly.", - "display_name": "User Data Storage_35b28a" - }, - { - "name": "UserDataSvc_35b28a", - "description": "Provides apps access to structured user data, including contact info, calendars, messages, and other content. If you stop or disable this service, apps that use this data might not work correctly.", - "display_name": "User Data Access_35b28a" - }, - { - "name": "WpnUserService_35b28a", - "description": "This service hosts Windows notification platform which provides support for local and push notifications. Supported notifications are tile, toast and raw.", - "display_name": "Windows Push Notifications User Service_35b28a" - }, - { - "name": "Mesh Agent", - "description": "Remote monitoring and management service.", - "display_name": "Mesh Agent background service" - }, - { - "name": "salt-minion", - "description": "Salt Minion from saltstack.com", - "display_name": "salt-minion" - }, - { - "name": "tacticalagent", - "description": "Tactical RMM Monitoring Agent", - "display_name": "Tactical RMM Agent" - } -] \ No newline at end of file diff --git a/api/tacticalrmm/services/permissions.py b/api/tacticalrmm/services/permissions.py index 2a1d89938d..ae9c5a97ac 100644 --- a/api/tacticalrmm/services/permissions.py +++ b/api/tacticalrmm/services/permissions.py @@ -1,8 +1,13 @@ from rest_framework import permissions -from tacticalrmm.permissions import _has_perm +from tacticalrmm.permissions import _has_perm, _has_perm_on_agent -class ManageWinSvcsPerms(permissions.BasePermission): +class WinSvcsPerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_manage_winsvcs") + if "agent_id" in view.kwargs.keys(): + return _has_perm(r, "can_manage_winsvcs") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) + else: + return _has_perm(r, "can_manage_winsvcs") diff --git a/api/tacticalrmm/services/serializers.py b/api/tacticalrmm/services/serializers.py deleted file mode 100644 index e695e58913..0000000000 --- a/api/tacticalrmm/services/serializers.py +++ /dev/null @@ -1,13 +0,0 @@ -from rest_framework import serializers - -from agents.models import Agent - - -class ServicesSerializer(serializers.ModelSerializer): - class Meta: - model = Agent - fields = ( - "hostname", - "pk", - "services", - ) diff --git a/api/tacticalrmm/services/tests.py b/api/tacticalrmm/services/tests.py index 3f49af1e3e..7fbf5ee16c 100644 --- a/api/tacticalrmm/services/tests.py +++ b/api/tacticalrmm/services/tests.py @@ -5,28 +5,22 @@ from agents.models import Agent from tacticalrmm.test import TacticalTestCase +base_url = "/services" + class TestServiceViews(TacticalTestCase): def setUp(self): self.authenticate() self.setup_coresettings() - def test_default_services(self): - url = "/services/defaultservices/" - resp = self.client.get(url, format="json") - self.assertEqual(resp.status_code, 200) - self.assertEqual(type(resp.data), list) - - self.check_not_authenticated("get", url) - @patch("agents.models.Agent.nats_cmd") def test_get_services(self, nats_cmd): # test a call where agent doesn't exist - resp = self.client.get("/services/500/services/", format="json") + resp = self.client.get("/services/500234hjk348982h/", format="json") self.assertEqual(resp.status_code, 404) agent = baker.make_recipe("agents.agent_with_services") - url = f"/services/{agent.pk}/services/" + url = f"{base_url}/{agent.agent_id}/" nats_return = [ { @@ -69,16 +63,16 @@ def test_get_services(self, nats_cmd): @patch("agents.models.Agent.nats_cmd") def test_service_action(self, nats_cmd): - url = "/services/serviceaction/" - invalid_data = {"pk": 500, "sv_name": "AeLookupSvc", "sv_action": "restart"} + data = {"sv_action": "restart"} # test a call where agent doesn't exist - resp = self.client.post(url, invalid_data, format="json") + resp = self.client.post( + f"{base_url}/kjhj4hj4khj34h34j/AeLookupSvc/", data, format="json" + ) self.assertEqual(resp.status_code, 404) agent = baker.make_recipe("agents.agent_with_services") - - data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "sv_action": "restart"} + url = f"/services/{agent.agent_id}/AeLookupSvc/" # test failed attempt nats_cmd.return_value = "timeout" @@ -107,7 +101,7 @@ def test_service_action(self, nats_cmd): def test_service_detail(self, nats_cmd): # test a call where agent doesn't exist resp = self.client.get( - "/services/500/doesntexist/servicedetail/", format="json" + f"{base_url}/34kjhj3h4jh3kjh34/service_name/", format="json" ) self.assertEqual(resp.status_code, 404) @@ -123,7 +117,7 @@ def test_service_detail(self, nats_cmd): } agent = baker.make_recipe("agents.agent") - url = f"/services/{agent.pk}/alg/servicedetail/" + url = f"{base_url}/{agent.agent_id}/alg/" # test failed attempt nats_cmd.return_value = "timeout" @@ -147,25 +141,25 @@ def test_service_detail(self, nats_cmd): @patch("agents.models.Agent.nats_cmd") def test_edit_service(self, nats_cmd): - url = "/services/editservice/" agent = baker.make_recipe("agents.agent_with_services") + url = f"{base_url}/{agent.agent_id}/AeLookupSvc/" - invalid_data = {"pk": 500, "sv_name": "AeLookupSvc", "edit_action": "autodelay"} + data = {"startType": "autodelay"} # test a call where agent doesn't exist - resp = self.client.post(url, invalid_data, format="json") + resp = self.client.put( + f"{base_url}/234kjh2k3hkj23h4kj3h4k3jh/service/", data, format="json" + ) self.assertEqual(resp.status_code, 404) - data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "autodelay"} - # test timeout nats_cmd.return_value = "timeout" - resp = self.client.post(url, data, format="json") + resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) nats_cmd.reset_mock() # test successful attempt autodelay nats_cmd.return_value = {"success": True, "errormsg": ""} - resp = self.client.post(url, data, format="json") + resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) nats_cmd.assert_called_with( { @@ -180,20 +174,61 @@ def test_edit_service(self, nats_cmd): nats_cmd.reset_mock() # test error message from agent - data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "auto"} + data = {"startType": "auto"} nats_cmd.return_value = { "success": False, "errormsg": "The parameter is incorrect", } - resp = self.client.post(url, data, format="json") + resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) nats_cmd.reset_mock() # test catch all - data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "auto"} nats_cmd.return_value = {"success": False, "errormsg": ""} - resp = self.client.post(url, data, format="json") + resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) self.assertEqual(resp.data, "Something went wrong") - self.check_not_authenticated("post", url) + self.check_not_authenticated("put", url) + + +class TestServicePermissions(TacticalTestCase): + def setUp(self): + self.setup_coresettings() + self.client_setup() + + @patch("agents.models.Agent.nats_cmd", return_value="ok") + def test_services_permissions(self, nats_cmd): + agent = baker.make_recipe("agents.agent_with_services") + unauthorized_agent = baker.make_recipe("agents.agent_with_services") + + test_data = [ + {"url": f"{base_url}/{agent.agent_id}/", "method": "get"}, + {"url": f"{base_url}/{agent.agent_id}/service_name/", "method": "get"}, + {"url": f"{base_url}/{agent.agent_id}/service_name/", "method": "post"}, + {"url": f"{base_url}/{agent.agent_id}/service_name/", "method": "put"}, + ] + + for data in test_data: + # test superuser + self.check_authorized_superuser(data["method"], data["url"]) + + # test user with no roles + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + self.check_not_authorized(data["method"], data["url"]) + + # test with correct role + user.role.can_manage_winsvcs = True + user.role.save() + + self.check_authorized(data["method"], data["url"]) + + # test limiting user to client + user.role.can_view_clients.set([agent.client]) + self.check_authorized(data["method"], data["url"]) + + user.role.can_view_clients.set([unauthorized_agent.client]) + + self.client.logout() diff --git a/api/tacticalrmm/services/urls.py b/api/tacticalrmm/services/urls.py index e7ba1a1304..a228235e77 100644 --- a/api/tacticalrmm/services/urls.py +++ b/api/tacticalrmm/services/urls.py @@ -3,9 +3,6 @@ from . import views urlpatterns = [ - path("/services/", views.get_services), - path("defaultservices/", views.default_services), - path("serviceaction/", views.service_action), - path("//servicedetail/", views.service_detail), - path("editservice/", views.edit_service), + path("/", views.GetServices.as_view()), + path("//", views.GetEditActionService.as_view()), ] diff --git a/api/tacticalrmm/services/views.py b/api/tacticalrmm/services/views.py index b1a5a6d647..1e391f5adb 100644 --- a/api/tacticalrmm/services/views.py +++ b/api/tacticalrmm/services/views.py @@ -3,54 +3,71 @@ from agents.models import Agent from checks.models import Check from django.shortcuts import get_object_or_404 -from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.views import APIView from tacticalrmm.utils import notify_error -from .permissions import ManageWinSvcsPerms -from .serializers import ServicesSerializer +from .permissions import WinSvcsPerms -@api_view() -def get_services(request, pk): - agent = get_object_or_404(Agent, pk=pk) - r = asyncio.run(agent.nats_cmd(data={"func": "winservices"}, timeout=10)) +class GetServices(APIView): + permission_classes = [IsAuthenticated, WinSvcsPerms] - if r == "timeout": - return notify_error("Unable to contact the agent") + def get(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + r = asyncio.run(agent.nats_cmd(data={"func": "winservices"}, timeout=10)) - agent.services = r - agent.save(update_fields=["services"]) - return Response(ServicesSerializer(agent).data) + if r == "timeout": + return notify_error("Unable to contact the agent") + agent.services = r + agent.save(update_fields=["services"]) + return Response(agent.services) -@api_view() -def default_services(request): - return Response(Check.load_default_services()) +class GetEditActionService(APIView): + permission_classes = [IsAuthenticated, WinSvcsPerms] -@api_view(["POST"]) -@permission_classes([IsAuthenticated, ManageWinSvcsPerms]) -def service_action(request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) - action = request.data["sv_action"] - data = { - "func": "winsvcaction", - "payload": { - "name": request.data["sv_name"], - }, - } - # response struct from agent: {success: bool, errormsg: string} - if action == "restart": - data["payload"]["action"] = "stop" - r = asyncio.run(agent.nats_cmd(data, timeout=32)) + # get agent service details + def get(self, request, agent_id, svcname): + agent = get_object_or_404(Agent, agent_id=agent_id) + data = {"func": "winsvcdetail", "payload": {"name": svcname}} + r = asyncio.run(agent.nats_cmd(data, timeout=10)) if r == "timeout": return notify_error("Unable to contact the agent") - elif not r["success"] and r["errormsg"]: - return notify_error(r["errormsg"]) - elif r["success"]: - data["payload"]["action"] = "start" + + return Response(r) + + # win service action + def post(self, request, agent_id, svcname): + agent = get_object_or_404(Agent, agent_id=agent_id) + action = request.data["sv_action"] + data = { + "func": "winsvcaction", + "payload": { + "name": svcname, + }, + } + # response struct from agent: {success: bool, errormsg: string} + if action == "restart": + data["payload"]["action"] = "stop" + r = asyncio.run(agent.nats_cmd(data, timeout=32)) + if r == "timeout": + return notify_error("Unable to contact the agent") + elif not r["success"] and r["errormsg"]: + return notify_error(r["errormsg"]) + elif r["success"]: + data["payload"]["action"] = "start" + r = asyncio.run(agent.nats_cmd(data, timeout=32)) + if r == "timeout": + return notify_error("Unable to contact the agent") + elif not r["success"] and r["errormsg"]: + return notify_error(r["errormsg"]) + elif r["success"]: + return Response("ok") + else: + data["payload"]["action"] = action r = asyncio.run(agent.nats_cmd(data, timeout=32)) if r == "timeout": return notify_error("Unable to contact the agent") @@ -58,9 +75,22 @@ def service_action(request): return notify_error(r["errormsg"]) elif r["success"]: return Response("ok") - else: - data["payload"]["action"] = action - r = asyncio.run(agent.nats_cmd(data, timeout=32)) + + return notify_error("Something went wrong") + + # edit win service + def put(self, request, agent_id, svcname): + agent = get_object_or_404(Agent, agent_id=agent_id) + data = { + "func": "editwinsvc", + "payload": { + "name": svcname, + "startType": request.data["startType"], + }, + } + + r = asyncio.run(agent.nats_cmd(data, timeout=10)) + # response struct from agent: {success: bool, errormsg: string} if r == "timeout": return notify_error("Unable to contact the agent") elif not r["success"] and r["errormsg"]: @@ -68,39 +98,4 @@ def service_action(request): elif r["success"]: return Response("ok") - return notify_error("Something went wrong") - - -@api_view() -def service_detail(request, pk, svcname): - agent = get_object_or_404(Agent, pk=pk) - data = {"func": "winsvcdetail", "payload": {"name": svcname}} - r = asyncio.run(agent.nats_cmd(data, timeout=10)) - if r == "timeout": - return notify_error("Unable to contact the agent") - - return Response(r) - - -@api_view(["POST"]) -@permission_classes([IsAuthenticated, ManageWinSvcsPerms]) -def edit_service(request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) - data = { - "func": "editwinsvc", - "payload": { - "name": request.data["sv_name"], - "startType": request.data["edit_action"], - }, - } - - r = asyncio.run(agent.nats_cmd(data, timeout=10)) - # response struct from agent: {success: bool, errormsg: string} - if r == "timeout": - return notify_error("Unable to contact the agent") - elif not r["success"] and r["errormsg"]: - return notify_error(r["errormsg"]) - elif r["success"]: - return Response("ok") - - return notify_error("Something went wrong") + return notify_error("Something went wrong") diff --git a/api/tacticalrmm/software/models.py b/api/tacticalrmm/software/models.py index ec7f546a87..e5e8d63233 100644 --- a/api/tacticalrmm/software/models.py +++ b/api/tacticalrmm/software/models.py @@ -1,5 +1,6 @@ from django.db import models +from tacticalrmm.models import PermissionManager from agents.models import Agent @@ -12,6 +13,9 @@ def __str__(self): class InstalledSoftware(models.Model): + objects = models.Manager() + permissions = PermissionManager() + agent = models.ForeignKey(Agent, on_delete=models.CASCADE) software = models.JSONField() diff --git a/api/tacticalrmm/software/permissions.py b/api/tacticalrmm/software/permissions.py index 21c8e6c771..74553b6745 100644 --- a/api/tacticalrmm/software/permissions.py +++ b/api/tacticalrmm/software/permissions.py @@ -1,11 +1,19 @@ from rest_framework import permissions -from tacticalrmm.permissions import _has_perm +from tacticalrmm.permissions import _has_perm, _has_perm_on_agent -class ManageSoftwarePerms(permissions.BasePermission): +class SoftwarePerms(permissions.BasePermission): def has_permission(self, r, view): if r.method == "GET": - return True + if "agent_id" in view.kwargs.keys(): + return _has_perm(r, "can_list_software") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) - return _has_perm(r, "can_manage_software") + return _has_perm(r, "can_list_software") + + else: + return _has_perm(r, "can_manage_software") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) diff --git a/api/tacticalrmm/software/tests.py b/api/tacticalrmm/software/tests.py index c069a368be..2c01bbb4e5 100644 --- a/api/tacticalrmm/software/tests.py +++ b/api/tacticalrmm/software/tests.py @@ -10,6 +10,8 @@ from .models import ChocoSoftware from .serializers import InstalledSoftwareSerializer +base_url = "/software" + class TestSoftwareViews(TacticalTestCase): def setUp(self): @@ -17,7 +19,7 @@ def setUp(self): self.setup_coresettings() def test_chocos_get(self): - url = "/software/chocos/" + url = f"{base_url}/chocos/" with open(os.path.join(settings.BASE_DIR, "software/chocos.json")) as f: chocos = json.load(f) @@ -29,13 +31,13 @@ def test_chocos_get(self): self.assertEqual(resp.status_code, 200) self.check_not_authenticated("get", url) - def test_chocos_installed(self): + def test_get_installed_software(self): # test a call where agent doesn't exist - resp = self.client.get("/software/installed/500/", format="json") + resp = self.client.get("/software/dytthgc/", format="json") self.assertEqual(resp.status_code, 404) agent = baker.make_recipe("agents.agent") - url = f"/software/installed/{agent.pk}/" + url = f"{base_url}/{agent.agent_id}/" # test without agent software resp = self.client.get(url, format="json") @@ -52,14 +54,26 @@ def test_chocos_installed(self): serializer = InstalledSoftwareSerializer(software) resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.data, serializer.data) # type: ignore + + # test checking all software (multiple agents) + serializer = InstalledSoftwareSerializer([software], many=True) + resp = self.client.get(f"{base_url}/", format="json") + self.assertEqual(resp.status_code, 200) self.assertEquals(resp.data, serializer.data) # type: ignore self.check_not_authenticated("get", url) @patch("agents.models.Agent.nats_cmd") - def test_install(self, nats_cmd): - url = "/software/install/" + def test_install_softare(self, nats_cmd): + # test agent doesn't exist + r = self.client.post(f"{base_url}/kjh34kj5hj45hj4/", format="json") + self.assertEqual(r.status_code, 404) + + # test old agent version old_agent = baker.make_recipe("agents.online_agent", version="1.4.7") + url = f"{base_url}/{old_agent.agent_id}/" + data = { "pk": old_agent.pk, "name": "duplicati", @@ -71,6 +85,7 @@ def test_install(self, nats_cmd): agent = baker.make_recipe( "agents.online_agent", version=settings.LATEST_AGENT_VER ) + url = f"{base_url}/{agent.agent_id}/" data = { "pk": agent.pk, "name": "duplicati", @@ -87,9 +102,9 @@ def test_install(self, nats_cmd): self.check_not_authenticated("post", url) @patch("agents.models.Agent.nats_cmd") - def test_refresh_installed(self, nats_cmd): - url = "/software/refresh/4827342/" - r = self.client.get(url, format="json") + def test_refresh_installed_software(self, nats_cmd): + url = f"{base_url}/76fytfuytff66565f65/" + r = self.client.put(url, format="json") self.assertEqual(r.status_code, 404) nats_cmd.return_value = "timeout" @@ -99,8 +114,8 @@ def test_refresh_installed(self, nats_cmd): agent=agent, software={}, ) - url = f"/software/refresh/{agent.pk}/" - r = self.client.get(url, format="json") + url = f"{base_url}/{agent.agent_id}/" + r = self.client.put(url, format="json") self.assertEqual(r.status_code, 400) with open( @@ -110,12 +125,98 @@ def test_refresh_installed(self, nats_cmd): nats_cmd.reset_mock() nats_cmd.return_value = sw - r = self.client.get(url, format="json") + r = self.client.put(url, format="json") self.assertEqual(r.status_code, 200) s = agent.installedsoftware_set.first() s.delete() - r = self.client.get(url, format="json") + r = self.client.put(url, format="json") self.assertEqual(r.status_code, 200) - self.check_not_authenticated("get", url) + self.check_not_authenticated("put", url) + + +class TestSoftwarePermissions(TacticalTestCase): + def setUp(self): + self.setup_coresettings() + self.client_setup() + + def test_list_software_permissions(self): + agent = baker.make_recipe("agents.agent") + unauthorized_agent = baker.make_recipe("agents.agent") + software = baker.make("software.InstalledSoftware", software={}, agent=agent) + unauthorized_software = baker.make( + "software.InstalledSoftware", software={}, agent=unauthorized_agent + ) + + # test super user access + self.check_authorized_superuser("get", f"{base_url}/") + self.check_authorized_superuser("get", f"{base_url}/{agent.agent_id}/") + + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + self.check_not_authorized("get", f"{base_url}/") + self.check_not_authorized("get", f"{base_url}/{agent.agent_id}/") + + # add list software role to user + user.role.can_list_software = True + user.role.save() + + r = self.check_authorized("get", f"{base_url}/") + self.assertEqual(len(r.data), 2) + self.check_authorized("get", f"{base_url}/{agent.agent_id}/") + + # test limiting to client + user.role.can_view_clients.set([software.agent.client]) + self.check_not_authorized("get", f"{base_url}/{unauthorized_agent.agent_id}/") + self.check_authorized("get", f"{base_url}/{agent.agent_id}/") + + # make sure queryset is limited too + r = self.client.get(f"{base_url}/") + self.assertEqual(len(r.data), 1) + + @patch("agents.models.Agent.nats_cmd") + def test_install_refresh_software_permissions(self, nats_cmd): + agent = baker.make_recipe("agents.agent") + unauthorized_agent = baker.make_recipe("agents.agent") + software = baker.make("software.InstalledSoftware", software={}, agent=agent) + unauthorized_software = baker.make( + "software.InstalledSoftware", software={}, agent=unauthorized_agent + ) + + for method in ["post", "put"]: + if method == "post": + nats_cmd.return_value = "ok" + else: + nats_cmd.return_value = [] + + # test superuser access + self.check_authorized_superuser(method, f"{base_url}/{agent.agent_id}/") + self.check_authorized_superuser( + method, f"{base_url}/{unauthorized_agent.agent_id}/" + ) + + # test user with no roles + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + self.check_not_authorized(method, f"{base_url}/{agent.agent_id}/") + self.check_not_authorized( + method, f"{base_url}/{unauthorized_agent.agent_id}/" + ) + + # add manage software role + user.role.can_manage_software = True + user.role.save() + + self.check_authorized(method, f"{base_url}/{agent.agent_id}/") + self.check_authorized(method, f"{base_url}/{unauthorized_agent.agent_id}/") + + # limit to specific site + user.role.can_view_sites.set([agent.site]) + + self.check_authorized(method, f"{base_url}/{agent.agent_id}/") + self.check_not_authorized( + method, f"{base_url}/{unauthorized_agent.agent_id}/" + ) diff --git a/api/tacticalrmm/software/urls.py b/api/tacticalrmm/software/urls.py index a29d009057..fc5ee27eaa 100644 --- a/api/tacticalrmm/software/urls.py +++ b/api/tacticalrmm/software/urls.py @@ -4,7 +4,6 @@ urlpatterns = [ path("chocos/", views.chocos), - path("install/", views.install), - path("installed//", views.get_installed), - path("refresh//", views.refresh_installed), + path("", views.GetSoftware.as_view()), + path("/", views.GetSoftware.as_view()), ] diff --git a/api/tacticalrmm/software/views.py b/api/tacticalrmm/software/views.py index 2b34cfee71..c144d75e1a 100644 --- a/api/tacticalrmm/software/views.py +++ b/api/tacticalrmm/software/views.py @@ -3,81 +3,86 @@ from django.shortcuts import get_object_or_404 from packaging import version as pyver -from rest_framework.decorators import api_view, permission_classes +from rest_framework.decorators import api_view from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.views import APIView from agents.models import Agent from logs.models import PendingAction from tacticalrmm.utils import filter_software, notify_error from .models import ChocoSoftware, InstalledSoftware -from .permissions import ManageSoftwarePerms +from .permissions import SoftwarePerms from .serializers import InstalledSoftwareSerializer -@api_view() +@api_view(["GET"]) def chocos(request): return Response(ChocoSoftware.objects.last().chocos) -@api_view(["POST"]) -@permission_classes([IsAuthenticated, ManageSoftwarePerms]) -def install(request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) - if pyver.parse(agent.version) < pyver.parse("1.4.8"): - return notify_error("Requires agent v1.4.8") - - name = request.data["name"] - - action = PendingAction.objects.create( - agent=agent, - action_type="chocoinstall", - details={"name": name, "output": None, "installed": False}, - ) - - nats_data = { - "func": "installwithchoco", - "choco_prog_name": name, - "pending_action_pk": action.pk, - } - - r = asyncio.run(agent.nats_cmd(nats_data, timeout=2)) - if r != "ok": - action.delete() - return notify_error("Unable to contact the agent") - - return Response( - f"{name} will be installed shortly on {agent.hostname}. Check the Pending Actions menu to see the status/output" - ) - - -@api_view() -def get_installed(request, pk): - agent = get_object_or_404(Agent, pk=pk) - try: - software = InstalledSoftware.objects.filter(agent=agent).get() - except Exception: - return Response([]) - else: - return Response(InstalledSoftwareSerializer(software).data) - - -@api_view() -def refresh_installed(request, pk): - agent = get_object_or_404(Agent, pk=pk) - - r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15)) - if r == "timeout" or r == "natsdown": - return notify_error("Unable to contact the agent") - - sw = filter_software(r) - - if not InstalledSoftware.objects.filter(agent=agent).exists(): - InstalledSoftware(agent=agent, software=sw).save() - else: - s = agent.installedsoftware_set.first() # type: ignore - s.software = sw - s.save(update_fields=["software"]) - - return Response("ok") +class GetSoftware(APIView): + permission_classes = [IsAuthenticated, SoftwarePerms] + + # get software list + def get(self, request, agent_id=None): + if agent_id: + agent = get_object_or_404(Agent, agent_id=agent_id) + + try: + software = InstalledSoftware.objects.filter(agent=agent).get() + return Response(InstalledSoftwareSerializer(software).data) + except Exception: + return Response([]) + else: + software = InstalledSoftware.permissions.filter_by_role(request.user) + return Response(InstalledSoftwareSerializer(software, many=True).data) + + # software install + def post(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + if pyver.parse(agent.version) < pyver.parse("1.4.8"): + return notify_error("Requires agent v1.4.8") + + name = request.data["name"] + + action = PendingAction.objects.create( + agent=agent, + action_type="chocoinstall", + details={"name": name, "output": None, "installed": False}, + ) + + nats_data = { + "func": "installwithchoco", + "choco_prog_name": name, + "pending_action_pk": action.pk, + } + + r = asyncio.run(agent.nats_cmd(nats_data, timeout=2)) + if r != "ok": + action.delete() + return notify_error("Unable to contact the agent") + + return Response( + f"{name} will be installed shortly on {agent.hostname}. Check the Pending Actions menu to see the status/output" + ) + + # refresh software list + def put(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + + r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15)) + if r == "timeout" or r == "natsdown": + return notify_error("Unable to contact the agent") + + sw = filter_software(r) + + if not InstalledSoftware.objects.filter(agent=agent).exists(): + InstalledSoftware(agent=agent, software=sw).save() + else: + s = agent.installedsoftware_set.first() # type: ignore + s.software = sw + s.save(update_fields=["software"]) + + return Response("ok") diff --git a/api/tacticalrmm/tacticalrmm/models.py b/api/tacticalrmm/tacticalrmm/models.py new file mode 100644 index 0000000000..83333cbb53 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/models.py @@ -0,0 +1,62 @@ +from django.db import models + + +class PermissionManager(models.Manager): + + # filters queryset based on permissions. Works different for Agent, Client, and Site + def filter_by_role(self, user): + + role = user.role + can_view_clients = ( + role.can_view_clients.all().values_list("pk", flat=True) if role else None + ) + can_view_sites = ( + role.can_view_sites.all().values_list("pk", flat=True) if role else None + ) + queryset = super(PermissionManager, self).get_queryset() + clients_queryset = models.Q() + sites_queryset = models.Q() + policy_queryset = models.Q() + model_name = self.model._meta.label.split(".")[1] + + # returns normal queryset if user is superuser + if user.is_superuser or (role and getattr(role, "is_superuser")): + return queryset + + # checks which sites and clients the user has access to and filters agents + if model_name == "Agent": + if can_view_clients: + clients_queryset = models.Q(site__client_id__in=can_view_clients) + + if can_view_sites: + sites_queryset = models.Q(site_id__in=can_view_sites) + + queryset = queryset.filter(clients_queryset | sites_queryset) + + # checks which sites and clients the user has access to and filters clients and sites + elif model_name == "Client" and can_view_clients: + queryset = queryset.filter(pk__in=can_view_clients) + elif model_name == "Site" and can_view_sites: + queryset = queryset.filter(pk__in=can_view_sites) + + # anything else just checks the agent_id field and if it has it will filter matched agents from the queryset + else: + if not hasattr(self.model, "agent"): + return queryset + + # if model that is being filtered is a Check or Automated task we need to allow checks/tasks that are associated with policies + if model_name in ["Check", "AutomatedTask"] and ( + can_view_clients or can_view_sites + ): + policy_queryset = models.Q(agent=None) # dont filter if agent is None + + if can_view_clients: + clients_queryset = models.Q(agent__site__client_id__in=can_view_clients) + if can_view_sites: + sites_queryset = models.Q(agent__site_id__in=can_view_sites) + + queryset = queryset.filter( + clients_queryset | sites_queryset | policy_queryset + ) + + return queryset diff --git a/api/tacticalrmm/tacticalrmm/permissions.py b/api/tacticalrmm/tacticalrmm/permissions.py index 3eb4fd710d..cfaf3291d8 100644 --- a/api/tacticalrmm/tacticalrmm/permissions.py +++ b/api/tacticalrmm/tacticalrmm/permissions.py @@ -1,5 +1,4 @@ -from rest_framework import permissions -from tacticalrmm.auth import APIAuthentication +from django.shortcuts import get_object_or_404 def _has_perm(request, perm): @@ -9,3 +8,26 @@ def _has_perm(request, perm): return True return request.user.role and getattr(request.user.role, perm) + + +def _has_perm_on_agent(user, agent_id): + from agents.models import Agent + + role = user.role + if user.is_superuser or (role and getattr(role, "is_superuser")): + return True + + agent = get_object_or_404(Agent, agent_id=agent_id) + can_view_clients = role.can_view_clients.all() if role else None + can_view_sites = role.can_view_sites.all() if role else None + + if not can_view_clients and not can_view_sites: + return True + + if can_view_clients and agent.client in can_view_clients: + return True + + if can_view_sites and agent.site in can_view_sites: + return True + + return False diff --git a/api/tacticalrmm/tacticalrmm/test.py b/api/tacticalrmm/tacticalrmm/test.py index 83cf11d02c..6cbdf69e6e 100644 --- a/api/tacticalrmm/tacticalrmm/test.py +++ b/api/tacticalrmm/tacticalrmm/test.py @@ -1,6 +1,6 @@ import uuid from django.test import TestCase, override_settings -from model_bakery import baker +from model_bakery import baker, seq from rest_framework.authtoken.models import Token from rest_framework.test import APIClient @@ -46,14 +46,8 @@ def setup_coresettings(self): def check_not_authenticated(self, method, url): self.client.logout() - switch = { - "get": self.client.get(url), - "post": self.client.post(url), - "put": self.client.put(url), - "patch": self.client.patch(url), - "delete": self.client.delete(url), - } - r = switch.get(method) + + r = getattr(self.client, method)(url) self.assertEqual(r.status_code, 401) def create_checks(self, policy=None, agent=None, script=None): @@ -81,3 +75,49 @@ def create_checks(self, policy=None, agent=None, script=None): baker.make_recipe(recipe, policy=policy, agent=agent, script=script) ) return checks + + def check_not_authorized(self, method: str, url: str, data: dict = {}): + try: + r = getattr(self.client, method)(url, data) + self.assertEqual(r.status_code, 403) + except KeyError: + pass + + def check_authorized(self, method: str, url: str, data: dict = {}): + try: + r = getattr(self.client, method)(url, data) + self.assertNotEqual(r.status_code, 403) + return r + except KeyError: + pass + + def check_authorized_superuser(self, method: str, url: str, data: dict = {}): + + try: + # create django superuser and test authorized + user = baker.make("accounts.User", is_active=True, is_superuser=True) + self.client.force_authenticate(user=user) + r = getattr(self.client, method)(url, data) + + self.assertNotEqual(r.status_code, 403) + + # test role superuser + user = self.create_user_with_roles(["is_superuser"]) + self.client.force_authenticate(user=user) + r = getattr(self.client, method)(url, data) + + self.assertNotEqual(r.status_code, 403) + self.client.logout() + return r + + # bypasses any data issues in the view since we just want to see if user is authorized + except KeyError: + pass + + def create_user_with_roles(self, roles: list[str]) -> User: + new_role = baker.make("accounts.Role") + for role in roles: + setattr(new_role, role, True) + + new_role.save() + return baker.make("accounts.User", role=new_role, is_active=True) diff --git a/api/tacticalrmm/tacticalrmm/urls.py b/api/tacticalrmm/tacticalrmm/urls.py index 703a612ba1..3cf46d282b 100644 --- a/api/tacticalrmm/tacticalrmm/urls.py +++ b/api/tacticalrmm/tacticalrmm/urls.py @@ -1,10 +1,23 @@ from django.conf import settings -from django.urls import include, path +from django.urls import include, path, register_converter from knox import views as knox_views from accounts.views import CheckCreds, LoginView from core.consumers import DashInfo + +class AgentIDConverter: + regex = "[\\w-]{20}[\\w-]+" + + def to_python(self, value): + return value + + def to_url(self, value): + return value + + +register_converter(AgentIDConverter, "agent") + urlpatterns = [ path("checkcreds/", CheckCreds.as_view()), path("login/", LoginView.as_view()), diff --git a/api/tacticalrmm/winupdate/migrations/0011_auto_20210917_1954.py b/api/tacticalrmm/winupdate/migrations/0011_auto_20210917_1954.py new file mode 100644 index 0000000000..a32ac81f51 --- /dev/null +++ b/api/tacticalrmm/winupdate/migrations/0011_auto_20210917_1954.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.6 on 2021-09-17 19:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("winupdate", "0010_auto_20210119_0052"), + ] + + operations = [ + migrations.AlterField( + model_name="winupdatepolicy", + name="created_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="winupdatepolicy", + name="modified_by", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/winupdate/permissions.py b/api/tacticalrmm/winupdate/permissions.py index b0d4d12576..a99b35d6cb 100644 --- a/api/tacticalrmm/winupdate/permissions.py +++ b/api/tacticalrmm/winupdate/permissions.py @@ -1,8 +1,13 @@ from rest_framework import permissions -from tacticalrmm.permissions import _has_perm +from tacticalrmm.permissions import _has_perm, _has_perm_on_agent -class ManageWinUpdatePerms(permissions.BasePermission): +class AgentWinUpdatePerms(permissions.BasePermission): def has_permission(self, r, view): - return _has_perm(r, "can_manage_winupdates") + if "agent_id" in view.kwargs.keys(): + return _has_perm(r, "can_manage_winupdates") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) + else: + return _has_perm(r, "can_manage_winupdates") diff --git a/api/tacticalrmm/winupdate/serializers.py b/api/tacticalrmm/winupdate/serializers.py index 89ef20f891..ae0ef638f9 100644 --- a/api/tacticalrmm/winupdate/serializers.py +++ b/api/tacticalrmm/winupdate/serializers.py @@ -1,28 +1,17 @@ import pytz from rest_framework import serializers -from agents.models import Agent - from .models import WinUpdate, WinUpdatePolicy class WinUpdateSerializer(serializers.ModelSerializer): - class Meta: - model = WinUpdate - fields = "__all__" - - -class WinUpdateSerializerTZAware(serializers.ModelSerializer): date_installed = serializers.SerializerMethodField() def get_date_installed(self, obj): if obj.date_installed is not None: - if obj.agent.time_zone is not None: - agent_tz = pytz.timezone(obj.agent.time_zone) - else: - agent_tz = self.context["default_tz"] - - return obj.date_installed.astimezone(agent_tz).strftime("%m %d %Y %H:%M") + return obj.date_installed.astimezone(self.context["default_tz"]).strftime( + "%m %d %Y %H:%M" + ) return None class Meta: @@ -30,18 +19,6 @@ class Meta: fields = "__all__" -class UpdateSerializer(serializers.ModelSerializer): - winupdates = WinUpdateSerializerTZAware(many=True, read_only=True) - - class Meta: - model = Agent - fields = ( - "pk", - "hostname", - "winupdates", - ) - - class WinUpdatePolicySerializer(serializers.ModelSerializer): class Meta: model = WinUpdatePolicy diff --git a/api/tacticalrmm/winupdate/tests.py b/api/tacticalrmm/winupdate/tests.py index 10abcefe53..c708f9f4b2 100644 --- a/api/tacticalrmm/winupdate/tests.py +++ b/api/tacticalrmm/winupdate/tests.py @@ -6,7 +6,9 @@ from tacticalrmm.test import TacticalTestCase from .models import WinUpdate -from .serializers import UpdateSerializer +from .serializers import WinUpdateSerializer + +base_url = "/winupdate" class TestWinUpdateViews(TacticalTestCase): @@ -17,115 +19,137 @@ def setUp(self): @patch("agents.models.Agent.nats_cmd") def test_run_update_scan(self, nats_cmd): agent = baker.make_recipe("agents.agent") - url = f"/winupdate/{agent.pk}/runupdatescan/" - r = self.client.get(url) + url = f"{base_url}/{agent.agent_id}/scan/" + r = self.client.post(url) self.assertEqual(r.status_code, 200) nats_cmd.assert_called_with({"func": "getwinupdates"}, wait=False) - self.check_not_authenticated("get", url) + self.check_not_authenticated("post", url) @patch("agents.models.Agent.nats_cmd") def test_install_updates(self, nats_cmd): agent = baker.make_recipe("agents.agent") baker.make("winupdate.WinUpdate", agent=agent, _quantity=4) baker.make("winupdate.WinUpdatePolicy", agent=agent) - url = f"/winupdate/{agent.pk}/installnow/" - r = self.client.get(url) + url = f"{base_url}/{agent.agent_id}/install/" + r = self.client.post(url) self.assertEqual(r.status_code, 200) nats_cmd.assert_called_once() - self.check_not_authenticated("get", url) + self.check_not_authenticated("post", url) def test_get_winupdates(self): agent = baker.make_recipe("agents.agent") baker.make("winupdate.WinUpdate", agent=agent, _quantity=4) # test a call where agent doesn't exist - resp = self.client.get("/winupdate/500/getwinupdates/", format="json") + resp = self.client.get(f"{base_url}/234kj34lk/", format="json") self.assertEqual(resp.status_code, 404) - url = f"/winupdate/{agent.pk}/getwinupdates/" + url = f"{base_url}/{agent.agent_id}/" resp = self.client.get(url, format="json") - serializer = UpdateSerializer(agent) + updates = WinUpdate.objects.filter(agent=agent).order_by("-id", "installed") + serializer = WinUpdateSerializer(updates, many=True) self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data["winupdates"]), 4) # type: ignore + self.assertEqual(len(resp.data), 4) # type: ignore self.assertEqual(resp.data, serializer.data) # type: ignore self.check_not_authenticated("get", url) - """ @patch("winupdate.tasks.check_for_updates_task.apply_async") - def test_run_update_scan(self, mock_task): + def test_edit_winupdate(self): + agent = baker.make_recipe("agents.agent") + winupdate = baker.make("winupdate.WinUpdate", agent=agent) + url = f"{base_url}/{winupdate.pk}/" - # test a call where agent doesn't exist - resp = self.client.get("/winupdate/500/runupdatescan/", format="json") + data = {"policy": "inherit"} + # test a call where winupdate doesn't exist + resp = self.client.put(f"{base_url}/500/", data, format="json") self.assertEqual(resp.status_code, 404) - agent = baker.make_recipe("agents.agent") - url = f"/winupdate/{agent.pk}/runupdatescan/" - - resp = self.client.get(url, format="json") + resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - mock_task.assert_called_with( - queue="wupdate", - kwargs={"pk": agent.pk, "wait": False, "auto_approve": True}, - ) - self.check_not_authenticated("get", url) """ + self.check_not_authenticated("put", url) - """ @patch("agents.models.Agent.salt_api_cmd") - def test_install_updates(self, mock_cmd): - # test a call where agent doesn't exist - resp = self.client.get("/winupdate/500/installnow/", format="json") - self.assertEqual(resp.status_code, 404) +class TestWinUpdatePermissions(TacticalTestCase): + def setUp(self): + self.setup_coresettings() + self.client_setup() + + @patch("agents.models.Agent.nats_cmd", return_value="ok") + def test_get_scan_install_permissions(self, nats_cmd): agent = baker.make_recipe("agents.agent") - url = f"/winupdate/{agent.pk}/installnow/" + baker.make("winupdate.WinUpdatePolicy", agent=agent) + unauthorized_agent = baker.make_recipe("agents.agent") + baker.make("winupdate.WinUpdatePolicy", agent=unauthorized_agent) + baker.make("winupdate.WinUpdate", agent=agent) + baker.make("winupdate.WinUpdate", agent=unauthorized_agent) - # test agent command timeout - mock_cmd.return_value = "timeout" - resp = self.client.get(url, format="json") - self.assertEqual(resp.status_code, 400) + test_data = [ + {"url": f"{base_url}/{agent.agent_id}/", "method": "get"}, + {"url": f"{base_url}/{agent.agent_id}/scan/", "method": "post"}, + {"url": f"{base_url}/{agent.agent_id}/install/", "method": "post"}, + ] - # test agent command error - mock_cmd.return_value = "error" - resp = self.client.get(url, format="json") - self.assertEqual(resp.status_code, 400) + for data in test_data: + # test superuser + self.check_authorized_superuser(data["method"], data["url"]) - # test agent command running - mock_cmd.return_value = "running" - resp = self.client.get(url, format="json") - self.assertEqual(resp.status_code, 400) + # test user with no roles + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) - # can't get this to work right - # test agent command no pid field - mock_cmd.return_value = {} - resp = self.client.get(url, format="json") - self.assertEqual(resp.status_code, 400) + self.check_not_authorized(data["method"], data["url"]) - # test agent command success - mock_cmd.return_value = {"pid": 3316} - resp = self.client.get(url, format="json") - self.assertEqual(resp.status_code, 200) + # test with correct role + user.role.can_manage_winupdates = True + user.role.save() + + self.check_authorized(data["method"], data["url"]) - self.check_not_authenticated("get", url) """ + # test limiting user to site + user.role.can_view_sites.set([agent.site]) + self.check_authorized(data["method"], data["url"]) - def test_edit_policy(self): - url = "/winupdate/editpolicy/" + user.role.can_view_sites.set([unauthorized_agent.site]) + self.check_not_authorized(data["method"], data["url"]) + + self.client.logout() + + def test_edit_winupdate_permissions(self): agent = baker.make_recipe("agents.agent") - winupdate = baker.make("winupdate.WinUpdate", agent=agent) + baker.make("winupdate.WinUpdatePolicy", agent=agent) + update = baker.make("winupdate.WinUpdate", agent=agent) - invalid_data = {"pk": 500, "policy": "inherit"} - # test a call where winupdate doesn't exist - resp = self.client.patch(url, invalid_data, format="json") - self.assertEqual(resp.status_code, 404) + unauthorized_agent = baker.make_recipe("agents.agent") + baker.make("winupdate.WinUpdatePolicy", agent=unauthorized_agent) + baker.make("winupdate.WinUpdate", agent=unauthorized_agent) - data = {"pk": winupdate.pk, "policy": "inherit"} # type: ignore + url = f"{base_url}/{update.pk}/" - resp = self.client.patch(url, data, format="json") - self.assertEqual(resp.status_code, 200) + # test superuser + self.check_authorized_superuser("put", url) + + # test user with no roles + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + self.check_not_authorized("put", url) - self.check_not_authenticated("patch", url) + # test with correct role + user.role.can_manage_winupdates = True + user.role.save() + + self.check_authorized("put", url) + + # test limiting user to site + user.role.can_view_sites.set([agent.site]) + self.check_authorized("put", url) + + user.role.can_view_sites.set([unauthorized_agent.site]) + self.check_not_authorized("put", url) class WinupdateTasks(TacticalTestCase): @@ -169,65 +193,3 @@ def test_auto_approve_task(self, mock_sleep, nats_cmd): winupdates = WinUpdate.objects.all() for update in winupdates: self.assertEqual(update.action, "approve") - - """ @patch("agents.models.Agent.salt_api_async") - def test_check_agent_update_daily_schedule(self, agent_salt_cmd): - from .tasks import check_agent_update_schedule_task - - # Setup data - # create an online agent with auto approval turned off - agent = baker.make_recipe("agents.online_agent") - baker.make("winupdate.WinUpdatePolicy", agent=agent) - - # create approved winupdates - baker.make_recipe( - "winupdate.approved_winupdate", - agent=cycle( - [self.online_agents[0], self.online_agents[1], self.offline_agent] - ), - _quantity=20, - ) - - # create daily patch policy schedules for the agents - winupdate_policy = baker.make_recipe( - "winupdate.winupdate_approve", - agent=cycle( - [self.online_agents[0], self.online_agents[1], self.offline_agent] - ), - _quantity=3, - ) - - check_agent_update_schedule_task() - agent_salt_cmd.assert_called_with(func="win_agent.install_updates") - self.assertEquals(agent_salt_cmd.call_count, 2) """ - - """ @patch("agents.models.Agent.salt_api_async") - def test_check_agent_update_monthly_schedule(self, agent_salt_cmd): - from .tasks import check_agent_update_schedule_task - - # Setup data - # create an online agent with auto approval turned off - agent = baker.make_recipe("agents.online_agent") - baker.make("winupdate.WinUpdatePolicy", agent=agent) - - # create approved winupdates - baker.make_recipe( - "winupdate.approved_winupdate", - agent=cycle( - [self.online_agents[0], self.online_agents[1], self.offline_agent] - ), - _quantity=20, - ) - - # create monthly patch policy schedules for the agents - winupdate_policy = baker.make_recipe( - "winupdate.winupdate_approve_monthly", - agent=cycle( - [self.online_agents[0], self.online_agents[1], self.offline_agent] - ), - _quantity=3, - ) - - check_agent_update_schedule_task() - agent_salt_cmd.assert_called_with(func="win_agent.install_updates") - self.assertEquals(agent_salt_cmd.call_count, 2) """ diff --git a/api/tacticalrmm/winupdate/urls.py b/api/tacticalrmm/winupdate/urls.py index b7c3c8f269..d0b97dc6e3 100644 --- a/api/tacticalrmm/winupdate/urls.py +++ b/api/tacticalrmm/winupdate/urls.py @@ -3,8 +3,8 @@ from . import views urlpatterns = [ - path("/getwinupdates/", views.get_win_updates), - path("/runupdatescan/", views.run_update_scan), - path("editpolicy/", views.edit_policy), - path("/installnow/", views.install_updates), + path("/", views.GetWindowsUpdates.as_view()), + path("/scan/", views.ScanWindowsUpdates.as_view()), + path("/install/", views.InstallWindowsUpdates.as_view()), + path("/", views.EditWindowsUpdates.as_view()), ] diff --git a/api/tacticalrmm/winupdate/views.py b/api/tacticalrmm/winupdate/views.py index 426f211ec5..be7e7bd983 100644 --- a/api/tacticalrmm/winupdate/views.py +++ b/api/tacticalrmm/winupdate/views.py @@ -1,53 +1,71 @@ import asyncio from django.shortcuts import get_object_or_404 -from rest_framework.decorators import api_view, permission_classes +from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.exceptions import PermissionDenied from agents.models import Agent from tacticalrmm.utils import get_default_timezone +from tacticalrmm.permissions import _has_perm_on_agent from .models import WinUpdate -from .permissions import ManageWinUpdatePerms -from .serializers import UpdateSerializer - - -@api_view() -def get_win_updates(request, pk): - agent = get_object_or_404(Agent, pk=pk) - ctx = {"default_tz": get_default_timezone()} - serializer = UpdateSerializer(agent, context=ctx) - return Response(serializer.data) - - -@api_view() -@permission_classes([IsAuthenticated, ManageWinUpdatePerms]) -def run_update_scan(request, pk): - agent = get_object_or_404(Agent, pk=pk) - agent.delete_superseded_updates() - asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) - return Response("ok") - - -@api_view() -@permission_classes([IsAuthenticated, ManageWinUpdatePerms]) -def install_updates(request, pk): - agent = get_object_or_404(Agent, pk=pk) - agent.delete_superseded_updates() - agent.approve_updates() - nats_data = { - "func": "installwinupdates", - "guids": agent.get_approved_update_guids(), - } - asyncio.run(agent.nats_cmd(nats_data, wait=False)) - return Response(f"Patches will now be installed on {agent.hostname}") - - -@api_view(["PATCH"]) -@permission_classes([IsAuthenticated, ManageWinUpdatePerms]) -def edit_policy(request): - patch = get_object_or_404(WinUpdate, pk=request.data["pk"]) - patch.action = request.data["policy"] - patch.save(update_fields=["action"]) - return Response("ok") +from .permissions import AgentWinUpdatePerms +from .serializers import WinUpdateSerializer + + +class GetWindowsUpdates(APIView): + permission_classes = [IsAuthenticated, AgentWinUpdatePerms] + + # list windows updates on agent + def get(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + updates = WinUpdate.objects.filter(agent=agent).order_by("-id", "installed") + ctx = {"default_tz": get_default_timezone()} + serializer = WinUpdateSerializer(updates, many=True, context=ctx) + return Response(serializer.data) + + +class ScanWindowsUpdates(APIView): + permission_classes = [IsAuthenticated, AgentWinUpdatePerms] + # scan for windows updates on agent + def post(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + agent.delete_superseded_updates() + asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) + return Response(f"A Windows update scan will performed on {agent.hostname}") + + +class InstallWindowsUpdates(APIView): + permission_classes = [IsAuthenticated, AgentWinUpdatePerms] + + # install approved windows updates on agent + def post(self, request, agent_id): + agent = get_object_or_404(Agent, agent_id=agent_id) + agent.delete_superseded_updates() + agent.approve_updates() + nats_data = { + "func": "installwinupdates", + "guids": agent.get_approved_update_guids(), + } + asyncio.run(agent.nats_cmd(nats_data, wait=False)) + return Response(f"Approved patches will now be installed on {agent.hostname}") + + +class EditWindowsUpdates(APIView): + permission_classes = [IsAuthenticated, AgentWinUpdatePerms] + + # change approval status of update + def put(self, request, pk): + update = get_object_or_404(WinUpdate, pk=pk) + + if not _has_perm_on_agent(request.user, update.agent.agent_id): + raise PermissionDenied() + + serializer = WinUpdateSerializer( + instance=update, data=request.data, partial=True + ) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(f"Windows update {update.kb} was changed to {update.action}") diff --git a/web/src/App.vue b/web/src/App.vue index 2087333d79..9cf7df4778 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -9,6 +9,9 @@ export default { \ No newline at end of file diff --git a/web/src/api/accounts.js b/web/src/api/accounts.js index 1dc01fc358..3ca976a660 100644 --- a/web/src/api/accounts.js +++ b/web/src/api/accounts.js @@ -7,16 +7,38 @@ export async function fetchUsers(params = {}) { try { const { data } = await axios.get(`${baseUrl}/users/`, { params: params }) return data - } catch (e) { } + } catch (e) { console.error(e) } } +// role api function +export async function fetchRoles(params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/roles/`, { params: params }) + return data + } catch (e) { console.error(e) } +} + +export async function removeRole(id) { + const { data } = await axios.delete(`${baseUrl}/roles/${id}/`) + return data +} + +export async function saveRole(payload) { + const { data } = await axios.post(`${baseUrl}/roles/`, payload) + return data +} + +export async function editRole(id, payload) { + const { data } = await axios.put(`${baseUrl}/roles/${id}/`, payload) + return data +} // api key api functions export async function fetchAPIKeys(params = {}) { try { const { data } = await axios.get(`${baseUrl}/apikeys/`, { params: params }) return data - } catch (e) { } + } catch (e) { console.error(e) } } export async function saveAPIKey(payload) { diff --git a/web/src/api/agents.js b/web/src/api/agents.js index 4069ceb36c..b161b5f061 100644 --- a/web/src/api/agents.js +++ b/web/src/api/agents.js @@ -2,30 +2,115 @@ import axios from "axios" const baseUrl = "/agents" -export async function fetchAgents() { +export async function fetchAgents(params = {}) { try { - const { data } = await axios.get(`${baseUrl}/listagentsnodetail/`) + const { data } = await axios.get(`${baseUrl}/`, { params: params }) return data - } catch (e) { } + } catch (e) { + console.error(e) + } } -export async function fetchAgentHistory(pk) { +export async function fetchAgent(agent_id, params = {}) { try { - const { data } = await axios.get(`${baseUrl}/history/${pk}/`) + const { data } = await axios.get(`${baseUrl}/${agent_id}/`, { params: params }) return data - } catch (e) { } + } catch (e) { console.error(e) } } -export async function runScript(payload) { +export async function fetchAgentHistory(agent_id, params = {}) { try { - const { data } = await axios.post(`${baseUrl}/runscript/`, payload) + const { data } = await axios.get(`${baseUrl}/${agent_id}/history/`, { params: params }) return data - } catch (e) { } + } catch (e) { console.error(e) } +} + +export async function fetchAgentChecks(agent_id, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/${agent_id}/checks/`, { params: params }) + return data + } catch (e) { console.error(e) } +} + +export async function fetchAgentTasks(agent_id, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/${agent_id}/tasks/`, { params: params }) + return data + } catch (e) { console.error(e) } +} + + +export async function refreshAgentWMI(agent_id) { + const { data } = await axios.post(`${baseUrl}/${agent_id}/wmi/`) + return data +} + +export async function runScript(agent_id, payload) { + const { data } = await axios.post(`${baseUrl}/${agent_id}/runscript/`, payload) + return data } export async function runBulkAction(payload) { + const { data } = await axios.post(`${baseUrl}/bulk/`, payload) + return data +} - const { data } = await axios.post("/agents/bulk/", payload) +export async function fetchAgentProcesses(agent_id, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/${agent_id}/processes/`, { params: params }) + return data + } catch (e) { + console.error(e) + } +} + +export async function killAgentProcess(agent_id, pid, params = {}) { + const { data } = await axios.delete(`${baseUrl}/${agent_id}/processes/${pid}/`, { params: params }) return data +} -} \ No newline at end of file +export async function fetchAgentEventLog(agent_id, logType, days, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/${agent_id}/eventlog/${logType}/${days}/`, { params: params }) + return data + } catch (e) { + console.error(e) + } +} + +export async function fetchAgentMeshCentralURLs(agent_id, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/${agent_id}/meshcentral/`, { params: params }) + return data + } catch (e) { + console.error(e) + } +} + +export async function sendAgentRecoverMesh(agent_id, params = {}) { + const { data } = await axios.post(`${baseUrl}/${agent_id}/meshcentral/recover/`, { params: params }) + return data +} + +// agent notes +export async function fetchAgentNotes(agent_id, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/${agent_id}/notes/`, { params: params }) + return data + } catch (e) { console.error(e) } +} + +export async function saveAgentNote(payload) { + const { data } = await axios.post(`${baseUrl}/notes/`, payload) + return data +} + +export async function editAgentNote(pk, payload) { + const { data } = await axios.put(`${baseUrl}/notes/${pk}/`, payload) + return data +} + +export async function removeAgentNote(pk) { + const { data } = await axios.delete(`${baseUrl}/notes/${pk}/`) + return data +} diff --git a/web/src/api/checks.js b/web/src/api/checks.js new file mode 100644 index 0000000000..ec5436a8ab --- /dev/null +++ b/web/src/api/checks.js @@ -0,0 +1,32 @@ +import axios from "axios" + +const baseUrl = "/checks" + +export async function fetchChecks(params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/`, { params: params }) + return data + } catch (e) { + console.error(e) + } +} + +export async function saveCheck(payload) { + const { data } = await axios.post(`${baseUrl}/`, payload) + return data +} + +export async function updateCheck(id, payload) { + const { data } = await axios.put(`${baseUrl}/${id}/`, payload) + return data +} + +export async function removeCheck(id) { + const { data } = await axios.delete(`${baseUrl}/${id}/`) + return data +} + +export async function resetCheck(id) { + const { data } = await axios.post(`${baseUrl}/${id}/reset/`) + return data +} \ No newline at end of file diff --git a/web/src/api/clients.js b/web/src/api/clients.js index a6a8ff294b..04fc59cea3 100644 --- a/web/src/api/clients.js +++ b/web/src/api/clients.js @@ -6,12 +6,12 @@ export async function fetchClients() { try { const { data } = await axios.get(`${baseUrl}/clients/`) return data - } catch (e) { } + } catch (e) { console.error(e) } } export async function fetchSites() { try { const { data } = await axios.get(`${baseUrl}/sites/`) return data - } catch (e) { } + } catch (e) { console.error(e) } } \ No newline at end of file diff --git a/web/src/api/core.js b/web/src/api/core.js index 8bb6a56dfa..6e61e56976 100644 --- a/web/src/api/core.js +++ b/web/src/api/core.js @@ -2,7 +2,7 @@ import axios from "axios" const baseUrl = "/core" -export async function fetchCustomFields(params) { +export async function fetchCustomFields(params = {}) { try { const { data } = await axios.get(`${baseUrl}/customfields/`, { params: params }) return data @@ -12,4 +12,9 @@ export async function fetchCustomFields(params) { export async function uploadMeshAgent(payload) { const { data } = await axios.put(`${baseUrl}/uploadmesh/`, payload) return data +} + +export async function fetchDashboardInfo(params = {}) { + const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params }) + return data } \ No newline at end of file diff --git a/web/src/api/scripts.js b/web/src/api/scripts.js index 192bdb63ec..62b5b700eb 100644 --- a/web/src/api/scripts.js +++ b/web/src/api/scripts.js @@ -10,9 +10,9 @@ export async function fetchScripts(params = {}) { } catch (e) { } } -export async function testScript(payload) { +export async function testScript(agent_id, payload) { try { - const { data } = await axios.post(`${baseUrl}/testscript/`, payload) + const { data } = await axios.post(`${baseUrl}/${agent_id}/test/`, payload) return data } catch (e) { } } @@ -34,7 +34,7 @@ export async function removeScript(id) { export async function downloadScript(id, params = {}) { try { - const { data } = await axios.get(`${baseUrl}/download/${id}/`, { params: params }) + const { data } = await axios.get(`${baseUrl}/${id}/download/`, { params: params }) return data } catch (e) { } } diff --git a/web/src/api/services.js b/web/src/api/services.js new file mode 100644 index 0000000000..ddd9a045c0 --- /dev/null +++ b/web/src/api/services.js @@ -0,0 +1,31 @@ +import axios from "axios" + +const baseUrl = "/services" + +export async function getAgentServices(agent_id, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/${agent_id}/`, { params: params }) + return data + } catch (e) { + console.error(e) + } +} + +export async function getAgentServiceDetails(agent_id, svcname, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/${agent_id}/${svcname}/`, { params: params }) + return data + } catch (e) { + console.error(e) + } +} + +export async function editAgentServiceStartType(agent_id, svcname, payload) { + const { data } = await axios.put(`${baseUrl}/${agent_id}/${svcname}/`, payload) + return data +} + +export async function sendAgentServiceAction(agent_id, svcname, payload) { + const { data } = await axios.post(`${baseUrl}/${agent_id}/${svcname}/`, payload) + return data +} diff --git a/web/src/api/software.js b/web/src/api/software.js new file mode 100644 index 0000000000..22c525e012 --- /dev/null +++ b/web/src/api/software.js @@ -0,0 +1,35 @@ +import axios from "axios" + +const baseUrl = "/software" + +export async function fetchChocosSoftware(params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/chocos/`, { params: params }) + return data + } catch (e) { + console.error(e) + } +} + +export async function fetchAgentSoftware(agent_id, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/${agent_id}/`, { params: params }) + return data.software + } catch (e) { + console.error(e) + } +} + +export async function installAgentSoftware(agent_id, payload) { + const { data } = await axios.post(`${baseUrl}/${agent_id}/`, payload) + return data +} + +export async function refreshAgentSoftware(agent_id) { + try { + const { data } = await axios.put(`${baseUrl}/${agent_id}/`) + return data + } catch (e) { + console.error(e) + } +} \ No newline at end of file diff --git a/web/src/api/tasks.js b/web/src/api/tasks.js new file mode 100644 index 0000000000..5d7eb339c8 --- /dev/null +++ b/web/src/api/tasks.js @@ -0,0 +1,32 @@ +import axios from "axios" + +const baseUrl = "/tasks" + +export async function fetchTasks(params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/`, { params: params }) + return data + } catch (e) { + console.error(e) + } +} + +export async function saveTasks(payload) { + const { data } = await axios.post(`${baseUrl}/`, payload) + return data +} + +export async function updateTask(id, payload) { + const { data } = await axios.put(`${baseUrl}/${id}/`, payload) + return data +} + +export async function removeTask(id) { + const { data } = await axios.delete(`${baseUrl}/${id}/`) + return data +} + +export async function runTask(id) { + const { data } = await axios.post(`${baseUrl}/${id}/run/`) + return data +} \ No newline at end of file diff --git a/web/src/api/winupdates.js b/web/src/api/winupdates.js new file mode 100644 index 0000000000..50e9734fd1 --- /dev/null +++ b/web/src/api/winupdates.js @@ -0,0 +1,22 @@ +import axios from "axios" + +const baseUrl = "/winupdate" + +// win updates api functions +export async function fetchAgentUpdates(agent_id, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/${agent_id}/`, { params: params }) + return data + } catch (e) { console.error(e) } +} + +export async function runAgentUpdateScan(agent_id) { + const { data } = await axios.post(`${baseUrl}/${agent_id}/scan/`) + return data + +} + +export async function editAgentUpdate(id, payload) { + const { data } = await axios.put(`${baseUrl}/${id}/`, payload) + return data +} diff --git a/web/src/boot/axios.js b/web/src/boot/axios.js index 6cf263fc67..ee1e4c9eef 100644 --- a/web/src/boot/axios.js +++ b/web/src/boot/axios.js @@ -35,15 +35,12 @@ export default function ({ app, router, store }) { function (response) { return response; }, - function (error) { + async function (error) { let text if (!error.response) { text = error.message } - else if (error.config.url === "/checkcreds/") { - text = "Bad credentials" - } // unauthorized else if (error.response.status === 401) { router.push({ path: "/expired" }); @@ -52,9 +49,14 @@ export default function ({ app, router, store }) { else if (error.response.status === 403) { text = error.response.data.detail; } - else if (error.response.status === 400) { + // catch all for other 400 error messages + else if (error.response.status >= 400 && error.response.status < 500) { + + if (error.config.responseType === "blob") { + text = (await error.response.data.text()).replace(/^"|"$/g, '') + } - if (error.response.data.non_field_errors) { + else if (error.response.data.non_field_errors) { text = error.response.data.non_field_errors[0] } else { @@ -65,13 +67,6 @@ export default function ({ app, router, store }) { text = key + ": " + value[0] } } - - } - else if (error.response.status === 406) { - text = "Missing 64 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral" - } - else if (error.response.status === 415) { - text = "Missing 32 bit meshagent-x86.exe. Upload it from Settings > Global Settings > MeshCentral" } if (text || error.response) { diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index 38febfe4c3..c9d71aad42 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -1,5 +1,5 @@ - - - - - @@ -198,15 +186,6 @@ import EventLogCheck from "@/components/checks/EventLogCheck"; export default { name: "PolicyChecksTab", - components: { - DiskSpaceCheck, - PingCheck, - CpuLoadCheck, - MemCheck, - WinSvcCheck, - ScriptCheck, - EventLogCheck, - }, mixins: [mixins], props: { selectedPolicy: !Number, @@ -214,9 +193,6 @@ export default { data() { return { checks: [], - dialogComponent: null, - showDialog: false, - editCheck: null, columns: [ { name: "smsalert", field: "text_alert", align: "left" }, { name: "emailalert", field: "email_alert", align: "left" }, @@ -241,7 +217,7 @@ export default { getChecks() { this.$q.loading.show(); this.$axios - .get(`/automation/${this.selectedPolicy}/policychecks/`) + .get(`/automation/policies/${this.selectedPolicy}/checks/`) .then(r => { this.checks = r.data; this.$q.loading.hide(); @@ -266,7 +242,7 @@ export default { const act = !action ? "enabled" : "disabled"; const color = !action ? "positive" : "warning"; this.$axios - .patch(`/checks/${id}/check/`, data) + .put(`/checks/${id}/`, data) .then(r => { this.$q.loading.hide(); this.$q.notify({ @@ -279,45 +255,6 @@ export default { this.$q.loading.hide(); }); }, - showAddDialog(component) { - this.dialogComponent = component; - this.showDialog = true; - }, - showEditDialog(check) { - switch (check.check_type) { - case "diskspace": - this.dialogComponent = "DiskSpaceCheck"; - break; - case "ping": - this.dialogComponent = "PingCheck"; - break; - case "cpuload": - this.dialogComponent = "CpuLoadCheck"; - break; - case "memory": - this.dialogComponent = "MemCheck"; - break; - case "winsvc": - this.dialogComponent = "WinSvcCheck"; - break; - case "script": - this.dialogComponent = "ScriptCheck"; - break; - case "eventlog": - this.dialogComponent = "EventLogCheck"; - break; - default: - return null; - } - this.editCheck = check; - this.showDialog = true; - }, - hideDialog() { - this.getChecks(); - this.showDialog = false; - this.dialogComponent = null; - this.editCheckPK = null; - }, deleteCheck(check) { this.$q .dialog({ @@ -328,7 +265,7 @@ export default { .onOk(() => { this.$q.loading.show(); this.$axios - .delete(`/checks/${check.id}/check/`) + .delete(`/checks/${check.id}/`) .then(r => { this.getChecks(); this.$q.loading.hide(); @@ -348,6 +285,28 @@ export default { }, }); }, + showCheckModal(type, check) { + let component; + + if (type === "diskspace") component = DiskSpaceCheck; + else if (type === "memory") component = MemCheck; + else if (type === "cpuload") component = CpuLoadCheck; + else if (type === "ping") component = PingCheck; + else if (type === "winsvc") component = WinSvcCheck; + else if (type === "eventlog") component = EventLogCheck; + else if (type === "script") component = ScriptCheck; + else return; + + this.$q + .dialog({ + component: component, + componentProps: { + check: check, + parent: !check ? { policy: this.selectedPolicy } : undefined, + }, + }) + .onOk(this.getChecks); + }, }, created() { this.getChecks(); diff --git a/web/src/components/automation/modals/PolicyStatus.vue b/web/src/components/automation/modals/PolicyStatus.vue index 462e74595d..229f12982b 100644 --- a/web/src/components/automation/modals/PolicyStatus.vue +++ b/web/src/components/automation/modals/PolicyStatus.vue @@ -33,6 +33,7 @@ - - - - @@ -119,9 +116,6 @@ import EventLogCheckOutput from "@/components/checks/EventLogCheckOutput"; export default { name: "PolicyStatus", emits: ["hide", "ok", "cancel"], - components: { - EventLogCheckOutput, - }, props: { item: { required: true, @@ -138,8 +132,6 @@ export default { }, data() { return { - showEventLogOutput: false, - evtLogData: {}, data: [], columns: [ { name: "agent", label: "Hostname", field: "agent", align: "left", sortable: true }, @@ -183,7 +175,7 @@ export default { getCheckData() { this.$q.loading.show(); this.$axios - .patch(`/automation/policycheckstatus/${this.item.id}/check/`) + .get(`/automation/checks/${this.item.id}/status/`) .then(r => { this.$q.loading.hide(); this.data = r.data; @@ -195,7 +187,7 @@ export default { getTaskData() { this.$q.loading.show(); this.$axios - .patch(`/automation/policyautomatedtaskstatus/${this.item.id}/task/`) + .get(`/automation/tasks/${this.item.id}/status/`) .then(r => { this.$q.loading.hide(); this.data = r.data; @@ -204,14 +196,6 @@ export default { this.$q.loading.hide(); }); }, - closeEventLogOutput() { - this.showEventLogOutput = false; - this.evtLogdata = {}; - }, - closeScriptOutput() { - this.showScriptOutput = false; - this.scriptInfo = {}; - }, pingInfo(check) { this.$q.dialog({ title: check.readable_desc, @@ -220,9 +204,13 @@ export default { html: true, }); }, - eventLogMoreInfo(check) { - this.evtLogData = check; - this.showEventLogOutput = true; + showEventInfo(data) { + this.$q.dialog({ + component: EventLogCheckOutput, + componentProps: { + evtLogData: data, + }, + }); }, showScriptOutput(script) { this.$q.dialog({ diff --git a/web/src/components/checks/WinSvcCheck.vue b/web/src/components/checks/WinSvcCheck.vue index 28047d40ac..3fff133885 100644 --- a/web/src/components/checks/WinSvcCheck.vue +++ b/web/src/components/checks/WinSvcCheck.vue @@ -2,7 +2,7 @@ - {{ check ? `Edit Memory Check` : "Add Memory Check" }} + {{ check ? `Edit Service Check` : "Add Service Check" }} Close diff --git a/web/src/components/modals/agents/PatchPolicyForm.vue b/web/src/components/modals/agents/PatchPolicyForm.vue index f245089e18..b41db1911a 100644 --- a/web/src/components/modals/agents/PatchPolicyForm.vue +++ b/web/src/components/modals/agents/PatchPolicyForm.vue @@ -240,7 +240,7 @@ export default { // editing patch policy if (this.editing) { this.$axios - .put(`/automation/winupdatepolicy/${this.winupdatepolicy.id}/`, this.winupdatepolicy) + .put(`/automation/patchpolicy/${this.winupdatepolicy.id}/`, this.winupdatepolicy) .then(response => { this.$q.loading.hide(); this.$emit("close"); @@ -252,7 +252,7 @@ export default { } else { // adding patch policy this.$axios - .post("/automation/winupdatepolicy/", this.winupdatepolicy) + .post("/automation/patchpolicy/", this.winupdatepolicy) .then(response => { this.$q.loading.hide(); this.$emit("close"); @@ -274,7 +274,7 @@ export default { .onOk(() => { this.$q.loading.show(); this.$axios - .delete(`/automation/winupdatepolicy/${policy.id}/`) + .delete(`/automation/patchpolicy/${policy.id}/`) .then(r => { this.$q.loading.hide(); this.$emit("close"); diff --git a/web/src/components/tasks/AddAutomatedTask.vue b/web/src/components/tasks/AddAutomatedTask.vue index ea88db11d4..c3d39e434d 100644 --- a/web/src/components/tasks/AddAutomatedTask.vue +++ b/web/src/components/tasks/AddAutomatedTask.vue @@ -184,7 +184,6 @@ \ No newline at end of file diff --git a/web/src/components/modals/alerts/AlertsOverview.vue b/web/src/components/modals/alerts/AlertsOverview.vue index 27552e67df..4625d50cd7 100644 --- a/web/src/components/modals/alerts/AlertsOverview.vue +++ b/web/src/components/modals/alerts/AlertsOverview.vue @@ -427,8 +427,9 @@ export default { this.$q.dialog({ component: ScriptOutput, - parent: this, - scriptInfo: results, + componentProps: { + scriptInfo: results, + }, }); }, alertColor(severity) { diff --git a/web/src/components/modals/logs/PendingActions.vue b/web/src/components/modals/logs/PendingActions.vue deleted file mode 100644 index f82035871e..0000000000 --- a/web/src/components/modals/logs/PendingActions.vue +++ /dev/null @@ -1,237 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/utils/format.js b/web/src/utils/format.js index 4924ae2851..7861f55dd1 100644 --- a/web/src/utils/format.js +++ b/web/src/utils/format.js @@ -167,6 +167,20 @@ export function formatDate(dateString) { return date.formatDate(d, "MMM-DD-YYYY - HH:mm"); } +export function getNextAgentUpdateTime() { + const d = new Date(); + let ret; + if (d.getMinutes() <= 35) { + ret = d.setMinutes(35); + } else { + ret = date.addToDate(d, { hours: 1 }); + ret.setMinutes(35); + } + const a = date.formatDate(ret, "MMM D, YYYY"); + const b = date.formatDate(ret, "h:mm A"); + return `${a} at ${b}`; +} + // string formatting From 2b47870032f24996a3334c371476a4700362b722 Mon Sep 17 00:00:00 2001 From: sadnub Date: Mon, 25 Oct 2021 08:50:31 -0400 Subject: [PATCH 048/106] fix patch scan and install in agent table --- web/src/components/AgentTable.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index e42212d9db..118c86d21c 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -204,7 +204,7 @@ Run Patch Status Scan - + Install Patches Now @@ -521,7 +521,7 @@ export default { }, runPatchStatusScan(agent) { this.$axios - .get(`/winupdate/${agent.agent_id}/scan/`) + .post(`/winupdate/${agent.agent_id}/scan/`) .then(r => { this.notifySuccess(`Scan will be run shortly on ${agent.hostname}`); }) @@ -530,7 +530,7 @@ export default { installPatches(agent) { this.$q.loading.show(); this.$axios - .get(`/winupdate/${agent.agent_id}/install/`) + .post(`/winupdate/${agent.agent_id}/install/`) .then(r => { this.$q.loading.hide(); this.notifySuccess(r.data); From a244a341ec6ee4d7df54efc3894284e81c9772d4 Mon Sep 17 00:00:00 2001 From: sadnub Date: Mon, 25 Oct 2021 09:20:23 -0400 Subject: [PATCH 049/106] rework agent recovery and permissions --- web/src/api/agents.js | 5 + web/src/components/AgentTable.vue | 18 +- .../modals/agents/AgentRecovery.vue | 176 ++++++++++-------- 3 files changed, 109 insertions(+), 90 deletions(-) diff --git a/web/src/api/agents.js b/web/src/api/agents.js index 7260710eb6..68845b2d92 100644 --- a/web/src/api/agents.js +++ b/web/src/api/agents.js @@ -39,6 +39,11 @@ export async function fetchAgentTasks(agent_id, params = {}) { } catch (e) { console.error(e) } } +export async function sendAgentRecovery(agent_id, payload) { + const { data } = await axios.post(`${baseUrl}/${agent_id}/recover/`, payload) + return data +} + export async function sendAgentCommand(agent_id, payload) { const { data } = await axios.post(`${baseUrl}/${agent_id}/cmd/`, payload) return data diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index 118c86d21c..eb0a7ffa8f 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -248,7 +248,7 @@ Assign Automation Policy - + @@ -384,10 +384,6 @@ - - - - @@ -407,9 +403,6 @@ export default { name: "AgentTable", props: ["frame", "columns", "userName", "search", "visibleColumns"], emits: ["edit"], - components: { - AgentRecovery, - }, mixins: [mixins], data() { return { @@ -418,7 +411,6 @@ export default { sortBy: "hostname", descending: false, }, - showAgentRecovery: false, favoriteScripts: [], urlActions: [], }; @@ -781,6 +773,14 @@ export default { }) .onOk(() => this.$emit("edit")); }, + showAgentRecovery(agent) { + this.$q.dialog({ + component: AgentRecovery, + componentProps: { + agent: agent, + }, + }); + }, }, computed: { ...mapGetters(["selectedAgentId", "agentTableHeight", "showCommunityScripts"]), diff --git a/web/src/components/modals/agents/AgentRecovery.vue b/web/src/components/modals/agents/AgentRecovery.vue index 30dd94bfc8..ddd1e5c95e 100644 --- a/web/src/components/modals/agents/AgentRecovery.vue +++ b/web/src/components/modals/agents/AgentRecovery.vue @@ -1,101 +1,115 @@ \ No newline at end of file From c9a52bd7d0b1006c8272ee4223c41a1f25213fcb Mon Sep 17 00:00:00 2001 From: sadnub Date: Mon, 25 Oct 2021 10:19:07 -0400 Subject: [PATCH 050/106] fix tests broken by url changes --- api/tacticalrmm/alerts/tests.py | 44 +++++++++--------- api/tacticalrmm/automation/tests.py | 45 +++++++++---------- api/tacticalrmm/core/tests.py | 16 +++---- api/tacticalrmm/core/views.py | 2 +- .../modals/agents/AgentRecovery.vue | 2 +- 5 files changed, 53 insertions(+), 56 deletions(-) diff --git a/api/tacticalrmm/alerts/tests.py b/api/tacticalrmm/alerts/tests.py index 3802fc3857..85151ff1f5 100644 --- a/api/tacticalrmm/alerts/tests.py +++ b/api/tacticalrmm/alerts/tests.py @@ -23,7 +23,7 @@ def setUp(self): self.setup_coresettings() def test_get_alerts(self): - url = "/alerts/alerts/" + url = "/alerts/" # create check, task, and agent to test each serializer function check = baker.make_recipe("checks.diskspace_check") @@ -116,7 +116,7 @@ def test_get_alerts(self): self.check_not_authenticated("patch", url) def test_add_alert(self): - url = "/alerts/alerts/" + url = "/alerts/" agent = baker.make_recipe("agents.agent") data = { @@ -133,11 +133,11 @@ def test_add_alert(self): def test_get_alert(self): # returns 404 for invalid alert pk - resp = self.client.get("/alerts/alerts/500/", format="json") + resp = self.client.get("/alerts/500/", format="json") self.assertEqual(resp.status_code, 404) alert = baker.make("alerts.Alert") - url = f"/alerts/alerts/{alert.pk}/" # type: ignore + url = f"/alerts/{alert.pk}/" # type: ignore resp = self.client.get(url, format="json") serializer = AlertSerializer(alert) @@ -149,16 +149,15 @@ def test_get_alert(self): def test_update_alert(self): # returns 404 for invalid alert pk - resp = self.client.put("/alerts/alerts/500/", format="json") + resp = self.client.put("/alerts/500/", format="json") self.assertEqual(resp.status_code, 404) alert = baker.make("alerts.Alert", resolved=False, snoozed=False) - url = f"/alerts/alerts/{alert.pk}/" # type: ignore + url = f"/alerts/{alert.pk}/" # type: ignore # test resolving alert data = { - "id": alert.pk, # type: ignore "type": "resolve", } resp = self.client.put(url, data, format="json") @@ -167,26 +166,26 @@ def test_update_alert(self): self.assertTrue(Alert.objects.get(pk=alert.pk).resolved_on) # type: ignore # test snoozing alert - data = {"id": alert.pk, "type": "snooze", "snooze_days": "30"} # type: ignore + data = {"type": "snooze", "snooze_days": "30"} # type: ignore resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) self.assertTrue(Alert.objects.get(pk=alert.pk).snoozed) # type: ignore self.assertTrue(Alert.objects.get(pk=alert.pk).snooze_until) # type: ignore # test snoozing alert without snooze_days - data = {"id": alert.pk, "type": "snooze"} # type: ignore + data = {"type": "snooze"} # type: ignore resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) # test unsnoozing alert - data = {"id": alert.pk, "type": "unsnooze"} # type: ignore + data = {"type": "unsnooze"} # type: ignore resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) self.assertFalse(Alert.objects.get(pk=alert.pk).snoozed) # type: ignore self.assertFalse(Alert.objects.get(pk=alert.pk).snooze_until) # type: ignore # test invalid type - data = {"id": alert.pk, "type": "invalid"} # type: ignore + data = {"type": "invalid"} # type: ignore resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) @@ -194,13 +193,13 @@ def test_update_alert(self): def test_delete_alert(self): # returns 404 for invalid alert pk - resp = self.client.put("/alerts/alerts/500/", format="json") + resp = self.client.put("/alerts/500/", format="json") self.assertEqual(resp.status_code, 404) alert = baker.make("alerts.Alert") # test delete alert - url = f"/alerts/alerts/{alert.pk}/" # type: ignore + url = f"/alerts/{alert.pk}/" # type: ignore resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) @@ -242,7 +241,7 @@ def test_bulk_alert_actions(self): self.assertTrue(Alert.objects.filter(snoozed=False).exists()) def test_get_alert_templates(self): - url = "/alerts/alerttemplates/" + url = "/alerts/templates/" alert_templates = baker.make("alerts.AlertTemplate", _quantity=3) resp = self.client.get(url, format="json") @@ -254,7 +253,7 @@ def test_get_alert_templates(self): self.check_not_authenticated("get", url) def test_add_alert_template(self): - url = "/alerts/alerttemplates/" + url = "/alerts/templates/" data = { "name": "Test Template", @@ -267,11 +266,11 @@ def test_add_alert_template(self): def test_get_alert_template(self): # returns 404 for invalid alert template pk - resp = self.client.get("/alerts/alerttemplates/500/", format="json") + resp = self.client.get("/alerts/templates/500/", format="json") self.assertEqual(resp.status_code, 404) alert_template = baker.make("alerts.AlertTemplate") - url = f"/alerts/alerttemplates/{alert_template.pk}/" # type: ignore + url = f"/alerts/templates/{alert_template.pk}/" # type: ignore resp = self.client.get(url, format="json") serializer = AlertTemplateSerializer(alert_template) @@ -283,16 +282,15 @@ def test_get_alert_template(self): def test_update_alert_template(self): # returns 404 for invalid alert pk - resp = self.client.put("/alerts/alerttemplates/500/", format="json") + resp = self.client.put("/alerts/templates/500/", format="json") self.assertEqual(resp.status_code, 404) alert_template = baker.make("alerts.AlertTemplate") - url = f"/alerts/alerttemplates/{alert_template.pk}/" # type: ignore + url = f"/alerts/templates/{alert_template.pk}/" # type: ignore # test data data = { - "id": alert_template.pk, # type: ignore "agent_email_on_resolved": True, "agent_text_on_resolved": True, "agent_include_desktops": True, @@ -308,13 +306,13 @@ def test_update_alert_template(self): def test_delete_alert_template(self): # returns 404 for invalid alert pk - resp = self.client.put("/alerts/alerttemplates/500/", format="json") + resp = self.client.put("/alerts/templates/500/", format="json") self.assertEqual(resp.status_code, 404) alert_template = baker.make("alerts.AlertTemplate") # test delete alert - url = f"/alerts/alerttemplates/{alert_template.pk}/" # type: ignore + url = f"/alerts/templates/{alert_template.pk}/" # type: ignore resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) @@ -332,7 +330,7 @@ def test_alert_template_related(self): core.alert_template = alert_template # type: ignore core.save() # type: ignore - url = f"/alerts/alerttemplates/{alert_template.pk}/related/" # type: ignore + url = f"/alerts/templates/{alert_template.pk}/related/" # type: ignore resp = self.client.get(url, format="json") serializer = AlertTemplateRelationSerializer(alert_template) diff --git a/api/tacticalrmm/automation/tests.py b/api/tacticalrmm/automation/tests.py index 5fd34f689d..6d0b698888 100644 --- a/api/tacticalrmm/automation/tests.py +++ b/api/tacticalrmm/automation/tests.py @@ -188,14 +188,14 @@ def test_get_policy_check_status(self): managed_by_policy=True, parent_check=policy_diskcheck.pk, ) - url = f"/automation/policycheckstatus/{policy_diskcheck.pk}/check/" + url = f"/automation/checks/{policy_diskcheck.pk}/status/" - resp = self.client.patch(url, format="json") + resp = self.client.get(url, format="json") serializer = PolicyCheckStatusSerializer([managed_check], many=True) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.data, serializer.data) # type: ignore - self.check_not_authenticated("patch", url) + self.check_not_authenticated("get", url) def test_policy_overview(self): from clients.models import Client @@ -255,15 +255,15 @@ def test_get_policy_task_status(self): "autotasks.AutomatedTask", parent_task=task.id, _quantity=5 # type: ignore ) - url = f"/automation/policyautomatedtaskstatus/{task.id}/task/" # type: ignore + url = f"/automation/tasks/{task.id}/status/" # type: ignore serializer = PolicyTaskStatusSerializer(policy_tasks, many=True) - resp = self.client.patch(url, format="json") + resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) self.assertEqual(resp.data, serializer.data) # type: ignore self.assertEqual(len(resp.data), 5) # type: ignore - self.check_not_authenticated("patch", url) + self.check_not_authenticated("get", url) @patch("automation.tasks.run_win_policy_autotasks_task.delay") def test_run_win_task(self, mock_task): @@ -276,16 +276,16 @@ def test_run_win_task(self, mock_task): _quantity=6, ) - url = "/automation/runwintask/1/" - resp = self.client.put(url, format="json") + url = "/automation/tasks/1/run/" + resp = self.client.post(url, format="json") self.assertEqual(resp.status_code, 200) mock_task.assert_called() # type: ignore - self.check_not_authenticated("put", url) + self.check_not_authenticated("post", url) def test_create_new_patch_policy(self): - url = "/automation/winupdatepolicy/" + url = "/automation/patchpolicy/" # test policy doesn't exist data = {"policy": 500} @@ -316,15 +316,14 @@ def test_create_new_patch_policy(self): def test_update_patch_policy(self): # test policy doesn't exist - resp = self.client.put("/automation/winupdatepolicy/500/", format="json") + resp = self.client.put("/automation/patchpolicy/500/", format="json") self.assertEqual(resp.status_code, 404) policy = baker.make("automation.Policy") patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy) - url = f"/automation/winupdatepolicy/{patch_policy.pk}/" # type: ignore + url = f"/automation/patchpolicy/{patch_policy.pk}/" # type: ignore data = { - "id": patch_policy.pk, # type: ignore "policy": policy.pk, # type: ignore "critical": "approve", "important": "approve", @@ -369,7 +368,7 @@ def test_reset_patch_policy(self): # test reset agents in site data = {"site": sites[0].id} # type: ignore - resp = self.client.patch(url, data, format="json") + resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) agents = Agent.objects.filter(site=sites[0]) # type: ignore @@ -381,7 +380,7 @@ def test_reset_patch_policy(self): # test reset agents in client data = {"client": clients[1].id} # type: ignore - resp = self.client.patch(url, data, format="json") + resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) agents = Agent.objects.filter(site__client=clients[1]) # type: ignore @@ -393,7 +392,7 @@ def test_reset_patch_policy(self): # test reset all agents data = {} - resp = self.client.patch(url, data, format="json") + resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) agents = Agent.objects.all() @@ -401,17 +400,17 @@ def test_reset_patch_policy(self): for k, v in inherit_fields.items(): self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v) - self.check_not_authenticated("patch", url) + self.check_not_authenticated("post", url) def test_delete_patch_policy(self): # test patch policy doesn't exist - resp = self.client.delete("/automation/winupdatepolicy/500/", format="json") + resp = self.client.delete("/automation/patchpolicy/500/", format="json") self.assertEqual(resp.status_code, 404) winupdate_policy = baker.make_recipe( "winupdate.winupdate_policy", policy__name="Test Policy" ) - url = f"/automation/winupdatepolicy/{winupdate_policy.pk}/" + url = f"/automation/patchpolicy/{winupdate_policy.pk}/" resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) @@ -477,7 +476,7 @@ def test_policy_related(self): self.assertEquals(len(resp.data["server_sites"]), 0) # type: ignore self.assertEquals(len(resp.data["workstation_clients"]), 1) # type: ignore self.assertEquals(len(resp.data["workstation_sites"]), 0) # type: ignore - self.assertEquals(len(resp.data["agents"]), 10) # type: ignore + self.assertEquals(len(resp.data["agents"]), 0) # type: ignore # Add Site to Policy policy.server_sites.add(server_agents[10].site) # type: ignore @@ -487,15 +486,15 @@ def test_policy_related(self): ) self.assertEquals(len(resp.data["server_sites"]), 1) # type: ignore self.assertEquals(len(resp.data["workstation_sites"]), 1) # type: ignore - self.assertEquals(len(resp.data["agents"]), 12) # type: ignore + self.assertEquals(len(resp.data["agents"]), 0) # type: ignore - # Add Agent to Policy and the agents length shouldn't change + # Add Agent to Policy policy.agents.add(server_agents[2]) # type: ignore policy.agents.add(workstation_agents[2]) # type: ignore resp = self.client.get( f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore ) - self.assertEquals(len(resp.data["agents"]), 14) # type: ignore + self.assertEquals(len(resp.data["agents"]), 2) # type: ignore def test_generating_agent_policy_checks(self): from .tasks import generate_agent_checks_task diff --git a/api/tacticalrmm/core/tests.py b/api/tacticalrmm/core/tests.py index b6c3f627a8..876a2a1a1f 100644 --- a/api/tacticalrmm/core/tests.py +++ b/api/tacticalrmm/core/tests.py @@ -82,7 +82,7 @@ def test_vue_version(self): self.check_not_authenticated("get", url) def test_get_core_settings(self): - url = "/core/getcoresettings/" + url = "/core/settings/" r = self.client.get(url) self.assertEqual(r.status_code, 200) @@ -90,7 +90,7 @@ def test_get_core_settings(self): @patch("automation.tasks.generate_agent_checks_task.delay") def test_edit_coresettings(self, generate_agent_checks_task): - url = "/core/editsettings/" + url = "/core/settings/" # setup policies = baker.make("automation.Policy", _quantity=2) @@ -99,7 +99,7 @@ def test_edit_coresettings(self, generate_agent_checks_task): "smtp_from_email": "newexample@example.com", "mesh_token": "New_Mesh_Token", } - r = self.client.patch(url, data) + r = self.client.put(url, data) self.assertEqual(r.status_code, 200) self.assertEqual( CoreSettings.objects.first().smtp_from_email, data["smtp_from_email"] @@ -113,7 +113,7 @@ def test_edit_coresettings(self, generate_agent_checks_task): "workstation_policy": policies[0].id, # type: ignore "server_policy": policies[1].id, # type: ignore } - r = self.client.patch(url, data) + r = self.client.put(url, data) self.assertEqual(r.status_code, 200) self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id) # type: ignore self.assertEqual( @@ -128,13 +128,13 @@ def test_edit_coresettings(self, generate_agent_checks_task): data = { "workstation_policy": "", } - r = self.client.patch(url, data) + r = self.client.put(url, data) self.assertEqual(r.status_code, 200) self.assertEqual(CoreSettings.objects.first().workstation_policy, None) self.assertEqual(generate_agent_checks_task.call_count, 1) - self.check_not_authenticated("patch", url) + self.check_not_authenticated("put", url) @patch("tacticalrmm.utils.reload_nats") @patch("autotasks.tasks.remove_orphaned_win_tasks.delay") @@ -404,10 +404,10 @@ def test_run_url_action(self): url = "/core/urlaction/run/" # test not found - r = self.client.patch(url, {"agent": 500, "action": 500}) + r = self.client.patch(url, {"agent_id": 500, "action": 500}) self.assertEqual(r.status_code, 404) - data = {"agent": agent.agent_id, "action": action.id} # type: ignore + data = {"agent_id": agent.agent_id, "action": action.id} # type: ignore r = self.client.patch(url, data) self.assertEqual(r.status_code, 200) diff --git a/api/tacticalrmm/core/views.py b/api/tacticalrmm/core/views.py index fe973640b0..42ae1cff10 100644 --- a/api/tacticalrmm/core/views.py +++ b/api/tacticalrmm/core/views.py @@ -358,7 +358,7 @@ def patch(self, request): from clients.models import Client, Site from tacticalrmm.utils import replace_db_values - if "agent" in request.data.keys(): + if "agent_id" in request.data.keys(): if not _has_perm_on_agent(request.user, request.data["agent_id"]): raise PermissionDenied() diff --git a/web/src/components/modals/agents/AgentRecovery.vue b/web/src/components/modals/agents/AgentRecovery.vue index ddd1e5c95e..bbb6f0e426 100644 --- a/web/src/components/modals/agents/AgentRecovery.vue +++ b/web/src/components/modals/agents/AgentRecovery.vue @@ -53,7 +53,7 @@ diff --git a/web/src/components/agents/remotebg/ServicesManager.vue b/web/src/components/agents/remotebg/ServicesManager.vue index 54acacc994..4a3b9f527f 100644 --- a/web/src/components/agents/remotebg/ServicesManager.vue +++ b/web/src/components/agents/remotebg/ServicesManager.vue @@ -1,174 +1,78 @@ \ No newline at end of file diff --git a/web/src/components/FileBar.vue b/web/src/components/FileBar.vue index adb3233c64..3a4d64cd83 100644 --- a/web/src/components/FileBar.vue +++ b/web/src/components/FileBar.vue @@ -48,7 +48,7 @@ Install Agent - + Manage Deployments @@ -163,10 +163,6 @@ - - - - @@ -195,7 +191,7 @@ import AdminManager from "@/components/AdminManager"; import InstallAgent from "@/components/modals/agents/InstallAgent"; import AuditManager from "@/components/logs/AuditManager"; import BulkAction from "@/components/modals/agents/BulkAction"; -import Deployment from "@/components/Deployment"; +import Deployment from "@/components/clients/Deployment"; import ServerMaintenance from "@/components/modals/core/ServerMaintenance"; import CodeSign from "@/components/modals/coresettings/CodeSign"; import PermissionsManager from "@/components/accounts/PermissionsManager"; @@ -208,7 +204,6 @@ export default { EditCoreSettings, InstallAgent, AdminManager, - Deployment, ServerMaintenance, CodeSign, PermissionsManager, @@ -220,7 +215,6 @@ export default { showEditCoreSettingsModal: false, showAdminManager: false, showInstallAgent: false, - showDeployment: false, showCodeSign: false, }; }, @@ -248,14 +242,6 @@ export default { } window.open(url, "_blank"); }, - showBulkActionModal(mode) { - this.bulkMode = mode; - this.showBulkAction = true; - }, - closeBulkActionModal() { - this.bulkMode = null; - this.showBulkAction = false; - }, showAutomationManager() { this.$q.dialog({ component: AutomationManager, @@ -338,6 +324,11 @@ export default { component: PendingActions, }); }, + showDeployments() { + this.$q.dialog({ + component: Deployment, + }); + }, edited() { this.$emit("edit"); }, diff --git a/web/src/components/clients/Deployment.vue b/web/src/components/clients/Deployment.vue new file mode 100644 index 0000000000..6f5b760a6a --- /dev/null +++ b/web/src/components/clients/Deployment.vue @@ -0,0 +1,171 @@ + + + \ No newline at end of file diff --git a/web/src/components/clients/NewDeployment.vue b/web/src/components/clients/NewDeployment.vue new file mode 100644 index 0000000000..0545ffa662 --- /dev/null +++ b/web/src/components/clients/NewDeployment.vue @@ -0,0 +1,132 @@ + + + \ No newline at end of file diff --git a/web/src/components/modals/clients/NewDeployment.vue b/web/src/components/modals/clients/NewDeployment.vue deleted file mode 100644 index 4f8261a52f..0000000000 --- a/web/src/components/modals/clients/NewDeployment.vue +++ /dev/null @@ -1,150 +0,0 @@ - - - \ No newline at end of file From eb813e6b22fd7ce8cf1ee53d94ce03138bcb843e Mon Sep 17 00:00:00 2001 From: sadnub Date: Wed, 3 Nov 2021 22:24:19 -0400 Subject: [PATCH 055/106] convert hr to q-separator --- web/src/components/accounts/RolesForm.vue | 32 +++++++++++-------- web/src/components/agents/SummaryTab.vue | 4 +-- .../agents/remotebg/ServiceDetail.vue | 4 +-- web/src/components/core/APIKeysTable.vue | 2 +- .../modals/agents/PatchPolicyForm.vue | 8 ++--- .../components/modals/agents/UpdateAgents.vue | 7 ++-- .../modals/coresettings/CustomFields.vue | 2 +- .../modals/coresettings/EditCoreSettings.vue | 12 +++---- .../modals/coresettings/KeyStoreTable.vue | 2 +- .../modals/coresettings/URLActionsTable.vue | 2 +- .../modals/coresettings/UserPreferences.vue | 2 +- 11 files changed, 40 insertions(+), 37 deletions(-) diff --git a/web/src/components/accounts/RolesForm.vue b/web/src/components/accounts/RolesForm.vue index f736544897..22c628aa4c 100644 --- a/web/src/components/accounts/RolesForm.vue +++ b/web/src/components/accounts/RolesForm.vue @@ -20,7 +20,7 @@
Super User
-
+
@@ -28,7 +28,7 @@
Accounts
-
+
@@ -39,7 +39,7 @@
Agents
-
+
@@ -60,7 +60,7 @@
Core
-
+
@@ -72,11 +72,13 @@ + +
Checks
-
+
@@ -86,7 +88,7 @@
Clients
-
+
@@ -124,7 +126,7 @@
Automation Policies
-
+
@@ -133,7 +135,7 @@
Tasks
-
+
@@ -143,7 +145,7 @@
Logs
-
+
@@ -154,7 +156,7 @@
Scripts
-
+
@@ -163,7 +165,7 @@
Alerts
-
+
@@ -174,7 +176,7 @@
Windows Services
-
+
@@ -182,7 +184,7 @@
Software
-
+
@@ -191,7 +193,7 @@
Windows Updates
-
+
@@ -266,6 +268,8 @@ export default { can_do_server_maint: false, can_code_sign: false, can_run_urlactions: false, + can_view_customfields: false, + can_manage_customfields: false, // api key perms can_list_api_keys: false, can_manage_api_keys: false, diff --git a/web/src/components/agents/SummaryTab.vue b/web/src/components/agents/SummaryTab.vue index a4a0f8e3d3..173a6fee4d 100644 --- a/web/src/components/agents/SummaryTab.vue +++ b/web/src/components/agents/SummaryTab.vue @@ -10,7 +10,7 @@ Maintenance Mode • {{ summary.operating_system }} • Agent v{{ summary.version }} -
+
@@ -104,7 +104,7 @@ {{ disk.device }} ({{ disk.fstype }}) {{ disk.free }} free of {{ disk.total }} -
+
diff --git a/web/src/components/agents/remotebg/ServiceDetail.vue b/web/src/components/agents/remotebg/ServiceDetail.vue index 7c12e8c989..c2951fb9eb 100644 --- a/web/src/components/agents/remotebg/ServiceDetail.vue +++ b/web/src/components/agents/remotebg/ServiceDetail.vue @@ -50,7 +50,7 @@
-
+
Service status:
@@ -65,7 +65,7 @@
-
+
-
+
Auto Approval
-
+
Severity
@@ -75,7 +75,7 @@
Installation Schedule
-
+
Schedule Frequency:
@@ -132,7 +132,7 @@
Reboot After Installation
-
+
@@ -148,7 +148,7 @@
Failed Patches
-
+
diff --git a/web/src/components/modals/agents/UpdateAgents.vue b/web/src/components/modals/agents/UpdateAgents.vue index 36a1cacb70..c220c5e3b5 100644 --- a/web/src/components/modals/agents/UpdateAgents.vue +++ b/web/src/components/modals/agents/UpdateAgents.vue @@ -21,10 +21,10 @@ Select Agent
-
+ -
+ { this.$emit("close"); - this.$emit("edit"); this.notifySuccess("Agents will now be updated"); }) .catch(e => {}); diff --git a/web/src/components/modals/coresettings/CustomFields.vue b/web/src/components/modals/coresettings/CustomFields.vue index d310f8f25c..bb9fbaa2b3 100644 --- a/web/src/components/modals/coresettings/CustomFields.vue +++ b/web/src/components/modals/coresettings/CustomFields.vue @@ -12,7 +12,7 @@ @click="addCustomField" />
-
+
General
-
+ Runs at 35mins past every hour @@ -137,7 +137,7 @@ />
-
+
Recipients
@@ -167,7 +167,7 @@
SMTP Settings
-
+
From:
@@ -244,7 +244,7 @@ />
-
+
Recipients
@@ -274,7 +274,7 @@
Twilio Settings
-
+
Twilio Number:
@@ -300,7 +300,7 @@
MeshCentral Settings
-
+
Username:
diff --git a/web/src/components/modals/coresettings/KeyStoreTable.vue b/web/src/components/modals/coresettings/KeyStoreTable.vue index 2599b44670..c64541327e 100644 --- a/web/src/components/modals/coresettings/KeyStoreTable.vue +++ b/web/src/components/modals/coresettings/KeyStoreTable.vue @@ -5,7 +5,7 @@
-
+
-
+
User Interface
-
+
Agent double-click action:
From 02b7f962e946c34b537215f5d5c19776b816a153 Mon Sep 17 00:00:00 2001 From: sadnub Date: Wed, 3 Nov 2021 22:26:33 -0400 Subject: [PATCH 056/106] move to inject/provide to refresh dashboard in nested components --- web/src/components/AgentTable.vue | 45 ++++++++-------- web/src/components/AlertsManager.vue | 3 +- web/src/components/FileBar.vue | 47 +++++++++------- web/src/components/agents/ChecksTab.vue | 6 ++- web/src/components/agents/WinUpdateTab.vue | 6 ++- web/src/components/clients/ClientsForm.vue | 5 -- web/src/components/clients/ClientsManager.vue | 5 -- web/src/components/clients/DeleteClient.vue | 5 -- web/src/components/clients/SitesForm.vue | 5 -- web/src/components/clients/SitesTable.vue | 5 -- .../components/modals/agents/EditAgent.vue | 3 +- .../components/modals/agents/RebootLater.vue | 5 +- web/src/views/Dashboard.vue | 54 +++++++++++-------- 13 files changed, 95 insertions(+), 99 deletions(-) diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index 360c74e75a..0190d503ea 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -402,7 +402,7 @@ import RunScript from "@/components/modals/agents/RunScript"; export default { name: "AgentTable", props: ["frame", "columns", "userName", "search", "visibleColumns"], - emits: ["edit"], + inject: ["refreshDashboard"], mixins: [mixins], data() { return { @@ -531,16 +531,15 @@ export default { this.$q.loading.hide(); }); }, - agentEdited() { - this.$emit("edit"); - }, showPendingActionsModal(agent) { - this.$q.dialog({ - component: PendingActions, - componentProps: { - agent: agent, - }, - }); + this.$q + .dialog({ + component: PendingActions, + componentProps: { + agent: agent, + }, + }) + .onDismiss(this.refreshDashboard); }, takeControl(agent_id) { const url = this.$router.resolve(`/takecontrol/${agent_id}`).href; @@ -582,9 +581,7 @@ export default { .delete(`/agents/${agent.agent_id}/`) .then(r => { this.notifySuccess(r.data); - setTimeout(() => { - location.reload(); - }, 2000); + this.refreshDashboard(); }) .catch(e => { this.$q.loading.hide(); @@ -686,9 +683,7 @@ export default { object: agent, }, }) - .onOk(() => { - this.$emit("edit"); - }); + .onOk(this.refreshDashboard); }, toggleMaintenance(agent) { let data = { @@ -701,7 +696,7 @@ export default { this.notifySuccess( `Maintenance mode was ${agent.maintenance_mode ? "disabled" : "enabled"} on ${agent.hostname}` ); - this.$emit("edit"); + this.refreshDashboard(); }) .catch(e => { console.log(e); @@ -756,12 +751,14 @@ export default { }); }, showRebootLaterModal(agent) { - this.$q.dialog({ - component: RebootLater, - componentProps: { - agent: agent, - }, - }); + this.$q + .dialog({ + component: RebootLater, + componentProps: { + agent: agent, + }, + }) + .onOk(this.refreshDashboard); }, showEditAgent(agent_id) { this.$q @@ -771,7 +768,7 @@ export default { agent_id: agent_id, }, }) - .onOk(() => this.$emit("edit")); + .onOk(this.refreshDashboard); }, showAgentRecovery(agent) { this.$q.dialog({ diff --git a/web/src/components/AlertsManager.vue b/web/src/components/AlertsManager.vue index 7b9ed8c763..54cc22a352 100644 --- a/web/src/components/AlertsManager.vue +++ b/web/src/components/AlertsManager.vue @@ -244,8 +244,9 @@ export default { this.$axios .delete(`alerts/templates/${template.id}/`) .then(r => { - this.getTemplates(); + this.refresh(); this.$q.loading.hide(); + this.notifySuccess(`Alert template ${template.name} was deleted!`); }) .catch(error => { diff --git a/web/src/components/FileBar.vue b/web/src/components/FileBar.vue index 3a4d64cd83..3958ba1a60 100644 --- a/web/src/components/FileBar.vue +++ b/web/src/components/FileBar.vue @@ -154,7 +154,7 @@
- +
@@ -198,7 +198,7 @@ import PermissionsManager from "@/components/accounts/PermissionsManager"; export default { name: "FileBar", - emits: ["edit"], + inject: ["refreshDashboard"], components: { UpdateAgents, EditCoreSettings, @@ -248,24 +248,32 @@ export default { }); }, showAlertsManager() { - this.$q.dialog({ - component: AlertsManager, - }); + this.$q + .dialog({ + component: AlertsManager, + }) + .onDismiss(this.refreshDashboard); }, showClientsManager() { - this.$q.dialog({ - component: ClientsManager, - }); + this.$q + .dialog({ + component: ClientsManager, + }) + .onDismiss(this.refreshDashboard); }, showAddClientModal() { - this.$q.dialog({ - component: ClientsForm, - }); + this.$q + .dialog({ + component: ClientsForm, + }) + .onOk(this.refreshDashboard); }, showAddSiteModal() { - this.$q.dialog({ - component: SitesForm, - }); + this.$q + .dialog({ + component: SitesForm, + }) + .onOk(this.refreshDashboard); }, showPermissionsManager() { this.$q.dialog({ @@ -320,18 +328,17 @@ export default { }); }, showPendingActions() { - this.$q.dialog({ - component: PendingActions, - }); + this.$q + .dialog({ + component: PendingActions, + }) + .onDismiss(this.refreshDashboard); }, showDeployments() { this.$q.dialog({ component: Deployment, }); }, - edited() { - this.$emit("edit"); - }, }, }; diff --git a/web/src/components/agents/ChecksTab.vue b/web/src/components/agents/ChecksTab.vue index 4f695fd4ea..78cae97ce1 100644 --- a/web/src/components/agents/ChecksTab.vue +++ b/web/src/components/agents/ChecksTab.vue @@ -284,7 +284,7 @@ \ No newline at end of file diff --git a/web/src/components/modals/alerts/AlertExclusions.vue b/web/src/components/modals/alerts/AlertExclusions.vue index 68eeac699d..cfd3c4ea5a 100644 --- a/web/src/components/modals/alerts/AlertExclusions.vue +++ b/web/src/components/modals/alerts/AlertExclusions.vue @@ -10,68 +10,37 @@ - - - - + mapOptions + /> - - - + mapOptions + /> @@ -90,8 +59,12 @@ \ No newline at end of file diff --git a/web/src/mixins/mixins.js b/web/src/mixins/mixins.js index ca55ae2ea8..cadc0452dc 100644 --- a/web/src/mixins/mixins.js +++ b/web/src/mixins/mixins.js @@ -195,11 +195,11 @@ export default { return options; }, - async getAgentOptions() { + async getAgentOptions(value_field = "agent_id") { const { data } = await axios.get("/agents/?detail=false") - return formatAgentOptions(data) + return formatAgentOptions(data, false, value_field) }, getNextAgentUpdateTime() { const d = new Date(); diff --git a/web/src/store/index.js b/web/src/store/index.js index 590a275427..09b69f8f3d 100644 --- a/web/src/store/index.js +++ b/web/src/store/index.js @@ -128,12 +128,6 @@ export default function () { }) .catch(e => { }); }, - loadClients(context) { - return axios.get("/clients/"); - }, - loadSites(context) { - return axios.get("/clients/sites/"); - }, loadTree({ commit, state }) { axios.get("/clients/").then(r => { diff --git a/web/src/utils/format.js b/web/src/utils/format.js index 7861f55dd1..1b8ed54629 100644 --- a/web/src/utils/format.js +++ b/web/src/utils/format.js @@ -57,17 +57,17 @@ export function formatScriptOptions(data, flat = false) { } } -export function formatAgentOptions(data, flat = false) { +export function formatAgentOptions(data, flat = false, value_field = "agent_id") { if (flat) { // returns just agent hostnames in array - return _formatOptions(data, { label: "hostname", value: "agent_id", flat: true, allowDuplicates: false }) + return _formatOptions(data, { label: "hostname", value: value_field, flat: true, allowDuplicates: false }) } else { // returns options with categories in object format let options = [] const agents = data.map(agent => ({ label: agent.hostname, - value: agent.agent_id, + value: agent[value_field], cat: `${agent.client} > ${agent.site}`, })); From b757ce1e383d0881f9175bece0d8a32cfba35927 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Fri, 5 Nov 2021 21:15:59 +0000 Subject: [PATCH 080/106] fix url action for agents --- web/src/components/AgentTable.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index ebbcb601e7..e839014835 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -126,7 +126,7 @@ dense clickable v-close-popup - @click="runURLAction(props.row.id, action.id)" + @click="runURLAction(props.row.agent_id, action.id)" > {{ action.name }} From 10852a94272ecc79661e960492997cebc9c46379 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Fri, 5 Nov 2021 22:34:20 +0000 Subject: [PATCH 081/106] stop loading after uninstall --- web/src/components/AgentTable.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index e839014835..f9338f9678 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -580,6 +580,7 @@ export default { this.$axios .delete(`/agents/${agent.agent_id}/`) .then(r => { + this.$q.loading.hide(); this.notifySuccess(r.data); this.refreshDashboard(); }) From a92d1d99586293b42c412a53bede3a8306de6609 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sat, 6 Nov 2021 08:11:43 +0000 Subject: [PATCH 082/106] fix pending actions --- web/src/components/logs/PendingActions.vue | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/web/src/components/logs/PendingActions.vue b/web/src/components/logs/PendingActions.vue index 64c2cdc703..5630652458 100644 --- a/web/src/components/logs/PendingActions.vue +++ b/web/src/components/logs/PendingActions.vue @@ -22,10 +22,6 @@ no-data-label="No Pending Actions" :loading="loading" > - -